From 21187c794d29203c6df3577bbc1ea9c4d2e9a6d5 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sun, 20 Nov 2022 21:00:54 -0800 Subject: [PATCH 001/153] update the automod criteria --- .../autoModeration/autoModeration.service.ts | 2 +- .../moderatorActions.service.interface.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 src/moderation/services/moderatorActions/moderatorActions.service.interface.ts diff --git a/src/moderation/services/autoModeration/autoModeration.service.ts b/src/moderation/services/autoModeration/autoModeration.service.ts index 253b947..134b9b6 100644 --- a/src/moderation/services/autoModeration/autoModeration.service.ts +++ b/src/moderation/services/autoModeration/autoModeration.service.ts @@ -51,7 +51,7 @@ export class AutoModerationService implements IAutoModerationService { // if moderation failed, throw error if (hateSpeechResponseDto.class === "flag") { - if (hateSpeechResponseDto.confidence >= 0.9) { + if (hateSpeechResponseDto.confidence >= 0.99) { // TODO: create a ticket for the admin to review await user.addWasOffendingRecord( diff --git a/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts b/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts new file mode 100644 index 0000000..ead0733 --- /dev/null +++ b/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts @@ -0,0 +1,13 @@ +export interface IModeratorActionsService { + allowPost(postId: string): Promise; + restrictPost(postId: string): Promise; + + allowComment(commentId: string): Promise; + restrictComment(commentId: string): Promise; + + deletePost(postId: string): Promise; + undeletePost(postId: string): Promise; + + deleteComment(commentId: string): Promise; + undeleteComment(commentId: string): Promise; +} From a6522b4801424baea4b7dc1f9bd9d6129786c69b Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Mon, 21 Nov 2022 17:40:55 -0800 Subject: [PATCH 002/153] add the throttler packages' --- package-lock.json | 78 +++++++++++++++++++ package.json | 1 + src/users/models/toPost/upVotes.props.ts | 11 --- .../{downVotes.props.ts => vote.props.ts} | 0 4 files changed, 79 insertions(+), 11 deletions(-) delete mode 100644 src/users/models/toPost/upVotes.props.ts rename src/users/models/toPost/{downVotes.props.ts => vote.props.ts} (100%) diff --git a/package-lock.json b/package-lock.json index 6e887aa..ce2efb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@nestjs/passport": "^9.0.0", "@nestjs/platform-express": "^9.0.0", "@nestjs/swagger": "^6.1.2", + "@nestjs/throttler": "^3.1.0", "@prisma/client": "^4.4.0", "bcrypt": "^5.1.0", "cache-manager": "^4.0.0", @@ -1838,6 +1839,19 @@ } } }, + "node_modules/@nestjs/throttler": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-3.1.0.tgz", + "integrity": "sha512-u9a5+rci6ybYtJ2is6gZWxE2dMZEpnK0qJ0C1OnchuNCvM21Bg6qym1TB6Uihhci+JfTv6E15WuASLXcIclsbA==", + "dependencies": { + "md5": "^2.2.1" + }, + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0", + "@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0", + "reflect-metadata": "^0.1.13" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3319,6 +3333,14 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "engines": { + "node": "*" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -3658,6 +3680,14 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -5157,6 +5187,11 @@ "node": ">=8" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "node_modules/is-core-module": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", @@ -6502,6 +6537,16 @@ "tmpl": "1.0.5" } }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -10414,6 +10459,14 @@ "tslib": "2.4.0" } }, + "@nestjs/throttler": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-3.1.0.tgz", + "integrity": "sha512-u9a5+rci6ybYtJ2is6gZWxE2dMZEpnK0qJ0C1OnchuNCvM21Bg6qym1TB6Uihhci+JfTv6E15WuASLXcIclsbA==", + "requires": { + "md5": "^2.2.1" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -11594,6 +11647,11 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==" + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -11861,6 +11919,11 @@ "which": "^2.0.1" } }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==" + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -12976,6 +13039,11 @@ "binary-extensions": "^2.0.0" } }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "is-core-module": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", @@ -14009,6 +14077,16 @@ "tmpl": "1.0.5" } }, + "md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "requires": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", diff --git a/package.json b/package.json index 2bb10b1..ca2392d 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@nestjs/passport": "^9.0.0", "@nestjs/platform-express": "^9.0.0", "@nestjs/swagger": "^6.1.2", + "@nestjs/throttler": "^3.1.0", "@prisma/client": "^4.4.0", "bcrypt": "^5.1.0", "cache-manager": "^4.0.0", diff --git a/src/users/models/toPost/upVotes.props.ts b/src/users/models/toPost/upVotes.props.ts deleted file mode 100644 index e732b66..0000000 --- a/src/users/models/toPost/upVotes.props.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { RelationshipProps } from "../../../neo4j/neo4j.helper.types"; - -export class UpVotesProps implements RelationshipProps { - @ApiProperty({ type: Number }) - upVotedAt: number; - - constructor(partial?: Partial) { - Object.assign(this, partial); - } -} diff --git a/src/users/models/toPost/downVotes.props.ts b/src/users/models/toPost/vote.props.ts similarity index 100% rename from src/users/models/toPost/downVotes.props.ts rename to src/users/models/toPost/vote.props.ts From c2f73fe4b780abcf07783c96dc9efa7863fbede2 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Mon, 21 Nov 2022 17:41:12 -0800 Subject: [PATCH 003/153] fix the voting for post and its props - draft --- src/posts/controllers/posts.controller.ts | 9 +++- src/posts/dtos/votePostPayload.dto.ts | 5 ++ .../services/posts/posts.service.interface.ts | 4 +- src/posts/services/posts/posts.service.ts | 49 +++++++++++-------- src/users/models/toPost/index.ts | 3 +- src/users/models/toPost/vote.props.ts | 6 +-- 6 files changed, 48 insertions(+), 28 deletions(-) diff --git a/src/posts/controllers/posts.controller.ts b/src/posts/controllers/posts.controller.ts index c835777..b414b6d 100644 --- a/src/posts/controllers/posts.controller.ts +++ b/src/posts/controllers/posts.controller.ts @@ -18,7 +18,7 @@ import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; import { DatabaseContext } from "../../database-access-layer/databaseContext"; import { _$ } from "../../_domain/injectableTokens"; import { Post as PostModel } from "../models"; -import { PostCreationPayloadDto } from "../dtos"; +import { PostCreationPayloadDto, VotePostPayloadDto } from "../dtos"; import { IPostsService } from "../services/posts/posts.service.interface"; @ApiTags("posts") @@ -85,4 +85,11 @@ export class PostsController { const post = await this._postsService.authorNewPost(postPayload); return await post.toJSON(); } + + @Post("vote") + @UseGuards(AuthGuard("jwt")) + public async votePost(@Body() votePostPayload: VotePostPayloadDto): Promise { + await this._postsService.votePost(votePostPayload); + return; + } } diff --git a/src/posts/dtos/votePostPayload.dto.ts b/src/posts/dtos/votePostPayload.dto.ts index bdf90d1..7d7bb7f 100644 --- a/src/posts/dtos/votePostPayload.dto.ts +++ b/src/posts/dtos/votePostPayload.dto.ts @@ -1,4 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; +import { IsEnum, IsNotEmpty, IsUUID } from "class-validator"; export enum VoteType { UPVOTES = "UPVOTES", @@ -7,9 +8,13 @@ export enum VoteType { export class VotePostPayloadDto { @ApiProperty({ type: String, format: "uuid" }) + @IsNotEmpty() + @IsUUID() postId: string; @ApiProperty({ type: VoteType }) + @IsNotEmpty() + @IsEnum(VoteType) voteType: VoteType; constructor(partial?: Partial) { diff --git a/src/posts/services/posts/posts.service.interface.ts b/src/posts/services/posts/posts.service.interface.ts index 868ee28..d0fd9a2 100644 --- a/src/posts/services/posts/posts.service.interface.ts +++ b/src/posts/services/posts/posts.service.interface.ts @@ -1,4 +1,4 @@ -import { PostCreationPayloadDto } from "../../dtos"; +import { PostCreationPayloadDto, VotePostPayloadDto } from "../../dtos"; import { Post } from "../../models"; export type postSortCallback = (postA: Post, postB: Post) => number; @@ -15,4 +15,6 @@ export interface IPostsService { findPostById(postId: string): Promise; markAsDeleted(postId: string): Promise; + + votePost(votePostPayload: VotePostPayloadDto): Promise; } diff --git a/src/posts/services/posts/posts.service.ts b/src/posts/services/posts/posts.service.ts index 07de57a..5a6ee3a 100644 --- a/src/posts/services/posts/posts.service.ts +++ b/src/posts/services/posts/posts.service.ts @@ -8,7 +8,7 @@ import { DatabaseContext } from "../../../database-access-layer/databaseContext" import { DeletedProps } from "../../models/toSelf"; import { REQUEST } from "@nestjs/core"; import { Request } from "express"; -import { UserToPostRelTypes } from "../../../users/models/toPost"; +import { UserToPostRelTypes, VoteProps } from "../../../users/models/toPost"; import { IAutoModerationService } from "../../../moderation/services/autoModeration/autoModeration.service.interface"; @Injectable({ scope: Scope.REQUEST }) @@ -151,7 +151,7 @@ export class PostsService implements IPostsService { const queryResult = await this._dbContext.neo4jService.tryReadAsync( ` - MATCH (u:User { userId: $userId })-[r:${UserToPostRelTypes.UPVOTES}|${UserToPostRelTypes.DOWN_VOTES}]->(p:Post { postId: $postId }) + MATCH (u:User { userId: $userId })-[r:${UserToPostRelTypes.UPVOTES}|${UserToPostRelTypes.DOWN_VOTES}]->(p:Post { postId: $postId }) RETURN r `, { userId: user.userId, @@ -160,39 +160,46 @@ export class PostsService implements IPostsService { ); if (queryResult.records.length > 0) { + // user has already voted on this post const relType = queryResult.records[0].get("r").type; - if ( - relType === UserToPostRelTypes.UPVOTES && - votePostPayload.voteType === VoteType.UPVOTES - ) { - throw new HttpException("User already upvoted this post", 400); - } else if ( - relType === UserToPostRelTypes.DOWN_VOTES && - votePostPayload.voteType === VoteType.DOWN_VOTES - ) { - throw new HttpException("User already downvoted this post", 400); - } else { - await this._dbContext.neo4jService.tryWriteAsync( - ` + + // remove the existing vote + await this._dbContext.neo4jService.tryWriteAsync( + ` MATCH (u:User { userId: $userId })-[r:${relType}]->(p:Post { postId: $postId }) DELETE r `, - { - userId: user.userId, - postId: votePostPayload.postId, - } - ); + { + userId: user.userId, + postId: votePostPayload.postId, + } + ); + + // don't add a new vote if the user is removing their vote (stop) + if ( + (relType === UserToPostRelTypes.UPVOTES && + votePostPayload.voteType === VoteType.UPVOTES) || + (relType === UserToPostRelTypes.DOWN_VOTES && + votePostPayload.voteType === VoteType.DOWN_VOTES) + ) { + return; } } + // add the new vote + const voteProps = new VoteProps({ + votedAt: new Date().getTime(), + }); await this._dbContext.neo4jService.tryWriteAsync( ` MATCH (u:User { userId: $userId }), (p:Post { postId: $postId }) - MERGE (u)-[r:${votePostPayload.voteType}]->(p) + MERGE (u)-[r:${votePostPayload.voteType} { votedAt: $votedAt }]->(p) `, { userId: user.userId, postId: votePostPayload.postId, + + votedAt: voteProps.votedAt, } ); } diff --git a/src/users/models/toPost/index.ts b/src/users/models/toPost/index.ts index d3479bf..09f4dc1 100644 --- a/src/users/models/toPost/index.ts +++ b/src/users/models/toPost/index.ts @@ -1,7 +1,6 @@ export { AuthoredProps } from "./authored.props"; export { ReadProps } from "./read.props"; -export { UpVotesProps } from "./upVotes.props"; -export { DownVotesProps } from "./downVotes.props"; +export { VoteProps } from "./vote.props"; export { FavoritesProps } from "./favorites.props"; export { ReportedProps } from "./reported.props"; diff --git a/src/users/models/toPost/vote.props.ts b/src/users/models/toPost/vote.props.ts index 4b728a7..7b6f2d8 100644 --- a/src/users/models/toPost/vote.props.ts +++ b/src/users/models/toPost/vote.props.ts @@ -1,11 +1,11 @@ import { ApiProperty } from "@nestjs/swagger"; import { RelationshipProps } from "../../../neo4j/neo4j.helper.types"; -export class DownVotesProps implements RelationshipProps { +export class VoteProps implements RelationshipProps { @ApiProperty({ type: Number }) - downVotedAt: number; + votedAt: number; - constructor(partial?: Partial) { + constructor(partial?: Partial) { Object.assign(this, partial); } } From 0584e023add17912cbfaea3922ba51230dfe47ae Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Mon, 21 Nov 2022 18:11:51 -0800 Subject: [PATCH 004/153] add throttling feature basics --- src/app.module.ts | 7 ++++++- src/posts/dtos/votePostPayload.dto.ts | 2 +- src/posts/posts.module.ts | 6 ++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index 66995c6..915c8c0 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -10,10 +10,15 @@ import { PostsModule } from "./posts/posts.module"; import { UsersModule } from "./users/users.module"; import { AppLoggerMiddleware } from "./_domain/middlewares/appLogger.middleware"; import { neo4jCredentials } from "./_domain/constants"; -import { ModerationModule } from './moderation/moderation.module'; +import { ModerationModule } from "./moderation/moderation.module"; +import { ThrottlerModule } from "@nestjs/throttler"; @Module({ imports: [ + ThrottlerModule.forRoot({ + ttl: 60, + limit: 10, + }), Neo4jModule.forRootAsync({ imports: [ConfigModule], inject: [ConfigService], diff --git a/src/posts/dtos/votePostPayload.dto.ts b/src/posts/dtos/votePostPayload.dto.ts index 7d7bb7f..ae46485 100644 --- a/src/posts/dtos/votePostPayload.dto.ts +++ b/src/posts/dtos/votePostPayload.dto.ts @@ -12,7 +12,7 @@ export class VotePostPayloadDto { @IsUUID() postId: string; - @ApiProperty({ type: VoteType }) + @ApiProperty({ enum: VoteType }) @IsNotEmpty() @IsEnum(VoteType) voteType: VoteType; diff --git a/src/posts/posts.module.ts b/src/posts/posts.module.ts index 0a0714f..e7772e3 100644 --- a/src/posts/posts.module.ts +++ b/src/posts/posts.module.ts @@ -13,10 +13,16 @@ import { PostTypesRepository } from "./repositories/postType/postTypes.repositor import { PostAwardRepository } from "./repositories/postAward/postAward.repository"; import { PostTypesController } from "./controllers/postTypes.controller"; import { ModerationModule } from "../moderation/moderation.module"; +import { ThrottlerGuard } from "@nestjs/throttler"; +import { APP_GUARD } from "@nestjs/core"; @Module({ imports: [forwardRef(() => DatabaseAccessLayerModule), HttpModule, ModerationModule], providers: [ + { + provide: APP_GUARD, + useClass: ThrottlerGuard, + }, { provide: _$.IPostsRepository, useClass: PostsRepository, From 4e677440a27b76fc5e0c04dcc584315a8c8eb704 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Mon, 21 Nov 2022 18:15:05 -0800 Subject: [PATCH 005/153] move the provider from post to app module --- src/app.module.ts | 9 ++++++++- src/posts/posts.module.ts | 6 ------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index 915c8c0..48f2086 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -11,7 +11,8 @@ import { UsersModule } from "./users/users.module"; import { AppLoggerMiddleware } from "./_domain/middlewares/appLogger.middleware"; import { neo4jCredentials } from "./_domain/constants"; import { ModerationModule } from "./moderation/moderation.module"; -import { ThrottlerModule } from "@nestjs/throttler"; +import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler"; +import { APP_GUARD } from "@nestjs/core"; @Module({ imports: [ @@ -44,6 +45,12 @@ import { ThrottlerModule } from "@nestjs/throttler"; DatabaseAccessLayerModule, ModerationModule, ], + providers: [ + { + provide: APP_GUARD, + useClass: ThrottlerGuard, + }, + ], }) export class AppModule { private readonly _logger = new Logger(AppModule.name); diff --git a/src/posts/posts.module.ts b/src/posts/posts.module.ts index e7772e3..0a0714f 100644 --- a/src/posts/posts.module.ts +++ b/src/posts/posts.module.ts @@ -13,16 +13,10 @@ import { PostTypesRepository } from "./repositories/postType/postTypes.repositor import { PostAwardRepository } from "./repositories/postAward/postAward.repository"; import { PostTypesController } from "./controllers/postTypes.controller"; import { ModerationModule } from "../moderation/moderation.module"; -import { ThrottlerGuard } from "@nestjs/throttler"; -import { APP_GUARD } from "@nestjs/core"; @Module({ imports: [forwardRef(() => DatabaseAccessLayerModule), HttpModule, ModerationModule], providers: [ - { - provide: APP_GUARD, - useClass: ThrottlerGuard, - }, { provide: _$.IPostsRepository, useClass: PostsRepository, From aa4f0437f27ddd7da666361d3b1c978badd26471 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Mon, 21 Nov 2022 20:36:56 -0800 Subject: [PATCH 006/153] added nested comments functionality --- .../dtos/commentCreationPayload.dto.ts | 2 +- src/comments/models/comment.ts | 33 +++++++++++++++++++ .../comment/comments.repository.interface.ts | 2 -- .../comment/comments.repository.ts | 10 ------ src/posts/controllers/posts.controller.ts | 16 +++++++++ src/posts/models/post.ts | 28 ++++++++++++++++ .../services/posts/posts.service.interface.ts | 8 +++++ src/posts/services/posts/posts.service.ts | 28 ++++++++++++++++ 8 files changed, 114 insertions(+), 13 deletions(-) diff --git a/src/comments/dtos/commentCreationPayload.dto.ts b/src/comments/dtos/commentCreationPayload.dto.ts index 35a3c29..659a51d 100644 --- a/src/comments/dtos/commentCreationPayload.dto.ts +++ b/src/comments/dtos/commentCreationPayload.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsBoolean, isNotEmpty, IsNotEmpty, IsString } from "class-validator"; +import { IsBoolean, IsNotEmpty, IsString } from "class-validator"; export class CommentCreationPayloadDto { @ApiProperty({ type: String, minLength: 5, maxLength: 2500 }) diff --git a/src/comments/models/comment.ts b/src/comments/models/comment.ts index da9142e..cee6c1e 100644 --- a/src/comments/models/comment.ts +++ b/src/comments/models/comment.ts @@ -8,6 +8,7 @@ import { AuthoredProps, UserToCommentRelTypes } from "../../users/models/toComme import { RestrictedProps, _ToSelfRelTypes } from "../../_domain/models/toSelf"; import { CommentToSelfRelTypes, DeletedProps } from "./toSelf"; import { PublicUserDto } from "../../users/dtos"; +import neo4j from "neo4j-driver"; @Labels("Comment") export class Comment extends Model { @@ -79,6 +80,38 @@ export class Comment extends Model { return { ...this }; } + public async toJSONNested() { + if (!this.childComments) { + return this.toJSON(); + } + for (const i in this.childComments) { + this.childComments[i] = await this.childComments[i].toJSONNested(); + } + return this.toJSON(); + } + + public async getChildrenComments(limit = 0): Promise { + const queryResult = await this.neo4jService.tryReadAsync( + ` + MATCH (c:Comment)-[:${ + CommentToSelfRelTypes.REPLIED + }]->(p:Comment) WHERE p.commentId = $parentId + RETURN c + ${limit > 0 ? `LIMIT $limit` : ""} + `, + { + parentId: this.commentId, + ...(limit > 0 ? { limit: neo4j.int(limit) } : {}), + } + ); + const records = queryResult.records; + if (records.length === 0) return []; + this.childComments = records.map( + record => new Comment(record.get("c").properties, this.neo4jService) + ); + return this.childComments; + } + public async getTotalVotes(): Promise { const queryResult = await this.neo4jService.tryReadAsync( ` diff --git a/src/comments/repositories/comment/comments.repository.interface.ts b/src/comments/repositories/comment/comments.repository.interface.ts index 88347b0..3665416 100644 --- a/src/comments/repositories/comment/comments.repository.interface.ts +++ b/src/comments/repositories/comment/comments.repository.interface.ts @@ -6,8 +6,6 @@ export interface ICommentsRepository { findCommentById(commentId: string): Promise; - findChildrenComments(parentId: string): Promise; - addCommentToComment(comment: Comment): Promise; addCommentToPost(comment: Comment): Promise; diff --git a/src/comments/repositories/comment/comments.repository.ts b/src/comments/repositories/comment/comments.repository.ts index 05d2f1a..6f333d2 100644 --- a/src/comments/repositories/comment/comments.repository.ts +++ b/src/comments/repositories/comment/comments.repository.ts @@ -27,16 +27,6 @@ export class CommentsRepository implements ICommentsRepository { return new Comment(comment.records[0].get("c").properties, this._neo4jService); } - public async findChildrenComments(parentId: string): Promise { - const comments = await this._neo4jService.read( - `MATCH (c:Comment)-[:${CommentToSelfRelTypes.REPLIED}]->(p:Comment) WHERE p.commentId = $parentId RETURN c`, - { parentId: parentId } - ); - const records = comments.records; - if (records.length === 0) return []; - return records.map(record => new Comment(record.get("c").properties, this._neo4jService)); - } - public async addCommentToComment(comment: Comment): Promise { if (comment.commentId === undefined) { comment.commentId = this._neo4jService.generateId(); diff --git a/src/posts/controllers/posts.controller.ts b/src/posts/controllers/posts.controller.ts index b414b6d..2fb5c43 100644 --- a/src/posts/controllers/posts.controller.ts +++ b/src/posts/controllers/posts.controller.ts @@ -18,6 +18,7 @@ import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; import { DatabaseContext } from "../../database-access-layer/databaseContext"; import { _$ } from "../../_domain/injectableTokens"; import { Post as PostModel } from "../models"; +import { Comment } from "../../comments/models"; import { PostCreationPayloadDto, VotePostPayloadDto } from "../dtos"; import { IPostsService } from "../services/posts/posts.service.interface"; @@ -68,6 +69,21 @@ export class PostsController { return await Promise.all(decoratedStories); } + @Get(":postId/nestedComments") + @UseGuards(AuthGuard("jwt")) + public async getNestedCommentsByPostId( + @Param("postId", new ParseUUIDPipe()) postId: string + ): Promise { + const topLevelComments = await this._postsService.findNestedCommentsByPostId( + postId, + 10, + 2, + 2 + ); + const decoratedTopLevelComments = topLevelComments.map(comment => comment.toJSONNested()); + return await Promise.all(decoratedTopLevelComments); + } + @Get(":postId") public async getPostById( @Param("postId", new ParseUUIDPipe()) postId: string diff --git a/src/posts/models/post.ts b/src/posts/models/post.ts index 168f4a5..bf8469b 100644 --- a/src/posts/models/post.ts +++ b/src/posts/models/post.ts @@ -16,6 +16,9 @@ import { AuthoredProps, UserToPostRelTypes } from "../../users/models/toPost"; import { DeletedProps, PostToSelfRelTypes } from "./toSelf"; import { Exclude } from "class-transformer"; import { PublicUserDto } from "../../users/dtos"; +import { PostToCommentRelTypes } from "./toComment"; +import { Comment } from "../../comments/models"; +import neo4j from "neo4j-driver"; @Labels("Post") export class Post extends Model { @@ -59,6 +62,9 @@ export class Post extends Model { @ApiProperty({ type: Number }) totalVotes: number; + @ApiProperty({ type: Comment, isArray: true }) + comments: Comment[]; + @ApiProperty({ type: Boolean }) @NodeProperty() @Exclude() @@ -87,6 +93,28 @@ export class Post extends Model { return { ...this }; } + public async getComments(limit = 0): Promise { + const queryResult = await this.neo4jService.tryReadAsync( + ` + MATCH (p:Post {postId: $postId})-[:${PostToCommentRelTypes.HAS_COMMENT}]->(c:Comment) + RETURN c + ${limit != 0 ? `LIMIT $limit` : ""} + `, + { + postId: this.postId, + ...(limit != 0 ? { limit: neo4j.int(limit) } : {}), + } + ); + const records = queryResult.records; + if (records.length === 0) { + this.comments = []; + return this.comments; + } + const comments = records.map(r => new Comment(r.get("c").properties, this.neo4jService)); + this.comments = comments; + return comments; + } + public async getTotalVotes(): Promise { const queryResult = await this.neo4jService.tryReadAsync( ` diff --git a/src/posts/services/posts/posts.service.interface.ts b/src/posts/services/posts/posts.service.interface.ts index d0fd9a2..2ad98b5 100644 --- a/src/posts/services/posts/posts.service.interface.ts +++ b/src/posts/services/posts/posts.service.interface.ts @@ -1,5 +1,6 @@ import { PostCreationPayloadDto, VotePostPayloadDto } from "../../dtos"; import { Post } from "../../models"; +import { Comment } from "../../../comments/models"; export type postSortCallback = (postA: Post, postB: Post) => number; @@ -14,6 +15,13 @@ export interface IPostsService { findPostById(postId: string): Promise; + findNestedCommentsByPostId( + postId: string, + limit: number, + nestedLimit: number, + nestedLevel: number + ): Promise; + markAsDeleted(postId: string): Promise; votePost(votePostPayload: VotePostPayloadDto): Promise; diff --git a/src/posts/services/posts/posts.service.ts b/src/posts/services/posts/posts.service.ts index 5a6ee3a..3f4241b 100644 --- a/src/posts/services/posts/posts.service.ts +++ b/src/posts/services/posts/posts.service.ts @@ -10,6 +10,7 @@ import { REQUEST } from "@nestjs/core"; import { Request } from "express"; import { UserToPostRelTypes, VoteProps } from "../../../users/models/toPost"; import { IAutoModerationService } from "../../../moderation/services/autoModeration/autoModeration.service.interface"; +import { Comment } from "../../../comments/models"; @Injectable({ scope: Scope.REQUEST }) export class PostsService implements IPostsService { @@ -126,6 +127,33 @@ export class PostsService implements IPostsService { return await foundPost.toJSON(); } + public async findNestedCommentsByPostId( + postId: string, + topLevelLimit: number, + nestedLimit: number, + nestedLevel: number + ): Promise { + const foundPost = await this._dbContext.Posts.findPostById(postId); + if (foundPost === null) throw new HttpException("Post not found", 404); + + // level 0 means no nesting + const comments = await foundPost.getComments(topLevelLimit); + if (nestedLevel === 0) return comments; + + await this.getNestedComments(comments, nestedLevel, nestedLimit); + + return comments; + } + + private async getNestedComments(comments, nestedLevel, nestedLimit) { + if (nestedLevel === 0) return; + for (const i in comments) { + const comment: Comment = comments[i]; + await comment.getChildrenComments(nestedLimit); + await this.getNestedComments(comment.childComments, nestedLevel - 1, nestedLimit); + } + } + public async markAsDeleted(postId: string): Promise { const post = await this._dbContext.Posts.findPostById(postId); if (post === undefined) throw new Error("Post not found"); From a15b9cbcb34909bcf5d07fd04d79626f24369780 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Mon, 21 Nov 2022 20:46:29 -0800 Subject: [PATCH 007/153] added nested comments for a comment thread --- src/comments/comments.module.ts | 8 ++++++- .../controllers/comments.controller.ts | 14 +++++++++++ .../comments/comments.service.interface.ts | 7 ++++++ .../services/comments/comments.service.ts | 24 ++++++++++++++++++- .../services/posts/posts.service.interface.ts | 4 +++- src/posts/services/posts/posts.service.ts | 2 +- 6 files changed, 55 insertions(+), 4 deletions(-) diff --git a/src/comments/comments.module.ts b/src/comments/comments.module.ts index 2c594a2..304b2d7 100644 --- a/src/comments/comments.module.ts +++ b/src/comments/comments.module.ts @@ -6,10 +6,16 @@ import { DatabaseAccessLayerModule } from "../database-access-layer/database-acc import { forwardRef, Module } from "@nestjs/common"; import { HttpModule } from "@nestjs/axios"; import { ModerationModule } from "../moderation/moderation.module"; +import { PostsModule } from "../posts/posts.module"; @Module({ controllers: [CommentsController], - imports: [forwardRef(() => DatabaseAccessLayerModule), HttpModule, ModerationModule], + imports: [ + forwardRef(() => DatabaseAccessLayerModule), + HttpModule, + ModerationModule, + PostsModule, + ], providers: [ { provide: _$.ICommentsService, diff --git a/src/comments/controllers/comments.controller.ts b/src/comments/controllers/comments.controller.ts index a4bad23..bc75f6a 100644 --- a/src/comments/controllers/comments.controller.ts +++ b/src/comments/controllers/comments.controller.ts @@ -46,6 +46,20 @@ export class CommentsController { return await Promise.all(decoratedComments); } + @Get(":commentId/nestedComments") + public async getNestedComments( + @Param("commentId", new ParseUUIDPipe()) commentId: string + ): Promise { + const comments = await this._commentsService.findNestedCommentsByCommentId( + commentId, + 10, + 2, + 3 + ); + const decoratedComments = comments.map(comment => comment.toJSONNested()); + return await Promise.all(decoratedComments); + } + @Get(":commentId") public async getCommentById( @Param("commentId", new ParseUUIDPipe()) commentId: string diff --git a/src/comments/services/comments/comments.service.interface.ts b/src/comments/services/comments/comments.service.interface.ts index f1e2345..4ed4e11 100644 --- a/src/comments/services/comments/comments.service.interface.ts +++ b/src/comments/services/comments/comments.service.interface.ts @@ -6,6 +6,13 @@ export interface ICommentsService { findCommentById(commentId: string): Promise; + findNestedCommentsByCommentId( + commentId: string, + topLevelLimit: number, + nestedLimit: number, + nestedLevel: number + ): Promise; + voteComment(votePayload: VoteCommentPayloadDto): Promise; markAsPinned(commentId: string): Promise; diff --git a/src/comments/services/comments/comments.service.ts b/src/comments/services/comments/comments.service.ts index 88ff9a5..00115ff 100644 --- a/src/comments/services/comments/comments.service.ts +++ b/src/comments/services/comments/comments.service.ts @@ -13,22 +13,26 @@ import { Comment } from "../../models"; import { CommentToSelfRelTypes, DeletedProps } from "../../models/toSelf"; import { ICommentsService } from "./comments.service.interface"; import { IAutoModerationService } from "../../../moderation/services/autoModeration/autoModeration.service.interface"; +import { IPostsService } from "../../../posts/services/posts/posts.service.interface"; @Injectable({ scope: Scope.REQUEST }) export class CommentsService implements ICommentsService { private readonly _request: Request; private readonly _dbContext: DatabaseContext; private readonly _autoModerationService: IAutoModerationService; + private readonly _postService: IPostsService; constructor( @Inject(REQUEST) request: Request, @Inject(_$.IDatabaseContext) databaseContext: DatabaseContext, httpService: HttpService, - @Inject(_$.IAutoModerationService) autoModerationService: IAutoModerationService + @Inject(_$.IAutoModerationService) autoModerationService: IAutoModerationService, + @Inject(_$.IPostsService) postsService: IPostsService ) { this._request = request; this._dbContext = databaseContext; this._autoModerationService = autoModerationService; + this._postService = postsService; } public async authorNewComment(commentPayload: CommentCreationPayloadDto): Promise { @@ -86,6 +90,24 @@ export class CommentsService implements ICommentsService { return await foundComment.toJSON(); } + public async findNestedCommentsByCommentId( + commentId: string, + topLevelLimit: number, + nestedLimit: number, + nestedLevel: number + ): Promise { + const foundComment = await this._dbContext.Comments.findCommentById(commentId); + if (foundComment === null) throw new HttpException("Comment not found", 404); + + // level 0 means no nesting + const comments = await foundComment.getChildrenComments(topLevelLimit); + if (nestedLevel === 0) return comments; + + await this._postService.getNestedComments(comments, nestedLevel, nestedLimit); + + return comments; + } + public async voteComment(voteCommentPayload: VoteCommentPayloadDto): Promise { const user = this.getUserFromRequest(); diff --git a/src/posts/services/posts/posts.service.interface.ts b/src/posts/services/posts/posts.service.interface.ts index 2ad98b5..a456a06 100644 --- a/src/posts/services/posts/posts.service.interface.ts +++ b/src/posts/services/posts/posts.service.interface.ts @@ -17,11 +17,13 @@ export interface IPostsService { findNestedCommentsByPostId( postId: string, - limit: number, + topLevelLimit: number, nestedLimit: number, nestedLevel: number ): Promise; + getNestedComments(comments, nestedLevel, nestedLimit): Promise; + markAsDeleted(postId: string): Promise; votePost(votePostPayload: VotePostPayloadDto): Promise; diff --git a/src/posts/services/posts/posts.service.ts b/src/posts/services/posts/posts.service.ts index 3f4241b..e6cc263 100644 --- a/src/posts/services/posts/posts.service.ts +++ b/src/posts/services/posts/posts.service.ts @@ -145,7 +145,7 @@ export class PostsService implements IPostsService { return comments; } - private async getNestedComments(comments, nestedLevel, nestedLimit) { + public async getNestedComments(comments, nestedLevel, nestedLimit): Promise { if (nestedLevel === 0) return; for (const i in comments) { const comment: Comment = comments[i]; From 8d0d85c9ec4a23367fff43e42c1e2b735883d066 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Mon, 21 Nov 2022 21:51:55 -0800 Subject: [PATCH 008/153] remove the auth jwt decorator --- src/posts/controllers/posts.controller.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/posts/controllers/posts.controller.ts b/src/posts/controllers/posts.controller.ts index 2fb5c43..6c0b205 100644 --- a/src/posts/controllers/posts.controller.ts +++ b/src/posts/controllers/posts.controller.ts @@ -70,7 +70,6 @@ export class PostsController { } @Get(":postId/nestedComments") - @UseGuards(AuthGuard("jwt")) public async getNestedCommentsByPostId( @Param("postId", new ParseUUIDPipe()) postId: string ): Promise { From 92f12fe09f05d08f8a896015f08f528e24332abf Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Tue, 22 Nov 2022 20:07:25 -0800 Subject: [PATCH 009/153] Adding type definitions --- src/posts/services/posts/posts.service.interface.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/posts/services/posts/posts.service.interface.ts b/src/posts/services/posts/posts.service.interface.ts index a456a06..da9f21d 100644 --- a/src/posts/services/posts/posts.service.interface.ts +++ b/src/posts/services/posts/posts.service.interface.ts @@ -1,6 +1,6 @@ +import { Comment } from "../../../comments/models"; import { PostCreationPayloadDto, VotePostPayloadDto } from "../../dtos"; import { Post } from "../../models"; -import { Comment } from "../../../comments/models"; export type postSortCallback = (postA: Post, postB: Post) => number; @@ -22,7 +22,7 @@ export interface IPostsService { nestedLevel: number ): Promise; - getNestedComments(comments, nestedLevel, nestedLimit): Promise; + getNestedComments(comments: Comment[], nestedLevel: number, nestedLimit: number): Promise; markAsDeleted(postId: string): Promise; From 4f0e25b3f1555fc5bd2fcf4a207f5dec82419ef7 Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Tue, 22 Nov 2022 20:08:34 -0800 Subject: [PATCH 010/153] adding type definitions --- src/posts/services/posts/posts.service.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/posts/services/posts/posts.service.ts b/src/posts/services/posts/posts.service.ts index e6cc263..3976fe1 100644 --- a/src/posts/services/posts/posts.service.ts +++ b/src/posts/services/posts/posts.service.ts @@ -1,16 +1,16 @@ import { HttpException, Inject, Injectable, Scope } from "@nestjs/common"; -import { PostCreationPayloadDto, VotePostPayloadDto, VoteType } from "../../dtos"; -import { User } from "../../../users/models"; -import { Post, PostTag } from "../../models"; -import { IPostsService, postSortCallback } from "./posts.service.interface"; -import { _$ } from "../../../_domain/injectableTokens"; -import { DatabaseContext } from "../../../database-access-layer/databaseContext"; -import { DeletedProps } from "../../models/toSelf"; import { REQUEST } from "@nestjs/core"; import { Request } from "express"; -import { UserToPostRelTypes, VoteProps } from "../../../users/models/toPost"; -import { IAutoModerationService } from "../../../moderation/services/autoModeration/autoModeration.service.interface"; import { Comment } from "../../../comments/models"; +import { DatabaseContext } from "../../../database-access-layer/databaseContext"; +import { IAutoModerationService } from "../../../moderation/services/autoModeration/autoModeration.service.interface"; +import { User } from "../../../users/models"; +import { UserToPostRelTypes, VoteProps } from "../../../users/models/toPost"; +import { _$ } from "../../../_domain/injectableTokens"; +import { PostCreationPayloadDto, VotePostPayloadDto, VoteType } from "../../dtos"; +import { Post, PostTag } from "../../models"; +import { DeletedProps } from "../../models/toSelf"; +import { IPostsService, postSortCallback } from "./posts.service.interface"; @Injectable({ scope: Scope.REQUEST }) export class PostsService implements IPostsService { @@ -145,7 +145,7 @@ export class PostsService implements IPostsService { return comments; } - public async getNestedComments(comments, nestedLevel, nestedLimit): Promise { + public async getNestedComments(comments: Comment[], nestedLevel: number, nestedLimit: number): Promise { if (nestedLevel === 0) return; for (const i in comments) { const comment: Comment = comments[i]; From b7406604241e07f68bdf015672585358c02740bc Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Tue, 22 Nov 2022 21:47:18 -0800 Subject: [PATCH 011/153] revise the seeding functionality so that they have total comments --- src/_domain/models/enums/index.ts | 1 + src/_domain/models/enums/voteType.enum.ts | 4 ++++ src/auth/guards/optionalJwtAuth.guard.ts | 8 ++++++++ src/neo4j/services/neo4j.seed.service.ts | 21 +++++++++++++++++---- 4 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 src/_domain/models/enums/index.ts create mode 100644 src/_domain/models/enums/voteType.enum.ts create mode 100644 src/auth/guards/optionalJwtAuth.guard.ts diff --git a/src/_domain/models/enums/index.ts b/src/_domain/models/enums/index.ts new file mode 100644 index 0000000..f7bd450 --- /dev/null +++ b/src/_domain/models/enums/index.ts @@ -0,0 +1 @@ +export { VoteType } from "./voteType.enum"; diff --git a/src/_domain/models/enums/voteType.enum.ts b/src/_domain/models/enums/voteType.enum.ts new file mode 100644 index 0000000..4134f29 --- /dev/null +++ b/src/_domain/models/enums/voteType.enum.ts @@ -0,0 +1,4 @@ +export enum VoteType { + UPVOTES = "UPVOTES", + DOWN_VOTES = "DOWN_VOTES", +} diff --git a/src/auth/guards/optionalJwtAuth.guard.ts b/src/auth/guards/optionalJwtAuth.guard.ts new file mode 100644 index 0000000..6caa130 --- /dev/null +++ b/src/auth/guards/optionalJwtAuth.guard.ts @@ -0,0 +1,8 @@ +import { AuthGuard } from "@nestjs/passport"; + +export class OptionalJwtAuthGuard extends AuthGuard("jwt") { + // Override handleRequest so it never throws an error + handleRequest(err, user) { + return user; + } +} diff --git a/src/neo4j/services/neo4j.seed.service.ts b/src/neo4j/services/neo4j.seed.service.ts index 0e96b97..35df549 100644 --- a/src/neo4j/services/neo4j.seed.service.ts +++ b/src/neo4j/services/neo4j.seed.service.ts @@ -244,7 +244,8 @@ export class Neo4jSeedService { updatedAt: $updatedAt, postTitle: $postTitle, postContent: $postContent, - pending: $pending + pending: $pending, + totalComments: $totalComments })${restrictedQueryString}<-[authoredRelationship:${UserToPostRelTypes.AUTHORED} { authoredAt: $authoredProps_authoredAt, anonymously: $authoredProps_anonymously @@ -292,6 +293,7 @@ export class Neo4jSeedService { postTitle: postEntity.postTitle, postContent: postEntity.postContent, pending: postEntity.pending, + totalComments: postEntity.totalComments, // PostType postTypeName: postEntity.postType.postTypeName.trim().toLowerCase(), @@ -326,12 +328,14 @@ export class Neo4jSeedService { anonymously: false, }); await this._neo4jService.tryWriteAsync( - `MATCH (authorUser:${this.userLabel}) WHERE authorUser.userId = $authorUserId + ` + MATCH (authorUser:${this.userLabel}) WHERE authorUser.userId = $authorUserId CREATE (comment:${this.commentLabel} { commentId: $commentId, commentContent: $commentContent, updatedAt: $updatedAt, - pending: $pending + pending: $pending, + totalComments: $totalComments })${restrictedQueryString}<-[authoredRelationship:${UserToPostRelTypes.AUTHORED} { authoredAt: $authoredProps_authoredAt, anonymously: $authoredProps_anonymously @@ -346,6 +350,7 @@ export class Neo4jSeedService { commentContent: commentEntity.commentContent, updatedAt: commentEntity.updatedAt, pending: commentEntity.pending, + totalComments: commentEntity.totalComments, // AuthoredProps authoredProps_authoredAt: authoredProps.authoredAt, @@ -360,6 +365,7 @@ export class Neo4jSeedService { await this._neo4jService.tryWriteAsync( `MATCH (comment:${this.commentLabel} { commentId: $commentId }) MATCH (parent) WHERE (parent:${this.postLabel} AND parent.postId = $parentId) OR (parent:${this.commentLabel} AND parent.commentId = $parentId) + SET parent.totalComments = parent.totalComments + 1 FOREACH (i in CASE WHEN parent:${this.postLabel} THEN [1] ELSE [] END | MERGE (comment)<-[:${PostToCommentRelTypes.HAS_COMMENT}]-(parent)) FOREACH (i in CASE WHEN parent:${this.commentLabel} THEN [1] ELSE [] END | @@ -373,7 +379,8 @@ export class Neo4jSeedService { if (commentEntity.pinned) { await this._neo4jService.tryWriteAsync( - `MATCH (comment:${this.commentLabel} { commentId: $commentId }) + ` + MATCH (comment:${this.commentLabel} { commentId: $commentId }) MATCH (parent) WHERE parent:${this.postLabel} AND parent.postId = $parentId MERGE (comment)-[:${PostToCommentRelTypes.PINNED_COMMENT}]->(parent) `, @@ -545,6 +552,7 @@ export class Neo4jSeedService { }), pending: false, totalVotes: 1, + totalComments: 0, awards: { [PostToAwardRelTypes.HAS_AWARD]: { records: (await this.getAwards()).slice(0, 2).map(award => ({ @@ -574,6 +582,7 @@ export class Neo4jSeedService { }), pending: true, totalVotes: 3, + totalComments: 0, awards: { [PostToAwardRelTypes.HAS_AWARD]: { records: (await this.getAwards()).slice(0, 2).map(award => ({ @@ -601,6 +610,7 @@ export class Neo4jSeedService { userId: "5c0f145b-ffad-4881-8ee6-7647c3c1b695", }), pinned: true, + totalComments: 0, pending: false, restrictedProps: null, childComments: [], @@ -615,6 +625,7 @@ export class Neo4jSeedService { userId: "5c0f145b-ffad-4881-8ee6-7647c3c1b695", }), pinned: false, + totalComments: 0, pending: true, restrictedProps: new RestrictedProps({ restrictedAt: 1665780000, @@ -632,6 +643,7 @@ export class Neo4jSeedService { userId: "3109f9e2-a262-4aef-b648-90d86d6fbf6c", }), pinned: false, + totalComments: 0, pending: true, restrictedProps: new RestrictedProps({ restrictedAt: 1665780000, @@ -650,6 +662,7 @@ export class Neo4jSeedService { userId: "3109f9e2-a262-4aef-b648-90d86d6fbf6c", }), pinned: false, + totalComments: 0, pending: false, restrictedProps: null, childComments: [], From 35862711923aeb51fae2a1919b71c91de81e3c4a Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Tue, 22 Nov 2022 23:33:42 -0800 Subject: [PATCH 012/153] COMMENT PARENT COMMENT PARENT COMMENT PARENT --- .../controllers/comments.controller.ts | 11 +++- .../services/comments/comments.service.ts | 60 ++++++++++++++----- 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/src/comments/controllers/comments.controller.ts b/src/comments/controllers/comments.controller.ts index bc75f6a..60c673c 100644 --- a/src/comments/controllers/comments.controller.ts +++ b/src/comments/controllers/comments.controller.ts @@ -11,14 +11,14 @@ import { ParseUUIDPipe, Post, UseGuards, - UseInterceptors, + UseInterceptors } from "@nestjs/common"; import { AuthGuard } from "@nestjs/passport"; import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; import { DatabaseContext } from "../../database-access-layer/databaseContext"; import { _$ } from "../../_domain/injectableTokens"; -import { Comment as CommentModel } from "../models"; import { CommentCreationPayloadDto } from "../dtos"; +import { Comment as CommentModel } from "../models"; import { ICommentsService } from "../services/comments/comments.service.interface"; @ApiTags("comments") @@ -69,6 +69,13 @@ export class CommentsController { return await comment.toJSON(); } + // pin comment + @Post(":commentId/pin") + @UseGuards(AuthGuard("jwt")) + public async pinComment(@Param("commentId", new ParseUUIDPipe()) commentId: string): Promise { + await this._commentsService.markAsPinned(commentId); + } + @Post("create") @UseGuards(AuthGuard("jwt")) public async createComment( diff --git a/src/comments/services/comments/comments.service.ts b/src/comments/services/comments/comments.service.ts index 00115ff..2cd07d1 100644 --- a/src/comments/services/comments/comments.service.ts +++ b/src/comments/services/comments/comments.service.ts @@ -3,8 +3,10 @@ import { HttpException, Inject, Injectable, Scope } from "@nestjs/common"; import { REQUEST } from "@nestjs/core"; import { Request } from "express"; import { DatabaseContext } from "../../../database-access-layer/databaseContext"; +import { IAutoModerationService } from "../../../moderation/services/autoModeration/autoModeration.service.interface"; import { Post } from "../../../posts/models"; import { PostToCommentRelTypes } from "../../../posts/models/toComment"; +import { IPostsService } from "../../../posts/services/posts/posts.service.interface"; import { User } from "../../../users/models"; import { UserToCommentRelTypes } from "../../../users/models/toComment"; import { _$ } from "../../../_domain/injectableTokens"; @@ -12,8 +14,6 @@ import { CommentCreationPayloadDto, VoteCommentPayloadDto, VoteType } from "../. import { Comment } from "../../models"; import { CommentToSelfRelTypes, DeletedProps } from "../../models/toSelf"; import { ICommentsService } from "./comments.service.interface"; -import { IAutoModerationService } from "../../../moderation/services/autoModeration/autoModeration.service.interface"; -import { IPostsService } from "../../../posts/services/posts/posts.service.interface"; @Injectable({ scope: Scope.REQUEST }) export class CommentsService implements ICommentsService { @@ -157,21 +157,32 @@ export class CommentsService implements ICommentsService { const comment = await this._dbContext.Comments.findCommentById(commentId); if (!comment) throw new HttpException("Comment not found", 404); - const user = this.getUserFromRequest(); const [parentPost] = await this.findParentCommentRoot(commentId); + const user = this.getUserFromRequest(); + const isPinned = await this.checkIfAnyCommentIsPinned(parentPost); + + console.log(parentPost) - if (parentPost.authorUser.userId !== user.userId) { + const postAuthor = await parentPost.getAuthorUser(); + + if (postAuthor.userId !== user.userId) { throw new HttpException("User is not the author of the post", 403); } + if (isPinned === true) { + throw new HttpException("Post already has a pinned comment", 400); + } + await this._dbContext.neo4jService.tryWriteAsync( ` - MATCH (c:Comment { commentId: $commentId }) - SET c.pinned = true - `, + MATCH (p:Post { postId: $postId }), (c:Comment { commentId: $commentId }) + CREATE (p)-[:${PostToCommentRelTypes.PINNED_COMMENT}]->(c) + ` + , { - commentId, + postId: parentPost.postId, + commentId: commentId, } ); } @@ -212,7 +223,7 @@ export class CommentsService implements ICommentsService { if (parentPost.records.length === 0) { throw new HttpException("Post not found", 404); } - return parentPost.records[0].get("p"); + return new Post(parentPost.records[0].get("p").properties, this._dbContext.neo4jService); } // gets the parent comment of any nested comment of the post @@ -226,7 +237,10 @@ export class CommentsService implements ICommentsService { commentId, } ); - return queryResult.records[0].get("commentParent"); + if (queryResult.records.length > 0) { + return true; + } + return false; } // gets the root comment of any nested comment @@ -236,24 +250,42 @@ export class CommentsService implements ICommentsService { ): Promise<[Post, boolean]> { const queryResult = await this._dbContext.neo4jService.tryReadAsync( ` - MATCH (c:Comment { commentId: $commentId })-[:${CommentToSelfRelTypes.REPLIED}]->(c:Comment) - RETURN c - `, + MATCH (c:Comment { commentId: $commentId })-[:${CommentToSelfRelTypes.REPLIED}]->(commentParent:Comment) + RETURN commentParent + `, { commentId, } ); if (queryResult.records.length > 0) { + console.log(commentId) return await this.findParentCommentRoot( - queryResult.records[0].get("c").properties.commentId, + queryResult.records[0].get("commentParent").properties.commentId, true ); } else { + console.log(commentId) const rootComment = await this._dbContext.Comments.findCommentById(commentId); return [await this.findParentPost(rootComment.commentId), isNestedComment]; } } + private async checkIfAnyCommentIsPinned(post: Post): Promise { + const queryResult = await this._dbContext.neo4jService.tryReadAsync( + ` + MATCH (p:Post { postId: $postId })-[:${PostToCommentRelTypes.PINNED_COMMENT}]->(c:Comment) + RETURN c + `, + { + postId: post.postId, + } + ); + if (queryResult.records.length > 0) { + return true; + } + return false; + } + private getUserFromRequest(): User { const user = this._request.user as User; if (user === undefined) throw new Error("User not found"); From 593d67413ced9feb82abf89c2802e6ff60e0f8e8 Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Tue, 22 Nov 2022 23:45:03 -0800 Subject: [PATCH 013/153] code cleanup --- src/comments/services/comments/comments.service.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/comments/services/comments/comments.service.ts b/src/comments/services/comments/comments.service.ts index 2cd07d1..6d8571f 100644 --- a/src/comments/services/comments/comments.service.ts +++ b/src/comments/services/comments/comments.service.ts @@ -162,8 +162,6 @@ export class CommentsService implements ICommentsService { const user = this.getUserFromRequest(); const isPinned = await this.checkIfAnyCommentIsPinned(parentPost); - console.log(parentPost) - const postAuthor = await parentPost.getAuthorUser(); if (postAuthor.userId !== user.userId) { @@ -258,13 +256,11 @@ export class CommentsService implements ICommentsService { } ); if (queryResult.records.length > 0) { - console.log(commentId) return await this.findParentCommentRoot( queryResult.records[0].get("commentParent").properties.commentId, true ); } else { - console.log(commentId) const rootComment = await this._dbContext.Comments.findCommentById(commentId); return [await this.findParentPost(rootComment.commentId), isNestedComment]; } From c70867972f319efdbb4819a6d940fc7b0939a2e6 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Wed, 23 Nov 2022 01:01:03 -0800 Subject: [PATCH 014/153] =?UTF-8?q?=F0=9F=92=ADadd=20nested=20comments=20c?= =?UTF-8?q?ounting=20-=20scuffed=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/posts/posts.service.interface.ts | 4 +- src/posts/services/posts/posts.service.ts | 59 +++++++++++++++---- 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/src/posts/services/posts/posts.service.interface.ts b/src/posts/services/posts/posts.service.interface.ts index a456a06..eaa940d 100644 --- a/src/posts/services/posts/posts.service.interface.ts +++ b/src/posts/services/posts/posts.service.interface.ts @@ -9,9 +9,9 @@ export interface IPostsService { getQueeryOfTheDay(): Promise; - findAllQueeries(sorted: null | postSortCallback): Promise; + findAllQueeries(): Promise; - findAllStories(sorted: null | postSortCallback): Promise; + findAllStories(): Promise; findPostById(postId: string): Promise; diff --git a/src/posts/services/posts/posts.service.ts b/src/posts/services/posts/posts.service.ts index e6cc263..daf29ee 100644 --- a/src/posts/services/posts/posts.service.ts +++ b/src/posts/services/posts/posts.service.ts @@ -1,5 +1,5 @@ import { HttpException, Inject, Injectable, Scope } from "@nestjs/common"; -import { PostCreationPayloadDto, VotePostPayloadDto, VoteType } from "../../dtos"; +import { PostCreationPayloadDto, VotePostPayloadDto } from "../../dtos"; import { User } from "../../../users/models"; import { Post, PostTag } from "../../models"; import { IPostsService, postSortCallback } from "./posts.service.interface"; @@ -11,6 +11,7 @@ import { Request } from "express"; import { UserToPostRelTypes, VoteProps } from "../../../users/models/toPost"; import { IAutoModerationService } from "../../../moderation/services/autoModeration/autoModeration.service.interface"; import { Comment } from "../../../comments/models"; +import { VoteType } from "../../../_domain/models/enums"; @Injectable({ scope: Scope.REQUEST }) export class PostsService implements IPostsService { @@ -95,20 +96,40 @@ export class PostsService implements IPostsService { return queeryPosts[queeryOfTheDayIndex]; } - public async findAllQueeries(sorted: null | postSortCallback): Promise { + public async findAllQueeries(): Promise { const queeries = await this._dbContext.Posts.findPostByPostType("queery"); - if (sorted !== null) { - queeries.sort(sorted); - } - return queeries; + return this.decoratePosts(queeries, (postA, postB) => postB.createdAt - postA.createdAt); } - public async findAllStories(sorted: null | postSortCallback): Promise { + public async findAllStories(): Promise { const stories = await this._dbContext.Posts.findPostByPostType("story"); + return this.decoratePosts(stories, (postA, postB) => postB.createdAt - postA.createdAt); + } + + private async decoratePosts(posts: Post[], sorted: null | postSortCallback): Promise { + posts = await Promise.all( + posts.map(async post => { + post.totalComments = await this.getTotalComments(post); + return post; + }) + ); if (sorted !== null) { - stories.sort(sorted); + posts.sort(sorted); + } + return posts; + } + + private async getTotalComments(post: Post, comments = null, result = 0): Promise { + if (comments === null) { + comments = await this.findNestedCommentsByPostId(post.postId, 0, 0, Infinity); + } + + if (comments.length === 0) return result; + result += comments.length; + for (const comment of comments) { + result = await this.getTotalComments(post, comment.childComments ?? [], result); } - return stories; + return result; } public async findPostById(postId: string): Promise { @@ -145,11 +166,28 @@ export class PostsService implements IPostsService { return comments; } + /** + * Recursively gets the total number of every comment's child comments that are **available**. + * @param comment + * @param result + * @private + */ + private async getTotalCommentsByComment(comment: Comment, result = 0): Promise { + if (!comment.childComments || comment.childComments.length === 0) return result; + result += comment.childComments.length; + for (const childComment of comment.childComments) { + result = await this.getTotalCommentsByComment(childComment, result); + childComment.totalComments = await this.getTotalCommentsByComment(childComment, 0); + } + return result; + } + public async getNestedComments(comments, nestedLevel, nestedLimit): Promise { if (nestedLevel === 0) return; for (const i in comments) { const comment: Comment = comments[i]; await comment.getChildrenComments(nestedLimit); + // comment.totalComments = await this.getTotalCommentsByComment(comment); await this.getNestedComments(comment.childComments, nestedLevel - 1, nestedLimit); } } @@ -179,7 +217,8 @@ export class PostsService implements IPostsService { const queryResult = await this._dbContext.neo4jService.tryReadAsync( ` - MATCH (u:User { userId: $userId })-[r:${UserToPostRelTypes.UPVOTES}|${UserToPostRelTypes.DOWN_VOTES}]->(p:Post { postId: $postId }) RETURN r + MATCH (u:User { userId: $userId })-[r:${UserToPostRelTypes.UPVOTES}|${UserToPostRelTypes.DOWN_VOTES}]->(p:Post { postId: $postId }) + RETURN r `, { userId: user.userId, From 22af89b824b1134e6442bad45b0909b7808188f9 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Wed, 23 Nov 2022 01:09:38 -0800 Subject: [PATCH 015/153] :sparkles: add util function for intiger problem in neo4j --- src/neo4j/neo4j.utils.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/neo4j/neo4j.utils.ts b/src/neo4j/neo4j.utils.ts index cb97f1e..818ca7c 100644 --- a/src/neo4j/neo4j.utils.ts +++ b/src/neo4j/neo4j.utils.ts @@ -20,3 +20,10 @@ export const createDriver = async (config: Neo4jConfig) => { // If everything is OK, return the driver return driver; }; + +export const fixNeo4jIntegers = (obj: any, propertyNames: string[]): any => { + for (const propertyName of propertyNames) { + obj[propertyName] = obj[propertyName]?.low ?? obj[propertyName]; + } + return obj; +}; From 6e407cc28eb3d1d86228a3efa1482ff4f0945943 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Wed, 23 Nov 2022 01:12:57 -0800 Subject: [PATCH 016/153] :hammer: fix the seed functionality --- src/neo4j/services/neo4j.seed.service.ts | 49 ++++++++++++++---------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/src/neo4j/services/neo4j.seed.service.ts b/src/neo4j/services/neo4j.seed.service.ts index 35df549..379db10 100644 --- a/src/neo4j/services/neo4j.seed.service.ts +++ b/src/neo4j/services/neo4j.seed.service.ts @@ -244,8 +244,7 @@ export class Neo4jSeedService { updatedAt: $updatedAt, postTitle: $postTitle, postContent: $postContent, - pending: $pending, - totalComments: $totalComments + pending: $pending })${restrictedQueryString}<-[authoredRelationship:${UserToPostRelTypes.AUTHORED} { authoredAt: $authoredProps_authoredAt, anonymously: $authoredProps_anonymously @@ -293,7 +292,6 @@ export class Neo4jSeedService { postTitle: postEntity.postTitle, postContent: postEntity.postContent, pending: postEntity.pending, - totalComments: postEntity.totalComments, // PostType postTypeName: postEntity.postType.postTypeName.trim().toLowerCase(), @@ -334,8 +332,7 @@ export class Neo4jSeedService { commentId: $commentId, commentContent: $commentContent, updatedAt: $updatedAt, - pending: $pending, - totalComments: $totalComments + pending: $pending })${restrictedQueryString}<-[authoredRelationship:${UserToPostRelTypes.AUTHORED} { authoredAt: $authoredProps_authoredAt, anonymously: $authoredProps_anonymously @@ -350,7 +347,6 @@ export class Neo4jSeedService { commentContent: commentEntity.commentContent, updatedAt: commentEntity.updatedAt, pending: commentEntity.pending, - totalComments: commentEntity.totalComments, // AuthoredProps authoredProps_authoredAt: authoredProps.authoredAt, @@ -361,11 +357,12 @@ export class Neo4jSeedService { } ); + // Connect Comment to its parent (either a Post or another Comment) if (commentEntity.parentId !== null) { await this._neo4jService.tryWriteAsync( - `MATCH (comment:${this.commentLabel} { commentId: $commentId }) + ` + MATCH (comment:${this.commentLabel} { commentId: $commentId }) MATCH (parent) WHERE (parent:${this.postLabel} AND parent.postId = $parentId) OR (parent:${this.commentLabel} AND parent.commentId = $parentId) - SET parent.totalComments = parent.totalComments + 1 FOREACH (i in CASE WHEN parent:${this.postLabel} THEN [1] ELSE [] END | MERGE (comment)<-[:${PostToCommentRelTypes.HAS_COMMENT}]-(parent)) FOREACH (i in CASE WHEN parent:${this.commentLabel} THEN [1] ELSE [] END | @@ -541,10 +538,13 @@ export class Neo4jSeedService { return new Array( new Post({ postId: "b73edbf4-ba84-4b11-a91c-e1d8b1366974", - postTitle: "Am I Lesbian?", - postContent: "today I kissed a girl! it felt so good.", + postTitle: "sister caught me checking out a guy on a camping trip", + postContent: + "I was on a camping trip and my sister caught me staring at someone across the site with his \n" + + " shirt off, for the the rest of the day she wouldn't stop asking me, even getting the other members \n" + + " who came with us to join in, I eventually gave in, she was super kind about it and came out as Bisexual the following months", updatedAt: 1665770000, - postType: (await this.getPostTypes())[0], + postType: (await this.getPostTypes())[1], postTags: (await this.getPostTags()).slice(0, 2), restrictedProps: null, authorUser: new User({ @@ -552,7 +552,6 @@ export class Neo4jSeedService { }), pending: false, totalVotes: 1, - totalComments: 0, awards: { [PostToAwardRelTypes.HAS_AWARD]: { records: (await this.getAwards()).slice(0, 2).map(award => ({ @@ -567,10 +566,25 @@ export class Neo4jSeedService { }), new Post({ postId: "596632ac-dd54-4700-a783-688618d99fa9", - postTitle: "Am I Gay?", - postContent: "today I kissed a boy! it felt so good.", + postTitle: "Coming out to my accepting family", + postContent: + "Growing up my family was really gay friendly. I had two gay uncles and everyone was accepting of them. \n " + + "When I was in fifth grade my mom told me that she would love me no matter who I loved. At the time I had \n " + + "no idea what she meant because all I cared about was playing soccer. However, as I got older I started \n " + + "to comprehend what my mother's words really meant. In middle school all my friends talked about boys and \n " + + "tried getting boyfriends, but I wasn't interested. That's when I started to realize that I was different from all my friends. \n\n " + + "During my first year of high school I realized I was a lesbian and it felt good to know that my mother was there to support me. \n " + + "At the end of my first year of high school I began dating my best friend, but that relationship only lasted three months before my \n " + + "girlfriend started cheating on me with my cousin. I hadn't openly come out to my family yet so I tried to keep the relationship and \n " + + "breakup a secret. One day one of my sisters saw me crying and she asked me what was wrong. I told her everything that had been going \n " + + "on between my cousin, my ex-girlfriend, and me. Instead of addressing the fact that I had dated a girl, my sister was mad about what \n " + + "my cousin had done. My sister told everyone in my family what had happened and everyone was upset about what my cousin had done. \n " + + "No one in my family was upset about the fact that my cousin and I were gay. \n\n " + + "Today my entire family knows that I am gay and they accept me. It is nice to have such an accepting family and I know that I am \n " + + "very fortunate to have a family that loves me unconditionally. I am grateful that my family has never judged me or made me feel \n " + + "uncomfortable expressing who I am.", updatedAt: 1665770000, - postType: (await this.getPostTypes())[0], + postType: (await this.getPostTypes())[1], postTags: (await this.getPostTags()).slice(0, 2), restrictedProps: new RestrictedProps({ restrictedAt: 1665780000, @@ -582,7 +596,6 @@ export class Neo4jSeedService { }), pending: true, totalVotes: 3, - totalComments: 0, awards: { [PostToAwardRelTypes.HAS_AWARD]: { records: (await this.getAwards()).slice(0, 2).map(award => ({ @@ -610,7 +623,6 @@ export class Neo4jSeedService { userId: "5c0f145b-ffad-4881-8ee6-7647c3c1b695", }), pinned: true, - totalComments: 0, pending: false, restrictedProps: null, childComments: [], @@ -625,7 +637,6 @@ export class Neo4jSeedService { userId: "5c0f145b-ffad-4881-8ee6-7647c3c1b695", }), pinned: false, - totalComments: 0, pending: true, restrictedProps: new RestrictedProps({ restrictedAt: 1665780000, @@ -643,7 +654,6 @@ export class Neo4jSeedService { userId: "3109f9e2-a262-4aef-b648-90d86d6fbf6c", }), pinned: false, - totalComments: 0, pending: true, restrictedProps: new RestrictedProps({ restrictedAt: 1665780000, @@ -662,7 +672,6 @@ export class Neo4jSeedService { userId: "3109f9e2-a262-4aef-b648-90d86d6fbf6c", }), pinned: false, - totalComments: 0, pending: false, restrictedProps: null, childComments: [], From 77b14d2d96948b6adeb32ca25943605bf04804b4 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Wed, 23 Nov 2022 01:15:31 -0800 Subject: [PATCH 017/153] :poop: added voting functionality - finalized --- .../controllers/comments.controller.ts | 33 ++++++++-- src/comments/dtos/index.ts | 2 +- src/comments/dtos/voteCommentPayload.dto.ts | 13 ++-- src/comments/models/comment.ts | 48 +++++++++++++-- .../comment/comments.repository.ts | 7 +-- .../services/comments/comments.service.ts | 60 ++++++++++++------- src/posts/controllers/posts.controller.ts | 38 +++++++----- src/posts/dtos/index.ts | 2 +- src/posts/dtos/votePostPayload.dto.ts | 6 +- src/posts/models/post.ts | 40 ++++++++++++- 10 files changed, 186 insertions(+), 63 deletions(-) diff --git a/src/comments/controllers/comments.controller.ts b/src/comments/controllers/comments.controller.ts index bc75f6a..218fdfc 100644 --- a/src/comments/controllers/comments.controller.ts +++ b/src/comments/controllers/comments.controller.ts @@ -18,8 +18,11 @@ import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; import { DatabaseContext } from "../../database-access-layer/databaseContext"; import { _$ } from "../../_domain/injectableTokens"; import { Comment as CommentModel } from "../models"; -import { CommentCreationPayloadDto } from "../dtos"; +import { CommentCreationPayloadDto, VoteCommentPayloadDto } from "../dtos"; import { ICommentsService } from "../services/comments/comments.service.interface"; +import { OptionalJwtAuthGuard } from "../../auth/guards/optionalJwtAuth.guard"; +import { AuthedUser } from "../../auth/decorators/authedUser.param.decorator"; +import { User } from "../../users/models"; @ApiTags("comments") @Controller("comments") @@ -38,16 +41,21 @@ export class CommentsController { } @Get() - @CacheTTL(10) // idk how many to cache this needs to change + @CacheTTL(4) + @UseGuards(OptionalJwtAuthGuard) @UseInterceptors(CacheInterceptor) - public async index(): Promise { + public async index(@AuthedUser() user: User): Promise { const comments = await this._dbContext.Comments.findAll(); - const decoratedComments = comments.map(comment => comment.toJSON()); + const decoratedComments = comments.map(comment => + comment.toJSON({ authenticatedUserId: user?.userId ?? undefined }) + ); return await Promise.all(decoratedComments); } @Get(":commentId/nestedComments") + @UseGuards(OptionalJwtAuthGuard) public async getNestedComments( + @AuthedUser() user: User, @Param("commentId", new ParseUUIDPipe()) commentId: string ): Promise { const comments = await this._commentsService.findNestedCommentsByCommentId( @@ -56,17 +64,21 @@ export class CommentsController { 2, 3 ); - const decoratedComments = comments.map(comment => comment.toJSONNested()); + const decoratedComments = comments.map(comment => + comment.toJSONNested({ authenticatedUserId: user?.userId ?? undefined }) + ); return await Promise.all(decoratedComments); } @Get(":commentId") + @UseGuards(OptionalJwtAuthGuard) public async getCommentById( + @AuthedUser() user: User, @Param("commentId", new ParseUUIDPipe()) commentId: string ): Promise { const comment = await this._dbContext.Comments.findCommentById(commentId); if (comment === undefined) throw new HttpException("Comment not found", 404); - return await comment.toJSON(); + return await comment.toJSON({ authenticatedUserId: user?.userId ?? undefined }); } @Post("create") @@ -77,4 +89,13 @@ export class CommentsController { const comment = await this._commentsService.authorNewComment(commentPayload); return await comment.toJSON(); } + + @Post("vote") + @UseGuards(AuthGuard("jwt")) + public async voteComment( + @Body() voteCommentPayload: VoteCommentPayloadDto + ): Promise { + await this._commentsService.voteComment(voteCommentPayload); + return; + } } diff --git a/src/comments/dtos/index.ts b/src/comments/dtos/index.ts index b34e73f..e63a19b 100644 --- a/src/comments/dtos/index.ts +++ b/src/comments/dtos/index.ts @@ -1,4 +1,4 @@ export { CommentCreationPayloadDto } from "./commentCreationPayload.dto"; export { HateSpeechRequestPayloadDto } from "./hateSpeechRequestPayload.dto"; export { HateSpeechResponseDto } from "./hateSpeechResponse.dto"; -export { VoteCommentPayloadDto, VoteType } from "./voteCommentPayload.dto"; +export { VoteCommentPayloadDto } from "./voteCommentPayload.dto"; diff --git a/src/comments/dtos/voteCommentPayload.dto.ts b/src/comments/dtos/voteCommentPayload.dto.ts index 1d73fa1..48c4193 100644 --- a/src/comments/dtos/voteCommentPayload.dto.ts +++ b/src/comments/dtos/voteCommentPayload.dto.ts @@ -1,15 +1,16 @@ import { ApiProperty } from "@nestjs/swagger"; - -export enum VoteType { - UPVOTES = "UPVOTES", - DOWN_VOTES = "DOWN_VOTES", -} +import { IsEnum, IsNotEmpty, IsUUID } from "class-validator"; +import { VoteType } from "../../_domain/models/enums"; export class VoteCommentPayloadDto { @ApiProperty({ type: String, format: "uuid" }) + @IsNotEmpty() + @IsUUID() commentId: string; - @ApiProperty({ type: VoteType }) + @ApiProperty({ enum: VoteType }) + @IsNotEmpty() + @IsEnum(VoteType) voteType: VoteType; constructor(partial?: Partial) { diff --git a/src/comments/models/comment.ts b/src/comments/models/comment.ts index cee6c1e..3242383 100644 --- a/src/comments/models/comment.ts +++ b/src/comments/models/comment.ts @@ -9,6 +9,8 @@ import { RestrictedProps, _ToSelfRelTypes } from "../../_domain/models/toSelf"; import { CommentToSelfRelTypes, DeletedProps } from "./toSelf"; import { PublicUserDto } from "../../users/dtos"; import neo4j from "neo4j-driver"; +import { VoteType } from "../../_domain/models/enums"; +import { IsOptional } from "class-validator"; @Labels("Comment") export class Comment extends Model { @@ -48,12 +50,20 @@ export class Comment extends Model { @ApiProperty({ type: Number }) totalVotes: number; + @ApiProperty({ type: Number }) + @IsOptional() + totalComments: number | undefined; + @ApiProperty({ type: RestrictedProps }) restrictedProps: Nullable = null; @ApiProperty({ type: Comment }) childComments: Comment[]; + @ApiProperty({ type: VoteType, nullable: true }) + @IsOptional() + userVote: Nullable | undefined = undefined; + @ApiProperty({ type: Boolean }) @NodeProperty() @Exclude() @@ -64,13 +74,14 @@ export class Comment extends Model { Object.assign(this, partial); } - public async toJSON() { + public async toJSON(props: ToJSONProps = {}) { if (this.neo4jService) { await Promise.all([ this.getRestricted(), this.getCreatedAt(), this.getTotalVotes(), this.getAuthorUser(), + ...(props.authenticatedUserId ? [this.getUserVote(props.authenticatedUserId)] : []), ]); } @@ -80,14 +91,14 @@ export class Comment extends Model { return { ...this }; } - public async toJSONNested() { + public async toJSONNested(props: ToJSONProps = {}) { if (!this.childComments) { - return this.toJSON(); + return this.toJSON(props); } for (const i in this.childComments) { - this.childComments[i] = await this.childComments[i].toJSONNested(); + this.childComments[i] = await this.childComments[i].toJSONNested(props); } - return this.toJSON(); + return this.toJSON(props); } public async getChildrenComments(limit = 0): Promise { @@ -112,6 +123,29 @@ export class Comment extends Model { return this.childComments; } + public async getUserVote(userId): Promise> { + const queryResult = await this.neo4jService.tryReadAsync( + ` + MATCH (u:User { userId: $userId })-[r:${UserToCommentRelTypes.UPVOTES}|${UserToCommentRelTypes.DOWN_VOTES}]->(c:Comment { commentId: $commentId }) + RETURN r + `, + { + userId, + commentId: this.commentId, + } + ); + + if (queryResult.records.length > 0) { + // user has already voted on this comment + const relType = queryResult.records[0].get("r").type as VoteType; + this.userVote = relType; + return relType; + } + + this.userVote = null; + return null; + } + public async getTotalVotes(): Promise { const queryResult = await this.neo4jService.tryReadAsync( ` @@ -214,3 +248,7 @@ export class Comment extends Model { return result; } } + +interface ToJSONProps { + authenticatedUserId?: string; +} diff --git a/src/comments/repositories/comment/comments.repository.ts b/src/comments/repositories/comment/comments.repository.ts index 6f333d2..9806699 100644 --- a/src/comments/repositories/comment/comments.repository.ts +++ b/src/comments/repositories/comment/comments.repository.ts @@ -3,7 +3,7 @@ import { Neo4jService } from "../../../neo4j/services/neo4j.service"; import { AuthoredProps, UserToCommentRelTypes } from "../../../users/models/toComment"; import { RestrictedProps, _ToSelfRelTypes } from "../../../_domain/models/toSelf"; import { Comment } from "../../models"; -import { CommentToSelfRelTypes, RepliedProps } from "../../models/toSelf"; +import { CommentToSelfRelTypes } from "../../models/toSelf"; import { ICommentsRepository } from "./comments.repository.interface"; import { PostToCommentRelTypes } from "../../../posts/models/toComment"; @@ -31,7 +31,6 @@ export class CommentsRepository implements ICommentsRepository { if (comment.commentId === undefined) { comment.commentId = this._neo4jService.generateId(); } - console.log(comment); let restrictedQueryString = ""; let restrictedQueryParams = {}; if (comment.restrictedProps !== null) { @@ -111,7 +110,7 @@ export class CommentsRepository implements ICommentsRepository { await this._neo4jService.tryWriteAsync( ` MATCH (u:User { userId: $userId }) - MATCH (commentParent:Post { postId: $parentId }) + MATCH (parentPost:Post { postId: $parentId }) CREATE (c:Comment { commentId: $commentId, updatedAt: $updatedAt, @@ -120,7 +119,7 @@ export class CommentsRepository implements ICommentsRepository { })${restrictedQueryString}<-[:${UserToCommentRelTypes.AUTHORED} { authoredAt: $authoredAt }]-(u), - (c)<-[:${PostToCommentRelTypes.HAS_COMMENT}]-(commentParent) + (c)<-[:${PostToCommentRelTypes.HAS_COMMENT}]-(parentPost) `, { // Comment properties diff --git a/src/comments/services/comments/comments.service.ts b/src/comments/services/comments/comments.service.ts index 00115ff..340f717 100644 --- a/src/comments/services/comments/comments.service.ts +++ b/src/comments/services/comments/comments.service.ts @@ -8,12 +8,14 @@ import { PostToCommentRelTypes } from "../../../posts/models/toComment"; import { User } from "../../../users/models"; import { UserToCommentRelTypes } from "../../../users/models/toComment"; import { _$ } from "../../../_domain/injectableTokens"; -import { CommentCreationPayloadDto, VoteCommentPayloadDto, VoteType } from "../../dtos"; +import { CommentCreationPayloadDto, VoteCommentPayloadDto } from "../../dtos"; import { Comment } from "../../models"; import { CommentToSelfRelTypes, DeletedProps } from "../../models/toSelf"; import { ICommentsService } from "./comments.service.interface"; import { IAutoModerationService } from "../../../moderation/services/autoModeration/autoModeration.service.interface"; import { IPostsService } from "../../../posts/services/posts/posts.service.interface"; +import { VoteType } from "../../../_domain/models/enums"; +import { VoteProps } from "../../../users/models/toPost"; @Injectable({ scope: Scope.REQUEST }) export class CommentsService implements ICommentsService { @@ -25,7 +27,6 @@ export class CommentsService implements ICommentsService { constructor( @Inject(REQUEST) request: Request, @Inject(_$.IDatabaseContext) databaseContext: DatabaseContext, - httpService: HttpService, @Inject(_$.IAutoModerationService) autoModerationService: IAutoModerationService, @Inject(_$.IPostsService) postsService: IPostsService ) { @@ -119,6 +120,7 @@ export class CommentsService implements ICommentsService { const queryResult = await this._dbContext.neo4jService.tryReadAsync( ` MATCH (u:User { userId: $userId })-[r:${UserToCommentRelTypes.UPVOTES}|${UserToCommentRelTypes.DOWN_VOTES}]->(c:Comment { commentId: $commentId }) + RETURN r `, { userId: user.userId, @@ -127,30 +129,48 @@ export class CommentsService implements ICommentsService { ); if (queryResult.records.length > 0) { + // user has already voted on this comment const relType = queryResult.records[0].get("r").type; - if ( - relType === UserToCommentRelTypes.UPVOTES && - voteCommentPayload.voteType === VoteType.UPVOTES - ) { - throw new HttpException("User already upvoted this comment", 400); - } else if ( - relType === UserToCommentRelTypes.DOWN_VOTES && - voteCommentPayload.voteType === VoteType.DOWN_VOTES - ) { - throw new HttpException("User already downvoted this comment", 400); - } else { - await this._dbContext.neo4jService.tryWriteAsync( - ` + + // remove the existing vote + await this._dbContext.neo4jService.tryWriteAsync( + ` MATCH (u:User { userId: $userId })-[r:${relType}]->(c:Comment { commentId: $commentId }) DELETE r `, - { - userId: user.userId, - commentId: voteCommentPayload.commentId, - } - ); + { + userId: user.userId, + commentId: voteCommentPayload.commentId, + } + ); + + // don't add a new vote if the user is removing their vote (stop) + if ( + (relType === UserToCommentRelTypes.UPVOTES && + voteCommentPayload.voteType === VoteType.UPVOTES) || + (relType === UserToCommentRelTypes.DOWN_VOTES && + voteCommentPayload.voteType === VoteType.DOWN_VOTES) + ) { + return; } } + + // add the new vote + const voteProps = new VoteProps({ + votedAt: new Date().getTime(), + }); + await this._dbContext.neo4jService.tryWriteAsync( + ` + MATCH (u:User { userId: $userId }), (c:Comment { commentId: $commentId }) + MERGE (u)-[r:${voteCommentPayload.voteType} { votedAt: $votedAt }]->(c) + `, + { + userId: user.userId, + commentId: voteCommentPayload.commentId, + + votedAt: voteProps.votedAt, + } + ); } public async markAsPinned(commentId: string): Promise { diff --git a/src/posts/controllers/posts.controller.ts b/src/posts/controllers/posts.controller.ts index 6c0b205..54e011a 100644 --- a/src/posts/controllers/posts.controller.ts +++ b/src/posts/controllers/posts.controller.ts @@ -21,6 +21,9 @@ import { Post as PostModel } from "../models"; import { Comment } from "../../comments/models"; import { PostCreationPayloadDto, VotePostPayloadDto } from "../dtos"; import { IPostsService } from "../services/posts/posts.service.interface"; +import { OptionalJwtAuthGuard } from "../../auth/guards/optionalJwtAuth.guard"; +import { AuthedUser } from "../../auth/decorators/authedUser.param.decorator"; +import { User } from "../../users/models"; @ApiTags("posts") @Controller("posts") @@ -39,33 +42,38 @@ export class PostsController { } @Get() - @CacheTTL(10) + @UseGuards(OptionalJwtAuthGuard) + @CacheTTL(5) @UseInterceptors(CacheInterceptor) - public async index(): Promise { + public async index(@AuthedUser() user: User): Promise { const posts = await this._dbContext.Posts.findAll(); - const decoratedPosts = posts.map(post => post.toJSON()); + const decoratedPosts = posts.map(post => + post.toJSON({ authenticatedUserId: user?.userId ?? undefined }) + ); return await Promise.all(decoratedPosts); } @Get("/queery") - @CacheTTL(10) + @UseGuards(OptionalJwtAuthGuard) + @CacheTTL(5) @UseInterceptors(CacheInterceptor) - public async getAllQueeries(): Promise { - const queeries = await this._postsService.findAllQueeries( - (postA, postB) => postB.createdAt - postA.createdAt + public async getAllQueeries(@AuthedUser() user: User): Promise { + const queeries = await this._postsService.findAllQueeries(); + const decoratedQueeries = queeries.map(queery => + queery.toJSON({ authenticatedUserId: user?.userId ?? undefined }) ); - const decoratedQueeries = queeries.map(queery => queery.toJSON()); return await Promise.all(decoratedQueeries); } @Get("/story") - @CacheTTL(10) + @UseGuards(OptionalJwtAuthGuard) + @CacheTTL(5) @UseInterceptors(CacheInterceptor) - public async getAllStories(): Promise { - const stories = await this._postsService.findAllStories( - (postA, postB) => postB.createdAt - postA.createdAt + public async getAllStories(@AuthedUser() user: User): Promise { + const stories = await this._postsService.findAllStories(); + const decoratedStories = stories.map(story => + story.toJSON({ authenticatedUserId: user?.userId ?? undefined }) ); - const decoratedStories = stories.map(story => story.toJSON()); return await Promise.all(decoratedStories); } @@ -84,12 +92,14 @@ export class PostsController { } @Get(":postId") + @UseGuards(OptionalJwtAuthGuard) public async getPostById( + @AuthedUser() user: User, @Param("postId", new ParseUUIDPipe()) postId: string ): Promise { const post = await this._dbContext.Posts.findPostById(postId); if (post === undefined) throw new HttpException("Post not found", 404); - return await post.toJSON(); + return await post.toJSON({ authenticatedUserId: user?.userId ?? undefined }); } @Post("create") diff --git a/src/posts/dtos/index.ts b/src/posts/dtos/index.ts index 9675913..2a505cd 100644 --- a/src/posts/dtos/index.ts +++ b/src/posts/dtos/index.ts @@ -1,6 +1,6 @@ export { HateSpeechResponseDto } from "./hateSpeechResponse.dto"; export { HateSpeechRequestPayloadDto } from "./hateSpeechRequestPayload.dto"; export { DeletePostPayloadDto } from "./deletePostPayload.dto"; -export { VotePostPayloadDto, VoteType } from "./votePostPayload.dto"; +export { VotePostPayloadDto } from "./votePostPayload.dto"; export { ReportPostPayloadDto } from "./reportPostPayload.dto"; export { PostCreationPayloadDto } from "./postCreationPayload.dto"; diff --git a/src/posts/dtos/votePostPayload.dto.ts b/src/posts/dtos/votePostPayload.dto.ts index ae46485..c83775b 100644 --- a/src/posts/dtos/votePostPayload.dto.ts +++ b/src/posts/dtos/votePostPayload.dto.ts @@ -1,10 +1,6 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsEnum, IsNotEmpty, IsUUID } from "class-validator"; - -export enum VoteType { - UPVOTES = "UPVOTES", - DOWN_VOTES = "DOWN_VOTES", -} +import { VoteType } from "../../_domain/models/enums"; export class VotePostPayloadDto { @ApiProperty({ type: String, format: "uuid" }) diff --git a/src/posts/models/post.ts b/src/posts/models/post.ts index bf8469b..fe45955 100644 --- a/src/posts/models/post.ts +++ b/src/posts/models/post.ts @@ -19,6 +19,8 @@ import { PublicUserDto } from "../../users/dtos"; import { PostToCommentRelTypes } from "./toComment"; import { Comment } from "../../comments/models"; import neo4j from "neo4j-driver"; +import { VoteType } from "../../_domain/models/enums"; +import { IsOptional } from "class-validator"; @Labels("Post") export class Post extends Model { @@ -62,6 +64,10 @@ export class Post extends Model { @ApiProperty({ type: Number }) totalVotes: number; + @ApiProperty({ type: Number }) + @IsOptional() + totalComments: number | undefined; + @ApiProperty({ type: Comment, isArray: true }) comments: Comment[]; @@ -70,12 +76,16 @@ export class Post extends Model { @Exclude() deletedProps: Nullable = null; + @ApiProperty({ type: VoteType, nullable: true }) + @IsOptional() + userVote: Nullable | undefined = undefined; + constructor(partial?: Partial, neo4jService?: Neo4jService) { super(neo4jService); Object.assign(this, partial); } - public async toJSON() { + public async toJSON(props: ToJSONProps = {}) { if (this.neo4jService) { await Promise.all([ this.getPostType(), @@ -85,6 +95,7 @@ export class Post extends Model { this.getCreatedAt(), this.getTotalVotes(), this.getAuthorUser(), + ...(props.authenticatedUserId ? [this.getUserVote(props.authenticatedUserId)] : []), ]); } @@ -115,6 +126,29 @@ export class Post extends Model { return comments; } + public async getUserVote(userId): Promise> { + const queryResult = await this.neo4jService.tryReadAsync( + ` + MATCH (u:User { userId: $userId })-[r:${UserToPostRelTypes.UPVOTES}|${UserToPostRelTypes.DOWN_VOTES}]->(p:Post { postId: $postId }) + RETURN r + `, + { + userId, + postId: this.postId, + } + ); + + if (queryResult.records.length > 0) { + // user has already voted on this post + const relType = queryResult.records[0].get("r").type as VoteType; + this.userVote = relType; + return relType; + } + + this.userVote = null; + return null; + } + public async getTotalVotes(): Promise { const queryResult = await this.neo4jService.tryReadAsync( ` @@ -274,3 +308,7 @@ export class Post extends Model { return result; } } + +interface ToJSONProps { + authenticatedUserId?: string; +} From b07200d9f9a7dd2b00562f2d5d462e0307988534 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Wed, 23 Nov 2022 03:49:03 -0800 Subject: [PATCH 018/153] fixage --- src/comments/services/comments/comments.service.ts | 7 +------ src/posts/services/posts/posts.service.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/comments/services/comments/comments.service.ts b/src/comments/services/comments/comments.service.ts index 71d5e56..100d009 100644 --- a/src/comments/services/comments/comments.service.ts +++ b/src/comments/services/comments/comments.service.ts @@ -1,12 +1,9 @@ -import { HttpService } from "@nestjs/axios"; import { HttpException, Inject, Injectable, Scope } from "@nestjs/common"; import { REQUEST } from "@nestjs/core"; import { Request } from "express"; import { DatabaseContext } from "../../../database-access-layer/databaseContext"; -import { IAutoModerationService } from "../../../moderation/services/autoModeration/autoModeration.service.interface"; import { Post } from "../../../posts/models"; import { PostToCommentRelTypes } from "../../../posts/models/toComment"; -import { IPostsService } from "../../../posts/services/posts/posts.service.interface"; import { User } from "../../../users/models"; import { UserToCommentRelTypes } from "../../../users/models/toComment"; import { _$ } from "../../../_domain/injectableTokens"; @@ -179,7 +176,6 @@ export class CommentsService implements ICommentsService { const comment = await this._dbContext.Comments.findCommentById(commentId); if (!comment) throw new HttpException("Comment not found", 404); - const [parentPost] = await this.findParentCommentRoot(commentId); const user = this.getUserFromRequest(); const isPinned = await this.checkIfAnyCommentIsPinned(parentPost); @@ -198,8 +194,7 @@ export class CommentsService implements ICommentsService { ` MATCH (p:Post { postId: $postId }), (c:Comment { commentId: $commentId }) CREATE (p)-[:${PostToCommentRelTypes.PINNED_COMMENT}]->(c) - ` - , + `, { postId: parentPost.postId, commentId: commentId, diff --git a/src/posts/services/posts/posts.service.ts b/src/posts/services/posts/posts.service.ts index 6916199..d44eaf1 100644 --- a/src/posts/services/posts/posts.service.ts +++ b/src/posts/services/posts/posts.service.ts @@ -8,7 +8,7 @@ import { Comment } from "../../../comments/models"; import { VoteType } from "../../../_domain/models/enums"; import { IAutoModerationService } from "../../../moderation/services/autoModeration/autoModeration.service.interface"; import { UserToPostRelTypes, VoteProps } from "../../../users/models/toPost"; -import { PostCreationPayloadDto, VotePostPayloadDto, VoteType } from "../../dtos"; +import { PostCreationPayloadDto, VotePostPayloadDto } from "../../dtos"; import { Post, PostTag } from "../../models"; import { DeletedProps } from "../../models/toSelf"; import { IPostsService, postSortCallback } from "./posts.service.interface"; @@ -182,7 +182,11 @@ export class PostsService implements IPostsService { return result; } - public async getNestedComments(comments: Comment[], nestedLevel: number, nestedLimit: number): Promise { + public async getNestedComments( + comments: Comment[], + nestedLevel: number, + nestedLimit: number + ): Promise { if (nestedLevel === 0) return; for (const i in comments) { const comment: Comment = comments[i]; From 169ee43e2ca2a9427fc2128401b04199d340c012 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Fri, 25 Nov 2022 08:57:43 -0800 Subject: [PATCH 019/153] implement the service methods --- src/_domain/injectableTokens.ts | 1 + .../models/toSelf/deleted.props.ts | 11 +- src/_domain/models/toSelf/index.ts | 2 + src/comments/models/comment.ts | 8 +- src/comments/models/toSelf/index.ts | 2 - .../services/comments/comments.service.ts | 5 +- src/moderation/dtos/moderatorActions/index.ts | 1 + .../moderatorActions/moderationPayload.dto.ts | 19 ++ src/moderation/moderation.module.ts | 12 +- .../moderatorActions.service.interface.ts | 20 +- .../moderatorActions.service.ts | 308 ++++++++++++++++++ src/posts/models/post.ts | 7 +- src/posts/models/toSelf/deleted.props.ts | 14 - src/posts/models/toSelf/index.ts | 5 - src/posts/services/posts/posts.service.ts | 4 +- src/users/dtos/index.ts | 2 +- src/users/dtos/publicUser.dto.ts | 93 +++--- 17 files changed, 419 insertions(+), 95 deletions(-) rename src/{comments => _domain}/models/toSelf/deleted.props.ts (59%) create mode 100644 src/moderation/dtos/moderatorActions/index.ts create mode 100644 src/moderation/dtos/moderatorActions/moderationPayload.dto.ts create mode 100644 src/moderation/services/moderatorActions/moderatorActions.service.ts delete mode 100644 src/posts/models/toSelf/deleted.props.ts delete mode 100644 src/posts/models/toSelf/index.ts diff --git a/src/_domain/injectableTokens.ts b/src/_domain/injectableTokens.ts index 3d31229..1a693ab 100644 --- a/src/_domain/injectableTokens.ts +++ b/src/_domain/injectableTokens.ts @@ -27,5 +27,6 @@ const injectableTokens = { // Moderation Module IAutoModerationService: Symbol("IAutoModerationService"), + IModeratorActionsService: Symbol("IModeratorActionsService"), }; export { injectableTokens as _$ }; diff --git a/src/comments/models/toSelf/deleted.props.ts b/src/_domain/models/toSelf/deleted.props.ts similarity index 59% rename from src/comments/models/toSelf/deleted.props.ts rename to src/_domain/models/toSelf/deleted.props.ts index f37f2d6..11186c5 100644 --- a/src/comments/models/toSelf/deleted.props.ts +++ b/src/_domain/models/toSelf/deleted.props.ts @@ -1,12 +1,15 @@ -import { ApiProperty } from "@nestjs/swagger"; import { RelationshipProps } from "../../../neo4j/neo4j.helper.types"; +import { IsNumber, IsString, IsUUID } from "class-validator"; export class DeletedProps implements RelationshipProps { - @ApiProperty({ type: Number }) + @IsNumber() deletedAt: number; - @ApiProperty({ type: String }) - deletedByUserId: string; + @IsUUID() + moderatorId: string; + + @IsString() + reason: string; constructor(partial?: Partial) { Object.assign(this, partial); diff --git a/src/_domain/models/toSelf/index.ts b/src/_domain/models/toSelf/index.ts index 1ce039d..ae3baa8 100644 --- a/src/_domain/models/toSelf/index.ts +++ b/src/_domain/models/toSelf/index.ts @@ -1,5 +1,7 @@ export { RestrictedProps } from "./restricted.props"; +export { DeletedProps } from "./deleted.props"; export enum _ToSelfRelTypes { RESTRICTED = "RESTRICTED", + DELETED = "DELETED", } diff --git a/src/comments/models/comment.ts b/src/comments/models/comment.ts index 3242383..8aa02b2 100644 --- a/src/comments/models/comment.ts +++ b/src/comments/models/comment.ts @@ -5,8 +5,8 @@ import { Model } from "../../neo4j/neo4j.helper.types"; import { Neo4jService } from "../../neo4j/services/neo4j.service"; import { User } from "../../users/models"; import { AuthoredProps, UserToCommentRelTypes } from "../../users/models/toComment"; -import { RestrictedProps, _ToSelfRelTypes } from "../../_domain/models/toSelf"; -import { CommentToSelfRelTypes, DeletedProps } from "./toSelf"; +import { RestrictedProps, _ToSelfRelTypes, DeletedProps } from "../../_domain/models/toSelf"; +import { CommentToSelfRelTypes } from "./toSelf"; import { PublicUserDto } from "../../users/dtos"; import neo4j from "neo4j-driver"; import { VoteType } from "../../_domain/models/enums"; @@ -204,7 +204,7 @@ export class Comment extends Model { public async getDeletedProps(): Promise { const queryResult = await this.neo4jService.tryReadAsync( ` - MATCH (c:Comment {commentId: $commentId})-[r:${CommentToSelfRelTypes.DELETED}]->(c) + MATCH (c:Comment {commentId: $commentId})-[r:${_ToSelfRelTypes.DELETED}]->(c) RETURN r `, { @@ -221,7 +221,7 @@ export class Comment extends Model { await this.neo4jService.tryWriteAsync( ` MATCH (c:Comment {commentId: $commentId}) - MERGE (c)-[r:${CommentToSelfRelTypes.DELETED}]->(c) + MERGE (c)-[r:${_ToSelfRelTypes.DELETED}]->(c) SET r = $deletedProps `, { diff --git a/src/comments/models/toSelf/index.ts b/src/comments/models/toSelf/index.ts index 7c0bd4a..452ad31 100644 --- a/src/comments/models/toSelf/index.ts +++ b/src/comments/models/toSelf/index.ts @@ -1,7 +1,5 @@ -export { DeletedProps } from "./deleted.props"; export { RepliedProps } from "./replied.props"; export enum CommentToSelfRelTypes { REPLIED = "REPLIED", - DELETED = "DELETED", } diff --git a/src/comments/services/comments/comments.service.ts b/src/comments/services/comments/comments.service.ts index 100d009..7d34301 100644 --- a/src/comments/services/comments/comments.service.ts +++ b/src/comments/services/comments/comments.service.ts @@ -9,11 +9,12 @@ import { UserToCommentRelTypes } from "../../../users/models/toComment"; import { _$ } from "../../../_domain/injectableTokens"; import { CommentCreationPayloadDto, VoteCommentPayloadDto } from "../../dtos"; import { Comment } from "../../models"; -import { CommentToSelfRelTypes, DeletedProps } from "../../models/toSelf"; +import { CommentToSelfRelTypes } from "../../models/toSelf"; import { ICommentsService } from "./comments.service.interface"; import { IAutoModerationService } from "../../../moderation/services/autoModeration/autoModeration.service.interface"; import { IPostsService } from "../../../posts/services/posts/posts.service.interface"; import { VoteType } from "../../../_domain/models/enums"; +import { DeletedProps } from "../../../_domain/models/toSelf"; import { VoteProps } from "../../../users/models/toPost"; @Injectable({ scope: Scope.REQUEST }) @@ -218,7 +219,7 @@ export class CommentsService implements ICommentsService { await comment.setDeletedProps( new DeletedProps({ deletedAt: new Date().getTime(), - deletedByUserId: comment.authorUser.userId, + moderatorId: comment.authorUser.userId, }) ); } diff --git a/src/moderation/dtos/moderatorActions/index.ts b/src/moderation/dtos/moderatorActions/index.ts new file mode 100644 index 0000000..349777c --- /dev/null +++ b/src/moderation/dtos/moderatorActions/index.ts @@ -0,0 +1 @@ +export * from "./moderationPayload.dto"; diff --git a/src/moderation/dtos/moderatorActions/moderationPayload.dto.ts b/src/moderation/dtos/moderatorActions/moderationPayload.dto.ts new file mode 100644 index 0000000..6233a02 --- /dev/null +++ b/src/moderation/dtos/moderatorActions/moderationPayload.dto.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsUUID } from "class-validator"; + +export class ModerationPayloadDto { + @ApiProperty({ type: String, format: "uuid" }) + @IsUUID() + id: string; + + @ApiProperty({ type: String }) + @IsUUID() + moderatorId: string; + + @ApiProperty({ type: String }) + reason: string; + + constructor(partial?: Partial) { + Object.assign(this, partial); + } +} diff --git a/src/moderation/moderation.module.ts b/src/moderation/moderation.module.ts index 2e36230..3bac4b7 100644 --- a/src/moderation/moderation.module.ts +++ b/src/moderation/moderation.module.ts @@ -1,21 +1,31 @@ import { Module } from "@nestjs/common"; import { AutoModerationService } from "./services/autoModeration/autoModeration.service"; +import { ModeratorActionsService } from "./services/moderatorActions/moderatorActions.service"; import { _$ } from "../_domain/injectableTokens"; import { HttpModule } from "@nestjs/axios"; +import { DatabaseAccessLayerModule } from "../database-access-layer/database-access-layer.module"; @Module({ - imports: [HttpModule], + imports: [HttpModule, DatabaseAccessLayerModule], providers: [ { provide: _$.IAutoModerationService, useClass: AutoModerationService, }, + { + provide: _$.IModeratorActionsService, + useClass: ModeratorActionsService, + }, ], exports: [ { provide: _$.IAutoModerationService, useClass: AutoModerationService, }, + { + provide: _$.IModeratorActionsService, + useClass: ModeratorActionsService, + }, ], }) export class ModerationModule {} diff --git a/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts b/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts index ead0733..9333310 100644 --- a/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts +++ b/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts @@ -1,13 +1,17 @@ +import { Post } from "../../../posts/models"; +import { Comment } from "../../../comments/models"; +import { ModerationPayloadDto } from "../../dtos/moderatorActions"; + export interface IModeratorActionsService { - allowPost(postId: string): Promise; - restrictPost(postId: string): Promise; + allowPost(postId: string): Promise; + restrictPost(payload: ModerationPayloadDto): Promise; - allowComment(commentId: string): Promise; - restrictComment(commentId: string): Promise; + allowComment(commentId: string): Promise; + restrictComment(payload: ModerationPayloadDto): Promise; - deletePost(postId: string): Promise; - undeletePost(postId: string): Promise; + deletePost(payload: ModerationPayloadDto): Promise; + undeletePost(postId: string): Promise; - deleteComment(commentId: string): Promise; - undeleteComment(commentId: string): Promise; + deleteComment(payload: ModerationPayloadDto): Promise; + undeleteComment(commentId: string): Promise; } diff --git a/src/moderation/services/moderatorActions/moderatorActions.service.ts b/src/moderation/services/moderatorActions/moderatorActions.service.ts new file mode 100644 index 0000000..d4f94a4 --- /dev/null +++ b/src/moderation/services/moderatorActions/moderatorActions.service.ts @@ -0,0 +1,308 @@ +import { IModeratorActionsService } from "./moderatorActions.service.interface"; +import { HttpException, Inject } from "@nestjs/common"; +import { _$ } from "../../../_domain/injectableTokens"; +import { DatabaseContext } from "../../../database-access-layer/databaseContext"; +import { _ToSelfRelTypes, DeletedProps, RestrictedProps } from "../../../_domain/models/toSelf"; +import { Comment } from "../../../comments/models"; +import { Post } from "../../../posts/models"; +import { ModerationPayloadDto } from "../../dtos/moderatorActions"; + +/** + * This service is responsible for moderating posts and comments. + * It is used by the moderator actions controller. + * Notes: + * - This service is not responsible to check if the user is a moderator. This is done by the guards used by controllers. + * @see src/moderation/controllers/moderatorActions/moderatorActions.controller.ts + */ +export class ModeratorActionsService implements IModeratorActionsService { + private readonly _dbContext: DatabaseContext; + + constructor(@Inject(_$.IDatabaseContext) dbContext: DatabaseContext) { + this._dbContext = dbContext; + } + + /** + * @description + * This method is to remove the restriction of a comment. It will remove the restriction self-relation from the comment. + * If the comment is already unrestricted, it will silently resolve the promise. + * Notes: + * * This method will not check if the end user has the permission to remove the restriction. + * * This method will give a http 404 if the comment was not found. + * @param commentId + */ + public async allowComment(commentId: string): Promise { + const comment = await this.acquireComment(commentId); + + await comment.getRestricted(); + if (!comment.restrictedProps) { + return comment; + } + + await this._dbContext.neo4jService.tryWriteAsync( + ` + MATCH (c:Comment { commentId: $commentId })-[r:${_ToSelfRelTypes.RESTRICTED}]->(c) + DELETE r + `, + { commentId } + ); + + return comment; + } + + /** + * @description + * This method is to remove the restriction of a post. It will remove the restriction self-relation from the post. + * If the post is already unrestricted, it will silently resolve the promise. + * Notes: + * * This method will not check if the end user has the permission to remove the restriction. + * * This method will give a http 404 if the post was not found. + * @param postId + */ + public async allowPost(postId: string): Promise { + const post = await this.acquirePost(postId); + + await post.getRestricted(); + if (!post.restrictedProps) { + return; + } + + await this._dbContext.neo4jService.tryWriteAsync( + ` + MATCH (p:Post { postId: $postId })-[r:${_ToSelfRelTypes.RESTRICTED}]->(p) + DELETE r + `, + { postId } + ); + + return; + } + + /** + * @description + * This method will mark a comment as deleted. It will add a self-relation to the comment. + * @param payload + */ + public async deleteComment(payload: ModerationPayloadDto): Promise { + const comment = await this.acquireComment(payload.id); + + await comment.getDeletedProps(); + if (comment.deletedProps) { + return comment; + } + + const deletedProps = new DeletedProps({ + deletedAt: Date.now(), + moderatorId: payload.moderatorId, + reason: payload.reason, + }); + await this._dbContext.neo4jService.tryWriteAsync( + ` + MATCH (c:Comment { commentId: $commentId })-[r:${_ToSelfRelTypes.DELETED} { + deletedAt: $deletedAt, + moderatorId: $moderatorId, + reason: $reason + }]->(c) + `, + { + commentId: payload.id, + deletedAt: deletedProps.deletedAt, + moderatorId: deletedProps.moderatorId, + reason: deletedProps.reason, + } + ); + comment.deletedProps = deletedProps; + + return comment; + } + + /** + * @description + * This method will mark a post as deleted. It will add a self-relation to the post. + * @param payload + */ + public async deletePost(payload: ModerationPayloadDto): Promise { + const post = await this.acquirePost(payload.id); + + await post.getDeletedProps(); + if (post.deletedProps) { + return post; + } + + const deletedProps = new DeletedProps({ + deletedAt: Date.now(), + moderatorId: payload.moderatorId, + reason: payload.reason, + }); + await this._dbContext.neo4jService.tryWriteAsync( + ` + MATCH (p:Post { postId: $postId })-[r:${_ToSelfRelTypes.DELETED} { + deletedAt: $deletedAt, + deletedByUserId: $deletedByUserId + }]->(p) + `, + { + postId: payload.id, + deletedAt: deletedProps.deletedAt, + moderatorId: deletedProps.moderatorId, + reason: deletedProps.reason, + } + ); + post.deletedProps = deletedProps; + + return post; + } + + /** + * @description + * This method will mark a comment as restricted. It will add a self-relation to the comment. + * @param payload + */ + public async restrictComment(payload: ModerationPayloadDto): Promise { + const comment = await this.acquireComment(payload.id); + + await comment.getRestricted(); + if (comment.restrictedProps) { + return comment; + } + + const restrictedProps = new RestrictedProps({ + restrictedAt: Date.now(), + moderatorId: payload.moderatorId, + reason: payload.moderatorId, + }); + await this._dbContext.neo4jService.tryWriteAsync( + ` + MATCH (c:Comment { commentId: $commentId })-[r:${_ToSelfRelTypes.RESTRICTED} { + restrictedAt: $restrictedAt, + moderatorId: $moderatorId, + reason: $reason + }]->(c) + `, + { + commentId: payload.id, + restrictedAt: restrictedProps.restrictedAt, + moderatorId: restrictedProps.moderatorId, + reason: restrictedProps.reason, + } + ); + comment.restrictedProps = restrictedProps; + + return comment; + } + + /** + * @description + * This method will mark a post as restricted. It will add a self-relation to the post. + * @param payload + */ + public async restrictPost(payload: ModerationPayloadDto): Promise { + const post = await this.acquirePost(payload.id); + + await post.getRestricted(); + if (post.restrictedProps) { + return post; + } + + const restrictedProps = new RestrictedProps({ + restrictedAt: Date.now(), + moderatorId: payload.moderatorId, + reason: payload.moderatorId, + }); + await this._dbContext.neo4jService.tryWriteAsync( + ` + MATCH (p:Post postId: $postId })-[r:${_ToSelfRelTypes.RESTRICTED} { + restrictedAt: $restrictedAt, + moderatorId: $moderatorId, + reason: $reason + }]->(p) + `, + { + postId: payload.id, + restrictedAt: restrictedProps.restrictedAt, + moderatorId: restrictedProps.moderatorId, + reason: restrictedProps.reason, + } + ); + post.restrictedProps = restrictedProps; + + return post; + } + + /** + * @description + * This method will mark a comment as undeleted. It will remove the self-relation from the comment. + * @param commentId + */ + public async undeleteComment(commentId: string): Promise { + const comment = await this.acquireComment(commentId); + + await comment.getDeletedProps(); + if (!comment.deletedProps) { + return comment; + } + + await this._dbContext.neo4jService.tryWriteAsync( + ` + MATCH (c:Comment { commentId: $commentId })-[r:${_ToSelfRelTypes.DELETED}]->(c) + DELETE r + `, + { commentId } + ); + comment.deletedProps = null; + + return comment; + } + + /** + * @description + * This method will mark a post as undeleted. It will remove the self-relation from the post. + * @param postId + */ + public async undeletePost(postId: string): Promise { + const post = await this.acquirePost(postId); + + await post.getDeletedProps(); + if (!post.deletedProps) { + return post; + } + + await this._dbContext.neo4jService.tryWriteAsync( + ` + MATCH (p:Post { postId: $postId })-[r:${_ToSelfRelTypes.DELETED}]->(p) + DELETE r + `, + { postId } + ); + post.deletedProps = null; + + return post; + } + + /** + * @description + * This method is to find a comment from the database and throw an error if it does not exist. if it does exist, it will return the comment. + * @param commentId + * @private + */ + private async acquireComment(commentId: string): Promise { + const comment = await this._dbContext.Comments.findCommentById(commentId); + if (!comment) { + throw new HttpException("Comment not found", 404); + } + return comment; + } + + /** + * @description + * This method is to find a post from the database and throw an error if it does not exist. if it does exist, it will return the post. + * @param postId + * @private + */ + private async acquirePost(postId: string): Promise { + const post = await this._dbContext.Posts.findPostById(postId); + if (!post) { + throw new HttpException("Post not found", 404); + } + return post; + } +} diff --git a/src/posts/models/post.ts b/src/posts/models/post.ts index fe45955..8defd14 100644 --- a/src/posts/models/post.ts +++ b/src/posts/models/post.ts @@ -7,13 +7,12 @@ import { } from "../../neo4j/neo4j.helper.types"; import { User } from "../../users/models"; import { PostType, PostTag, Award } from "./index"; -import { _ToSelfRelTypes, RestrictedProps } from "../../_domain/models/toSelf"; +import { _ToSelfRelTypes, RestrictedProps, DeletedProps } from "../../_domain/models/toSelf"; import { HasAwardProps, PostToAwardRelTypes } from "./toAward"; import { Neo4jService } from "../../neo4j/services/neo4j.service"; import { PostToPostTypeRelTypes } from "./toPostType"; import { PostToPostTagRelTypes } from "./toTags"; import { AuthoredProps, UserToPostRelTypes } from "../../users/models/toPost"; -import { DeletedProps, PostToSelfRelTypes } from "./toSelf"; import { Exclude } from "class-transformer"; import { PublicUserDto } from "../../users/dtos"; import { PostToCommentRelTypes } from "./toComment"; @@ -207,7 +206,7 @@ export class Post extends Model { public async getDeletedProps(): Promise { const queryResult = await this.neo4jService.tryReadAsync( ` - MATCH (p:Post {postId: $postId})-[r:${PostToSelfRelTypes.DELETED}]->(p) + MATCH (p:Post {postId: $postId})-[r:${_ToSelfRelTypes.DELETED}]->(p) RETURN r `, { @@ -224,7 +223,7 @@ export class Post extends Model { await this.neo4jService.tryWriteAsync( ` MATCH (p:Post {postId: $postId}) - MERGE (p)-[r:${PostToSelfRelTypes.DELETED}]->(p) + MERGE (p)-[r:${_ToSelfRelTypes.DELETED}]->(p) SET r = $deletedProps `, { diff --git a/src/posts/models/toSelf/deleted.props.ts b/src/posts/models/toSelf/deleted.props.ts deleted file mode 100644 index cce4bab..0000000 --- a/src/posts/models/toSelf/deleted.props.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { RelationshipProps } from "../../../neo4j/neo4j.helper.types"; -import { ApiProperty } from "@nestjs/swagger"; - -export class DeletedProps implements RelationshipProps { - @ApiProperty({ type: Number }) - deletedAt: number; - - @ApiProperty({ type: String }) - deletedByUserId: string; - - constructor(partial?: Partial) { - Object.assign(this, partial); - } -} diff --git a/src/posts/models/toSelf/index.ts b/src/posts/models/toSelf/index.ts deleted file mode 100644 index 088a945..0000000 --- a/src/posts/models/toSelf/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { DeletedProps } from "./deleted.props"; - -export enum PostToSelfRelTypes { - DELETED = "DELETED", -} diff --git a/src/posts/services/posts/posts.service.ts b/src/posts/services/posts/posts.service.ts index d44eaf1..191cd0b 100644 --- a/src/posts/services/posts/posts.service.ts +++ b/src/posts/services/posts/posts.service.ts @@ -6,11 +6,11 @@ import { REQUEST } from "@nestjs/core"; import { Request } from "express"; import { Comment } from "../../../comments/models"; import { VoteType } from "../../../_domain/models/enums"; +import { DeletedProps } from "../../../_domain/models/toSelf"; import { IAutoModerationService } from "../../../moderation/services/autoModeration/autoModeration.service.interface"; import { UserToPostRelTypes, VoteProps } from "../../../users/models/toPost"; import { PostCreationPayloadDto, VotePostPayloadDto } from "../../dtos"; import { Post, PostTag } from "../../models"; -import { DeletedProps } from "../../models/toSelf"; import { IPostsService, postSortCallback } from "./posts.service.interface"; @Injectable({ scope: Scope.REQUEST }) @@ -208,7 +208,7 @@ export class PostsService implements IPostsService { await post.setDeletedProps( new DeletedProps({ deletedAt: new Date().getTime(), - deletedByUserId: post.authorUser.userId, + moderatorId: post.authorUser.userId, }) ); } diff --git a/src/users/dtos/index.ts b/src/users/dtos/index.ts index c323612..016da96 100644 --- a/src/users/dtos/index.ts +++ b/src/users/dtos/index.ts @@ -1 +1 @@ -export { PublicUserDto } from "./publicUser.dto"; +export { PublicUserDto } from "./publicUser.dto"; diff --git a/src/users/dtos/publicUser.dto.ts b/src/users/dtos/publicUser.dto.ts index 8d8f1f3..1055667 100644 --- a/src/users/dtos/publicUser.dto.ts +++ b/src/users/dtos/publicUser.dto.ts @@ -1,48 +1,45 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsNotEmpty } from "class-validator"; -import { Gender } from "../models/gender"; -import { Sexuality } from "../models/sexuality"; -import { AvatarAscii, AvatarUrl } from "../models/user"; -import { User } from "../models/user"; - -export class PublicUserDto { - @ApiProperty({ type: String, format: "uuid" }) - @IsNotEmpty() - userId: string; - - @ApiProperty({ type: String }) - @IsNotEmpty() - avatar: AvatarAscii | AvatarUrl; - - @ApiProperty({ type: String }) - @IsNotEmpty() - username: string; - - @ApiProperty({ type: Number }) - @IsNotEmpty() - level: number; - - @ApiProperty({ type: Sexuality }) - @IsNotEmpty() - sexuality: Nullable; - - @ApiProperty({ type: Gender }) - @IsNotEmpty() - gender: Nullable; - - constructor(partial?: Partial) { - Object.assign(this, partial); - } - - static fromUser(user: User): PublicUserDto { - return new PublicUserDto({ - userId: user.userId, - username: user.username, - avatar: user.avatar || null, - level: user.level, - sexuality: user.sexuality || null, - gender: user.gender || null, - }); - } -} - +import { ApiProperty } from "@nestjs/swagger"; +import { IsNotEmpty } from "class-validator"; +import { Gender, Sexuality, User } from "../models"; +import { AvatarAscii, AvatarUrl } from "../models/user"; + +export class PublicUserDto { + @ApiProperty({ type: String, format: "uuid" }) + @IsNotEmpty() + userId: string; + + @ApiProperty({ type: String }) + @IsNotEmpty() + avatar: AvatarAscii | AvatarUrl; + + @ApiProperty({ type: String }) + @IsNotEmpty() + username: string; + + @ApiProperty({ type: Number }) + @IsNotEmpty() + level: number; + + @ApiProperty({ type: Sexuality }) + @IsNotEmpty() + sexuality: Nullable; + + @ApiProperty({ type: Gender }) + @IsNotEmpty() + gender: Nullable; + + constructor(partial?: Partial) { + Object.assign(this, partial); + } + + static fromUser(user: User): PublicUserDto { + return new PublicUserDto({ + userId: user.userId, + username: user.username, + avatar: user.avatar || null, + level: user.level, + sexuality: user.sexuality || null, + gender: user.gender || null, + }); + } +} From 8faeeaad1efc8dfe5bb5953182ad22a90c26815e Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Fri, 25 Nov 2022 15:16:05 -0800 Subject: [PATCH 020/153] fix the dtos and models decorators --- .../dtos/hateSpeechRequestPayload.dto.ts | 4 ++ src/comments/dtos/hateSpeechResponse.dto.ts | 5 ++ src/comments/models/comment.ts | 45 +++++++++------ .../dtos/hateSpeechRequestPayload.dto.ts | 4 ++ .../dtos/hateSpeechResponse.dto.ts | 5 ++ src/moderation/dtos/index.ts | 2 + .../moderatorActions/moderationPayload.dto.ts | 4 +- src/moderation/moderation.module.ts | 4 +- .../autoModeration/autoModeration.service.ts | 2 +- .../moderatorActions.service.interface.ts | 4 ++ .../moderatorActions.service.ts | 8 +++ src/posts/dtos/deletePostPayload.dto.ts | 2 + src/posts/dtos/index.ts | 2 - src/posts/models/award.ts | 7 ++- src/posts/models/post.ts | 50 ++++++++++------- src/posts/models/postTag.ts | 6 +- src/posts/models/postType.ts | 4 +- src/posts/models/toAward/hasAward.props.ts | 4 +- .../models/toComment/hasComment.props.ts | 4 +- .../postReport/postsReport.service.ts | 7 +-- src/users/models/gender.ts | 10 ++-- src/users/models/openness.ts | 8 +-- src/users/models/sexuality.ts | 7 ++- src/users/models/toComment/authored.props.ts | 6 +- src/users/models/toComment/downVotes.props.ts | 4 +- src/users/models/toComment/reported.props.ts | 7 ++- src/users/models/toComment/upVotes.props.ts | 4 +- src/users/models/toPost/authored.props.ts | 6 +- src/users/models/toPost/favorites.props.ts | 4 +- src/users/models/toPost/read.props.ts | 4 +- src/users/models/toPost/reported.props.ts | 7 ++- src/users/models/toPost/vote.props.ts | 4 +- src/users/models/toSelf/wasOffending.props.ts | 8 +-- src/users/models/user.ts | 55 ++++++++++++------- 34 files changed, 191 insertions(+), 116 deletions(-) rename src/{posts => moderation}/dtos/hateSpeechRequestPayload.dto.ts (68%) rename src/{posts => moderation}/dtos/hateSpeechResponse.dto.ts (63%) create mode 100644 src/moderation/dtos/index.ts diff --git a/src/comments/dtos/hateSpeechRequestPayload.dto.ts b/src/comments/dtos/hateSpeechRequestPayload.dto.ts index 3336512..4fed731 100644 --- a/src/comments/dtos/hateSpeechRequestPayload.dto.ts +++ b/src/comments/dtos/hateSpeechRequestPayload.dto.ts @@ -1,6 +1,10 @@ +import { IsString } from "class-validator"; + export class HateSpeechRequestPayloadDto { + @IsString() token: string; + @IsString() text: string; constructor(partial?: Partial) { diff --git a/src/comments/dtos/hateSpeechResponse.dto.ts b/src/comments/dtos/hateSpeechResponse.dto.ts index e31e26e..7776aa7 100644 --- a/src/comments/dtos/hateSpeechResponse.dto.ts +++ b/src/comments/dtos/hateSpeechResponse.dto.ts @@ -1,8 +1,13 @@ +import { IsNumber, IsString } from "class-validator"; + export class HateSpeechResponseDto { + @IsString() response: string; + @IsString() class: string; + @IsNumber() confidence: number; constructor(partial?: Partial) { diff --git a/src/comments/models/comment.ts b/src/comments/models/comment.ts index 8aa02b2..ba26d7c 100644 --- a/src/comments/models/comment.ts +++ b/src/comments/models/comment.ts @@ -1,5 +1,4 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { Exclude } from "class-transformer"; +import { Exclude, Type } from "class-transformer"; import { Labels, NodeProperty } from "../../neo4j/neo4j.decorators"; import { Model } from "../../neo4j/neo4j.helper.types"; import { Neo4jService } from "../../neo4j/services/neo4j.service"; @@ -10,12 +9,21 @@ import { CommentToSelfRelTypes } from "./toSelf"; import { PublicUserDto } from "../../users/dtos"; import neo4j from "neo4j-driver"; import { VoteType } from "../../_domain/models/enums"; -import { IsOptional } from "class-validator"; +import { + IsArray, + IsBoolean, + IsEnum, + IsInstance, + IsNumber, + IsOptional, + IsString, + IsUUID, +} from "class-validator"; @Labels("Comment") export class Comment extends Model { - @ApiProperty({ type: String, format: "uuid" }) @NodeProperty() + @IsUUID() commentId: string; /** @@ -23,50 +31,53 @@ export class Comment extends Model { * properties of (u:User)-[authored:AUTHORED]->(c:Comment) RETURN c, authored * where authored.createdAt is the value of this property. */ - @ApiProperty({ type: Number }) + @IsNumber() createdAt: number; - @ApiProperty({ type: String }) @NodeProperty() + @IsString() commentContent: string; - @ApiProperty({ type: String, format: "uuid" }) + @IsUUID() + @IsOptional() parentId: Nullable; - @ApiProperty({ type: Boolean }) + @IsBoolean() pinned: boolean; - @ApiProperty({ type: String }) @NodeProperty() + @IsString() updatedAt: number; - @ApiProperty({ type: User }) + @IsInstance(User) authorUser: User | any; - @ApiProperty({ type: Boolean }) @NodeProperty() + @IsBoolean() pending: boolean; - @ApiProperty({ type: Number }) + @IsNumber() totalVotes: number; - @ApiProperty({ type: Number }) + @IsNumber() @IsOptional() totalComments: number | undefined; - @ApiProperty({ type: RestrictedProps }) + @IsInstance(RestrictedProps) + @IsOptional() restrictedProps: Nullable = null; - @ApiProperty({ type: Comment }) + @IsArray({ each: true }) + @Type(() => RestrictedProps) childComments: Comment[]; - @ApiProperty({ type: VoteType, nullable: true }) + @IsEnum(VoteType) @IsOptional() userVote: Nullable | undefined = undefined; - @ApiProperty({ type: Boolean }) @NodeProperty() @Exclude() + @IsBoolean() deletedProps: Nullable = null; constructor(partial?: Partial, neo4jService?: Neo4jService) { diff --git a/src/posts/dtos/hateSpeechRequestPayload.dto.ts b/src/moderation/dtos/hateSpeechRequestPayload.dto.ts similarity index 68% rename from src/posts/dtos/hateSpeechRequestPayload.dto.ts rename to src/moderation/dtos/hateSpeechRequestPayload.dto.ts index 3336512..4fed731 100644 --- a/src/posts/dtos/hateSpeechRequestPayload.dto.ts +++ b/src/moderation/dtos/hateSpeechRequestPayload.dto.ts @@ -1,6 +1,10 @@ +import { IsString } from "class-validator"; + export class HateSpeechRequestPayloadDto { + @IsString() token: string; + @IsString() text: string; constructor(partial?: Partial) { diff --git a/src/posts/dtos/hateSpeechResponse.dto.ts b/src/moderation/dtos/hateSpeechResponse.dto.ts similarity index 63% rename from src/posts/dtos/hateSpeechResponse.dto.ts rename to src/moderation/dtos/hateSpeechResponse.dto.ts index e31e26e..7776aa7 100644 --- a/src/posts/dtos/hateSpeechResponse.dto.ts +++ b/src/moderation/dtos/hateSpeechResponse.dto.ts @@ -1,8 +1,13 @@ +import { IsNumber, IsString } from "class-validator"; + export class HateSpeechResponseDto { + @IsString() response: string; + @IsString() class: string; + @IsNumber() confidence: number; constructor(partial?: Partial) { diff --git a/src/moderation/dtos/index.ts b/src/moderation/dtos/index.ts new file mode 100644 index 0000000..91b0013 --- /dev/null +++ b/src/moderation/dtos/index.ts @@ -0,0 +1,2 @@ +export * from "./hateSpeechRequestPayload.dto"; +export * from "./hateSpeechResponse.dto"; diff --git a/src/moderation/dtos/moderatorActions/moderationPayload.dto.ts b/src/moderation/dtos/moderatorActions/moderationPayload.dto.ts index 6233a02..2800e58 100644 --- a/src/moderation/dtos/moderatorActions/moderationPayload.dto.ts +++ b/src/moderation/dtos/moderatorActions/moderationPayload.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsUUID } from "class-validator"; +import { IsNotEmpty, IsString, IsUUID } from "class-validator"; export class ModerationPayloadDto { @ApiProperty({ type: String, format: "uuid" }) @@ -11,6 +11,8 @@ export class ModerationPayloadDto { moderatorId: string; @ApiProperty({ type: String }) + @IsString() + @IsNotEmpty() reason: string; constructor(partial?: Partial) { diff --git a/src/moderation/moderation.module.ts b/src/moderation/moderation.module.ts index 3bac4b7..2823c67 100644 --- a/src/moderation/moderation.module.ts +++ b/src/moderation/moderation.module.ts @@ -1,4 +1,4 @@ -import { Module } from "@nestjs/common"; +import { forwardRef, Module } from "@nestjs/common"; import { AutoModerationService } from "./services/autoModeration/autoModeration.service"; import { ModeratorActionsService } from "./services/moderatorActions/moderatorActions.service"; import { _$ } from "../_domain/injectableTokens"; @@ -6,7 +6,7 @@ import { HttpModule } from "@nestjs/axios"; import { DatabaseAccessLayerModule } from "../database-access-layer/database-access-layer.module"; @Module({ - imports: [HttpModule, DatabaseAccessLayerModule], + imports: [HttpModule, forwardRef(() => DatabaseAccessLayerModule)], providers: [ { provide: _$.IAutoModerationService, diff --git a/src/moderation/services/autoModeration/autoModeration.service.ts b/src/moderation/services/autoModeration/autoModeration.service.ts index 134b9b6..1275445 100644 --- a/src/moderation/services/autoModeration/autoModeration.service.ts +++ b/src/moderation/services/autoModeration/autoModeration.service.ts @@ -5,7 +5,7 @@ import { ConfigService } from "@nestjs/config"; import { catchError, lastValueFrom, map, throwError } from "rxjs"; import { HttpService } from "@nestjs/axios"; import { IAutoModerationService } from "./autoModeration.service.interface"; -import { HateSpeechRequestPayloadDto, HateSpeechResponseDto } from "../../../posts/dtos"; +import { HateSpeechRequestPayloadDto, HateSpeechResponseDto } from "../../dtos"; import { WasOffendingProps } from "../../../users/models/toSelf"; import { User } from "../../../users/models"; diff --git a/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts b/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts index 9333310..db049f3 100644 --- a/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts +++ b/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts @@ -1,6 +1,7 @@ import { Post } from "../../../posts/models"; import { Comment } from "../../../comments/models"; import { ModerationPayloadDto } from "../../dtos/moderatorActions"; +import { User } from "../../../users/models"; export interface IModeratorActionsService { allowPost(postId: string): Promise; @@ -14,4 +15,7 @@ export interface IModeratorActionsService { deleteComment(payload: ModerationPayloadDto): Promise; undeleteComment(commentId: string): Promise; + + banUser(payload: ModerationPayloadDto): Promise; + unbanUser(userId: string): Promise; } diff --git a/src/moderation/services/moderatorActions/moderatorActions.service.ts b/src/moderation/services/moderatorActions/moderatorActions.service.ts index d4f94a4..8d57e5d 100644 --- a/src/moderation/services/moderatorActions/moderatorActions.service.ts +++ b/src/moderation/services/moderatorActions/moderatorActions.service.ts @@ -6,6 +6,7 @@ import { _ToSelfRelTypes, DeletedProps, RestrictedProps } from "../../../_domain import { Comment } from "../../../comments/models"; import { Post } from "../../../posts/models"; import { ModerationPayloadDto } from "../../dtos/moderatorActions"; +import { User } from "../../../users/models"; /** * This service is responsible for moderating posts and comments. @@ -21,6 +22,13 @@ export class ModeratorActionsService implements IModeratorActionsService { this._dbContext = dbContext; } + public async banUser(payload: ModerationPayloadDto): Promise { + throw new Error("Method not implemented."); + } + public async unbanUser(userId: string): Promise { + throw new Error("Method not implemented."); + } + /** * @description * This method is to remove the restriction of a comment. It will remove the restriction self-relation from the comment. diff --git a/src/posts/dtos/deletePostPayload.dto.ts b/src/posts/dtos/deletePostPayload.dto.ts index 4cf9709..befbd35 100644 --- a/src/posts/dtos/deletePostPayload.dto.ts +++ b/src/posts/dtos/deletePostPayload.dto.ts @@ -1,7 +1,9 @@ import { ApiProperty } from "@nestjs/swagger"; +import { IsUUID } from "class-validator"; export class DeletePostPayloadDto { @ApiProperty({ type: String }) + @IsUUID() postId: string; constructor(partial?: Partial) { diff --git a/src/posts/dtos/index.ts b/src/posts/dtos/index.ts index 2a505cd..0414651 100644 --- a/src/posts/dtos/index.ts +++ b/src/posts/dtos/index.ts @@ -1,5 +1,3 @@ -export { HateSpeechResponseDto } from "./hateSpeechResponse.dto"; -export { HateSpeechRequestPayloadDto } from "./hateSpeechRequestPayload.dto"; export { DeletePostPayloadDto } from "./deletePostPayload.dto"; export { VotePostPayloadDto } from "./votePostPayload.dto"; export { ReportPostPayloadDto } from "./reportPostPayload.dto"; diff --git a/src/posts/models/award.ts b/src/posts/models/award.ts index 4b9995a..9e7c05b 100644 --- a/src/posts/models/award.ts +++ b/src/posts/models/award.ts @@ -1,18 +1,19 @@ import { ApiProperty } from "@nestjs/swagger"; import { Labels, NodeProperty } from "../../neo4j/neo4j.decorators"; +import { IsString, IsUUID } from "class-validator"; @Labels("Award") export class Award { - @ApiProperty({ type: String, format: "uuid" }) @NodeProperty() + @IsUUID() awardId: string; - @ApiProperty({ type: String }) @NodeProperty() + @IsString() awardName: string; - @ApiProperty({ type: String }) @NodeProperty() + @IsString() awardSvg: string; constructor(partial?: Partial) { diff --git a/src/posts/models/post.ts b/src/posts/models/post.ts index 8defd14..2f264f7 100644 --- a/src/posts/models/post.ts +++ b/src/posts/models/post.ts @@ -13,70 +13,82 @@ import { Neo4jService } from "../../neo4j/services/neo4j.service"; import { PostToPostTypeRelTypes } from "./toPostType"; import { PostToPostTagRelTypes } from "./toTags"; import { AuthoredProps, UserToPostRelTypes } from "../../users/models/toPost"; -import { Exclude } from "class-transformer"; +import { Exclude, Type } from "class-transformer"; import { PublicUserDto } from "../../users/dtos"; import { PostToCommentRelTypes } from "./toComment"; import { Comment } from "../../comments/models"; import neo4j from "neo4j-driver"; import { VoteType } from "../../_domain/models/enums"; -import { IsOptional } from "class-validator"; +import { + IsArray, + IsBoolean, + IsEnum, + IsInstance, + IsNumber, + IsOptional, + IsString, + IsUUID, +} from "class-validator"; @Labels("Post") export class Post extends Model { - @ApiProperty({ type: String, format: "uuid" }) @NodeProperty() + @IsUUID() postId: string; - @ApiProperty({ type: PostType }) + @IsInstance(PostType) + @IsOptional() postType: PostType; - @ApiProperty({ type: PostTag, isArray: true }) + @IsArray() + @IsOptional() postTags: PostTag[] = new Array(); - @ApiProperty({ type: Award, isArray: true }) + @IsOptional() awards: RichRelatedEntities; - @ApiProperty({ type: Number }) + @IsNumber() createdAt: number; - @ApiProperty({ type: Number }) @NodeProperty() updatedAt: number; - @ApiProperty({ type: String }) @NodeProperty() + @IsString() postTitle: string; - @ApiProperty({ type: String }) @NodeProperty() + @IsString() postContent: string; - @ApiProperty({ type: User }) + @IsInstance(User) authorUser: User | any; - @ApiProperty({ type: Boolean }) @NodeProperty() + @IsBoolean() pending: boolean; - @ApiProperty({ type: RestrictedProps }) + @IsInstance(RestrictedProps) + @IsOptional() restrictedProps: Nullable = null; - @ApiProperty({ type: Number }) + @IsNumber() totalVotes: number; - @ApiProperty({ type: Number }) + @IsNumber() @IsOptional() totalComments: number | undefined; - @ApiProperty({ type: Comment, isArray: true }) + @IsArray({ each: true }) + @Type(() => Comment) comments: Comment[]; - @ApiProperty({ type: Boolean }) @NodeProperty() - @Exclude() + @IsOptional() + @IsInstance(DeletedProps) deletedProps: Nullable = null; - @ApiProperty({ type: VoteType, nullable: true }) @IsOptional() + @IsEnum(VoteType) userVote: Nullable | undefined = undefined; constructor(partial?: Partial, neo4jService?: Neo4jService) { diff --git a/src/posts/models/postTag.ts b/src/posts/models/postTag.ts index 8e01cfc..65d528f 100644 --- a/src/posts/models/postTag.ts +++ b/src/posts/models/postTag.ts @@ -1,14 +1,14 @@ -import { ApiProperty } from "@nestjs/swagger"; import { Labels, NodeProperty } from "../../neo4j/neo4j.decorators"; +import { IsString } from "class-validator"; @Labels("PostTag") export class PostTag { - @ApiProperty({ type: String }) @NodeProperty() + @IsString() tagName: string; - @ApiProperty({ type: String }) @NodeProperty() + @IsString() tagColor: string; constructor(partial?: Partial) { diff --git a/src/posts/models/postType.ts b/src/posts/models/postType.ts index 4c543ba..7c31918 100644 --- a/src/posts/models/postType.ts +++ b/src/posts/models/postType.ts @@ -1,10 +1,10 @@ -import { ApiProperty } from "@nestjs/swagger"; import { Labels, NodeProperty } from "../../neo4j/neo4j.decorators"; +import { IsString } from "class-validator"; @Labels("PostType") export class PostType { - @ApiProperty({ type: String }) @NodeProperty() + @IsString() postTypeName: string; constructor(partial?: Partial) { diff --git a/src/posts/models/toAward/hasAward.props.ts b/src/posts/models/toAward/hasAward.props.ts index 11e5c9d..b132145 100644 --- a/src/posts/models/toAward/hasAward.props.ts +++ b/src/posts/models/toAward/hasAward.props.ts @@ -1,8 +1,8 @@ import { RelationshipProps } from "../../../neo4j/neo4j.helper.types"; -import { ApiProperty } from "@nestjs/swagger"; +import { IsUUID } from "class-validator"; export class HasAwardProps implements RelationshipProps { - @ApiProperty({ type: String, format: "uuid" }) + @IsUUID() awardedBy: string; constructor(partial?: Partial) { diff --git a/src/posts/models/toComment/hasComment.props.ts b/src/posts/models/toComment/hasComment.props.ts index 0040681..426526a 100644 --- a/src/posts/models/toComment/hasComment.props.ts +++ b/src/posts/models/toComment/hasComment.props.ts @@ -1,8 +1,8 @@ import { RelationshipProps } from "../../../neo4j/neo4j.helper.types"; -import { ApiProperty } from "@nestjs/swagger"; +import { IsBoolean } from "class-validator"; export class HasCommentProps implements RelationshipProps { - @ApiProperty({ type: Boolean }) + @IsBoolean() pinned: boolean; constructor(partial?: Partial) { diff --git a/src/posts/services/postReport/postsReport.service.ts b/src/posts/services/postReport/postsReport.service.ts index d9a5ce3..a24d096 100644 --- a/src/posts/services/postReport/postsReport.service.ts +++ b/src/posts/services/postReport/postsReport.service.ts @@ -37,7 +37,7 @@ export class PostsReportService implements IPostsReportService { const reports = await this.getReportsForPost(post.postId); - if (reports.some(r => r.reportedBy.userId === user.userId)) { + if (reports.some(r => r.moderatorId === user.userId)) { throw new HttpException("Post already reported", 400); } @@ -68,10 +68,7 @@ export class PostsReportService implements IPostsReportService { ); return queryResult.records.map(record => { const reportedProps = new ReportedProps(record.get("r").properties); - reportedProps.reportedBy = new User( - record.get("u").properties, - this._dbContext.neo4jService - ); + reportedProps.moderatorId = record.get("u").properties.userId; return reportedProps; }); } diff --git a/src/users/models/gender.ts b/src/users/models/gender.ts index 3885346..0cbc2d0 100644 --- a/src/users/models/gender.ts +++ b/src/users/models/gender.ts @@ -1,22 +1,22 @@ import { Labels, NodeProperty } from "../../neo4j/neo4j.decorators"; -import { ApiProperty } from "@nestjs/swagger"; +import { IsNumber, IsString } from "class-validator"; @Labels("Gender") export class Gender { - @ApiProperty({ type: String, format: "uuid" }) @NodeProperty() + @IsNumber() genderId: string; - @ApiProperty({ type: String }) @NodeProperty() + @IsString() genderName: string; - @ApiProperty({ type: String }) @NodeProperty() + @IsString() genderPronouns: string; - @ApiProperty({ type: String }) @NodeProperty() + @IsString() genderFlagSvg: string; constructor(partial?: Partial) { diff --git a/src/users/models/openness.ts b/src/users/models/openness.ts index e97fd9e..b94614a 100644 --- a/src/users/models/openness.ts +++ b/src/users/models/openness.ts @@ -1,18 +1,18 @@ -import { ApiProperty } from "@nestjs/swagger"; import { Labels, NodeProperty } from "../../neo4j/neo4j.decorators"; +import { IsNumber, IsString, IsUUID } from "class-validator"; @Labels("Openness") export class Openness { - @ApiProperty({ type: String, format: "uuid" }) @NodeProperty() + @IsUUID() opennessId: string; - @ApiProperty({ type: Number }) @NodeProperty() + @IsNumber() opennessLevel: number; - @ApiProperty({ type: Number }) @NodeProperty() + @IsString() opennessDescription: string; constructor(partial?: Partial) { diff --git a/src/users/models/sexuality.ts b/src/users/models/sexuality.ts index 7661153..403906c 100644 --- a/src/users/models/sexuality.ts +++ b/src/users/models/sexuality.ts @@ -1,18 +1,19 @@ import { ApiProperty } from "@nestjs/swagger"; import { Labels, NodeProperty } from "../../neo4j/neo4j.decorators"; +import { IsString, IsUUID } from "class-validator"; @Labels("Sexuality") export class Sexuality { - @ApiProperty({ type: String, format: "uuid" }) @NodeProperty() + @IsUUID() sexualityId: string; - @ApiProperty({ type: String }) @NodeProperty() + @IsString() sexualityName: string; - @ApiProperty({ type: String }) @NodeProperty() + @IsString() sexualityFlagSvg: string; constructor(partial?: Partial) { diff --git a/src/users/models/toComment/authored.props.ts b/src/users/models/toComment/authored.props.ts index d0a07c5..7d020b6 100644 --- a/src/users/models/toComment/authored.props.ts +++ b/src/users/models/toComment/authored.props.ts @@ -1,11 +1,11 @@ -import { ApiProperty } from "@nestjs/swagger"; import { RelationshipProps } from "../../../neo4j/neo4j.helper.types"; +import { IsBoolean, IsNumber } from "class-validator"; export class AuthoredProps implements RelationshipProps { - @ApiProperty({ type: Number }) + @IsNumber() authoredAt: number; - @ApiProperty({ type: Boolean }) + @IsBoolean() anonymously: boolean; constructor(partial?: Partial) { diff --git a/src/users/models/toComment/downVotes.props.ts b/src/users/models/toComment/downVotes.props.ts index 4b728a7..4048e31 100644 --- a/src/users/models/toComment/downVotes.props.ts +++ b/src/users/models/toComment/downVotes.props.ts @@ -1,8 +1,8 @@ -import { ApiProperty } from "@nestjs/swagger"; import { RelationshipProps } from "../../../neo4j/neo4j.helper.types"; +import { IsNumber } from "class-validator"; export class DownVotesProps implements RelationshipProps { - @ApiProperty({ type: Number }) + @IsNumber() downVotedAt: number; constructor(partial?: Partial) { diff --git a/src/users/models/toComment/reported.props.ts b/src/users/models/toComment/reported.props.ts index d3f9de5..69b841b 100644 --- a/src/users/models/toComment/reported.props.ts +++ b/src/users/models/toComment/reported.props.ts @@ -1,11 +1,14 @@ import { RelationshipProps } from "../../../neo4j/neo4j.helper.types"; -import { User } from "../user"; +import { IsNumber, IsString, IsUUID } from "class-validator"; export class ReportedProps implements RelationshipProps { - reportedBy: User; + @IsUUID() + moderatorId: string; + @IsNumber() reportedAt: number; + @IsString() reason: string; constructor(partial?: Partial) { diff --git a/src/users/models/toComment/upVotes.props.ts b/src/users/models/toComment/upVotes.props.ts index e732b66..f5a98c6 100644 --- a/src/users/models/toComment/upVotes.props.ts +++ b/src/users/models/toComment/upVotes.props.ts @@ -1,8 +1,8 @@ -import { ApiProperty } from "@nestjs/swagger"; import { RelationshipProps } from "../../../neo4j/neo4j.helper.types"; +import { IsNumber } from "class-validator"; export class UpVotesProps implements RelationshipProps { - @ApiProperty({ type: Number }) + @IsNumber() upVotedAt: number; constructor(partial?: Partial) { diff --git a/src/users/models/toPost/authored.props.ts b/src/users/models/toPost/authored.props.ts index d0a07c5..7d020b6 100644 --- a/src/users/models/toPost/authored.props.ts +++ b/src/users/models/toPost/authored.props.ts @@ -1,11 +1,11 @@ -import { ApiProperty } from "@nestjs/swagger"; import { RelationshipProps } from "../../../neo4j/neo4j.helper.types"; +import { IsBoolean, IsNumber } from "class-validator"; export class AuthoredProps implements RelationshipProps { - @ApiProperty({ type: Number }) + @IsNumber() authoredAt: number; - @ApiProperty({ type: Boolean }) + @IsBoolean() anonymously: boolean; constructor(partial?: Partial) { diff --git a/src/users/models/toPost/favorites.props.ts b/src/users/models/toPost/favorites.props.ts index b478e13..27501ed 100644 --- a/src/users/models/toPost/favorites.props.ts +++ b/src/users/models/toPost/favorites.props.ts @@ -1,8 +1,8 @@ -import { ApiProperty } from "@nestjs/swagger"; import { RelationshipProps } from "../../../neo4j/neo4j.helper.types"; +import { IsNumber } from "class-validator"; export class FavoritesProps implements RelationshipProps { - @ApiProperty({ type: Number }) + @IsNumber() favoritedAt: number; constructor(partial?: Partial) { diff --git a/src/users/models/toPost/read.props.ts b/src/users/models/toPost/read.props.ts index a6f67be..287aa71 100644 --- a/src/users/models/toPost/read.props.ts +++ b/src/users/models/toPost/read.props.ts @@ -1,8 +1,8 @@ -import { ApiProperty } from "@nestjs/swagger"; import { RelationshipProps } from "../../../neo4j/neo4j.helper.types"; +import { IsNumber } from "class-validator"; export class ReadProps implements RelationshipProps { - @ApiProperty({ type: Number }) + @IsNumber() readAt: number; constructor(partial?: Partial) { diff --git a/src/users/models/toPost/reported.props.ts b/src/users/models/toPost/reported.props.ts index d3f9de5..69b841b 100644 --- a/src/users/models/toPost/reported.props.ts +++ b/src/users/models/toPost/reported.props.ts @@ -1,11 +1,14 @@ import { RelationshipProps } from "../../../neo4j/neo4j.helper.types"; -import { User } from "../user"; +import { IsNumber, IsString, IsUUID } from "class-validator"; export class ReportedProps implements RelationshipProps { - reportedBy: User; + @IsUUID() + moderatorId: string; + @IsNumber() reportedAt: number; + @IsString() reason: string; constructor(partial?: Partial) { diff --git a/src/users/models/toPost/vote.props.ts b/src/users/models/toPost/vote.props.ts index 7b6f2d8..1113f6d 100644 --- a/src/users/models/toPost/vote.props.ts +++ b/src/users/models/toPost/vote.props.ts @@ -1,8 +1,8 @@ -import { ApiProperty } from "@nestjs/swagger"; import { RelationshipProps } from "../../../neo4j/neo4j.helper.types"; +import { IsUUID } from "class-validator"; export class VoteProps implements RelationshipProps { - @ApiProperty({ type: Number }) + @IsUUID() votedAt: number; constructor(partial?: Partial) { diff --git a/src/users/models/toSelf/wasOffending.props.ts b/src/users/models/toSelf/wasOffending.props.ts index e56dfd5..f10e0c2 100644 --- a/src/users/models/toSelf/wasOffending.props.ts +++ b/src/users/models/toSelf/wasOffending.props.ts @@ -1,14 +1,14 @@ -import { ApiProperty } from "@nestjs/swagger"; import { RelationshipProps } from "../../../neo4j/neo4j.helper.types"; +import { IsNumber, IsString } from "class-validator"; export class WasOffendingProps implements RelationshipProps { - @ApiProperty({ type: Number }) + @IsNumber() timestamp: number; - @ApiProperty({ type: String }) + @IsString() userContent: string; - @ApiProperty({ type: Number }) + @IsNumber() autoModConfidenceLevel: number; constructor(partial?: Partial) { diff --git a/src/users/models/user.ts b/src/users/models/user.ts index cb22d73..a767b79 100644 --- a/src/users/models/user.ts +++ b/src/users/models/user.ts @@ -1,4 +1,3 @@ -import { ApiProperty } from "@nestjs/swagger"; import { Exclude } from "class-transformer"; import { Comment } from "../../comments/models"; import { Labels, NodeProperty } from "../../neo4j/neo4j.decorators"; @@ -20,77 +19,91 @@ import { UserToOpennessRelTypes } from "./toOpenness"; import { AuthoredProps, FavoritesProps, UserToPostRelTypes } from "./toPost"; import { UserToSelfRelTypes, WasOffendingProps } from "./toSelf"; import { UserToSexualityRelTypes } from "./toSexuality"; +import { + IsArray, + IsBoolean, + IsEnum, + IsInstance, + IsNumber, + IsOptional, + IsString, + IsUUID, +} from "class-validator"; export type AvatarUrl = string; export type AvatarAscii = string; @Labels("User") export class User extends Model { - @ApiProperty({ type: String, format: "uuid" }) @NodeProperty() + @IsUUID() userId: string; - @ApiProperty({ type: Date }) @NodeProperty() + @IsNumber() createdAt: number; - @ApiProperty({ type: Date }) @NodeProperty() + @IsNumber() updatedAt: number; - @ApiProperty({ type: String }) @NodeProperty() + @IsString() avatar: AvatarAscii | AvatarUrl; - @ApiProperty({ type: String }) @NodeProperty() + @IsString() email: string; - @ApiProperty({ type: Boolean }) @NodeProperty() + @IsBoolean() emailVerified: boolean; - @ApiProperty({ type: String }) @NodeProperty() + @IsString() + @IsOptional() phoneNumber: Nullable; - @ApiProperty({ type: Boolean }) @NodeProperty() + @IsBoolean() phoneNumberVerified: boolean; - @ApiProperty({ type: String }) @NodeProperty() + @IsString() username: string; - @ApiProperty({ type: String }) @NodeProperty() + @IsString() normalizedUsername: string; - @ApiProperty({ type: String }) @NodeProperty() + @IsString() @Exclude() passwordHash: string; - @ApiProperty({ type: Number }) @NodeProperty() + @IsNumber() level: number; - @ApiProperty({ type: Role }) + @IsEnum(Role) roles: Role[]; - @ApiProperty({ type: Post, isArray: true }) + @IsOptional() posts: RichRelatedEntities; - @ApiProperty({ type: Comment, isArray: true }) + @IsOptional() comments: RichRelatedEntities; - @ApiProperty({ type: Sexuality }) + @IsInstance(Sexuality) + @IsOptional() sexuality: Nullable; - @ApiProperty({ type: Gender }) + @IsInstance(Gender) + @IsOptional() gender: Nullable; - @ApiProperty({ type: Openness }) + @IsInstance(Openness) + @IsOptional() openness: Nullable; - @ApiProperty({ type: WasOffendingProps, isArray: true }) - @Exclude() + @IsArray() + @IsOptional() wasOffendingRecords: WasOffendingProps[] = []; constructor(partial?: Partial, neo4jService?: Neo4jService) { From 778dfae40135ce908c0afee3daf51c1270e09bd6 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sat, 26 Nov 2022 07:11:16 -0800 Subject: [PATCH 021/153] make sure neo4j session closes after running the query --- src/neo4j/services/neo4j.service.ts | 2 ++ src/users/models/toSelf/gotBanned.props.ts | 0 2 files changed, 2 insertions(+) create mode 100644 src/users/models/toSelf/gotBanned.props.ts diff --git a/src/neo4j/services/neo4j.service.ts b/src/neo4j/services/neo4j.service.ts index dfb5e93..bd4659d 100644 --- a/src/neo4j/services/neo4j.service.ts +++ b/src/neo4j/services/neo4j.service.ts @@ -54,6 +54,7 @@ export class Neo4jService { return await session.run(cypher, params); } catch (error) { this._logger.debug(error); + } finally { await session.close(); } } @@ -69,6 +70,7 @@ export class Neo4jService { return await session.run(cypher, params); } catch (error) { this._logger.debug(error); + } finally { await session.close(); } } diff --git a/src/users/models/toSelf/gotBanned.props.ts b/src/users/models/toSelf/gotBanned.props.ts new file mode 100644 index 0000000..e69de29 From d343b6bfeb250fbcc404beec073d79814d7fc2fe Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sat, 26 Nov 2022 07:11:39 -0800 Subject: [PATCH 022/153] add gotBanned.props --- src/users/models/toSelf/gotBanned.props.ts | 18 ++++++++++++++++++ src/users/models/toSelf/index.ts | 2 ++ 2 files changed, 20 insertions(+) diff --git a/src/users/models/toSelf/gotBanned.props.ts b/src/users/models/toSelf/gotBanned.props.ts index e69de29..d419994 100644 --- a/src/users/models/toSelf/gotBanned.props.ts +++ b/src/users/models/toSelf/gotBanned.props.ts @@ -0,0 +1,18 @@ +import { RelationshipProps } from "../../../neo4j/neo4j.helper.types"; +import { IsNotEmpty, IsNumber, IsString, IsUUID } from "class-validator"; + +export class GotBannedProps implements RelationshipProps { + @IsNumber() + bannedAt: number; + + @IsUUID() + moderatorId: string; + + @IsString() + @IsNotEmpty() + reason: string; + + constructor(partial?: Partial) { + Object.assign(this, partial); + } +} diff --git a/src/users/models/toSelf/index.ts b/src/users/models/toSelf/index.ts index 80e8e05..39fccd2 100644 --- a/src/users/models/toSelf/index.ts +++ b/src/users/models/toSelf/index.ts @@ -1,5 +1,7 @@ export { WasOffendingProps } from "./wasOffending.props"; +export { GotBannedProps } from "./gotBanned.props"; export enum UserToSelfRelTypes { WAS_OFFENDING = "WAS_OFFENDING", + GOT_BANNED = "GOT_BANNED", } From 0b4285b2c4e96df8826fabd052afd75a8d27f1bd Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sat, 26 Nov 2022 07:12:01 -0800 Subject: [PATCH 023/153] add getGotBannedProps method on user model --- src/users/models/user.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/users/models/user.ts b/src/users/models/user.ts index a767b79..76d5f20 100644 --- a/src/users/models/user.ts +++ b/src/users/models/user.ts @@ -17,7 +17,7 @@ import { AuthoredProps as UserToCommentAuthoredProps, UserToCommentRelTypes } fr import { UserToGenderRelTypes } from "./toGender"; import { UserToOpennessRelTypes } from "./toOpenness"; import { AuthoredProps, FavoritesProps, UserToPostRelTypes } from "./toPost"; -import { UserToSelfRelTypes, WasOffendingProps } from "./toSelf"; +import { GotBannedProps, UserToSelfRelTypes, WasOffendingProps } from "./toSelf"; import { UserToSexualityRelTypes } from "./toSexuality"; import { IsArray, @@ -106,6 +106,10 @@ export class User extends Model { @IsOptional() wasOffendingRecords: WasOffendingProps[] = []; + @IsInstance(GotBannedProps) + @IsOptional() + gotBannedProps: Nullable; + constructor(partial?: Partial, neo4jService?: Neo4jService) { super(neo4jService); Object.assign(this, partial); @@ -122,6 +126,24 @@ export class User extends Model { return { ...this }; } + public async getGotBannedProps(): Promise> { + const queryResult = await this.neo4jService.tryReadAsync( + ` + MATCH (u:User { userId: $userId})-[r:${UserToSelfRelTypes.GOT_BANNED}]-(u) + RETURN r + `, + { + userId: this.userId, + } + ); + if (queryResult.records.length === 0) { + this.gotBannedProps = null; + return null; + } + this.gotBannedProps = new GotBannedProps(queryResult.records[0].get("r").properties); + return this.gotBannedProps; + } + public async addWasOffendingRecord(record: WasOffendingProps): Promise { await this.neo4jService.tryWriteAsync( ` From 17050cce79b9991a37ca4a9d47eb7d5dc8dc5e57 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sat, 26 Nov 2022 07:13:41 -0800 Subject: [PATCH 024/153] add comments for a method --- src/users/models/user.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/users/models/user.ts b/src/users/models/user.ts index 76d5f20..f3d1d62 100644 --- a/src/users/models/user.ts +++ b/src/users/models/user.ts @@ -126,6 +126,11 @@ export class User extends Model { return { ...this }; } + /** + * Checks with the database if the user has a GOT_BANNED relationship, and if it has, it will get its properties + * and assigns it to the instance's .gotBannedProps property. + * If the user has no GOT_BANNED relationship, it will assign null to the instance's .gotBannedProps property. + */ public async getGotBannedProps(): Promise> { const queryResult = await this.neo4jService.tryReadAsync( ` From c38e73b0359ac2eb1a521977ea11f8eae767fd96 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sat, 26 Nov 2022 17:09:10 -0800 Subject: [PATCH 025/153] make sure that admin can do anything --- src/auth/guards/roles.guard.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/auth/guards/roles.guard.ts b/src/auth/guards/roles.guard.ts index 96a838e..bd172fa 100644 --- a/src/auth/guards/roles.guard.ts +++ b/src/auth/guards/roles.guard.ts @@ -23,6 +23,11 @@ export class RolesGuard implements CanActivate { return false; } + // If the user is an admin, they can do anything. + if (user.roles?.includes(Role.ADMIN)) { + return true; + } + return requiredRoles.some(role => user.roles?.includes(role)); } } From 9a695a80d5dd3b885d97cd5e3a819df053fc4149 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sat, 26 Nov 2022 17:25:24 -0800 Subject: [PATCH 026/153] add the class-validator decorator for the Neo4j Model class --- src/neo4j/neo4j.helper.types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/neo4j/neo4j.helper.types.ts b/src/neo4j/neo4j.helper.types.ts index fcf9b87..36f0fcb 100644 --- a/src/neo4j/neo4j.helper.types.ts +++ b/src/neo4j/neo4j.helper.types.ts @@ -1,5 +1,6 @@ import { Neo4jService } from "./services/neo4j.service"; import { Exclude } from "class-transformer"; +import { IsInstance } from "class-validator"; export class RelationshipProps {} @@ -26,6 +27,7 @@ export type RichRelatedEntities = { }; export class Model { + @IsInstance(Neo4jService) @Exclude() protected neo4jService: Neo4jService; From f0b6519c8084cad10692918731130461a2b728b1 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sat, 26 Nov 2022 17:25:58 -0800 Subject: [PATCH 027/153] add UUID as a type aliases of string --- src/global.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/global.d.ts b/src/global.d.ts index 7073af4..5667f36 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1 +1,3 @@ type Nullable = T | null; + +type UUID = string; From 8c1bb861ab792458d5cc29f924949ad7fa5e8e5d Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sat, 26 Nov 2022 17:26:49 -0800 Subject: [PATCH 028/153] make sure that post model will not write to the database --- src/posts/models/post.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/posts/models/post.ts b/src/posts/models/post.ts index 2f264f7..b419f76 100644 --- a/src/posts/models/post.ts +++ b/src/posts/models/post.ts @@ -231,21 +231,6 @@ export class Post extends Model { return result; } - public async setDeletedProps(deletedProps: DeletedProps): Promise { - await this.neo4jService.tryWriteAsync( - ` - MATCH (p:Post {postId: $postId}) - MERGE (p)-[r:${_ToSelfRelTypes.DELETED}]->(p) - SET r = $deletedProps - `, - { - postId: this.postId, - deletedProps, - } - ); - this.deletedProps = deletedProps; - } - public async getRestricted(): Promise> { const queryResult = await this.neo4jService.tryReadAsync( ` From 926f1a9818d742c57b1a96520826ce14b86834f6 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sat, 26 Nov 2022 17:27:46 -0800 Subject: [PATCH 029/153] .wasOffendingProps should not have default value. --- src/users/models/user.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/users/models/user.ts b/src/users/models/user.ts index f3d1d62..3e1c28e 100644 --- a/src/users/models/user.ts +++ b/src/users/models/user.ts @@ -104,7 +104,7 @@ export class User extends Model { @IsArray() @IsOptional() - wasOffendingRecords: WasOffendingProps[] = []; + wasOffendingRecords: WasOffendingProps[]; @IsInstance(GotBannedProps) @IsOptional() From e394431d60750db91b24acb2eed21b7d47fda9e5 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sat, 26 Nov 2022 17:33:21 -0800 Subject: [PATCH 030/153] prevent BOLA for /users/username/:username endpoint --- src/users/controllers/users.controller.ts | 53 ++++++++++++++++++++--- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/src/users/controllers/users.controller.ts b/src/users/controllers/users.controller.ts index ec62b5d..5a80a75 100644 --- a/src/users/controllers/users.controller.ts +++ b/src/users/controllers/users.controller.ts @@ -1,4 +1,5 @@ import { + Body, ClassSerializerInterceptor, Controller, Get, @@ -6,6 +7,7 @@ import { Inject, Param, ParseUUIDPipe, + Patch, UseGuards, UseInterceptors, } from "@nestjs/common"; @@ -16,18 +18,26 @@ import { RolesGuard } from "../../auth/guards/roles.guard"; import { _$ } from "../../_domain/injectableTokens"; import { Role, User } from "../models"; import { IUsersRepository } from "../repositories/users/users.repository.interface"; +import { ModerationPayloadDto } from "../../moderation/dtos/moderatorActions"; +import { AuthedUser } from "../../auth/decorators/authedUser.param.decorator"; +import { OptionalJwtAuthGuard } from "../../auth/guards/optionalJwtAuth.guard"; +import { PublicUserDto } from "../dtos"; @UseInterceptors(ClassSerializerInterceptor) @ApiTags("users") @ApiBearerAuth() @Controller("users") export class UsersController { - constructor(@Inject(_$.IUsersRepository) private _usersRepository: IUsersRepository) {} + private readonly _usersRepository: IUsersRepository; + + constructor(@Inject(_$.IUsersRepository) usersRepository: IUsersRepository) { + this._usersRepository = usersRepository; + } @Get() @Roles(Role.ADMIN) @UseGuards(AuthGuard("jwt"), RolesGuard) - public async index(): Promise { + public async index(): Promise { const users = await this._usersRepository.findAll(); const decoratedUsers = users.map(user => user.toJSON()); return await Promise.all(decoratedUsers); @@ -36,18 +46,47 @@ export class UsersController { @Get(":userId") @Roles(Role.ADMIN) @UseGuards(AuthGuard("jwt"), RolesGuard) - public async getUserById( - @Param("userId", new ParseUUIDPipe()) userId: string - ): Promise { + public async getUserById(@Param("userId", new ParseUUIDPipe()) userId: string): Promise { const user = await this._usersRepository.findUserById(userId); if (user === undefined) throw new HttpException("User not found", 404); return user; } @Get("/username/:username") - public async getUserByUsername(@Param("username") username: string): Promise { + @UseGuards(OptionalJwtAuthGuard) + public async getUserByUsername( + @Param("username") username: string, + @AuthedUser() authedUser?: User + ): Promise { const user = await this._usersRepository.findUserByUsername(username); if (user === undefined) throw new HttpException("User not found", 404); - return user; + + // if the found user is the same as the authed user, return the full user object + if (authedUser?.userId === user.userId) { + return await user.toJSON(); + } + return PublicUserDto.fromUser(user); + } + + @Patch("/ban/:userId") + @Roles(Role.MODERATOR) + @UseGuards(AuthGuard("jwt"), RolesGuard) + public async banUser( + @AuthedUser() authedUser: User, + @Body() moderationPayloadDto: ModerationPayloadDto + ): Promise { + moderationPayloadDto.moderatorId = authedUser.userId; + throw new HttpException("Not implemented", 501); + } + + @Patch("/unban/:userId") + @Roles(Role.MODERATOR) + @UseGuards(AuthGuard("jwt"), RolesGuard) + public async unbanUser( + @AuthedUser() authedUser: User, + @Body() moderationPayloadDto: ModerationPayloadDto + ): Promise { + moderationPayloadDto.moderatorId = authedUser.userId; + throw new HttpException("Not implemented", 501); } } From 768ca99f3f9521283ee8141cef3d390b9666e3a2 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sat, 26 Nov 2022 17:41:01 -0800 Subject: [PATCH 031/153] add moderation endpoints --- src/posts/controllers/posts.controller.ts | 72 +++++++++++++++++------ 1 file changed, 54 insertions(+), 18 deletions(-) diff --git a/src/posts/controllers/posts.controller.ts b/src/posts/controllers/posts.controller.ts index 54e011a..bbf56a5 100644 --- a/src/posts/controllers/posts.controller.ts +++ b/src/posts/controllers/posts.controller.ts @@ -4,6 +4,7 @@ import { CacheTTL, ClassSerializerInterceptor, Controller, + Delete, Get, HttpException, Inject, @@ -23,7 +24,11 @@ import { PostCreationPayloadDto, VotePostPayloadDto } from "../dtos"; import { IPostsService } from "../services/posts/posts.service.interface"; import { OptionalJwtAuthGuard } from "../../auth/guards/optionalJwtAuth.guard"; import { AuthedUser } from "../../auth/decorators/authedUser.param.decorator"; -import { User } from "../../users/models"; +import { Role, User } from "../../users/models"; +import { Roles } from "../../auth/decorators/roles.decorator"; +import { RolesGuard } from "../../auth/guards/roles.guard"; +import { ModerationPayloadDto } from "../../moderation/dtos/moderatorActions"; +import { IModeratorActionsService } from "../../moderation/services/moderatorActions/moderatorActions.service.interface"; @ApiTags("posts") @Controller("posts") @@ -32,20 +37,24 @@ import { User } from "../../users/models"; export class PostsController { private readonly _dbContext: DatabaseContext; private readonly _postsService: IPostsService; + private readonly _moderationActionsService: IModeratorActionsService; constructor( @Inject(_$.IDatabaseContext) dbContext: DatabaseContext, - @Inject(_$.IPostsService) postsService: IPostsService + @Inject(_$.IPostsService) postsService: IPostsService, + @Inject(_$.IModeratorActionsService) moderationActionsService: IModeratorActionsService ) { this._dbContext = dbContext; this._postsService = postsService; + this._moderationActionsService = moderationActionsService; } @Get() - @UseGuards(OptionalJwtAuthGuard) + @Roles(Role.MODERATOR) + @UseGuards(OptionalJwtAuthGuard, RolesGuard) @CacheTTL(5) @UseInterceptors(CacheInterceptor) - public async index(@AuthedUser() user: User): Promise { + public async index(@AuthedUser() user: User): Promise { const posts = await this._dbContext.Posts.findAll(); const decoratedPosts = posts.map(post => post.toJSON({ authenticatedUserId: user?.userId ?? undefined }) @@ -57,7 +66,7 @@ export class PostsController { @UseGuards(OptionalJwtAuthGuard) @CacheTTL(5) @UseInterceptors(CacheInterceptor) - public async getAllQueeries(@AuthedUser() user: User): Promise { + public async getAllQueeries(@AuthedUser() user: User): Promise { const queeries = await this._postsService.findAllQueeries(); const decoratedQueeries = queeries.map(queery => queery.toJSON({ authenticatedUserId: user?.userId ?? undefined }) @@ -69,7 +78,7 @@ export class PostsController { @UseGuards(OptionalJwtAuthGuard) @CacheTTL(5) @UseInterceptors(CacheInterceptor) - public async getAllStories(@AuthedUser() user: User): Promise { + public async getAllStories(@AuthedUser() user: User): Promise { const stories = await this._postsService.findAllStories(); const decoratedStories = stories.map(story => story.toJSON({ authenticatedUserId: user?.userId ?? undefined }) @@ -77,10 +86,10 @@ export class PostsController { return await Promise.all(decoratedStories); } - @Get(":postId/nestedComments") + @Get("/:postId/nestedComments") public async getNestedCommentsByPostId( - @Param("postId", new ParseUUIDPipe()) postId: string - ): Promise { + @Param("postId", new ParseUUIDPipe()) postId: UUID + ): Promise { const topLevelComments = await this._postsService.findNestedCommentsByPostId( postId, 10, @@ -91,30 +100,57 @@ export class PostsController { return await Promise.all(decoratedTopLevelComments); } - @Get(":postId") + @Get("/:postId") @UseGuards(OptionalJwtAuthGuard) public async getPostById( @AuthedUser() user: User, - @Param("postId", new ParseUUIDPipe()) postId: string - ): Promise { + @Param("postId", new ParseUUIDPipe()) postId: UUID + ): Promise { const post = await this._dbContext.Posts.findPostById(postId); if (post === undefined) throw new HttpException("Post not found", 404); return await post.toJSON({ authenticatedUserId: user?.userId ?? undefined }); } - @Post("create") + @Post("/create") @UseGuards(AuthGuard("jwt")) - public async createPost( - @Body() postPayload: PostCreationPayloadDto - ): Promise { + public async createPost(@Body() postPayload: PostCreationPayloadDto): Promise { const post = await this._postsService.authorNewPost(postPayload); return await post.toJSON(); } - @Post("vote") + @Delete("/") + @Roles(Role.MODERATOR) + @UseGuards(AuthGuard("jwt"), RolesGuard) + public async deletePost( + @AuthedUser() user: User, + @Body() deletePostPayload: ModerationPayloadDto + ): Promise { + deletePostPayload.moderatorId = user.userId; + await this._moderationActionsService.deletePost(deletePostPayload); + } + + @Post("/vote") @UseGuards(AuthGuard("jwt")) - public async votePost(@Body() votePostPayload: VotePostPayloadDto): Promise { + public async votePost(@Body() votePostPayload: VotePostPayloadDto): Promise { await this._postsService.votePost(votePostPayload); return; } + + @Post("/report") + @UseGuards(AuthGuard("jwt")) + public async reportPost( + @AuthedUser() user: User, + @Body() reportPayload: ModerationPayloadDto + ): Promise { + reportPayload.moderatorId = user.userId; + throw new Error("Not implemented"); + } + + @Post("/allow/:postId") + @Roles(Role.MODERATOR) + @UseGuards(AuthGuard("jwt"), RolesGuard) + public async allowPost(@Param("postId", new ParseUUIDPipe()) postId: UUID): Promise { + await this._moderationActionsService.allowPost(postId); + throw new Error("Not implemented"); + } } From 1f16bc31fc42c5a182974b4bf07093243f5d026a Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sat, 26 Nov 2022 17:41:32 -0800 Subject: [PATCH 032/153] add .getDeletedProps to the toJSON method --- src/posts/models/post.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/posts/models/post.ts b/src/posts/models/post.ts index b419f76..62cca83 100644 --- a/src/posts/models/post.ts +++ b/src/posts/models/post.ts @@ -103,6 +103,7 @@ export class Post extends Model { this.getPostTags(), this.getAwards(), this.getRestricted(), + this.getDeletedProps(), this.getCreatedAt(), this.getTotalVotes(), this.getAuthorUser(), From c2f023f6ef1d3ccf88ef7ed7c2d8a8e32660664c Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sat, 26 Nov 2022 17:44:50 -0800 Subject: [PATCH 033/153] remove the markAsDeleted method --- .../services/posts/posts.service.interface.ts | 2 -- src/posts/services/posts/posts.service.ts | 17 ----------------- 2 files changed, 19 deletions(-) diff --git a/src/posts/services/posts/posts.service.interface.ts b/src/posts/services/posts/posts.service.interface.ts index 454940e..82095d2 100644 --- a/src/posts/services/posts/posts.service.interface.ts +++ b/src/posts/services/posts/posts.service.interface.ts @@ -24,7 +24,5 @@ export interface IPostsService { getNestedComments(comments: Comment[], nestedLevel: number, nestedLimit: number): Promise; - markAsDeleted(postId: string): Promise; - votePost(votePostPayload: VotePostPayloadDto): Promise; } diff --git a/src/posts/services/posts/posts.service.ts b/src/posts/services/posts/posts.service.ts index 191cd0b..fe0fded 100644 --- a/src/posts/services/posts/posts.service.ts +++ b/src/posts/services/posts/posts.service.ts @@ -196,23 +196,6 @@ export class PostsService implements IPostsService { } } - public async markAsDeleted(postId: string): Promise { - const post = await this._dbContext.Posts.findPostById(postId); - if (post === undefined) throw new Error("Post not found"); - - await post.getDeletedProps(); - if (post.deletedProps !== null) throw new Error("Post already deleted"); - - await post.getAuthorUser(); - - await post.setDeletedProps( - new DeletedProps({ - deletedAt: new Date().getTime(), - moderatorId: post.authorUser.userId, - }) - ); - } - public async votePost(votePostPayload: VotePostPayloadDto): Promise { const user = this.getUserFromRequest(); From 94fbf2ab6006c8d2e2713e9a4ebfadf18d703484 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sat, 26 Nov 2022 17:45:17 -0800 Subject: [PATCH 034/153] revise the moderator actions --- .../moderatorActions.service.interface.ts | 68 +++++++ .../moderatorActions.service.ts | 183 +++++------------- 2 files changed, 114 insertions(+), 137 deletions(-) diff --git a/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts b/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts index db049f3..bb247fc 100644 --- a/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts +++ b/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts @@ -4,18 +4,86 @@ import { ModerationPayloadDto } from "../../dtos/moderatorActions"; import { User } from "../../../users/models"; export interface IModeratorActionsService { + /** + * Updates the pending status of a post, and makes it visible to the public. + * @param postId + */ allowPost(postId: string): Promise; + + /** + * Restricts a post by an adding a self relationship to the post. + * @param payload + */ restrictPost(payload: ModerationPayloadDto): Promise; + /** + * Removes the restriction of a post. It will remove the restriction self-relation from the post. + * If the post is already unrestricted, it will silently resolve the promise. + * Notes: + * * This method will not check if the end user has the permission to remove the restriction. + * * This method will give a http 404 if the post was not found. + * @param postId + */ + unrestrictPost(postId: string): Promise; + /** + * Updates the pending status of a comment, and makes it visible to the public. + * @param commentId + */ allowComment(commentId: string): Promise; + + /** + * Restricts a post by an adding a self relationship to the post. + * @param payload + */ restrictComment(payload: ModerationPayloadDto): Promise; + /** + * Removes the restriction of a comment. It will remove the restriction self-relation from the comment. + * If the comment is already unrestricted, it will silently resolve the promise. + * Notes: + * * This method will not check if the end user has the permission to remove the restriction. + * * This method will give a http 404 if the comment was not found. + * @param commentId + */ + unrestrictComment(commentId: string): Promise; + /** + * Adds the mark of "deleted" to the post. + * @param payload + */ deletePost(payload: ModerationPayloadDto): Promise; + /** + * Removes the mark of "deleted" from the post. + * @param postId + */ undeletePost(postId: string): Promise; + /** + * Adds the mark of "deleted" to the comment. + * @param payload + */ deleteComment(payload: ModerationPayloadDto): Promise; + /** + * Removes the mark of deleted from a comment. + * @param commentId + */ undeleteComment(commentId: string): Promise; + /** + * Bans a user. A banned user cannot post, comment, or vote. + * TODO: + * - Implement this. + * - Somehow add these actions to a history of actions records, so that it can be tracked and analyzed in the future. + * - This will be a stretch goal. For now, we will just ban the user by adding a self relationship to the user node. + * @param payload + */ banUser(payload: ModerationPayloadDto): Promise; + /** + * Unbans a user. + * TODO: + * - Implement this. + * - Somehow add these actions to a history of actions records, so that it can be tracked and analyzed in the future. + * - This will be a stretch goal. For now, we will just unban the user by removing the self relationship to the user node. + * @param userId + */ unbanUser(userId: string): Promise; } diff --git a/src/moderation/services/moderatorActions/moderatorActions.service.ts b/src/moderation/services/moderatorActions/moderatorActions.service.ts index 8d57e5d..19b9ffc 100644 --- a/src/moderation/services/moderatorActions/moderatorActions.service.ts +++ b/src/moderation/services/moderatorActions/moderatorActions.service.ts @@ -2,7 +2,7 @@ import { IModeratorActionsService } from "./moderatorActions.service.interface"; import { HttpException, Inject } from "@nestjs/common"; import { _$ } from "../../../_domain/injectableTokens"; import { DatabaseContext } from "../../../database-access-layer/databaseContext"; -import { _ToSelfRelTypes, DeletedProps, RestrictedProps } from "../../../_domain/models/toSelf"; +import { DeletedProps, RestrictedProps } from "../../../_domain/models/toSelf"; import { Comment } from "../../../comments/models"; import { Post } from "../../../posts/models"; import { ModerationPayloadDto } from "../../dtos/moderatorActions"; @@ -29,16 +29,7 @@ export class ModeratorActionsService implements IModeratorActionsService { throw new Error("Method not implemented."); } - /** - * @description - * This method is to remove the restriction of a comment. It will remove the restriction self-relation from the comment. - * If the comment is already unrestricted, it will silently resolve the promise. - * Notes: - * * This method will not check if the end user has the permission to remove the restriction. - * * This method will give a http 404 if the comment was not found. - * @param commentId - */ - public async allowComment(commentId: string): Promise { + public async unrestrictComment(commentId: string): Promise { const comment = await this.acquireComment(commentId); await comment.getRestricted(); @@ -46,50 +37,26 @@ export class ModeratorActionsService implements IModeratorActionsService { return comment; } - await this._dbContext.neo4jService.tryWriteAsync( - ` - MATCH (c:Comment { commentId: $commentId })-[r:${_ToSelfRelTypes.RESTRICTED}]->(c) - DELETE r - `, - { commentId } - ); + await this._dbContext.Comments.unrestrictComment(commentId); + comment.restrictedProps = null; return comment; } - /** - * @description - * This method is to remove the restriction of a post. It will remove the restriction self-relation from the post. - * If the post is already unrestricted, it will silently resolve the promise. - * Notes: - * * This method will not check if the end user has the permission to remove the restriction. - * * This method will give a http 404 if the post was not found. - * @param postId - */ - public async allowPost(postId: string): Promise { + public async unrestrictPost(postId: string): Promise { const post = await this.acquirePost(postId); await post.getRestricted(); if (!post.restrictedProps) { - return; + return post; } - await this._dbContext.neo4jService.tryWriteAsync( - ` - MATCH (p:Post { postId: $postId })-[r:${_ToSelfRelTypes.RESTRICTED}]->(p) - DELETE r - `, - { postId } - ); + await this._dbContext.Posts.unrestrictPost(postId); + post.restrictedProps = null; - return; + return post; } - /** - * @description - * This method will mark a comment as deleted. It will add a self-relation to the comment. - * @param payload - */ public async deleteComment(payload: ModerationPayloadDto): Promise { const comment = await this.acquireComment(payload.id); @@ -103,31 +70,12 @@ export class ModeratorActionsService implements IModeratorActionsService { moderatorId: payload.moderatorId, reason: payload.reason, }); - await this._dbContext.neo4jService.tryWriteAsync( - ` - MATCH (c:Comment { commentId: $commentId })-[r:${_ToSelfRelTypes.DELETED} { - deletedAt: $deletedAt, - moderatorId: $moderatorId, - reason: $reason - }]->(c) - `, - { - commentId: payload.id, - deletedAt: deletedProps.deletedAt, - moderatorId: deletedProps.moderatorId, - reason: deletedProps.reason, - } - ); + await this._dbContext.Comments.markAsDeleted(payload.id, deletedProps); comment.deletedProps = deletedProps; return comment; } - /** - * @description - * This method will mark a post as deleted. It will add a self-relation to the post. - * @param payload - */ public async deletePost(payload: ModerationPayloadDto): Promise { const post = await this.acquirePost(payload.id); @@ -141,30 +89,12 @@ export class ModeratorActionsService implements IModeratorActionsService { moderatorId: payload.moderatorId, reason: payload.reason, }); - await this._dbContext.neo4jService.tryWriteAsync( - ` - MATCH (p:Post { postId: $postId })-[r:${_ToSelfRelTypes.DELETED} { - deletedAt: $deletedAt, - deletedByUserId: $deletedByUserId - }]->(p) - `, - { - postId: payload.id, - deletedAt: deletedProps.deletedAt, - moderatorId: deletedProps.moderatorId, - reason: deletedProps.reason, - } - ); + await this._dbContext.Posts.markAsDeleted(payload.id, deletedProps); post.deletedProps = deletedProps; return post; } - /** - * @description - * This method will mark a comment as restricted. It will add a self-relation to the comment. - * @param payload - */ public async restrictComment(payload: ModerationPayloadDto): Promise { const comment = await this.acquireComment(payload.id); @@ -178,31 +108,12 @@ export class ModeratorActionsService implements IModeratorActionsService { moderatorId: payload.moderatorId, reason: payload.moderatorId, }); - await this._dbContext.neo4jService.tryWriteAsync( - ` - MATCH (c:Comment { commentId: $commentId })-[r:${_ToSelfRelTypes.RESTRICTED} { - restrictedAt: $restrictedAt, - moderatorId: $moderatorId, - reason: $reason - }]->(c) - `, - { - commentId: payload.id, - restrictedAt: restrictedProps.restrictedAt, - moderatorId: restrictedProps.moderatorId, - reason: restrictedProps.reason, - } - ); + await this._dbContext.Comments.restrictComment(payload.id, restrictedProps); comment.restrictedProps = restrictedProps; return comment; } - /** - * @description - * This method will mark a post as restricted. It will add a self-relation to the post. - * @param payload - */ public async restrictPost(payload: ModerationPayloadDto): Promise { const post = await this.acquirePost(payload.id); @@ -216,31 +127,12 @@ export class ModeratorActionsService implements IModeratorActionsService { moderatorId: payload.moderatorId, reason: payload.moderatorId, }); - await this._dbContext.neo4jService.tryWriteAsync( - ` - MATCH (p:Post postId: $postId })-[r:${_ToSelfRelTypes.RESTRICTED} { - restrictedAt: $restrictedAt, - moderatorId: $moderatorId, - reason: $reason - }]->(p) - `, - { - postId: payload.id, - restrictedAt: restrictedProps.restrictedAt, - moderatorId: restrictedProps.moderatorId, - reason: restrictedProps.reason, - } - ); + await this._dbContext.Posts.restrictPost(payload.id, restrictedProps); post.restrictedProps = restrictedProps; return post; } - /** - * @description - * This method will mark a comment as undeleted. It will remove the self-relation from the comment. - * @param commentId - */ public async undeleteComment(commentId: string): Promise { const comment = await this.acquireComment(commentId); @@ -249,23 +141,12 @@ export class ModeratorActionsService implements IModeratorActionsService { return comment; } - await this._dbContext.neo4jService.tryWriteAsync( - ` - MATCH (c:Comment { commentId: $commentId })-[r:${_ToSelfRelTypes.DELETED}]->(c) - DELETE r - `, - { commentId } - ); + await this._dbContext.Comments.removeDeletedMark(commentId); comment.deletedProps = null; return comment; } - /** - * @description - * This method will mark a post as undeleted. It will remove the self-relation from the post. - * @param postId - */ public async undeletePost(postId: string): Promise { const post = await this.acquirePost(postId); @@ -274,14 +155,42 @@ export class ModeratorActionsService implements IModeratorActionsService { return post; } + await this._dbContext.Posts.removeDeletedMark(postId); + post.deletedProps = null; + + return post; + } + + public async allowComment(commentId: string): Promise { + const comment = await this.acquireComment(commentId); + + if (!comment.pending) { + return comment; + } + + comment.pending = false; + await this._dbContext.Comments.updateComment(comment); + + return comment; + } + + public async allowPost(postId: string): Promise { + const post = await this.acquirePost(postId); + + if (!post.pending) { + return post; + } + await this._dbContext.neo4jService.tryWriteAsync( ` - MATCH (p:Post { postId: $postId })-[r:${_ToSelfRelTypes.DELETED}]->(p) - DELETE r + MATCH (p:Post { postId: $postId }) + SET p.pending = false `, - { postId } + { + postId, + } ); - post.deletedProps = null; + post.pending = false; return post; } From 72f39d9cf4a8c04e23e92fc4c0b2bfe0bd1098a1 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sat, 26 Nov 2022 17:46:16 -0800 Subject: [PATCH 035/153] add mark as delete methods to the post repository * fix the update method's updatedAt property value --- .../post/posts.repository.interface.ts | 6 +++- .../repositories/post/posts.repository.ts | 30 +++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/posts/repositories/post/posts.repository.interface.ts b/src/posts/repositories/post/posts.repository.interface.ts index d5e0f64..cf23a6f 100644 --- a/src/posts/repositories/post/posts.repository.interface.ts +++ b/src/posts/repositories/post/posts.repository.interface.ts @@ -1,5 +1,5 @@ import { Post } from "../../models"; -import { RestrictedProps } from "../../../_domain/models/toSelf"; +import { DeletedProps, RestrictedProps } from "../../../_domain/models/toSelf"; export interface IPostsRepository { findAll(): Promise; @@ -14,6 +14,10 @@ export interface IPostsRepository { deletePost(postId: string): Promise; + markAsDeleted(postId: string, deletedProps: DeletedProps): Promise; + + removeDeletedMark(postId: string): Promise; + restrictPost(postId: string, restrictedProps: RestrictedProps): Promise; unrestrictPost(postId: string): Promise; diff --git a/src/posts/repositories/post/posts.repository.ts b/src/posts/repositories/post/posts.repository.ts index 8f34590..1a71058 100644 --- a/src/posts/repositories/post/posts.repository.ts +++ b/src/posts/repositories/post/posts.repository.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from "@nestjs/common"; import { IPostsRepository } from "./posts.repository.interface"; import { Neo4jService } from "../../../neo4j/services/neo4j.service"; -import { _ToSelfRelTypes, RestrictedProps } from "../../../_domain/models/toSelf"; +import { _ToSelfRelTypes, DeletedProps, RestrictedProps } from "../../../_domain/models/toSelf"; import { PostToPostTypeRelTypes } from "../../models/toPostType"; import { PostToPostTagRelTypes } from "../../models/toTags"; import { HasAwardProps, PostToAwardRelTypes } from "../../models/toAward"; @@ -147,7 +147,7 @@ export class PostsRepository implements IPostsRepository { `, { postId: post.postId, - updatedAt: post.updatedAt, + updatedAt: Date.now(), postTitle: post.postTitle, postContent: post.postContent, pending: post.pending, @@ -161,6 +161,32 @@ export class PostsRepository implements IPostsRepository { }); } + public async markAsDeleted(postId: string, deletedProps: DeletedProps): Promise { + await this._neo4jService.tryWriteAsync( + ` + MATCH (p:Post {postId: $postId}) + MERGE (p)-[r:${_ToSelfRelTypes.DELETED}]->(p) + SET r = $deletedProps + `, + { + postId, + deletedProps, + } + ); + } + + public async removeDeletedMark(postId: string): Promise { + await this._neo4jService.tryWriteAsync( + ` + MATCH (p:Post {postId: $postId})-[r:${_ToSelfRelTypes.DELETED}]->(p) + DELETE r + `, + { + postId, + } + ); + } + public async restrictPost(postId: string, restrictedProps: RestrictedProps): Promise { await this._neo4jService.tryWriteAsync( `MATCH (p:Post) WHERE p.postId = $postId From 195313610a32bb9f244f6e98ea42c3a112e5ca26 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sat, 26 Nov 2022 17:46:49 -0800 Subject: [PATCH 036/153] add a few moderation related endpoints --- .../controllers/comments.controller.ts | 60 ++++++++++++++----- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/src/comments/controllers/comments.controller.ts b/src/comments/controllers/comments.controller.ts index bdcf5a0..55bf5ed 100644 --- a/src/comments/controllers/comments.controller.ts +++ b/src/comments/controllers/comments.controller.ts @@ -4,6 +4,7 @@ import { CacheTTL, ClassSerializerInterceptor, Controller, + Delete, Get, HttpException, Inject, @@ -11,7 +12,7 @@ import { ParseUUIDPipe, Post, UseGuards, - UseInterceptors + UseInterceptors, } from "@nestjs/common"; import { AuthGuard } from "@nestjs/passport"; import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; @@ -22,7 +23,11 @@ import { CommentCreationPayloadDto, VoteCommentPayloadDto } from "../dtos"; import { ICommentsService } from "../services/comments/comments.service.interface"; import { OptionalJwtAuthGuard } from "../../auth/guards/optionalJwtAuth.guard"; import { AuthedUser } from "../../auth/decorators/authedUser.param.decorator"; -import { User } from "../../users/models"; +import { Role, User } from "../../users/models"; +import { RolesGuard } from "../../auth/guards/roles.guard"; +import { Roles } from "../../auth/decorators/roles.decorator"; +import { ModerationPayloadDto } from "../../moderation/dtos/moderatorActions"; +import { IModeratorActionsService } from "../../moderation/services/moderatorActions/moderatorActions.service.interface"; @ApiTags("comments") @Controller("comments") @@ -31,20 +36,23 @@ import { User } from "../../users/models"; export class CommentsController { private readonly _dbContext: DatabaseContext; private readonly _commentsService: ICommentsService; + private readonly _moderatorActionsService: IModeratorActionsService; constructor( @Inject(_$.IDatabaseContext) dbContext: DatabaseContext, - @Inject(_$.ICommentsService) commentsService: ICommentsService + @Inject(_$.ICommentsService) commentsService: ICommentsService, + @Inject(_$.IModeratorActionsService) moderatorActionsService: IModeratorActionsService ) { this._dbContext = dbContext; this._commentsService = commentsService; + this._moderatorActionsService = moderatorActionsService; } @Get() @CacheTTL(4) @UseGuards(OptionalJwtAuthGuard) @UseInterceptors(CacheInterceptor) - public async index(@AuthedUser() user: User): Promise { + public async index(@AuthedUser() user: User): Promise { const comments = await this._dbContext.Comments.findAll(); const decoratedComments = comments.map(comment => comment.toJSON({ authenticatedUserId: user?.userId ?? undefined }) @@ -56,8 +64,8 @@ export class CommentsController { @UseGuards(OptionalJwtAuthGuard) public async getNestedComments( @AuthedUser() user: User, - @Param("commentId", new ParseUUIDPipe()) commentId: string - ): Promise { + @Param("commentId", new ParseUUIDPipe()) commentId: UUID + ): Promise { const comments = await this._commentsService.findNestedCommentsByCommentId( commentId, 10, @@ -74,17 +82,18 @@ export class CommentsController { @UseGuards(OptionalJwtAuthGuard) public async getCommentById( @AuthedUser() user: User, - @Param("commentId", new ParseUUIDPipe()) commentId: string + @Param("commentId", new ParseUUIDPipe()) commentId: UUID ): Promise { const comment = await this._dbContext.Comments.findCommentById(commentId); if (comment === undefined) throw new HttpException("Comment not found", 404); return await comment.toJSON({ authenticatedUserId: user?.userId ?? undefined }); } - // pin comment @Post(":commentId/pin") @UseGuards(AuthGuard("jwt")) - public async pinComment(@Param("commentId", new ParseUUIDPipe()) commentId: string): Promise { + public async pinComment( + @Param("commentId", new ParseUUIDPipe()) commentId: UUID + ): Promise { await this._commentsService.markAsPinned(commentId); } @@ -92,17 +101,40 @@ export class CommentsController { @UseGuards(AuthGuard("jwt")) public async createComment( @Body() commentPayload: CommentCreationPayloadDto - ): Promise { + ): Promise { const comment = await this._commentsService.authorNewComment(commentPayload); return await comment.toJSON(); } + @Delete("/") + @Roles(Role.MODERATOR) + @UseGuards(AuthGuard("jwt"), RolesGuard) + public async deleteComment( + @AuthedUser() user: User, + @Body() moderationPayload: ModerationPayloadDto + ): Promise { + moderationPayload.moderatorId = user.userId; + await this._moderatorActionsService.deleteComment(moderationPayload); + } + @Post("vote") @UseGuards(AuthGuard("jwt")) - public async voteComment( - @Body() voteCommentPayload: VoteCommentPayloadDto - ): Promise { + public async voteComment(@Body() voteCommentPayload: VoteCommentPayloadDto): Promise { await this._commentsService.voteComment(voteCommentPayload); - return; + } + + @Post("/report") + @UseGuards(AuthGuard("jwt")) + public async reportComment(@Body() reportCommentPayload: VoteCommentPayloadDto): Promise { + throw new Error("Method not implemented."); + } + + @Post("/allow/:commentId") + @Roles(Role.MODERATOR) + @UseGuards(AuthGuard("jwt"), RolesGuard) + public async allowComment( + @Param("commentId", new ParseUUIDPipe()) commentId: UUID + ): Promise { + await this._moderatorActionsService.allowComment(commentId); } } From a025b5da9b47b5e7bd10247f8efefbca45d63796 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sat, 26 Nov 2022 17:47:30 -0800 Subject: [PATCH 037/153] add markAsDeleted methods and update method --- .../comment/comments.repository.interface.ts | 7 ++- .../comment/comments.repository.ts | 46 ++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/comments/repositories/comment/comments.repository.interface.ts b/src/comments/repositories/comment/comments.repository.interface.ts index 3665416..9b049df 100644 --- a/src/comments/repositories/comment/comments.repository.interface.ts +++ b/src/comments/repositories/comment/comments.repository.interface.ts @@ -1,4 +1,4 @@ -import { RestrictedProps } from "../../../_domain/models/toSelf"; +import { DeletedProps, RestrictedProps } from "../../../_domain/models/toSelf"; import { Comment } from "../../models"; export interface ICommentsRepository { @@ -6,6 +6,8 @@ export interface ICommentsRepository { findCommentById(commentId: string): Promise; + updateComment(comment: Comment): Promise; + addCommentToComment(comment: Comment): Promise; addCommentToPost(comment: Comment): Promise; @@ -15,4 +17,7 @@ export interface ICommentsRepository { restrictComment(commentId: string, restrictedProps: RestrictedProps): Promise; unrestrictComment(commentId: string): Promise; + + markAsDeleted(commentId: string, deletedProps: DeletedProps): Promise; + removeDeletedMark(commentId: string): Promise; } diff --git a/src/comments/repositories/comment/comments.repository.ts b/src/comments/repositories/comment/comments.repository.ts index 9806699..0eaf0c2 100644 --- a/src/comments/repositories/comment/comments.repository.ts +++ b/src/comments/repositories/comment/comments.repository.ts @@ -1,11 +1,12 @@ import { Inject, Injectable } from "@nestjs/common"; import { Neo4jService } from "../../../neo4j/services/neo4j.service"; import { AuthoredProps, UserToCommentRelTypes } from "../../../users/models/toComment"; -import { RestrictedProps, _ToSelfRelTypes } from "../../../_domain/models/toSelf"; +import { RestrictedProps, _ToSelfRelTypes, DeletedProps } from "../../../_domain/models/toSelf"; import { Comment } from "../../models"; import { CommentToSelfRelTypes } from "../../models/toSelf"; import { ICommentsRepository } from "./comments.repository.interface"; import { PostToCommentRelTypes } from "../../../posts/models/toComment"; +import { Post } from "../../../posts/models"; @Injectable() export class CommentsRepository implements ICommentsRepository { @@ -27,6 +28,23 @@ export class CommentsRepository implements ICommentsRepository { return new Comment(comment.records[0].get("c").properties, this._neo4jService); } + public async updateComment(comment: Comment): Promise { + await this._neo4jService.tryWriteAsync( + ` + MATCH (c:Comment) WHERE c.commentId = $commentId + SET c.updatedAt = $updatedAt, + p.commentContent = $commentContent, + p.pending = $pending + `, + { + commentId: comment.commentId, + updatedAt: Date.now(), + commentContent: comment.commentContent, + pending: comment.pending, + } + ); + } + public async addCommentToComment(comment: Comment): Promise { if (comment.commentId === undefined) { comment.commentId = this._neo4jService.generateId(); @@ -179,4 +197,30 @@ export class CommentsRepository implements ICommentsRepository { { commentId: commentId } ); } + + public async markAsDeleted(commentId: UUID, deletedProps: DeletedProps): Promise { + await this._neo4jService.tryWriteAsync( + ` + MATCH (c:Comment {commentId: $commentId}) + MERGE (c)-[r:${_ToSelfRelTypes.DELETED}]->(c) + SET r = $deletedProps + `, + { + commentId, + deletedProps, + } + ); + } + + public async removeDeletedMark(commentId: UUID): Promise { + await this._neo4jService.tryWriteAsync( + ` + MATCH (c:Comment {commentId: $commentId})-[r:${_ToSelfRelTypes.DELETED}]->(c) + DELETE r + `, + { + commentId, + } + ); + } } From f98ebeda6aebcb0fab1ed73cb5cafe2157c332e6 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sat, 26 Nov 2022 17:48:07 -0800 Subject: [PATCH 038/153] revise the property types and class-validator decorators --- .../dtos/moderatorActions/moderationPayload.dto.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/moderation/dtos/moderatorActions/moderationPayload.dto.ts b/src/moderation/dtos/moderatorActions/moderationPayload.dto.ts index 2800e58..277d0f8 100644 --- a/src/moderation/dtos/moderatorActions/moderationPayload.dto.ts +++ b/src/moderation/dtos/moderatorActions/moderationPayload.dto.ts @@ -1,14 +1,16 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsNotEmpty, IsString, IsUUID } from "class-validator"; +import { IsNotEmpty, IsOptional, IsString, IsUUID } from "class-validator"; +import { Exclude } from "class-transformer"; export class ModerationPayloadDto { @ApiProperty({ type: String, format: "uuid" }) @IsUUID() - id: string; + id: UUID; - @ApiProperty({ type: String }) @IsUUID() - moderatorId: string; + @IsOptional() + @Exclude() + moderatorId: UUID; @ApiProperty({ type: String }) @IsString() From 3f2b452381fd2611a8bb94f43aefcbee43660b9c Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sat, 26 Nov 2022 17:48:31 -0800 Subject: [PATCH 039/153] remove markAsDeleted method --- .../comments/comments.service.interface.ts | 8 ++--- .../services/comments/comments.service.ts | 31 ++----------------- 2 files changed, 5 insertions(+), 34 deletions(-) diff --git a/src/comments/services/comments/comments.service.interface.ts b/src/comments/services/comments/comments.service.interface.ts index 4ed4e11..f716264 100644 --- a/src/comments/services/comments/comments.service.interface.ts +++ b/src/comments/services/comments/comments.service.interface.ts @@ -4,10 +4,10 @@ import { VoteCommentPayloadDto, CommentCreationPayloadDto } from "../../dtos"; export interface ICommentsService { authorNewComment(commentPayload: CommentCreationPayloadDto): Promise; - findCommentById(commentId: string): Promise; + findCommentById(commentId: UUID): Promise; findNestedCommentsByCommentId( - commentId: string, + commentId: UUID, topLevelLimit: number, nestedLimit: number, nestedLevel: number @@ -15,7 +15,5 @@ export interface ICommentsService { voteComment(votePayload: VoteCommentPayloadDto): Promise; - markAsPinned(commentId: string): Promise; - - markAsDeleted(commentId: string): Promise; + markAsPinned(commentId: UUID): Promise; } diff --git a/src/comments/services/comments/comments.service.ts b/src/comments/services/comments/comments.service.ts index 7d34301..6dcedd9 100644 --- a/src/comments/services/comments/comments.service.ts +++ b/src/comments/services/comments/comments.service.ts @@ -203,27 +203,6 @@ export class CommentsService implements ICommentsService { ); } - public async markAsDeleted(commentId: string): Promise { - const comment = await this._dbContext.Comments.findCommentById(commentId); - if (!comment) { - throw new HttpException("Comment not found", 404); - } - - await comment.getDeletedProps(); - if (comment.deletedProps) { - throw new HttpException("Comment was already deleted", 400); - } - - await comment.getAuthorUser(); - - await comment.setDeletedProps( - new DeletedProps({ - deletedAt: new Date().getTime(), - moderatorId: comment.authorUser.userId, - }) - ); - } - // gets the parent post of any nested comment of the post private async findParentPost(commentId: string): Promise { const parentPost = await this._dbContext.neo4jService.tryReadAsync( @@ -253,10 +232,7 @@ export class CommentsService implements ICommentsService { commentId, } ); - if (queryResult.records.length > 0) { - return true; - } - return false; + return queryResult.records.length > 0; } // gets the root comment of any nested comment @@ -294,10 +270,7 @@ export class CommentsService implements ICommentsService { postId: post.postId, } ); - if (queryResult.records.length > 0) { - return true; - } - return false; + return queryResult.records.length > 0; } private getUserFromRequest(): User { From 64ddbf5a007c402c5872cb370f3351f5d9eacd38 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sat, 26 Nov 2022 17:54:41 -0800 Subject: [PATCH 040/153] add JwtTokenPayloadDto --- src/auth/dtos/jwtTokenPayload.dto.ts | 13 +++++++++++++ src/auth/services/auth.service.ts | 3 ++- src/auth/strategy/jwt.strategy.ts | 3 ++- 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 src/auth/dtos/jwtTokenPayload.dto.ts diff --git a/src/auth/dtos/jwtTokenPayload.dto.ts b/src/auth/dtos/jwtTokenPayload.dto.ts new file mode 100644 index 0000000..61b7a53 --- /dev/null +++ b/src/auth/dtos/jwtTokenPayload.dto.ts @@ -0,0 +1,13 @@ +import { IsString, IsUUID } from "class-validator"; + +export class JwtTokenPayloadDto { + @IsUUID() + sub: UUID; + + @IsString() + username: string; + + constructor(partials?: Partial) { + Object.assign(this, partials); + } +} diff --git a/src/auth/services/auth.service.ts b/src/auth/services/auth.service.ts index c6e13e4..2595536 100644 --- a/src/auth/services/auth.service.ts +++ b/src/auth/services/auth.service.ts @@ -7,6 +7,7 @@ import { SignInPayloadDto, SignTokenDto, SignUpPayloadDto } from "../dtos"; import { IAuthService } from "./auth.service.interface"; import { User } from "../../users/models"; import { _$ } from "../../_domain/injectableTokens"; +import { JwtTokenPayloadDto } from "../dtos/jwtTokenPayload.dto"; @Injectable({}) export class AuthService implements IAuthService { @@ -67,7 +68,7 @@ export class AuthService implements IAuthService { } private async signToken(user: User): Promise { - const payload = { + const payload: JwtTokenPayloadDto = { sub: user.userId, username: user.username, }; diff --git a/src/auth/strategy/jwt.strategy.ts b/src/auth/strategy/jwt.strategy.ts index 6fa4dbb..96cbabc 100644 --- a/src/auth/strategy/jwt.strategy.ts +++ b/src/auth/strategy/jwt.strategy.ts @@ -4,6 +4,7 @@ import { PassportStrategy } from "@nestjs/passport"; import { ExtractJwt, Strategy } from "passport-jwt"; import { IUsersRepository } from "../../users/repositories/users/users.repository.interface"; import { _$ } from "../../_domain/injectableTokens"; +import { JwtTokenPayloadDto } from "../dtos/jwtTokenPayload.dto"; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { @@ -17,7 +18,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) { }); } - async validate(payload: any) { + async validate(payload: JwtTokenPayloadDto) { return await this._usersRepository.findUserByUsername(payload.username); } } From 90ef452d93ea4153982f83b70b334e7acc383b97 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sat, 26 Nov 2022 18:05:43 -0800 Subject: [PATCH 041/153] convert any uuid string type to UUID type --- src/_domain/models/toSelf/deleted.props.ts | 2 +- src/_domain/models/toSelf/restricted.props.ts | 2 +- .../dtos/commentCreationPayload.dto.ts | 2 +- .../dtos/hateSpeechRequestPayload.dto.ts | 13 ------------- src/comments/dtos/hateSpeechResponse.dto.ts | 16 ---------------- src/comments/dtos/index.ts | 2 -- src/comments/dtos/voteCommentPayload.dto.ts | 2 +- src/comments/models/comment.ts | 4 ++-- .../comment/comments.repository.interface.ts | 12 ++++++------ .../comment/comments.repository.ts | 10 +++------- .../services/comments/comments.service.ts | 8 ++++---- .../moderatorActions.service.interface.ts | 14 +++++++------- .../moderatorActions.service.ts | 18 +++++++++--------- src/posts/dtos/deletePostPayload.dto.ts | 4 ++-- src/posts/dtos/reportPostPayload.dto.ts | 2 +- src/posts/dtos/votePostPayload.dto.ts | 2 +- src/posts/models/award.ts | 3 +-- src/posts/models/post.ts | 5 ++--- src/posts/models/toAward/hasAward.props.ts | 2 +- src/users/controllers/users.controller.ts | 2 +- src/users/dtos/publicUser.dto.ts | 2 +- src/users/models/gender.ts | 6 +++--- src/users/models/openness.ts | 2 +- src/users/models/sexuality.ts | 3 +-- src/users/models/toComment/reported.props.ts | 2 +- src/users/models/toPost/reported.props.ts | 2 +- src/users/models/toPost/vote.props.ts | 4 ++-- src/users/models/toSelf/gotBanned.props.ts | 2 +- src/users/models/user.ts | 2 +- .../gender/gender.repository.interface.ts | 4 ++-- .../repositories/gender/gender.repository.ts | 4 ++-- .../openness/openness.repository.interface.ts | 4 ++-- .../openness/openness.repository.ts | 4 ++-- .../sexuality.repository.interface.ts | 4 ++-- .../sexuality/sexuality.repository.ts | 4 ++-- .../users/users.repository.interface.ts | 4 ++-- .../repositories/users/users.repository.ts | 4 ++-- 37 files changed, 72 insertions(+), 110 deletions(-) delete mode 100644 src/comments/dtos/hateSpeechRequestPayload.dto.ts delete mode 100644 src/comments/dtos/hateSpeechResponse.dto.ts diff --git a/src/_domain/models/toSelf/deleted.props.ts b/src/_domain/models/toSelf/deleted.props.ts index 11186c5..fcfed40 100644 --- a/src/_domain/models/toSelf/deleted.props.ts +++ b/src/_domain/models/toSelf/deleted.props.ts @@ -6,7 +6,7 @@ export class DeletedProps implements RelationshipProps { deletedAt: number; @IsUUID() - moderatorId: string; + moderatorId: UUID; @IsString() reason: string; diff --git a/src/_domain/models/toSelf/restricted.props.ts b/src/_domain/models/toSelf/restricted.props.ts index fc751db..8373768 100644 --- a/src/_domain/models/toSelf/restricted.props.ts +++ b/src/_domain/models/toSelf/restricted.props.ts @@ -6,7 +6,7 @@ export class RestrictedProps implements RelationshipProps { restrictedAt: number; @ApiProperty({ type: String }) - moderatorId: string; + moderatorId: UUID; @ApiProperty({ type: String }) reason: string; diff --git a/src/comments/dtos/commentCreationPayload.dto.ts b/src/comments/dtos/commentCreationPayload.dto.ts index 659a51d..742f5b8 100644 --- a/src/comments/dtos/commentCreationPayload.dto.ts +++ b/src/comments/dtos/commentCreationPayload.dto.ts @@ -10,7 +10,7 @@ export class CommentCreationPayloadDto { @ApiProperty({ type: String, format: "uuid" }) @IsString() @IsNotEmpty() - parentId: string; + parentId: UUID; @ApiProperty({ type: Boolean }) @IsBoolean() diff --git a/src/comments/dtos/hateSpeechRequestPayload.dto.ts b/src/comments/dtos/hateSpeechRequestPayload.dto.ts deleted file mode 100644 index 4fed731..0000000 --- a/src/comments/dtos/hateSpeechRequestPayload.dto.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { IsString } from "class-validator"; - -export class HateSpeechRequestPayloadDto { - @IsString() - token: string; - - @IsString() - text: string; - - constructor(partial?: Partial) { - Object.assign(this, partial); - } -} diff --git a/src/comments/dtos/hateSpeechResponse.dto.ts b/src/comments/dtos/hateSpeechResponse.dto.ts deleted file mode 100644 index 7776aa7..0000000 --- a/src/comments/dtos/hateSpeechResponse.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { IsNumber, IsString } from "class-validator"; - -export class HateSpeechResponseDto { - @IsString() - response: string; - - @IsString() - class: string; - - @IsNumber() - confidence: number; - - constructor(partial?: Partial) { - Object.assign(this, partial); - } -} diff --git a/src/comments/dtos/index.ts b/src/comments/dtos/index.ts index e63a19b..596fc89 100644 --- a/src/comments/dtos/index.ts +++ b/src/comments/dtos/index.ts @@ -1,4 +1,2 @@ export { CommentCreationPayloadDto } from "./commentCreationPayload.dto"; -export { HateSpeechRequestPayloadDto } from "./hateSpeechRequestPayload.dto"; -export { HateSpeechResponseDto } from "./hateSpeechResponse.dto"; export { VoteCommentPayloadDto } from "./voteCommentPayload.dto"; diff --git a/src/comments/dtos/voteCommentPayload.dto.ts b/src/comments/dtos/voteCommentPayload.dto.ts index 48c4193..694017e 100644 --- a/src/comments/dtos/voteCommentPayload.dto.ts +++ b/src/comments/dtos/voteCommentPayload.dto.ts @@ -6,7 +6,7 @@ export class VoteCommentPayloadDto { @ApiProperty({ type: String, format: "uuid" }) @IsNotEmpty() @IsUUID() - commentId: string; + commentId: UUID; @ApiProperty({ enum: VoteType }) @IsNotEmpty() diff --git a/src/comments/models/comment.ts b/src/comments/models/comment.ts index ba26d7c..18bee8f 100644 --- a/src/comments/models/comment.ts +++ b/src/comments/models/comment.ts @@ -24,7 +24,7 @@ import { export class Comment extends Model { @NodeProperty() @IsUUID() - commentId: string; + commentId: UUID; /** * The time the comment was created. Its value will be derived from the relationship @@ -40,7 +40,7 @@ export class Comment extends Model { @IsUUID() @IsOptional() - parentId: Nullable; + parentId: Nullable; @IsBoolean() pinned: boolean; diff --git a/src/comments/repositories/comment/comments.repository.interface.ts b/src/comments/repositories/comment/comments.repository.interface.ts index 9b049df..2084a3b 100644 --- a/src/comments/repositories/comment/comments.repository.interface.ts +++ b/src/comments/repositories/comment/comments.repository.interface.ts @@ -4,7 +4,7 @@ import { Comment } from "../../models"; export interface ICommentsRepository { findAll(): Promise; - findCommentById(commentId: string): Promise; + findCommentById(commentId: UUID): Promise; updateComment(comment: Comment): Promise; @@ -12,12 +12,12 @@ export interface ICommentsRepository { addCommentToPost(comment: Comment): Promise; - deleteComment(commentId: string): Promise; + deleteComment(commentId: UUID): Promise; - restrictComment(commentId: string, restrictedProps: RestrictedProps): Promise; + restrictComment(commentId: UUID, restrictedProps: RestrictedProps): Promise; - unrestrictComment(commentId: string): Promise; + unrestrictComment(commentId: UUID): Promise; - markAsDeleted(commentId: string, deletedProps: DeletedProps): Promise; - removeDeletedMark(commentId: string): Promise; + markAsDeleted(commentId: UUID, deletedProps: DeletedProps): Promise; + removeDeletedMark(commentId: UUID): Promise; } diff --git a/src/comments/repositories/comment/comments.repository.ts b/src/comments/repositories/comment/comments.repository.ts index 0eaf0c2..53e7999 100644 --- a/src/comments/repositories/comment/comments.repository.ts +++ b/src/comments/repositories/comment/comments.repository.ts @@ -6,7 +6,6 @@ import { Comment } from "../../models"; import { CommentToSelfRelTypes } from "../../models/toSelf"; import { ICommentsRepository } from "./comments.repository.interface"; import { PostToCommentRelTypes } from "../../../posts/models/toComment"; -import { Post } from "../../../posts/models"; @Injectable() export class CommentsRepository implements ICommentsRepository { @@ -164,17 +163,14 @@ export class CommentsRepository implements ICommentsRepository { return await this.findCommentById(comment.commentId); } - public async deleteComment(commentId: string): Promise { + public async deleteComment(commentId: UUID): Promise { await this._neo4jService.tryWriteAsync( `MATCH (c:Comment { commentId: $commentId }) DETACH DELETE c`, { commentId: commentId } ); } - public async restrictComment( - commentId: string, - restrictedProps: RestrictedProps - ): Promise { + public async restrictComment(commentId: UUID, restrictedProps: RestrictedProps): Promise { await this._neo4jService.tryWriteAsync( `MATCH (c:Comment { commentId: $commentId }) CREATE (c)-[:${_ToSelfRelTypes.RESTRICTED} { @@ -191,7 +187,7 @@ export class CommentsRepository implements ICommentsRepository { ); } - public async unrestrictComment(commentId: string): Promise { + public async unrestrictComment(commentId: UUID): Promise { await this._neo4jService.tryWriteAsync( `MATCH (c:Comment { commentId: $commentId })-[r:${_ToSelfRelTypes.RESTRICTED}]->(c) DELETE r`, { commentId: commentId } diff --git a/src/comments/services/comments/comments.service.ts b/src/comments/services/comments/comments.service.ts index 6dcedd9..eaec346 100644 --- a/src/comments/services/comments/comments.service.ts +++ b/src/comments/services/comments/comments.service.ts @@ -68,7 +68,7 @@ export class CommentsService implements ICommentsService { ); } - public async findCommentById(commentId: string): Promise { + public async findCommentById(commentId: UUID): Promise { const foundComment = await this._dbContext.Comments.findCommentById(commentId); if (!foundComment) { throw new HttpException("Comment not found", 404); @@ -173,7 +173,7 @@ export class CommentsService implements ICommentsService { ); } - public async markAsPinned(commentId: string): Promise { + public async markAsPinned(commentId: UUID): Promise { const comment = await this._dbContext.Comments.findCommentById(commentId); if (!comment) throw new HttpException("Comment not found", 404); @@ -204,7 +204,7 @@ export class CommentsService implements ICommentsService { } // gets the parent post of any nested comment of the post - private async findParentPost(commentId: string): Promise { + private async findParentPost(commentId: UUID): Promise { const parentPost = await this._dbContext.neo4jService.tryReadAsync( ` MATCH (p:Post)-[:${PostToCommentRelTypes.HAS_COMMENT}]->(c:Comment { commentId: $commentId }) @@ -222,7 +222,7 @@ export class CommentsService implements ICommentsService { } // gets the parent comment of any nested comment of the post - private async findComment(commentId: string): Promise { + private async findComment(commentId: UUID): Promise { const queryResult = await this._dbContext.neo4jService.tryReadAsync( ` MATCH (c:Comment { commentId: $commentId })-[:${CommentToSelfRelTypes.REPLIED}]->(commentParent:Comment) diff --git a/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts b/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts index bb247fc..3df3ee0 100644 --- a/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts +++ b/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts @@ -8,7 +8,7 @@ export interface IModeratorActionsService { * Updates the pending status of a post, and makes it visible to the public. * @param postId */ - allowPost(postId: string): Promise; + allowPost(postId: UUID): Promise; /** * Restricts a post by an adding a self relationship to the post. @@ -23,13 +23,13 @@ export interface IModeratorActionsService { * * This method will give a http 404 if the post was not found. * @param postId */ - unrestrictPost(postId: string): Promise; + unrestrictPost(postId: UUID): Promise; /** * Updates the pending status of a comment, and makes it visible to the public. * @param commentId */ - allowComment(commentId: string): Promise; + allowComment(commentId: UUID): Promise; /** * Restricts a post by an adding a self relationship to the post. @@ -44,7 +44,7 @@ export interface IModeratorActionsService { * * This method will give a http 404 if the comment was not found. * @param commentId */ - unrestrictComment(commentId: string): Promise; + unrestrictComment(commentId: UUID): Promise; /** * Adds the mark of "deleted" to the post. @@ -55,7 +55,7 @@ export interface IModeratorActionsService { * Removes the mark of "deleted" from the post. * @param postId */ - undeletePost(postId: string): Promise; + undeletePost(postId: UUID): Promise; /** * Adds the mark of "deleted" to the comment. @@ -66,7 +66,7 @@ export interface IModeratorActionsService { * Removes the mark of deleted from a comment. * @param commentId */ - undeleteComment(commentId: string): Promise; + undeleteComment(commentId: UUID): Promise; /** * Bans a user. A banned user cannot post, comment, or vote. @@ -85,5 +85,5 @@ export interface IModeratorActionsService { * - This will be a stretch goal. For now, we will just unban the user by removing the self relationship to the user node. * @param userId */ - unbanUser(userId: string): Promise; + unbanUser(userId: UUID): Promise; } diff --git a/src/moderation/services/moderatorActions/moderatorActions.service.ts b/src/moderation/services/moderatorActions/moderatorActions.service.ts index 19b9ffc..c8258bc 100644 --- a/src/moderation/services/moderatorActions/moderatorActions.service.ts +++ b/src/moderation/services/moderatorActions/moderatorActions.service.ts @@ -25,11 +25,11 @@ export class ModeratorActionsService implements IModeratorActionsService { public async banUser(payload: ModerationPayloadDto): Promise { throw new Error("Method not implemented."); } - public async unbanUser(userId: string): Promise { + public async unbanUser(userId: UUID): Promise { throw new Error("Method not implemented."); } - public async unrestrictComment(commentId: string): Promise { + public async unrestrictComment(commentId: UUID): Promise { const comment = await this.acquireComment(commentId); await comment.getRestricted(); @@ -43,7 +43,7 @@ export class ModeratorActionsService implements IModeratorActionsService { return comment; } - public async unrestrictPost(postId: string): Promise { + public async unrestrictPost(postId: UUID): Promise { const post = await this.acquirePost(postId); await post.getRestricted(); @@ -133,7 +133,7 @@ export class ModeratorActionsService implements IModeratorActionsService { return post; } - public async undeleteComment(commentId: string): Promise { + public async undeleteComment(commentId: UUID): Promise { const comment = await this.acquireComment(commentId); await comment.getDeletedProps(); @@ -147,7 +147,7 @@ export class ModeratorActionsService implements IModeratorActionsService { return comment; } - public async undeletePost(postId: string): Promise { + public async undeletePost(postId: UUID): Promise { const post = await this.acquirePost(postId); await post.getDeletedProps(); @@ -161,7 +161,7 @@ export class ModeratorActionsService implements IModeratorActionsService { return post; } - public async allowComment(commentId: string): Promise { + public async allowComment(commentId: UUID): Promise { const comment = await this.acquireComment(commentId); if (!comment.pending) { @@ -174,7 +174,7 @@ export class ModeratorActionsService implements IModeratorActionsService { return comment; } - public async allowPost(postId: string): Promise { + public async allowPost(postId: UUID): Promise { const post = await this.acquirePost(postId); if (!post.pending) { @@ -201,7 +201,7 @@ export class ModeratorActionsService implements IModeratorActionsService { * @param commentId * @private */ - private async acquireComment(commentId: string): Promise { + private async acquireComment(commentId: UUID): Promise { const comment = await this._dbContext.Comments.findCommentById(commentId); if (!comment) { throw new HttpException("Comment not found", 404); @@ -215,7 +215,7 @@ export class ModeratorActionsService implements IModeratorActionsService { * @param postId * @private */ - private async acquirePost(postId: string): Promise { + private async acquirePost(postId: UUID): Promise { const post = await this._dbContext.Posts.findPostById(postId); if (!post) { throw new HttpException("Post not found", 404); diff --git a/src/posts/dtos/deletePostPayload.dto.ts b/src/posts/dtos/deletePostPayload.dto.ts index befbd35..7c23200 100644 --- a/src/posts/dtos/deletePostPayload.dto.ts +++ b/src/posts/dtos/deletePostPayload.dto.ts @@ -2,9 +2,9 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsUUID } from "class-validator"; export class DeletePostPayloadDto { - @ApiProperty({ type: String }) + @ApiProperty({ type: String, format: "uuid" }) @IsUUID() - postId: string; + postId: UUID; constructor(partial?: Partial) { Object.assign(this, partial); diff --git a/src/posts/dtos/reportPostPayload.dto.ts b/src/posts/dtos/reportPostPayload.dto.ts index 8ad0d2a..c232f14 100644 --- a/src/posts/dtos/reportPostPayload.dto.ts +++ b/src/posts/dtos/reportPostPayload.dto.ts @@ -2,7 +2,7 @@ import { ApiProperty } from "@nestjs/swagger"; export class ReportPostPayloadDto { @ApiProperty({ type: String, format: "uuid" }) - postId: string; + postId: UUID; @ApiProperty({ type: String, minLength: 5, maxLength: 500 }) reason: string; diff --git a/src/posts/dtos/votePostPayload.dto.ts b/src/posts/dtos/votePostPayload.dto.ts index c83775b..5e6743f 100644 --- a/src/posts/dtos/votePostPayload.dto.ts +++ b/src/posts/dtos/votePostPayload.dto.ts @@ -6,7 +6,7 @@ export class VotePostPayloadDto { @ApiProperty({ type: String, format: "uuid" }) @IsNotEmpty() @IsUUID() - postId: string; + postId: UUID; @ApiProperty({ enum: VoteType }) @IsNotEmpty() diff --git a/src/posts/models/award.ts b/src/posts/models/award.ts index 9e7c05b..4203909 100644 --- a/src/posts/models/award.ts +++ b/src/posts/models/award.ts @@ -1,4 +1,3 @@ -import { ApiProperty } from "@nestjs/swagger"; import { Labels, NodeProperty } from "../../neo4j/neo4j.decorators"; import { IsString, IsUUID } from "class-validator"; @@ -6,7 +5,7 @@ import { IsString, IsUUID } from "class-validator"; export class Award { @NodeProperty() @IsUUID() - awardId: string; + awardId: UUID; @NodeProperty() @IsString() diff --git a/src/posts/models/post.ts b/src/posts/models/post.ts index 62cca83..b28ea6e 100644 --- a/src/posts/models/post.ts +++ b/src/posts/models/post.ts @@ -1,4 +1,3 @@ -import { ApiProperty } from "@nestjs/swagger"; import { Labels, NodeProperty } from "../../neo4j/neo4j.decorators"; import { Model, @@ -13,7 +12,7 @@ import { Neo4jService } from "../../neo4j/services/neo4j.service"; import { PostToPostTypeRelTypes } from "./toPostType"; import { PostToPostTagRelTypes } from "./toTags"; import { AuthoredProps, UserToPostRelTypes } from "../../users/models/toPost"; -import { Exclude, Type } from "class-transformer"; +import { Type } from "class-transformer"; import { PublicUserDto } from "../../users/dtos"; import { PostToCommentRelTypes } from "./toComment"; import { Comment } from "../../comments/models"; @@ -34,7 +33,7 @@ import { export class Post extends Model { @NodeProperty() @IsUUID() - postId: string; + postId: UUID; @IsInstance(PostType) @IsOptional() diff --git a/src/posts/models/toAward/hasAward.props.ts b/src/posts/models/toAward/hasAward.props.ts index b132145..206444f 100644 --- a/src/posts/models/toAward/hasAward.props.ts +++ b/src/posts/models/toAward/hasAward.props.ts @@ -3,7 +3,7 @@ import { IsUUID } from "class-validator"; export class HasAwardProps implements RelationshipProps { @IsUUID() - awardedBy: string; + awardedBy: UUID; constructor(partial?: Partial) { Object.assign(this, partial); diff --git a/src/users/controllers/users.controller.ts b/src/users/controllers/users.controller.ts index 5a80a75..5afc329 100644 --- a/src/users/controllers/users.controller.ts +++ b/src/users/controllers/users.controller.ts @@ -68,7 +68,7 @@ export class UsersController { return PublicUserDto.fromUser(user); } - @Patch("/ban/:userId") + @Patch("/ban") @Roles(Role.MODERATOR) @UseGuards(AuthGuard("jwt"), RolesGuard) public async banUser( diff --git a/src/users/dtos/publicUser.dto.ts b/src/users/dtos/publicUser.dto.ts index 1055667..e978f5a 100644 --- a/src/users/dtos/publicUser.dto.ts +++ b/src/users/dtos/publicUser.dto.ts @@ -6,7 +6,7 @@ import { AvatarAscii, AvatarUrl } from "../models/user"; export class PublicUserDto { @ApiProperty({ type: String, format: "uuid" }) @IsNotEmpty() - userId: string; + userId: UUID; @ApiProperty({ type: String }) @IsNotEmpty() diff --git a/src/users/models/gender.ts b/src/users/models/gender.ts index 0cbc2d0..9604dfc 100644 --- a/src/users/models/gender.ts +++ b/src/users/models/gender.ts @@ -1,11 +1,11 @@ import { Labels, NodeProperty } from "../../neo4j/neo4j.decorators"; -import { IsNumber, IsString } from "class-validator"; +import { IsString, IsUUID } from "class-validator"; @Labels("Gender") export class Gender { @NodeProperty() - @IsNumber() - genderId: string; + @IsUUID() + genderId: UUID; @NodeProperty() @IsString() diff --git a/src/users/models/openness.ts b/src/users/models/openness.ts index b94614a..6614ad9 100644 --- a/src/users/models/openness.ts +++ b/src/users/models/openness.ts @@ -5,7 +5,7 @@ import { IsNumber, IsString, IsUUID } from "class-validator"; export class Openness { @NodeProperty() @IsUUID() - opennessId: string; + opennessId: UUID; @NodeProperty() @IsNumber() diff --git a/src/users/models/sexuality.ts b/src/users/models/sexuality.ts index 403906c..f7c49fc 100644 --- a/src/users/models/sexuality.ts +++ b/src/users/models/sexuality.ts @@ -1,4 +1,3 @@ -import { ApiProperty } from "@nestjs/swagger"; import { Labels, NodeProperty } from "../../neo4j/neo4j.decorators"; import { IsString, IsUUID } from "class-validator"; @@ -6,7 +5,7 @@ import { IsString, IsUUID } from "class-validator"; export class Sexuality { @NodeProperty() @IsUUID() - sexualityId: string; + sexualityId: UUID; @NodeProperty() @IsString() diff --git a/src/users/models/toComment/reported.props.ts b/src/users/models/toComment/reported.props.ts index 69b841b..557cce7 100644 --- a/src/users/models/toComment/reported.props.ts +++ b/src/users/models/toComment/reported.props.ts @@ -3,7 +3,7 @@ import { IsNumber, IsString, IsUUID } from "class-validator"; export class ReportedProps implements RelationshipProps { @IsUUID() - moderatorId: string; + moderatorId: UUID; @IsNumber() reportedAt: number; diff --git a/src/users/models/toPost/reported.props.ts b/src/users/models/toPost/reported.props.ts index 69b841b..557cce7 100644 --- a/src/users/models/toPost/reported.props.ts +++ b/src/users/models/toPost/reported.props.ts @@ -3,7 +3,7 @@ import { IsNumber, IsString, IsUUID } from "class-validator"; export class ReportedProps implements RelationshipProps { @IsUUID() - moderatorId: string; + moderatorId: UUID; @IsNumber() reportedAt: number; diff --git a/src/users/models/toPost/vote.props.ts b/src/users/models/toPost/vote.props.ts index 1113f6d..c209c03 100644 --- a/src/users/models/toPost/vote.props.ts +++ b/src/users/models/toPost/vote.props.ts @@ -1,8 +1,8 @@ import { RelationshipProps } from "../../../neo4j/neo4j.helper.types"; -import { IsUUID } from "class-validator"; +import { IsNumber } from "class-validator"; export class VoteProps implements RelationshipProps { - @IsUUID() + @IsNumber() votedAt: number; constructor(partial?: Partial) { diff --git a/src/users/models/toSelf/gotBanned.props.ts b/src/users/models/toSelf/gotBanned.props.ts index d419994..406df2d 100644 --- a/src/users/models/toSelf/gotBanned.props.ts +++ b/src/users/models/toSelf/gotBanned.props.ts @@ -6,7 +6,7 @@ export class GotBannedProps implements RelationshipProps { bannedAt: number; @IsUUID() - moderatorId: string; + moderatorId: UUID; @IsString() @IsNotEmpty() diff --git a/src/users/models/user.ts b/src/users/models/user.ts index 3e1c28e..c6aa1e9 100644 --- a/src/users/models/user.ts +++ b/src/users/models/user.ts @@ -37,7 +37,7 @@ export type AvatarAscii = string; export class User extends Model { @NodeProperty() @IsUUID() - userId: string; + userId: UUID; @NodeProperty() @IsNumber() diff --git a/src/users/repositories/gender/gender.repository.interface.ts b/src/users/repositories/gender/gender.repository.interface.ts index 08f7021..16498dc 100644 --- a/src/users/repositories/gender/gender.repository.interface.ts +++ b/src/users/repositories/gender/gender.repository.interface.ts @@ -3,11 +3,11 @@ import { Gender } from "../../models"; export interface IGenderRepository { findAll(): Promise; - findGenderById(genderId: string): Promise; + findGenderById(genderId: UUID): Promise; addGender(gender: Gender): Promise; updateGender(gender: Gender): Promise; - deleteGender(genderId: string): Promise; + deleteGender(genderId: UUID): Promise; } diff --git a/src/users/repositories/gender/gender.repository.ts b/src/users/repositories/gender/gender.repository.ts index c535513..de0cf8e 100644 --- a/src/users/repositories/gender/gender.repository.ts +++ b/src/users/repositories/gender/gender.repository.ts @@ -14,7 +14,7 @@ export class GenderRepository implements IGenderRepository { return records.map(record => new Gender(record.get("g").properties)); } - public async findGenderById(genderId: string): Promise { + public async findGenderById(genderId: UUID): Promise { const gender = await this._neo4jService.read( `MATCH (g:Gender) WHERE g.genderId = $genderId RETURN g`, { genderId: genderId } @@ -72,7 +72,7 @@ export class GenderRepository implements IGenderRepository { ); } - public async deleteGender(genderId: string): Promise { + public async deleteGender(genderId: UUID): Promise { await this._neo4jService.tryWriteAsync( ` MATCH (g:Gender) WHERE g.genderId = $genderId diff --git a/src/users/repositories/openness/openness.repository.interface.ts b/src/users/repositories/openness/openness.repository.interface.ts index ed8ed51..8736ba2 100644 --- a/src/users/repositories/openness/openness.repository.interface.ts +++ b/src/users/repositories/openness/openness.repository.interface.ts @@ -3,11 +3,11 @@ import { Openness } from "../../models"; export interface IOpennessRepository { findAll(): Promise; - findOpennessById(opennessId: string): Promise; + findOpennessById(opennessId: UUID): Promise; addOpenness(openness: Openness): Promise; updateOpenness(openness: Openness): Promise; - deleteOpenness(opennessId: string): Promise; + deleteOpenness(opennessId: UUID): Promise; } diff --git a/src/users/repositories/openness/openness.repository.ts b/src/users/repositories/openness/openness.repository.ts index 8014e72..070bdc4 100644 --- a/src/users/repositories/openness/openness.repository.ts +++ b/src/users/repositories/openness/openness.repository.ts @@ -14,7 +14,7 @@ export class OpennessRepository implements IOpennessRepository { return records.map(record => new Openness(record.get("o").properties)); } - public async findOpennessById(opennessId: string): Promise { + public async findOpennessById(opennessId: UUID): Promise { const openness = await this._neo4jService.read( `MATCH (o:Openness) WHERE o.opennessId = $opennessId RETURN o`, { opennessId: opennessId } @@ -66,7 +66,7 @@ export class OpennessRepository implements IOpennessRepository { ); } - public async deleteOpenness(opennessId: string): Promise { + public async deleteOpenness(opennessId: UUID): Promise { await this._neo4jService.tryWriteAsync( ` MATCH (o:Openness) WHERE o.opennessId = $opennessId diff --git a/src/users/repositories/sexuality/sexuality.repository.interface.ts b/src/users/repositories/sexuality/sexuality.repository.interface.ts index c44b85b..2368979 100644 --- a/src/users/repositories/sexuality/sexuality.repository.interface.ts +++ b/src/users/repositories/sexuality/sexuality.repository.interface.ts @@ -3,11 +3,11 @@ import { Sexuality } from "../../models"; export interface ISexualityRepository { findAll(): Promise; - findSexualityById(sexualityId: string): Promise; + findSexualityById(sexualityId: UUID): Promise; addSexuality(sexuality: Sexuality): Promise; updateSexuality(sexuality: Sexuality): Promise; - deleteSexuality(sexualityId: string): Promise; + deleteSexuality(sexualityId: UUID): Promise; } diff --git a/src/users/repositories/sexuality/sexuality.repository.ts b/src/users/repositories/sexuality/sexuality.repository.ts index 2de25d1..e06225b 100644 --- a/src/users/repositories/sexuality/sexuality.repository.ts +++ b/src/users/repositories/sexuality/sexuality.repository.ts @@ -14,7 +14,7 @@ export class SexualityRepository implements ISexualityRepository { return records.map(record => new Sexuality(record.get("s").properties)); } - public async findSexualityById(sexualityId: string): Promise { + public async findSexualityById(sexualityId: UUID): Promise { const sexuality = await this._neo4jService.read( `MATCH (s:Sexuality) WHERE s.sexualityId = $sexualityId RETURN s`, { sexualityId: sexualityId } @@ -66,7 +66,7 @@ export class SexualityRepository implements ISexualityRepository { ); } - public async deleteSexuality(sexualityId: string): Promise { + public async deleteSexuality(sexualityId: UUID): Promise { await this._neo4jService.tryWriteAsync( ` MATCH (s:Sexuality) WHERE s.sexualityId = $sexualityId diff --git a/src/users/repositories/users/users.repository.interface.ts b/src/users/repositories/users/users.repository.interface.ts index ccabd8e..86a3be2 100644 --- a/src/users/repositories/users/users.repository.interface.ts +++ b/src/users/repositories/users/users.repository.interface.ts @@ -7,11 +7,11 @@ export interface IUsersRepository { findUserByEmail(email: string): Promise; - findUserById(userId: string): Promise; + findUserById(userId: UUID): Promise; addUser(user: User): Promise; updateUser(user: User): Promise; - deleteUser(userId: string): Promise; + deleteUser(userId: UUID): Promise; } diff --git a/src/users/repositories/users/users.repository.ts b/src/users/repositories/users/users.repository.ts index 1e14892..990428f 100644 --- a/src/users/repositories/users/users.repository.ts +++ b/src/users/repositories/users/users.repository.ts @@ -41,7 +41,7 @@ export class UsersRepository implements IUsersRepository { return new User(props, this._neo4jService); } - public async findUserById(userId: string): Promise { + public async findUserById(userId: UUID): Promise { const queryResult = await this._neo4jService.read( `MATCH (u:User {userId: $userId}) RETURN u`, { @@ -175,7 +175,7 @@ export class UsersRepository implements IUsersRepository { ); } - public async deleteUser(userId: string): Promise { + public async deleteUser(userId: UUID): Promise { await this._neo4jService.tryWriteAsync(`MATCH (u:User {userId: $userId}) DETACH DELETE u`, { userId: userId, }); From 9790b7af733e2d6e86743c19b3eee766bea74034 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sat, 26 Nov 2022 18:14:18 -0800 Subject: [PATCH 042/153] add .getDeletedProps to Comment.toJSON --- src/comments/models/comment.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/comments/models/comment.ts b/src/comments/models/comment.ts index 18bee8f..f811b9d 100644 --- a/src/comments/models/comment.ts +++ b/src/comments/models/comment.ts @@ -89,6 +89,7 @@ export class Comment extends Model { if (this.neo4jService) { await Promise.all([ this.getRestricted(), + this.getDeletedProps(), this.getCreatedAt(), this.getTotalVotes(), this.getAuthorUser(), From 46883d1b43540d73481a0f06df4d5dd1069ddbd4 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sat, 26 Nov 2022 20:04:35 -0800 Subject: [PATCH 043/153] properly decorate a single post once requested --- src/posts/controllers/posts.controller.ts | 3 +-- src/posts/services/posts/posts.service.ts | 15 +++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/posts/controllers/posts.controller.ts b/src/posts/controllers/posts.controller.ts index bbf56a5..3fb0e3b 100644 --- a/src/posts/controllers/posts.controller.ts +++ b/src/posts/controllers/posts.controller.ts @@ -106,8 +106,7 @@ export class PostsController { @AuthedUser() user: User, @Param("postId", new ParseUUIDPipe()) postId: UUID ): Promise { - const post = await this._dbContext.Posts.findPostById(postId); - if (post === undefined) throw new HttpException("Post not found", 404); + const post = await this._postsService.findPostById(postId); return await post.toJSON({ authenticatedUserId: user?.userId ?? undefined }); } diff --git a/src/posts/services/posts/posts.service.ts b/src/posts/services/posts/posts.service.ts index fe0fded..19e146a 100644 --- a/src/posts/services/posts/posts.service.ts +++ b/src/posts/services/posts/posts.service.ts @@ -133,19 +133,18 @@ export class PostsService implements IPostsService { } public async findPostById(postId: string): Promise { - const foundPost = await this._dbContext.Posts.findPostById(postId); + let foundPost = await this._dbContext.Posts.findPostById(postId); if (foundPost === null) throw new HttpException("Post not found", 404); if (foundPost.pending) - throw new HttpException("Post cannot be shown publicly due to striking policies", 403); - - await foundPost.getDeletedProps(); - if (foundPost.deletedProps !== null) throw new HttpException("Post was deleted", 404); + throw new HttpException( + "Post cannot be shown publicly at the moment. please try again later.", + 403 + ); - await foundPost.getRestricted(); - if (foundPost.restrictedProps !== null) throw new HttpException("Post is restricted", 404); + foundPost = (await this.decoratePosts([foundPost], null))[0]; - return await foundPost.toJSON(); + return foundPost; } public async findNestedCommentsByPostId( From 06a6907c42a3a897644355387e0aa8d57047f310 Mon Sep 17 00:00:00 2001 From: Sean Ng Date: Sat, 26 Nov 2022 21:21:53 -0800 Subject: [PATCH 044/153] Update neo4j.seed.service.ts --- src/neo4j/services/neo4j.seed.service.ts | 185 ++++++++++------------- 1 file changed, 76 insertions(+), 109 deletions(-) diff --git a/src/neo4j/services/neo4j.seed.service.ts b/src/neo4j/services/neo4j.seed.service.ts index 379db10..bce8f12 100644 --- a/src/neo4j/services/neo4j.seed.service.ts +++ b/src/neo4j/services/neo4j.seed.service.ts @@ -427,34 +427,34 @@ export class Neo4jSeedService { return new Array( new User({ - userId: "5c0f145b-ffad-4881-8ee6-7647c3c1b695", + userId: "c612c987-6825-473c-882f-129f17d906b4", createdAt: new Date().getTime(), updatedAt: new Date().getTime(), avatar: ":^)", - username: "alice", - normalizedUsername: "ALICE", - passwordHash: "password", + username: "gabriel", + normalizedUsername: "GABRIEL", + passwordHash: "someotherpassword", phoneNumber: null, phoneNumberVerified: false, - email: "a@a.com", + email: "email@domain.com", emailVerified: false, level: 0, - roles: [Role.MODERATOR], + roles: [Role.USER], gender: new Gender({ genderId: "d2945763-d1fb-46aa-b896-7f701b4ca699", }), sexuality: new Sexuality({ - sexualityId: "1b67cf76-752d-4ea5-9584-a4232998b838", + sexualityId: "55da84cc-5f17-454a-a653-227458763edb", }), openness: new Openness({ - opennessId: "d5c97584-cd1b-4aa6-82ad-b5ddd3577bee", + opennessId: "c8921055-563f-4a54-8773-b408efcfb7ac", }), posts: { [UserToPostRelTypes.AUTHORED]: { records: (await this.getPosts()).slice(0, 2).map(post => ({ entity: post, relProps: new AuthoredProps({ - authoredAt: new Date("June 1st, 2022").getTime(), + authoredAt: new Date("10/11/2022").getTime(), anonymously: false, }), })), @@ -468,30 +468,30 @@ export class Neo4jSeedService { createdAt: new Date().getTime(), updatedAt: new Date().getTime(), avatar: "^_^", - username: "leo", - normalizedUsername: "LEO", - passwordHash: "123", + username: "alphonse", + normalizedUsername: "ALPHONSE", + passwordHash: "num3r1ca1pa55w0rd", phoneNumber: null, phoneNumberVerified: false, - email: "b@b.com", + email: "admin@domain.com", emailVerified: false, level: 0, - roles: [Role.USER, Role.MODERATOR], + roles: [Role.USER], gender: new Gender({ genderId: "585d31aa-d5b3-4b8d-9690-ffcd57ce2862", }), sexuality: new Sexuality({ - sexualityId: "9164d89b-8d71-4fd1-af61-155d1d7ffe53", + sexualityId: "d2945763-d1fb-46aa-b896-7f701b4ca699", }), openness: new Openness({ - opennessId: "ae90b960-5f00-4298-b509-fac92a59b406", + opennessId: "d5c97584-cd1b-4aa6-82ad-b5ddd3577bee", }), posts: { [UserToPostRelTypes.AUTHORED]: { - records: (await this.getPosts()).slice(2).map(post => ({ + records: (await this.getPosts()).slice(1, 3).map(post => ({ entity: post, relProps: new AuthoredProps({ - authoredAt: new Date("June 1st, 2022").getTime(), + authoredAt: new Date("09/6/2022").getTime(), anonymously: false, }), })), @@ -499,37 +499,6 @@ export class Neo4jSeedService { }, ...onlyAuthoredPosts, }, - }), - new User({ - userId: "8f0c1ecf-6853-4642-9199-6e8244b89312", - createdAt: new Date().getTime(), - updatedAt: new Date().getTime(), - avatar: "🤠", - username: "ilia", - normalizedUsername: "ILIA", - passwordHash: "$2b$10$nBR48Qq3e27FWfO3Yxezseaz7GRDe9qo4wXnwT7XoaDnCx9.Id7x6", - phoneNumber: null, - phoneNumberVerified: false, - email: "b@b.com", - emailVerified: true, - level: 3, - roles: [Role.ADMIN, Role.MODERATOR], - gender: new Gender({ - genderId: "585d31aa-d5b3-4b8d-9690-ffcd57ce2862", - }), - sexuality: new Sexuality({ - sexualityId: "9164d89b-8d71-4fd1-af61-155d1d7ffe53", - }), - openness: new Openness({ - opennessId: "ae90b960-5f00-4298-b509-fac92a59b406", - }), - posts: { - [UserToPostRelTypes.AUTHORED]: { - records: [], - relType: UserToPostRelTypes.AUTHORED, - }, - ...onlyAuthoredPosts, - }, }) ); } @@ -537,35 +506,30 @@ export class Neo4jSeedService { public async getPosts(): Promise { return new Array( new Post({ - postId: "b73edbf4-ba84-4b11-a91c-e1d8b1366974", - postTitle: "sister caught me checking out a guy on a camping trip", + postId: "bcddeb57-939d-441b-b4ea-71e1d2055f32", + postTitle: "Sister caught me checking out a guy on a camping trip", postContent: "I was on a camping trip and my sister caught me staring at someone across the site with his \n" + " shirt off, for the the rest of the day she wouldn't stop asking me, even getting the other members \n" + " who came with us to join in, I eventually gave in, she was super kind about it and came out as Bisexual the following months", - updatedAt: 1665770000, + updatedAt: 1666690000, postType: (await this.getPostTypes())[1], - postTags: (await this.getPostTags()).slice(0, 2), + postTags: (await this.getPostTags()).slice(-1), restrictedProps: null, authorUser: new User({ - userId: "3109f9e2-a262-4aef-b648-90d86d6fbf6c", + userId: "c612c987-6825-473c-882f-129f17d906b4", }), pending: false, - totalVotes: 1, + totalVotes: 0, awards: { [PostToAwardRelTypes.HAS_AWARD]: { - records: (await this.getAwards()).slice(0, 2).map(award => ({ - entity: award, - relProps: new HasAwardProps({ - awardedBy: "5c0f145b-ffad-4881-8ee6-7647c3c1b695", - }), - })), + records: [], relType: PostToAwardRelTypes.HAS_AWARD, }, }, }), new Post({ - postId: "596632ac-dd54-4700-a783-688618d99fa9", + postId: "be9ab5e4-eb7c-469b-a1e3-592dca2a00d0", postTitle: "Coming out to my accepting family", postContent: "Growing up my family was really gay friendly. I had two gay uncles and everyone was accepting of them. \n " + @@ -583,9 +547,9 @@ export class Neo4jSeedService { "Today my entire family knows that I am gay and they accept me. It is nice to have such an accepting family and I know that I am \n " + "very fortunate to have a family that loves me unconditionally. I am grateful that my family has never judged me or made me feel \n " + "uncomfortable expressing who I am.", - updatedAt: 1665770000, + updatedAt: 1666770000, postType: (await this.getPostTypes())[1], - postTags: (await this.getPostTags()).slice(0, 2), + postTags: (await this.getPostTags()).slice(0, 1), restrictedProps: new RestrictedProps({ restrictedAt: 1665780000, moderatorId: "3109f9e2-a262-4aef-b648-90d86d6fbf6c", @@ -598,10 +562,10 @@ export class Neo4jSeedService { totalVotes: 3, awards: { [PostToAwardRelTypes.HAS_AWARD]: { - records: (await this.getAwards()).slice(0, 2).map(award => ({ + records: (await this.getAwards()).slice(-1, 1).map(award => ({ entity: award, relProps: new HasAwardProps({ - awardedBy: "5c0f145b-ffad-4881-8ee6-7647c3c1b695", + awardedBy: "6bd7b8a2-bf8c-49e6-9c28-ee3d89be2453", }), })), relType: PostToAwardRelTypes.HAS_AWARD, @@ -614,13 +578,13 @@ export class Neo4jSeedService { public async getComments(): Promise { return new Array( new Comment({ - commentId: "37fbb7c9-013f-4057-bc90-f38498b69295", - parentId: "596632ac-dd54-4700-a783-688618d99fa9", - commentContent: "I think this post is great!", - createdAt: 1665770000, - updatedAt: 1665770000, + commentId: "f8959b32-5b68-4f68-97bc-59afdc0d09cb", + parentId: "bcddeb57-939d-441b-b4ea-71e1d2055f32", + commentContent: "Wow that's so nice to hear!", + createdAt: 1666990000, + updatedAt: 1666990000, authorUser: new User({ - userId: "5c0f145b-ffad-4881-8ee6-7647c3c1b695", + userId: "6bd7b8a2-bf8c-49e6-9c28-ee3d89be2453", }), pinned: true, pending: false, @@ -628,53 +592,51 @@ export class Neo4jSeedService { childComments: [], }), new Comment({ - commentId: "13cc9fd9-4c99-4daa-bf17-750bd1efa5d8", - parentId: "596632ac-dd54-4700-a783-688618d99fa9", - commentContent: "First comment! :yay:", - createdAt: 1665770000, - updatedAt: 1665770000, + commentId: "c13c4349-bbf9-45a7-a573-7a04efa66e3c", + parentId: "be9ab5e4-eb7c-469b-a1e3-592dca2a00d0", + commentContent: `Congratulations! That's so nice you have such a supportive family. + Sorry to hear about your ex-girlfriend and your cousin though. I hope you're doing better now.`, + createdAt: 1666890000, + updatedAt: 1666890000, authorUser: new User({ - userId: "5c0f145b-ffad-4881-8ee6-7647c3c1b695", + userId: "6bd7b8a2-bf8c-49e6-9c28-ee3d89be2453", }), pinned: false, pending: true, - restrictedProps: new RestrictedProps({ - restrictedAt: 1665780000, - moderatorId: "3109f9e2-a262-4aef-b648-90d86d6fbf6c", - reason: "The moderator thinks there is profanity in this comment", - }), + restrictedProps: null, childComments: [ new Comment({ - commentId: "04658465-f9ea-427b-9f5b-ed5e93db27ff", - parentId: "13cc9fd9-4c99-4daa-bf17-750bd1efa5d8", - commentContent: "Second comment! :yay:", - createdAt: 1665770000, - updatedAt: 1665770000, + commentId: "3ee2801a-998d-437a-a49e-3974919f35c1", + parentId: "be9ab5e4-eb7c-469b-a1e3-592dca2a00d0", + commentContent: `Wow that's so scummy of your cousin to do that to you! I would have never forgiven her.`, + createdAt: 1666990000, + updatedAt: 1666990000, authorUser: new User({ userId: "3109f9e2-a262-4aef-b648-90d86d6fbf6c", }), pinned: false, pending: true, - restrictedProps: new RestrictedProps({ - restrictedAt: 1665780000, - moderatorId: "3109f9e2-a262-4aef-b648-90d86d6fbf6c", - reason: "The moderator died of cringe", - }), - childComments: [], - }), - new Comment({ - commentId: "acba2871-2435-4439-82c3-adebc7cdc942", - parentId: "13cc9fd9-4c99-4daa-bf17-750bd1efa5d8", - commentContent: "First-Second comment! :yay:", - createdAt: 1665770000, - updatedAt: 1665770000, - authorUser: new User({ - userId: "3109f9e2-a262-4aef-b648-90d86d6fbf6c", - }), - pinned: false, - pending: false, restrictedProps: null, - childComments: [], + childComments: [ + new Comment({ + commentId: "287ca219-005e-41fa-af40-abf59c2c2caf", + parentId: "3ee2801a-998d-437a-a49e-3974919f35c1", + commentContent: `I agree! I would have never forgiven her either.`, + createdAt: 1667000000, + updatedAt: 1667000000, + authorUser: new User({ + userId: "f0b42305-9513-4fe7-a918-320d2b488e61", + }), + pinned: false, + pending: true, + restrictedProps: new RestrictedProps({ + restrictedAt: 1667000001, + moderatorId: "3109f9e2-a262-4aef-b648-90d86d6fbf6c", + reason: "The moderator died of cringe", + }), + childComments: [], + }), + ], }), ], }) @@ -724,14 +686,19 @@ export class Neo4jSeedService { public async getAwards(): Promise { return new Array( new Award({ - awardId: "2049221e-1f45-4430-8edc-95db808db072", - awardName: "Sean's Mom Award", + awardId: "032930d2-9994-46cc-ad35-559bb41a9d05", + awardName: "Ian's Mom Award", awardSvg: "", }), new Award({ awardId: "375608ce-ca65-4293-8402-da34cd2c42c7", awardName: "", awardSvg: "", + }), + new Award({ + awardId: "bf99f8f5-66f7-41ce-8014-5f70e5145174", + awardName: "Ilia's Mom Award", + awardSvg: "", }) ); } From 042b42ee2316fc0de800cdfa38ed2610de93feb3 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sat, 26 Nov 2022 21:48:29 -0800 Subject: [PATCH 045/153] fix the import for dtos --- src/auth/dtos/index.ts | 2 ++ src/auth/strategy/jwt.strategy.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/auth/dtos/index.ts b/src/auth/dtos/index.ts index aa33ee7..632274f 100644 --- a/src/auth/dtos/index.ts +++ b/src/auth/dtos/index.ts @@ -1,3 +1,5 @@ export { SignTokenDto } from "./signToken.dto"; export { SignInPayloadDto } from "./signInPayload.dto"; export { SignUpPayloadDto } from "./signUpPayload.dto"; +export { JwtTokenPayloadDto } from "./jwtTokenPayload.dto"; +export { ChangePasswordAdminDto } from "./changePasswordAdmin.dto"; diff --git a/src/auth/strategy/jwt.strategy.ts b/src/auth/strategy/jwt.strategy.ts index 96cbabc..0cd1360 100644 --- a/src/auth/strategy/jwt.strategy.ts +++ b/src/auth/strategy/jwt.strategy.ts @@ -4,7 +4,7 @@ import { PassportStrategy } from "@nestjs/passport"; import { ExtractJwt, Strategy } from "passport-jwt"; import { IUsersRepository } from "../../users/repositories/users/users.repository.interface"; import { _$ } from "../../_domain/injectableTokens"; -import { JwtTokenPayloadDto } from "../dtos/jwtTokenPayload.dto"; +import { JwtTokenPayloadDto } from "../dtos"; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { From aced5adbacd445416e39f4bec4ef1a5f0441c383 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sat, 26 Nov 2022 21:48:48 -0800 Subject: [PATCH 046/153] add the endpoint and service method --- src/auth/controllers/auth.controller.ts | 13 ++++++++-- src/auth/dtos/changePasswordAdmin.dto.ts | 16 ++++++++++++ src/auth/services/auth.service.interface.ts | 3 +++ src/auth/services/auth.service.ts | 25 ++++++++++++++++--- .../repositories/users/users.repository.ts | 4 +-- 5 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 src/auth/dtos/changePasswordAdmin.dto.ts diff --git a/src/auth/controllers/auth.controller.ts b/src/auth/controllers/auth.controller.ts index ce23320..c251f5c 100644 --- a/src/auth/controllers/auth.controller.ts +++ b/src/auth/controllers/auth.controller.ts @@ -1,11 +1,13 @@ import { Body, Controller, Inject, Post, UseGuards } from "@nestjs/common"; import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; -import { SignInPayloadDto, SignTokenDto, SignUpPayloadDto } from "../dtos"; +import { SignInPayloadDto, SignTokenDto, SignUpPayloadDto, ChangePasswordAdminDto } from "../dtos"; import { IAuthService } from "../services/auth.service.interface"; import { AuthGuard } from "@nestjs/passport"; import { _$ } from "../../_domain/injectableTokens"; import { AuthedUser } from "../decorators/authedUser.param.decorator"; -import { User } from "../../users/models"; +import { Role, User } from "../../users/models"; +import { Roles } from "../decorators/roles.decorator"; +import { RolesGuard } from "../guards/roles.guard"; @ApiTags("authentication") @ApiBearerAuth() @@ -28,4 +30,11 @@ export class AuthController { public async authenticate(@AuthedUser() user: User) { return await user.toJSON(); } + + @Post("change-password-admin") + @Roles(Role.ADMIN) + @UseGuards(AuthGuard("jwt"), RolesGuard) + public async changePasswordAdmin(@Body() payload: ChangePasswordAdminDto): Promise { + await this._authService.changePasswordAdmin(payload); + } } diff --git a/src/auth/dtos/changePasswordAdmin.dto.ts b/src/auth/dtos/changePasswordAdmin.dto.ts new file mode 100644 index 0000000..653b89f --- /dev/null +++ b/src/auth/dtos/changePasswordAdmin.dto.ts @@ -0,0 +1,16 @@ +import { IsString } from "class-validator"; +import { ApiProperty } from "@nestjs/swagger"; + +export class ChangePasswordAdminDto { + @ApiProperty({ type: String }) + @IsString() + newPassword: string; + + @ApiProperty({ type: String }) + @IsString() + username: string; + + constructor(partial?: Partial) { + Object.assign(this, partial); + } +} diff --git a/src/auth/services/auth.service.interface.ts b/src/auth/services/auth.service.interface.ts index 38e1e62..0ed9d83 100644 --- a/src/auth/services/auth.service.interface.ts +++ b/src/auth/services/auth.service.interface.ts @@ -1,7 +1,10 @@ import { SignInPayloadDto, SignTokenDto, SignUpPayloadDto } from "../dtos"; +import { ChangePasswordAdminDto } from "../dtos/changePasswordAdmin.dto"; export interface IAuthService { signup(signUpPayloadDto: SignUpPayloadDto): Promise; signIn(signInPayloadDto: SignInPayloadDto): Promise; + + changePasswordAdmin(payload: ChangePasswordAdminDto): Promise; } diff --git a/src/auth/services/auth.service.ts b/src/auth/services/auth.service.ts index 2595536..0461ce4 100644 --- a/src/auth/services/auth.service.ts +++ b/src/auth/services/auth.service.ts @@ -3,11 +3,16 @@ import { ConfigService } from "@nestjs/config"; import { JwtService } from "@nestjs/jwt"; import * as bcrypt from "bcrypt"; import { IUsersRepository } from "../../users/repositories/users/users.repository.interface"; -import { SignInPayloadDto, SignTokenDto, SignUpPayloadDto } from "../dtos"; +import { + ChangePasswordAdminDto, + SignInPayloadDto, + SignTokenDto, + SignUpPayloadDto, + JwtTokenPayloadDto, +} from "../dtos"; import { IAuthService } from "./auth.service.interface"; import { User } from "../../users/models"; import { _$ } from "../../_domain/injectableTokens"; -import { JwtTokenPayloadDto } from "../dtos/jwtTokenPayload.dto"; @Injectable({}) export class AuthService implements IAuthService { @@ -53,11 +58,11 @@ export class AuthService implements IAuthService { public async signIn(signInPayloadDto: SignInPayloadDto): Promise { const user = await this._usersRepository.findUserByUsername(signInPayloadDto.username); if (!user) { - throw new HttpException("User not found", 404); + throw new HttpException("Authentication failed.", 400); } const isMatch = await bcrypt.compare(signInPayloadDto.password, user.passwordHash); if (!isMatch) { - throw new HttpException("Incorrect password", 400); + throw new HttpException("Authentication failed.", 400); } const token = await this.signToken(user); @@ -80,4 +85,16 @@ export class AuthService implements IAuthService { secret: secret, }); } + + public async changePasswordAdmin(payload: ChangePasswordAdminDto): Promise { + const user = await this._usersRepository.findUserByUsername(payload.username); + if (!user) { + throw new HttpException("User not found", 404); + } + + const salt = await bcrypt.genSalt(10); + user.passwordHash = await bcrypt.hash(payload.newPassword, salt); + + await this._usersRepository.updateUser(user); + } } diff --git a/src/users/repositories/users/users.repository.ts b/src/users/repositories/users/users.repository.ts index 990428f..910946f 100644 --- a/src/users/repositories/users/users.repository.ts +++ b/src/users/repositories/users/users.repository.ts @@ -161,7 +161,7 @@ export class UsersRepository implements IUsersRepository { `, { userId: user.userId, - phoneNumber: user.phoneNumber, + phoneNumber: user.phoneNumber ?? "", phoneNumberVerified: user.phoneNumberVerified, username: user.username, normalizedUsername: user.username.toUpperCase(), @@ -171,7 +171,7 @@ export class UsersRepository implements IUsersRepository { level: user.level, roles: user.roles, updatedAt: new Date().getTime(), - } as Omit + } as User ); } From 29bb2d98e7023f5bbef5b0dfb868e921c7da0e21 Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Sat, 26 Nov 2022 22:51:14 -0800 Subject: [PATCH 047/153] Added comments unpin endpoint and method --- .../controllers/comments.controller.ts | 26 +++++++---- .../comments/comments.service.interface.ts | 4 +- .../services/comments/comments.service.ts | 44 ++++++++++++++++--- 3 files changed, 58 insertions(+), 16 deletions(-) diff --git a/src/comments/controllers/comments.controller.ts b/src/comments/controllers/comments.controller.ts index 55bf5ed..1ce3b97 100644 --- a/src/comments/controllers/comments.controller.ts +++ b/src/comments/controllers/comments.controller.ts @@ -12,22 +12,22 @@ import { ParseUUIDPipe, Post, UseGuards, - UseInterceptors, + UseInterceptors } from "@nestjs/common"; import { AuthGuard } from "@nestjs/passport"; import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; -import { DatabaseContext } from "../../database-access-layer/databaseContext"; -import { _$ } from "../../_domain/injectableTokens"; -import { Comment as CommentModel } from "../models"; -import { CommentCreationPayloadDto, VoteCommentPayloadDto } from "../dtos"; -import { ICommentsService } from "../services/comments/comments.service.interface"; -import { OptionalJwtAuthGuard } from "../../auth/guards/optionalJwtAuth.guard"; import { AuthedUser } from "../../auth/decorators/authedUser.param.decorator"; -import { Role, User } from "../../users/models"; -import { RolesGuard } from "../../auth/guards/roles.guard"; import { Roles } from "../../auth/decorators/roles.decorator"; +import { OptionalJwtAuthGuard } from "../../auth/guards/optionalJwtAuth.guard"; +import { RolesGuard } from "../../auth/guards/roles.guard"; +import { DatabaseContext } from "../../database-access-layer/databaseContext"; import { ModerationPayloadDto } from "../../moderation/dtos/moderatorActions"; import { IModeratorActionsService } from "../../moderation/services/moderatorActions/moderatorActions.service.interface"; +import { Role, User } from "../../users/models"; +import { _$ } from "../../_domain/injectableTokens"; +import { CommentCreationPayloadDto, VoteCommentPayloadDto } from "../dtos"; +import { Comment as CommentModel } from "../models"; +import { ICommentsService } from "../services/comments/comments.service.interface"; @ApiTags("comments") @Controller("comments") @@ -97,6 +97,14 @@ export class CommentsController { await this._commentsService.markAsPinned(commentId); } + @Post(":commentId/unpin") + @UseGuards(AuthGuard("jwt")) + public async unpinComment( + @Param("commentId", new ParseUUIDPipe()) commentId: UUID + ): Promise { + await this._commentsService.markAsUnpinned(commentId); + } + @Post("create") @UseGuards(AuthGuard("jwt")) public async createComment( diff --git a/src/comments/services/comments/comments.service.interface.ts b/src/comments/services/comments/comments.service.interface.ts index f716264..db1e0b8 100644 --- a/src/comments/services/comments/comments.service.interface.ts +++ b/src/comments/services/comments/comments.service.interface.ts @@ -1,5 +1,5 @@ +import { CommentCreationPayloadDto, VoteCommentPayloadDto } from "../../dtos"; import { Comment } from "../../models"; -import { VoteCommentPayloadDto, CommentCreationPayloadDto } from "../../dtos"; export interface ICommentsService { authorNewComment(commentPayload: CommentCreationPayloadDto): Promise; @@ -16,4 +16,6 @@ export interface ICommentsService { voteComment(votePayload: VoteCommentPayloadDto): Promise; markAsPinned(commentId: UUID): Promise; + + markAsUnpinned(commentId: UUID): Promise; } diff --git a/src/comments/services/comments/comments.service.ts b/src/comments/services/comments/comments.service.ts index eaec346..b313fd2 100644 --- a/src/comments/services/comments/comments.service.ts +++ b/src/comments/services/comments/comments.service.ts @@ -2,20 +2,20 @@ import { HttpException, Inject, Injectable, Scope } from "@nestjs/common"; import { REQUEST } from "@nestjs/core"; import { Request } from "express"; import { DatabaseContext } from "../../../database-access-layer/databaseContext"; +import { IAutoModerationService } from "../../../moderation/services/autoModeration/autoModeration.service.interface"; import { Post } from "../../../posts/models"; import { PostToCommentRelTypes } from "../../../posts/models/toComment"; +import { IPostsService } from "../../../posts/services/posts/posts.service.interface"; import { User } from "../../../users/models"; import { UserToCommentRelTypes } from "../../../users/models/toComment"; +import { VoteProps } from "../../../users/models/toPost"; import { _$ } from "../../../_domain/injectableTokens"; +import { VoteType } from "../../../_domain/models/enums"; +import { DeletedProps } from "../../../_domain/models/toSelf"; import { CommentCreationPayloadDto, VoteCommentPayloadDto } from "../../dtos"; import { Comment } from "../../models"; import { CommentToSelfRelTypes } from "../../models/toSelf"; import { ICommentsService } from "./comments.service.interface"; -import { IAutoModerationService } from "../../../moderation/services/autoModeration/autoModeration.service.interface"; -import { IPostsService } from "../../../posts/services/posts/posts.service.interface"; -import { VoteType } from "../../../_domain/models/enums"; -import { DeletedProps } from "../../../_domain/models/toSelf"; -import { VoteProps } from "../../../users/models/toPost"; @Injectable({ scope: Scope.REQUEST }) export class CommentsService implements ICommentsService { @@ -194,7 +194,7 @@ export class CommentsService implements ICommentsService { await this._dbContext.neo4jService.tryWriteAsync( ` MATCH (p:Post { postId: $postId }), (c:Comment { commentId: $commentId }) - CREATE (p)-[:${PostToCommentRelTypes.PINNED_COMMENT}]->(c) + MERGE (p)-[:${PostToCommentRelTypes.PINNED_COMMENT}]->(c) `, { postId: parentPost.postId, @@ -203,6 +203,38 @@ export class CommentsService implements ICommentsService { ); } + public async markAsUnpinned(commentId: UUID): Promise { + const comment = await this._dbContext.Comments.findCommentById(commentId); + if (!comment) throw new HttpException("Comment not found", 404); + + const [parentPost] = await this.findParentCommentRoot(commentId); + const user = this.getUserFromRequest(); + + const postAuthor = await parentPost.getAuthorUser(); + + if (postAuthor.userId !== user.userId) { + throw new HttpException("User is not the author of the post", 403); + } + + const queryResult = await this._dbContext.neo4jService.tryWriteAsync( + ` + MATCH (p:Post { postId: $postId })-[r:${PostToCommentRelTypes.PINNED_COMMENT}]->(c:Comment { commentId: $commentId }) + DELETE r + `, + { + postId: parentPost.postId, + commentId: commentId, + } + ); + + // Checks if a change was made to the database (If the comment was unpinned) + if (queryResult.summary.counters.containsUpdates() === false) { + throw new HttpException("Comment is not pinned", 400); + } + } + + + // gets the parent post of any nested comment of the post private async findParentPost(commentId: UUID): Promise { const parentPost = await this._dbContext.neo4jService.tryReadAsync( From 6997218b540fb90369424af04aacdbd7c5f63c03 Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Sat, 26 Nov 2022 22:53:30 -0800 Subject: [PATCH 048/153] Removed unused private method I actually don't know what the purpose of this was, therefore it is removed. --- .../services/comments/comments.service.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/comments/services/comments/comments.service.ts b/src/comments/services/comments/comments.service.ts index b313fd2..1bcc4a1 100644 --- a/src/comments/services/comments/comments.service.ts +++ b/src/comments/services/comments/comments.service.ts @@ -233,8 +233,6 @@ export class CommentsService implements ICommentsService { } } - - // gets the parent post of any nested comment of the post private async findParentPost(commentId: UUID): Promise { const parentPost = await this._dbContext.neo4jService.tryReadAsync( @@ -253,20 +251,6 @@ export class CommentsService implements ICommentsService { return new Post(parentPost.records[0].get("p").properties, this._dbContext.neo4jService); } - // gets the parent comment of any nested comment of the post - private async findComment(commentId: UUID): Promise { - const queryResult = await this._dbContext.neo4jService.tryReadAsync( - ` - MATCH (c:Comment { commentId: $commentId })-[:${CommentToSelfRelTypes.REPLIED}]->(commentParent:Comment) - RETURN commentParent - `, - { - commentId, - } - ); - return queryResult.records.length > 0; - } - // gets the root comment of any nested comment private async findParentCommentRoot( commentId: string, From 6abf9bbfa1761f8859f78030b2a24f3b382cf170 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sun, 27 Nov 2022 11:30:48 -0800 Subject: [PATCH 049/153] fix an import --- src/auth/services/auth.service.interface.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/auth/services/auth.service.interface.ts b/src/auth/services/auth.service.interface.ts index 0ed9d83..23779ae 100644 --- a/src/auth/services/auth.service.interface.ts +++ b/src/auth/services/auth.service.interface.ts @@ -1,5 +1,4 @@ -import { SignInPayloadDto, SignTokenDto, SignUpPayloadDto } from "../dtos"; -import { ChangePasswordAdminDto } from "../dtos/changePasswordAdmin.dto"; +import { SignInPayloadDto, SignTokenDto, SignUpPayloadDto, ChangePasswordAdminDto } from "../dtos"; export interface IAuthService { signup(signUpPayloadDto: SignUpPayloadDto): Promise; From f765a4f629b98dd0e9cb052bad397c33bd29ff8f Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sun, 27 Nov 2022 11:30:58 -0800 Subject: [PATCH 050/153] fix the voting on comments --- src/comments/controllers/comments.controller.ts | 4 ++-- src/posts/controllers/posts.controller.ts | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/comments/controllers/comments.controller.ts b/src/comments/controllers/comments.controller.ts index 1ce3b97..fcc8e88 100644 --- a/src/comments/controllers/comments.controller.ts +++ b/src/comments/controllers/comments.controller.ts @@ -12,7 +12,7 @@ import { ParseUUIDPipe, Post, UseGuards, - UseInterceptors + UseInterceptors, } from "@nestjs/common"; import { AuthGuard } from "@nestjs/passport"; import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; @@ -125,7 +125,7 @@ export class CommentsController { await this._moderatorActionsService.deleteComment(moderationPayload); } - @Post("vote") + @Post("/vote") @UseGuards(AuthGuard("jwt")) public async voteComment(@Body() voteCommentPayload: VoteCommentPayloadDto): Promise { await this._commentsService.voteComment(voteCommentPayload); diff --git a/src/posts/controllers/posts.controller.ts b/src/posts/controllers/posts.controller.ts index 3fb0e3b..46ab17f 100644 --- a/src/posts/controllers/posts.controller.ts +++ b/src/posts/controllers/posts.controller.ts @@ -87,7 +87,9 @@ export class PostsController { } @Get("/:postId/nestedComments") + @UseGuards(OptionalJwtAuthGuard) public async getNestedCommentsByPostId( + @AuthedUser() user: User, @Param("postId", new ParseUUIDPipe()) postId: UUID ): Promise { const topLevelComments = await this._postsService.findNestedCommentsByPostId( @@ -96,7 +98,9 @@ export class PostsController { 2, 2 ); - const decoratedTopLevelComments = topLevelComments.map(comment => comment.toJSONNested()); + const decoratedTopLevelComments = topLevelComments.map(comment => + comment.toJSONNested({ authenticatedUserId: user?.userId ?? undefined }) + ); return await Promise.all(decoratedTopLevelComments); } From 5b18a8ea19c520bded016c44acfd1c04ffb8a22a Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sun, 27 Nov 2022 14:25:04 -0800 Subject: [PATCH 051/153] add the privacy feature for openness, gender, and sexuality of the user --- src/auth/controllers/auth.controller.ts | 18 ++++++++++- src/auth/dtos/changePasswordUser.dto.ts | 22 +++++++++++++ src/auth/dtos/index.ts | 1 + src/auth/services/auth.service.interface.ts | 10 +++++- src/auth/services/auth.service.ts | 32 ++++++++++++++++--- src/users/controllers/users.controller.ts | 2 +- src/users/dtos/publicUser.dto.ts | 11 +++++-- src/users/models/toGender/hasGender.props.ts | 11 +++++++ src/users/models/toGender/index.ts | 2 ++ .../models/toOpenness/hasOpenness.props.ts | 11 +++++++ src/users/models/toOpenness/index.ts | 2 ++ .../models/toSexuality/hasSexuality.props.ts | 11 +++++++ src/users/models/toSexuality/index.ts | 2 ++ src/users/models/user.ts | 21 ++++++++++-- 14 files changed, 142 insertions(+), 14 deletions(-) create mode 100644 src/auth/dtos/changePasswordUser.dto.ts create mode 100644 src/users/models/toGender/hasGender.props.ts create mode 100644 src/users/models/toOpenness/hasOpenness.props.ts create mode 100644 src/users/models/toSexuality/hasSexuality.props.ts diff --git a/src/auth/controllers/auth.controller.ts b/src/auth/controllers/auth.controller.ts index c251f5c..8d5407d 100644 --- a/src/auth/controllers/auth.controller.ts +++ b/src/auth/controllers/auth.controller.ts @@ -1,6 +1,12 @@ import { Body, Controller, Inject, Post, UseGuards } from "@nestjs/common"; import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; -import { SignInPayloadDto, SignTokenDto, SignUpPayloadDto, ChangePasswordAdminDto } from "../dtos"; +import { + SignInPayloadDto, + SignTokenDto, + SignUpPayloadDto, + ChangePasswordAdminDto, + ChangePasswordUserDto, +} from "../dtos"; import { IAuthService } from "../services/auth.service.interface"; import { AuthGuard } from "@nestjs/passport"; import { _$ } from "../../_domain/injectableTokens"; @@ -31,6 +37,16 @@ export class AuthController { return await user.toJSON(); } + @Post("change-password") + @UseGuards(AuthGuard("jwt")) + public async changePasswordUser( + @AuthedUser() user: User, + @Body() payload: ChangePasswordUserDto + ): Promise { + payload.user = user; + await this._authService.changePasswordUser(payload); + } + @Post("change-password-admin") @Roles(Role.ADMIN) @UseGuards(AuthGuard("jwt"), RolesGuard) diff --git a/src/auth/dtos/changePasswordUser.dto.ts b/src/auth/dtos/changePasswordUser.dto.ts new file mode 100644 index 0000000..c225ed0 --- /dev/null +++ b/src/auth/dtos/changePasswordUser.dto.ts @@ -0,0 +1,22 @@ +import { IsInstance, IsOptional, IsString } from "class-validator"; +import { ApiProperty } from "@nestjs/swagger"; +import { Exclude } from "class-transformer"; +import { User } from "../../users/models"; + +export class ChangePasswordUserDto { + @ApiProperty({ type: String }) + @IsString() + previousPassword: string; + + @ApiProperty({ type: String }) + @IsString() + newPassword: string; + + @IsInstance(User) + @Exclude() + user: User; + + constructor(partial?: Partial) { + Object.assign(this, partial); + } +} diff --git a/src/auth/dtos/index.ts b/src/auth/dtos/index.ts index 632274f..92dae31 100644 --- a/src/auth/dtos/index.ts +++ b/src/auth/dtos/index.ts @@ -3,3 +3,4 @@ export { SignInPayloadDto } from "./signInPayload.dto"; export { SignUpPayloadDto } from "./signUpPayload.dto"; export { JwtTokenPayloadDto } from "./jwtTokenPayload.dto"; export { ChangePasswordAdminDto } from "./changePasswordAdmin.dto"; +export { ChangePasswordUserDto } from "./changePasswordUser.dto"; diff --git a/src/auth/services/auth.service.interface.ts b/src/auth/services/auth.service.interface.ts index 23779ae..2401a78 100644 --- a/src/auth/services/auth.service.interface.ts +++ b/src/auth/services/auth.service.interface.ts @@ -1,9 +1,17 @@ -import { SignInPayloadDto, SignTokenDto, SignUpPayloadDto, ChangePasswordAdminDto } from "../dtos"; +import { + SignInPayloadDto, + SignTokenDto, + SignUpPayloadDto, + ChangePasswordAdminDto, + ChangePasswordUserDto, +} from "../dtos"; export interface IAuthService { signup(signUpPayloadDto: SignUpPayloadDto): Promise; signIn(signInPayloadDto: SignInPayloadDto): Promise; + changePasswordUser(payload: ChangePasswordUserDto): Promise; + changePasswordAdmin(payload: ChangePasswordAdminDto): Promise; } diff --git a/src/auth/services/auth.service.ts b/src/auth/services/auth.service.ts index 0461ce4..04f82dd 100644 --- a/src/auth/services/auth.service.ts +++ b/src/auth/services/auth.service.ts @@ -9,6 +9,7 @@ import { SignTokenDto, SignUpPayloadDto, JwtTokenPayloadDto, + ChangePasswordUserDto, } from "../dtos"; import { IAuthService } from "./auth.service.interface"; import { User } from "../../users/models"; @@ -23,8 +24,7 @@ export class AuthService implements IAuthService { ) {} public async signup(signUpPayloadDto: SignUpPayloadDto): Promise { - const salt = await bcrypt.genSalt(10); - const hash = await bcrypt.hash(signUpPayloadDto.password, salt); + const hash = await this.makePasswordHash(signUpPayloadDto.password); const foundUser = await this._usersRepository.findUserByUsername(signUpPayloadDto.username); if (foundUser) { @@ -60,7 +60,7 @@ export class AuthService implements IAuthService { if (!user) { throw new HttpException("Authentication failed.", 400); } - const isMatch = await bcrypt.compare(signInPayloadDto.password, user.passwordHash); + const isMatch = await this.verifyPassword(signInPayloadDto.password, user.passwordHash); if (!isMatch) { throw new HttpException("Authentication failed.", 400); } @@ -86,15 +86,37 @@ export class AuthService implements IAuthService { }); } + public async changePasswordUser(payload: ChangePasswordUserDto): Promise { + const previousPasswordMatch = await this.verifyPassword( + payload.previousPassword, + payload.user.passwordHash + ); + if (!previousPasswordMatch) { + throw new HttpException("Previous password is incorrect", 400); + } + + payload.user.passwordHash = await this.makePasswordHash(payload.newPassword); + + await this._usersRepository.updateUser(payload.user); + } + public async changePasswordAdmin(payload: ChangePasswordAdminDto): Promise { const user = await this._usersRepository.findUserByUsername(payload.username); if (!user) { throw new HttpException("User not found", 404); } - const salt = await bcrypt.genSalt(10); - user.passwordHash = await bcrypt.hash(payload.newPassword, salt); + user.passwordHash = await this.makePasswordHash(payload.newPassword); await this._usersRepository.updateUser(user); } + + private async verifyPassword(password: string, hash: string): Promise { + return await bcrypt.compare(password, hash); + } + + private async makePasswordHash(rawPasswor: string): Promise { + const salt = await bcrypt.genSalt(10); + return await bcrypt.hash(rawPasswor, salt); + } } diff --git a/src/users/controllers/users.controller.ts b/src/users/controllers/users.controller.ts index 5afc329..e833348 100644 --- a/src/users/controllers/users.controller.ts +++ b/src/users/controllers/users.controller.ts @@ -65,7 +65,7 @@ export class UsersController { if (authedUser?.userId === user.userId) { return await user.toJSON(); } - return PublicUserDto.fromUser(user); + return PublicUserDto.fromUser(await user.toJSON()); } @Patch("/ban") diff --git a/src/users/dtos/publicUser.dto.ts b/src/users/dtos/publicUser.dto.ts index e978f5a..9cf787e 100644 --- a/src/users/dtos/publicUser.dto.ts +++ b/src/users/dtos/publicUser.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsNotEmpty } from "class-validator"; -import { Gender, Sexuality, User } from "../models"; +import { Gender, Openness, Sexuality, User } from "../models"; import { AvatarAscii, AvatarUrl } from "../models/user"; export class PublicUserDto { @@ -28,6 +28,10 @@ export class PublicUserDto { @IsNotEmpty() gender: Nullable; + @ApiProperty({ type: Openness }) + @IsNotEmpty() + openness: Nullable; + constructor(partial?: Partial) { Object.assign(this, partial); } @@ -38,8 +42,9 @@ export class PublicUserDto { username: user.username, avatar: user.avatar || null, level: user.level, - sexuality: user.sexuality || null, - gender: user.gender || null, + sexuality: user.sexuality && !user.isSexualityPrivate ? user.sexuality : null, + gender: user.gender && !user.isGenderPrivate ? user.gender : null, + openness: user.openness && !user.isOpennessPrivate ? user.openness : null, }); } } diff --git a/src/users/models/toGender/hasGender.props.ts b/src/users/models/toGender/hasGender.props.ts new file mode 100644 index 0000000..305de69 --- /dev/null +++ b/src/users/models/toGender/hasGender.props.ts @@ -0,0 +1,11 @@ +import { RelationshipProps } from "../../../neo4j/neo4j.helper.types"; +import { IsBoolean } from "class-validator"; + +export class HasGenderProps implements RelationshipProps { + @IsBoolean() + isPrivate: boolean; + + constructor(partial?: Partial) { + Object.assign(this, partial); + } +} diff --git a/src/users/models/toGender/index.ts b/src/users/models/toGender/index.ts index b8798d0..4578753 100644 --- a/src/users/models/toGender/index.ts +++ b/src/users/models/toGender/index.ts @@ -1,3 +1,5 @@ +export { HasGenderProps } from "./hasGender.props"; + export enum UserToGenderRelTypes { HAS_GENDER = "HAS_GENDER", } diff --git a/src/users/models/toOpenness/hasOpenness.props.ts b/src/users/models/toOpenness/hasOpenness.props.ts new file mode 100644 index 0000000..4cb3754 --- /dev/null +++ b/src/users/models/toOpenness/hasOpenness.props.ts @@ -0,0 +1,11 @@ +import { RelationshipProps } from "../../../neo4j/neo4j.helper.types"; +import { IsBoolean } from "class-validator"; + +export class HasOpennessProps implements RelationshipProps { + @IsBoolean() + isPrivate: boolean; + + constructor(partial?: Partial) { + Object.assign(this, partial); + } +} diff --git a/src/users/models/toOpenness/index.ts b/src/users/models/toOpenness/index.ts index ff8e988..94751ec 100644 --- a/src/users/models/toOpenness/index.ts +++ b/src/users/models/toOpenness/index.ts @@ -1,3 +1,5 @@ +export { HasOpennessProps } from "./hasOpenness.props"; + export enum UserToOpennessRelTypes { HAS_OPENNESS_LEVEL_OF = "HAS_OPENNESS_LEVEL_OF", } diff --git a/src/users/models/toSexuality/hasSexuality.props.ts b/src/users/models/toSexuality/hasSexuality.props.ts new file mode 100644 index 0000000..b3ee058 --- /dev/null +++ b/src/users/models/toSexuality/hasSexuality.props.ts @@ -0,0 +1,11 @@ +import { RelationshipProps } from "../../../neo4j/neo4j.helper.types"; +import { IsBoolean } from "class-validator"; + +export class HasSexualityProps implements RelationshipProps { + @IsBoolean() + isPrivate: boolean; + + constructor(partial?: Partial) { + Object.assign(this, partial); + } +} diff --git a/src/users/models/toSexuality/index.ts b/src/users/models/toSexuality/index.ts index 7fd45e1..05c4cae 100644 --- a/src/users/models/toSexuality/index.ts +++ b/src/users/models/toSexuality/index.ts @@ -1,3 +1,5 @@ +export { HasSexualityProps } from "./hasSexuality.props"; + export enum UserToSexualityRelTypes { HAS_SEXUALITY = "HAS_SEXUALITY", } diff --git a/src/users/models/user.ts b/src/users/models/user.ts index c6aa1e9..d17b6a9 100644 --- a/src/users/models/user.ts +++ b/src/users/models/user.ts @@ -14,11 +14,11 @@ import { Openness } from "./openness"; import { Role } from "./role"; import { Sexuality } from "./sexuality"; import { AuthoredProps as UserToCommentAuthoredProps, UserToCommentRelTypes } from "./toComment"; -import { UserToGenderRelTypes } from "./toGender"; -import { UserToOpennessRelTypes } from "./toOpenness"; +import { HasGenderProps, UserToGenderRelTypes } from "./toGender"; +import { HasOpennessProps, UserToOpennessRelTypes } from "./toOpenness"; import { AuthoredProps, FavoritesProps, UserToPostRelTypes } from "./toPost"; import { GotBannedProps, UserToSelfRelTypes, WasOffendingProps } from "./toSelf"; -import { UserToSexualityRelTypes } from "./toSexuality"; +import { HasSexualityProps, UserToSexualityRelTypes } from "./toSexuality"; import { IsArray, IsBoolean, @@ -93,14 +93,23 @@ export class User extends Model { @IsInstance(Sexuality) @IsOptional() sexuality: Nullable; + @IsBoolean() + @IsOptional() + isSexualityPrivate: boolean; @IsInstance(Gender) @IsOptional() gender: Nullable; + @IsBoolean() + @IsOptional() + isGenderPrivate: boolean; @IsInstance(Openness) @IsOptional() openness: Nullable; + @IsBoolean() + @IsOptional() + isOpennessPrivate: boolean; @IsArray() @IsOptional() @@ -247,7 +256,9 @@ export class User extends Model { if (queryResult.records.length === 0) { return null; } + const hasSexualityProps = new HasSexualityProps(queryResult.records[0].get("r").properties); this.sexuality = new Sexuality(queryResult.records[0].get("s").properties); + this.isSexualityPrivate = hasSexualityProps.isPrivate || false; return this.sexuality; } @@ -301,7 +312,9 @@ export class User extends Model { if (queryResult.records.length === 0) { return null; } + const hasGenderProps = new HasGenderProps(queryResult.records[0].get("r").properties); this.gender = new Gender(queryResult.records[0].get("g").properties); + this.isGenderPrivate = hasGenderProps.isPrivate || false; return this.gender; } @@ -318,7 +331,9 @@ export class User extends Model { if (queryResult.records.length === 0) { return null; } + const hasOpennessProps = new HasOpennessProps(queryResult.records[0].get("r").properties); this.openness = new Openness(queryResult.records[0].get("o").properties); + this.isOpennessPrivate = hasOpennessProps.isPrivate || false; return this.openness; } } From d1b0427624a0e088d69ad3867d19cb04c8d54676 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sun, 27 Nov 2022 15:15:41 -0800 Subject: [PATCH 052/153] add the isPrivate property to the openness, gender, and sexuality of the user in the seeding functionality --- src/neo4j/services/neo4j.seed.service.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/neo4j/services/neo4j.seed.service.ts b/src/neo4j/services/neo4j.seed.service.ts index bce8f12..5991ee0 100644 --- a/src/neo4j/services/neo4j.seed.service.ts +++ b/src/neo4j/services/neo4j.seed.service.ts @@ -184,9 +184,9 @@ export class Neo4jSeedService { level: $level, roles: $roles - })-[:${UserToSexualityRelTypes.HAS_SEXUALITY}]->(s), - (u)-[:${UserToGenderRelTypes.HAS_GENDER}]->(g), - (u)-[:${UserToOpennessRelTypes.HAS_OPENNESS_LEVEL_OF}]->(o)`, + })-[:${UserToSexualityRelTypes.HAS_SEXUALITY} { isPrivate: $isSexualityPrivate }]->(s), + (u)-[:${UserToGenderRelTypes.HAS_GENDER} { isPrivate: $isGenderPrivate }]->(g), + (u)-[:${UserToOpennessRelTypes.HAS_OPENNESS_LEVEL_OF} { isPrivate: $isOpennessPrivate }]->(o)`, { userId: userEntity.userId, createdAt: userEntity.createdAt, @@ -202,8 +202,13 @@ export class Neo4jSeedService { roles: userEntity.roles, sexualityId: userEntity.sexuality.sexualityId, + isSexualityPrivate: userEntity.isSexualityPrivate, + genderId: userEntity.gender.genderId, + isGenderPrivate: userEntity.isGenderPrivate, + opennessId: userEntity.openness.opennessId, + isOpennessPrivate: userEntity.isOpennessPrivate, } ); } @@ -443,12 +448,15 @@ export class Neo4jSeedService { gender: new Gender({ genderId: "d2945763-d1fb-46aa-b896-7f701b4ca699", }), + isGenderPrivate: false, sexuality: new Sexuality({ sexualityId: "55da84cc-5f17-454a-a653-227458763edb", }), + isSexualityPrivate: false, openness: new Openness({ opennessId: "c8921055-563f-4a54-8773-b408efcfb7ac", }), + isOpennessPrivate: false, posts: { [UserToPostRelTypes.AUTHORED]: { records: (await this.getPosts()).slice(0, 2).map(post => ({ @@ -480,12 +488,15 @@ export class Neo4jSeedService { gender: new Gender({ genderId: "585d31aa-d5b3-4b8d-9690-ffcd57ce2862", }), + isGenderPrivate: false, sexuality: new Sexuality({ sexualityId: "d2945763-d1fb-46aa-b896-7f701b4ca699", }), + isSexualityPrivate: false, openness: new Openness({ opennessId: "d5c97584-cd1b-4aa6-82ad-b5ddd3577bee", }), + isOpennessPrivate: false, posts: { [UserToPostRelTypes.AUTHORED]: { records: (await this.getPosts()).slice(1, 3).map(post => ({ From 0cd4fdebf4444ccab287aea9c16c1812f3a0b7dc Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sun, 27 Nov 2022 15:22:43 -0800 Subject: [PATCH 053/153] add bio and avatar to the seeding functionality --- src/neo4j/services/neo4j.seed.service.ts | 9 +++++++++ src/users/models/user.ts | 4 ++++ src/users/services/.gitkeep | 0 3 files changed, 13 insertions(+) delete mode 100644 src/users/services/.gitkeep diff --git a/src/neo4j/services/neo4j.seed.service.ts b/src/neo4j/services/neo4j.seed.service.ts index 5991ee0..c55add5 100644 --- a/src/neo4j/services/neo4j.seed.service.ts +++ b/src/neo4j/services/neo4j.seed.service.ts @@ -181,6 +181,9 @@ export class Neo4jSeedService { email: $email, emailVerified: $emailVerified, + bio: $bio, + avatar: $avatar, + level: $level, roles: $roles @@ -198,6 +201,10 @@ export class Neo4jSeedService { phoneNumberVerified: userEntity.phoneNumberVerified, email: userEntity.email, emailVerified: userEntity.emailVerified, + + bio: userEntity.bio, + avatar: userEntity.avatar, + level: userEntity.level, roles: userEntity.roles, @@ -436,6 +443,7 @@ export class Neo4jSeedService { createdAt: new Date().getTime(), updatedAt: new Date().getTime(), avatar: ":^)", + bio: "My name is gabriel.", username: "gabriel", normalizedUsername: "GABRIEL", passwordHash: "someotherpassword", @@ -476,6 +484,7 @@ export class Neo4jSeedService { createdAt: new Date().getTime(), updatedAt: new Date().getTime(), avatar: "^_^", + bio: "My name is alphonse.", username: "alphonse", normalizedUsername: "ALPHONSE", passwordHash: "num3r1ca1pa55w0rd", diff --git a/src/users/models/user.ts b/src/users/models/user.ts index d17b6a9..162b52a 100644 --- a/src/users/models/user.ts +++ b/src/users/models/user.ts @@ -50,6 +50,10 @@ export class User extends Model { @IsString() avatar: AvatarAscii | AvatarUrl; + @NodeProperty() + @IsString() + bio: string; + @NodeProperty() @IsString() email: string; diff --git a/src/users/services/.gitkeep b/src/users/services/.gitkeep deleted file mode 100644 index e69de29..0000000 From f116803b04717b4a77237bb52632718e66a4445c Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sun, 27 Nov 2022 15:23:46 -0800 Subject: [PATCH 054/153] add bio property to the dto --- src/users/dtos/publicUser.dto.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/users/dtos/publicUser.dto.ts b/src/users/dtos/publicUser.dto.ts index 9cf787e..cb157ab 100644 --- a/src/users/dtos/publicUser.dto.ts +++ b/src/users/dtos/publicUser.dto.ts @@ -12,6 +12,10 @@ export class PublicUserDto { @IsNotEmpty() avatar: AvatarAscii | AvatarUrl; + @ApiProperty({ type: String }) + @IsNotEmpty() + bio: string; + @ApiProperty({ type: String }) @IsNotEmpty() username: string; @@ -41,6 +45,7 @@ export class PublicUserDto { userId: user.userId, username: user.username, avatar: user.avatar || null, + bio: user.bio || null, level: user.level, sexuality: user.sexuality && !user.isSexualityPrivate ? user.sexuality : null, gender: user.gender && !user.isGenderPrivate ? user.gender : null, From 20ddd4d36254fd0b5c3fe94cff8ffb1fe3848d2e Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Sun, 27 Nov 2022 16:57:26 -0800 Subject: [PATCH 055/153] check for pinned comment --- src/posts/controllers/posts.controller.ts | 23 +++++++++------- .../services/posts/posts.service.interface.ts | 6 +++-- src/posts/services/posts/posts.service.ts | 27 ++++++++++++++----- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/posts/controllers/posts.controller.ts b/src/posts/controllers/posts.controller.ts index 46ab17f..c1a11d2 100644 --- a/src/posts/controllers/posts.controller.ts +++ b/src/posts/controllers/posts.controller.ts @@ -12,23 +12,23 @@ import { ParseUUIDPipe, Post, UseGuards, - UseInterceptors, + UseInterceptors } from "@nestjs/common"; import { AuthGuard } from "@nestjs/passport"; import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; -import { DatabaseContext } from "../../database-access-layer/databaseContext"; -import { _$ } from "../../_domain/injectableTokens"; -import { Post as PostModel } from "../models"; -import { Comment } from "../../comments/models"; -import { PostCreationPayloadDto, VotePostPayloadDto } from "../dtos"; -import { IPostsService } from "../services/posts/posts.service.interface"; -import { OptionalJwtAuthGuard } from "../../auth/guards/optionalJwtAuth.guard"; import { AuthedUser } from "../../auth/decorators/authedUser.param.decorator"; -import { Role, User } from "../../users/models"; import { Roles } from "../../auth/decorators/roles.decorator"; +import { OptionalJwtAuthGuard } from "../../auth/guards/optionalJwtAuth.guard"; import { RolesGuard } from "../../auth/guards/roles.guard"; +import { Comment } from "../../comments/models"; +import { DatabaseContext } from "../../database-access-layer/databaseContext"; import { ModerationPayloadDto } from "../../moderation/dtos/moderatorActions"; import { IModeratorActionsService } from "../../moderation/services/moderatorActions/moderatorActions.service.interface"; +import { Role, User } from "../../users/models"; +import { _$ } from "../../_domain/injectableTokens"; +import { PostCreationPayloadDto, VotePostPayloadDto } from "../dtos"; +import { Post as PostModel } from "../models"; +import { IPostsService } from "../services/posts/posts.service.interface"; @ApiTags("posts") @Controller("posts") @@ -156,4 +156,9 @@ export class PostsController { await this._moderationActionsService.allowPost(postId); throw new Error("Not implemented"); } + + @Get(":postId/checkForPin") + public async checkForPin(@Param("postId", new ParseUUIDPipe()) postId: UUID): Promise { + return await this._postsService.checkForPinnedComment(postId); + } } diff --git a/src/posts/services/posts/posts.service.interface.ts b/src/posts/services/posts/posts.service.interface.ts index 82095d2..52495e7 100644 --- a/src/posts/services/posts/posts.service.interface.ts +++ b/src/posts/services/posts/posts.service.interface.ts @@ -13,10 +13,10 @@ export interface IPostsService { findAllStories(): Promise; - findPostById(postId: string): Promise; + findPostById(postId: UUID): Promise; findNestedCommentsByPostId( - postId: string, + postId: UUID, topLevelLimit: number, nestedLimit: number, nestedLevel: number @@ -24,5 +24,7 @@ export interface IPostsService { getNestedComments(comments: Comment[], nestedLevel: number, nestedLimit: number): Promise; + checkForPinnedComment(postId: UUID): Promise; + votePost(votePostPayload: VotePostPayloadDto): Promise; } diff --git a/src/posts/services/posts/posts.service.ts b/src/posts/services/posts/posts.service.ts index 19e146a..242d46b 100644 --- a/src/posts/services/posts/posts.service.ts +++ b/src/posts/services/posts/posts.service.ts @@ -1,14 +1,14 @@ import { HttpException, Inject, Injectable, Scope } from "@nestjs/common"; -import { User } from "../../../users/models"; -import { _$ } from "../../../_domain/injectableTokens"; -import { DatabaseContext } from "../../../database-access-layer/databaseContext"; import { REQUEST } from "@nestjs/core"; import { Request } from "express"; import { Comment } from "../../../comments/models"; -import { VoteType } from "../../../_domain/models/enums"; -import { DeletedProps } from "../../../_domain/models/toSelf"; +import { DatabaseContext } from "../../../database-access-layer/databaseContext"; import { IAutoModerationService } from "../../../moderation/services/autoModeration/autoModeration.service.interface"; +import { PostToCommentRelTypes } from "../../../posts/models/toComment"; +import { User } from "../../../users/models"; import { UserToPostRelTypes, VoteProps } from "../../../users/models/toPost"; +import { _$ } from "../../../_domain/injectableTokens"; +import { VoteType } from "../../../_domain/models/enums"; import { PostCreationPayloadDto, VotePostPayloadDto } from "../../dtos"; import { Post, PostTag } from "../../models"; import { IPostsService, postSortCallback } from "./posts.service.interface"; @@ -148,7 +148,7 @@ export class PostsService implements IPostsService { } public async findNestedCommentsByPostId( - postId: string, + postId: UUID, topLevelLimit: number, nestedLimit: number, nestedLevel: number @@ -165,6 +165,21 @@ export class PostsService implements IPostsService { return comments; } + public async checkForPinnedComment(postId: UUID): Promise { + const foundPost = await this._dbContext.Posts.findPostById(postId); + if (foundPost === null) throw new HttpException("Post not found", 404); + + const queryResult = await this._dbContext.neo4jService.tryReadAsync( + `MATCH (p:Post {postId: $postId})-[:${PostToCommentRelTypes.PINNED_COMMENT}]->(c:Comment) + RETURN c`, + { + postId: postId, + } + ) + if (queryResult.records.length === 0) return null; + return new Comment(queryResult.records[0].get("c").properties); + } + /** * Recursively gets the total number of every comment's child comments that are **available**. * @param comment From 0c34a0806b2d92e6af866d32403f876ed97e3fcf Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Sun, 27 Nov 2022 16:58:25 -0800 Subject: [PATCH 056/153] string -> UUID --- src/posts/services/posts/posts.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/posts/services/posts/posts.service.ts b/src/posts/services/posts/posts.service.ts index 242d46b..6c29b4b 100644 --- a/src/posts/services/posts/posts.service.ts +++ b/src/posts/services/posts/posts.service.ts @@ -132,7 +132,7 @@ export class PostsService implements IPostsService { return result; } - public async findPostById(postId: string): Promise { + public async findPostById(postId: UUID): Promise { let foundPost = await this._dbContext.Posts.findPostById(postId); if (foundPost === null) throw new HttpException("Post not found", 404); From f38bd5a682ee89a6e9d2fb097fdc215f067350c8 Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Sun, 27 Nov 2022 19:39:05 -0800 Subject: [PATCH 057/153] sending pinned as a property Co-Authored-By: Ilia Abedian <37903573+iliaamiri@users.noreply.github.com> --- src/comments/models/comment.ts | 42 +++++++++++++------ src/posts/controllers/posts.controller.ts | 5 --- .../services/posts/posts.service.interface.ts | 2 - src/posts/services/posts/posts.service.ts | 15 ------- 4 files changed, 29 insertions(+), 35 deletions(-) diff --git a/src/comments/models/comment.ts b/src/comments/models/comment.ts index f811b9d..f5a4eb1 100644 --- a/src/comments/models/comment.ts +++ b/src/comments/models/comment.ts @@ -1,14 +1,4 @@ import { Exclude, Type } from "class-transformer"; -import { Labels, NodeProperty } from "../../neo4j/neo4j.decorators"; -import { Model } from "../../neo4j/neo4j.helper.types"; -import { Neo4jService } from "../../neo4j/services/neo4j.service"; -import { User } from "../../users/models"; -import { AuthoredProps, UserToCommentRelTypes } from "../../users/models/toComment"; -import { RestrictedProps, _ToSelfRelTypes, DeletedProps } from "../../_domain/models/toSelf"; -import { CommentToSelfRelTypes } from "./toSelf"; -import { PublicUserDto } from "../../users/dtos"; -import neo4j from "neo4j-driver"; -import { VoteType } from "../../_domain/models/enums"; import { IsArray, IsBoolean, @@ -17,8 +7,19 @@ import { IsNumber, IsOptional, IsString, - IsUUID, + IsUUID } from "class-validator"; +import neo4j from "neo4j-driver"; +import { Labels, NodeProperty } from "../../neo4j/neo4j.decorators"; +import { Model } from "../../neo4j/neo4j.helper.types"; +import { Neo4jService } from "../../neo4j/services/neo4j.service"; +import { PostToCommentRelTypes } from "../../posts/models/toComment"; +import { PublicUserDto } from "../../users/dtos"; +import { User } from "../../users/models"; +import { AuthoredProps, UserToCommentRelTypes } from "../../users/models/toComment"; +import { VoteType } from "../../_domain/models/enums"; +import { DeletedProps, RestrictedProps, _ToSelfRelTypes } from "../../_domain/models/toSelf"; +import { CommentToSelfRelTypes } from "./toSelf"; @Labels("Comment") export class Comment extends Model { @@ -93,6 +94,7 @@ export class Comment extends Model { this.getCreatedAt(), this.getTotalVotes(), this.getAuthorUser(), + this.getPinStatus(), ...(props.authenticatedUserId ? [this.getUserVote(props.authenticatedUserId)] : []), ]); } @@ -116,8 +118,7 @@ export class Comment extends Model { public async getChildrenComments(limit = 0): Promise { const queryResult = await this.neo4jService.tryReadAsync( ` - MATCH (c:Comment)-[:${ - CommentToSelfRelTypes.REPLIED + MATCH (c:Comment)-[:${CommentToSelfRelTypes.REPLIED }]->(p:Comment) WHERE p.commentId = $parentId RETURN c ${limit > 0 ? `LIMIT $limit` : ""} @@ -135,6 +136,21 @@ export class Comment extends Model { return this.childComments; } + public async getPinStatus(): Promise { + const queryResult = await this.neo4jService.tryReadAsync( + ` + MATCH (n)-[r:${PostToCommentRelTypes.PINNED_COMMENT}]->(c:Comment { commentId: $commentId }) + RETURN r, c + `, + { + commentId: this.commentId, + } + ); + + this.pinned = queryResult.records.length > 0; + return this.pinned; + } + public async getUserVote(userId): Promise> { const queryResult = await this.neo4jService.tryReadAsync( ` diff --git a/src/posts/controllers/posts.controller.ts b/src/posts/controllers/posts.controller.ts index c1a11d2..717e7a4 100644 --- a/src/posts/controllers/posts.controller.ts +++ b/src/posts/controllers/posts.controller.ts @@ -156,9 +156,4 @@ export class PostsController { await this._moderationActionsService.allowPost(postId); throw new Error("Not implemented"); } - - @Get(":postId/checkForPin") - public async checkForPin(@Param("postId", new ParseUUIDPipe()) postId: UUID): Promise { - return await this._postsService.checkForPinnedComment(postId); - } } diff --git a/src/posts/services/posts/posts.service.interface.ts b/src/posts/services/posts/posts.service.interface.ts index 52495e7..7df7466 100644 --- a/src/posts/services/posts/posts.service.interface.ts +++ b/src/posts/services/posts/posts.service.interface.ts @@ -24,7 +24,5 @@ export interface IPostsService { getNestedComments(comments: Comment[], nestedLevel: number, nestedLimit: number): Promise; - checkForPinnedComment(postId: UUID): Promise; - votePost(votePostPayload: VotePostPayloadDto): Promise; } diff --git a/src/posts/services/posts/posts.service.ts b/src/posts/services/posts/posts.service.ts index 6c29b4b..ccbe532 100644 --- a/src/posts/services/posts/posts.service.ts +++ b/src/posts/services/posts/posts.service.ts @@ -165,21 +165,6 @@ export class PostsService implements IPostsService { return comments; } - public async checkForPinnedComment(postId: UUID): Promise { - const foundPost = await this._dbContext.Posts.findPostById(postId); - if (foundPost === null) throw new HttpException("Post not found", 404); - - const queryResult = await this._dbContext.neo4jService.tryReadAsync( - `MATCH (p:Post {postId: $postId})-[:${PostToCommentRelTypes.PINNED_COMMENT}]->(c:Comment) - RETURN c`, - { - postId: postId, - } - ) - if (queryResult.records.length === 0) return null; - return new Comment(queryResult.records[0].get("c").properties); - } - /** * Recursively gets the total number of every comment's child comments that are **available**. * @param comment From 48aa65521c45f00e4997ceedd6addb23a01218ff Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sun, 27 Nov 2022 23:25:09 -0800 Subject: [PATCH 058/153] fix the seeding functionality --- src/neo4j/services/neo4j.seed.service.ts | 32 ++-- src/users/controllers/genders.controller.ts | 0 src/users/controllers/openness.controller.ts | 0 .../controllers/sexualities.controller.ts | 0 src/users/dtos/setupProfile.dto.ts | 0 src/users/dtos/updateAvatar.dto.ts | 0 src/users/dtos/updateBio.dto.ts | 0 src/users/dtos/updateGender.dto.ts | 0 src/users/dtos/updateOpenness.dto.ts | 0 src/users/dtos/updateSexuality.dto.ts | 0 .../profileSetup.service.interface.ts | 22 +++ .../profileSetup/profileSetup.service.ts | 172 ++++++++++++++++++ 12 files changed, 214 insertions(+), 12 deletions(-) create mode 100644 src/users/controllers/genders.controller.ts create mode 100644 src/users/controllers/openness.controller.ts create mode 100644 src/users/controllers/sexualities.controller.ts create mode 100644 src/users/dtos/setupProfile.dto.ts create mode 100644 src/users/dtos/updateAvatar.dto.ts create mode 100644 src/users/dtos/updateBio.dto.ts create mode 100644 src/users/dtos/updateGender.dto.ts create mode 100644 src/users/dtos/updateOpenness.dto.ts create mode 100644 src/users/dtos/updateSexuality.dto.ts create mode 100644 src/users/services/profileSetup/profileSetup.service.interface.ts create mode 100644 src/users/services/profileSetup/profileSetup.service.ts diff --git a/src/neo4j/services/neo4j.seed.service.ts b/src/neo4j/services/neo4j.seed.service.ts index c55add5..21f95d1 100644 --- a/src/neo4j/services/neo4j.seed.service.ts +++ b/src/neo4j/services/neo4j.seed.service.ts @@ -274,18 +274,26 @@ export class Neo4jSeedService { }) WHERE postTag.tagName = postTagNameToBeConnected MERGE (p1)-[:${PostToPostTypeRelTypes.HAS_POST_TYPE}]->(postType) MERGE (p1)-[:${PostToPostTagRelTypes.HAS_POST_TAG}]->(postTag) - WITH [${postEntity.awards[PostToAwardRelTypes.HAS_AWARD].records - .map(record => `"${record.entity.awardId}"`) - .join(",")}] AS awardIDsToBeConnected - UNWIND awardIDsToBeConnected as awardIdToBeConnected - MATCH (p1:${this.postLabel}) WHERE p1.postId = $postId - MATCH (award:${this.awardLabel}) WHERE award.awardId = awardIdToBeConnected - MERGE (p1)-[:${PostToAwardRelTypes.HAS_AWARD} { awardedBy: "${ - ( - postEntity.awards[PostToAwardRelTypes.HAS_AWARD].records[0] - .relProps as HasAwardProps - ).awardedBy - }" } ]->(award) + ${ + postEntity.awards[PostToAwardRelTypes.HAS_AWARD].records.length > 0 + ? `WITH [${postEntity.awards[PostToAwardRelTypes.HAS_AWARD].records + .map(record => `"${record.entity.awardId}"`) + .join(",")}] AS awardIDsToBeConnected + UNWIND awardIDsToBeConnected as awardIdToBeConnected + MATCH (p1:${this.postLabel}) WHERE p1.postId = $postId + MATCH (award:${ + this.awardLabel + }) WHERE award.awardId = awardIdToBeConnected + MERGE (p1)-[:${ + PostToAwardRelTypes.HAS_AWARD + } { awardedBy: "${ + ( + postEntity.awards[PostToAwardRelTypes.HAS_AWARD] + .records[0].relProps as HasAwardProps + ).awardedBy + }" } ]->(award)` + : "" + } WITH [${votesUserIds .map(voterUserId => `"${voterUserId}"`) .join(",")}] AS voterUserIdsToBeConnected diff --git a/src/users/controllers/genders.controller.ts b/src/users/controllers/genders.controller.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/users/controllers/openness.controller.ts b/src/users/controllers/openness.controller.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/users/controllers/sexualities.controller.ts b/src/users/controllers/sexualities.controller.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/users/dtos/setupProfile.dto.ts b/src/users/dtos/setupProfile.dto.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/users/dtos/updateAvatar.dto.ts b/src/users/dtos/updateAvatar.dto.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/users/dtos/updateBio.dto.ts b/src/users/dtos/updateBio.dto.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/users/dtos/updateGender.dto.ts b/src/users/dtos/updateGender.dto.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/users/dtos/updateOpenness.dto.ts b/src/users/dtos/updateOpenness.dto.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/users/dtos/updateSexuality.dto.ts b/src/users/dtos/updateSexuality.dto.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/users/services/profileSetup/profileSetup.service.interface.ts b/src/users/services/profileSetup/profileSetup.service.interface.ts new file mode 100644 index 0000000..c9ee90a --- /dev/null +++ b/src/users/services/profileSetup/profileSetup.service.interface.ts @@ -0,0 +1,22 @@ +import { + SetupProfileDto, + UpdateAvatarDto, + UpdateBioDto, + UpdateGenderDto, + UpdateOpennessDto, + UpdateSexualityDto, +} from "../../dtos"; + +export interface IProfileSetupService { + setupProfile(payload: SetupProfileDto): Promise; + + updateBio(payload: UpdateBioDto): Promise; + + updateAvatar(payload: UpdateAvatarDto): Promise; + + updateGender(payload: UpdateGenderDto): Promise; + + updateSexuality(payload: UpdateSexualityDto): Promise; + + updateOpenness(payload: UpdateOpennessDto): Promise; +} diff --git a/src/users/services/profileSetup/profileSetup.service.ts b/src/users/services/profileSetup/profileSetup.service.ts new file mode 100644 index 0000000..02033d4 --- /dev/null +++ b/src/users/services/profileSetup/profileSetup.service.ts @@ -0,0 +1,172 @@ +import { IProfileSetupService } from "./profileSetup.service.interface"; +import { + SetupProfileDto, + UpdateAvatarDto, + UpdateBioDto, + UpdateGenderDto, + UpdateOpennessDto, + UpdateSexualityDto, +} from "../../dtos"; +import { HttpException, Inject, Injectable, Scope } from "@nestjs/common"; +import { Request } from "express"; +import { REQUEST } from "@nestjs/core"; +import { User } from "../../models"; +import { DatabaseContext } from "../../../database-access-layer/databaseContext"; +import { _$ } from "../../../_domain/injectableTokens"; +import { UserToSexualityRelTypes } from "../../models/toSexuality"; +import { UserToGenderRelTypes } from "../../models/toGender"; +import { UserToOpennessRelTypes } from "../../models/toOpenness"; + +@Injectable({ scope: Scope.DEFAULT }) +export class ProfileSetupService implements IProfileSetupService { + private readonly _request: Request; + private readonly _dbContext: DatabaseContext; + + constructor( + @Inject(REQUEST) request: Request, + @Inject(_$.IDatabaseContext) dbContext: DatabaseContext + ) { + this._request = request; + this._dbContext = dbContext; + } + + public async setupProfile(payload: SetupProfileDto): Promise { + const user: User = this.getUserFromRequest(); + + user.bio = payload.bio; + user.avatar = payload.avatar; + await this._dbContext.Users.updateUser(user); + + const userSexuality = await user.getSexuality(); + if (userSexuality === null) { + const sexuality = await this._dbContext.Sexualities.findSexualityById( + payload.sexualityId + ); + if (!sexuality) throw new HttpException("Sexuality not found.", 404); + + await this._dbContext.neo4jService.tryWriteAsync( + ` + MATCH (u:User { userId: $userId }), (s:Sexuality { sexualityId: $sexualityId }) + MERGE (u)-[:${UserToSexualityRelTypes.HAS_SEXUALITY} { + isPrivate: $isPrivate + }]->(s) + `, + { + userId: user.userId, + sexualityId: sexuality.sexualityId, + isPrivate: payload.isSexualityOpen, + } + ); + } else { + if (payload.isSexualityOpen !== user.isSexualityPrivate) { + await this._dbContext.neo4jService.tryWriteAsync( + ` + MATCH (u:User { userId: $userId })-[r:${UserToSexualityRelTypes.HAS_SEXUALITY}]->(s:Sexuality) + SET r.isPrivate = $isPrivate + `, + { + userId: user.userId, + isPrivate: payload.isSexualityOpen, + } + ); + } + } + + const userGender = await user.getGender(); + if (userGender === null) { + const gender = await this._dbContext.Genders.findGenderById(payload.genderId); + if (!gender) throw new HttpException("Gender not found.", 404); + + await this._dbContext.neo4jService.tryWriteAsync( + ` + MATCH (u:User { userId : $userId }), (g:Gender { genderId: $genderId }) + MERGE (u)-[:${UserToGenderRelTypes.HAS_GENDER} { + isPrivate: $isPrivate + }]->(g) + `, + { + userId: user.userId, + genderId: gender.genderId, + isPrivate: payload.isGenderPrivate, + } + ); + } else { + if (payload.isGenderPrivate !== user.isGenderPrivate) { + await this._dbContext.neo4jService.tryWriteAsync( + ` + MATCH (u:User { userId: $userId })-[r:${UserToGenderRelTypes.HAS_GENDER}]->(g:Gender) + SET r.isPrivate = $isPrivate + `, + { + userId: user.userId, + isPrivate: payload.isSexualityOpen, + } + ); + } + } + + const userOpenness = await user.getOpenness(); + if (userOpenness === null) { + const openness = await this._dbContext.Openness.findOpennessById(payload.opennessId); + if (!openness) throw new HttpException("Openness not found.", 404); + + await this._dbContext.neo4jService.tryWriteAsync( + ` + MATCH (u:User { userId : $userId }), (g:Openness { opennessId: $opennessId }) + MERGE (u)-[:${UserToOpennessRelTypes.HAS_OPENNESS_LEVEL_OF} { + isPrivate: $isPrivate + }]->(g) + `, + { + userId: user.userId, + opennessId: openness.opennessId, + isPrivate: payload.isGenderPrivate, + } + ); + } else { + if (payload.isOpennessPrivate !== user.isOpennessPrivate) { + await this._dbContext.neo4jService.tryWriteAsync( + ` + MATCH (u:User { userId: $userId })-[r:${UserToOpennessRelTypes.HAS_OPENNESS_LEVEL_OF}]->(o:Openness) + SET r.isPrivate = $isPrivate + `, + { + userId: user.userId, + isPrivate: payload.isSexualityOpen, + } + ); + } + } + } + + public async updateBio(payload: UpdateBioDto): Promise { + const user: User = this.getUserFromRequest(); + throw new Error("Method not implemented."); + } + + public async updateAvatar(payload: UpdateAvatarDto): Promise { + const user: User = this.getUserFromRequest(); + throw new Error("Method not implemented."); + } + + public async updateGender(payload: UpdateGenderDto): Promise { + const user: User = this.getUserFromRequest(); + throw new Error("Method not implemented."); + } + + public async updateSexuality(payload: UpdateSexualityDto): Promise { + const user: User = this.getUserFromRequest(); + throw new Error("Method not implemented."); + } + + public async updateOpenness(payload: UpdateOpennessDto): Promise { + const user: User = this.getUserFromRequest(); + throw new Error("Method not implemented."); + } + + private getUserFromRequest(): User { + const user = this._request.user as User; + if (user === undefined) throw new HttpException("Authentication failed.", 403); + return user; + } +} From ffcdc9f84d67d1c48b25c16a92b63ca648d08919 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sun, 27 Nov 2022 23:46:29 -0800 Subject: [PATCH 059/153] give more thorttler timeout --- src/app.module.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index 48f2086..f4b044a 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -17,8 +17,8 @@ import { APP_GUARD } from "@nestjs/core"; @Module({ imports: [ ThrottlerModule.forRoot({ - ttl: 60, - limit: 10, + ttl: 69, + limit: 42, }), Neo4jModule.forRootAsync({ imports: [ConfigModule], From dd9438f8f018ed22039e3ebb8d3867a4cbd71d29 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sun, 27 Nov 2022 23:49:45 -0800 Subject: [PATCH 060/153] add the dtos for profile setup --- src/users/dtos/index.ts | 6 +++++ src/users/dtos/publicUser.dto.ts | 4 +-- src/users/dtos/setupProfile.dto.ts | 38 +++++++++++++++++++++++++++ src/users/dtos/updateAvatar.dto.ts | 16 +++++++++++ src/users/dtos/updateBio.dto.ts | 12 +++++++++ src/users/dtos/updateGender.dto.ts | 12 +++++++++ src/users/dtos/updateOpenness.dto.ts | 12 +++++++++ src/users/dtos/updateSexuality.dto.ts | 12 +++++++++ 8 files changed, 110 insertions(+), 2 deletions(-) diff --git a/src/users/dtos/index.ts b/src/users/dtos/index.ts index 016da96..9c878ec 100644 --- a/src/users/dtos/index.ts +++ b/src/users/dtos/index.ts @@ -1 +1,7 @@ export { PublicUserDto } from "./publicUser.dto"; +export { UpdateBioDto } from "./updateBio.dto"; +export { UpdateAvatarDto } from "./updateAvatar.dto"; +export { UpdateGenderDto } from "./updateGender.dto"; +export { UpdateOpennessDto } from "./updateOpenness.dto"; +export { UpdateSexualityDto } from "./updateSexuality.dto"; +export { SetupProfileDto } from "./setupProfile.dto"; diff --git a/src/users/dtos/publicUser.dto.ts b/src/users/dtos/publicUser.dto.ts index cb157ab..2729825 100644 --- a/src/users/dtos/publicUser.dto.ts +++ b/src/users/dtos/publicUser.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsNotEmpty } from "class-validator"; import { Gender, Openness, Sexuality, User } from "../models"; -import { AvatarAscii, AvatarUrl } from "../models/user"; +import { UserAvatar } from "../models/user"; export class PublicUserDto { @ApiProperty({ type: String, format: "uuid" }) @@ -10,7 +10,7 @@ export class PublicUserDto { @ApiProperty({ type: String }) @IsNotEmpty() - avatar: AvatarAscii | AvatarUrl; + avatar: UserAvatar; @ApiProperty({ type: String }) @IsNotEmpty() diff --git a/src/users/dtos/setupProfile.dto.ts b/src/users/dtos/setupProfile.dto.ts index e69de29..d84bacd 100644 --- a/src/users/dtos/setupProfile.dto.ts +++ b/src/users/dtos/setupProfile.dto.ts @@ -0,0 +1,38 @@ +import { IsBoolean, IsString, IsUUID } from "class-validator"; +import { UserAvatar } from "../models/user"; +import { ApiProperty } from "@nestjs/swagger"; + +export class SetupProfileDto { + @ApiProperty({ type: String }) + @IsString() + bio: string; + + @ApiProperty({ type: String }) + @IsString() + avatar: UserAvatar; + + @ApiProperty({ type: String, format: "uuid" }) + @IsUUID() + genderId: UUID; + @ApiProperty({ type: Boolean }) + @IsBoolean() + isGenderPrivate: boolean; + + @ApiProperty({ type: String, format: "uuid" }) + @IsUUID() + sexualityId: UUID; + @ApiProperty({ type: Boolean }) + @IsBoolean() + isSexualityOpen: boolean; + + @ApiProperty({ type: String, format: "uuid" }) + @IsUUID() + opennessId: UUID; + @ApiProperty({ type: Boolean }) + @IsBoolean() + isOpennessPrivate: boolean; + + constructor(partial?: Partial) { + Object.assign(this, partial); + } +} diff --git a/src/users/dtos/updateAvatar.dto.ts b/src/users/dtos/updateAvatar.dto.ts index e69de29..fe5211e 100644 --- a/src/users/dtos/updateAvatar.dto.ts +++ b/src/users/dtos/updateAvatar.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsString } from "class-validator"; +import { UserAvatar } from "../models/user"; + +export class UpdateAvatarDto { + @ApiProperty({ + type: String, + description: "Can be an Ascii, an URL pointing to an image, or an SVG.", + }) + @IsString() + avatar: UserAvatar; + + constructor(partial?: Partial) { + Object.assign(this, partial); + } +} diff --git a/src/users/dtos/updateBio.dto.ts b/src/users/dtos/updateBio.dto.ts index e69de29..237f1a8 100644 --- a/src/users/dtos/updateBio.dto.ts +++ b/src/users/dtos/updateBio.dto.ts @@ -0,0 +1,12 @@ +import { IsString } from "class-validator"; +import { ApiProperty } from "@nestjs/swagger"; + +export class UpdateBioDto { + @ApiProperty({ type: String }) + @IsString() + bio: string; + + constructor(partial?: Partial) { + Object.assign(this, partial); + } +} diff --git a/src/users/dtos/updateGender.dto.ts b/src/users/dtos/updateGender.dto.ts index e69de29..a9b337d 100644 --- a/src/users/dtos/updateGender.dto.ts +++ b/src/users/dtos/updateGender.dto.ts @@ -0,0 +1,12 @@ +import { IsUUID } from "class-validator"; +import { ApiProperty } from "@nestjs/swagger"; + +export class UpdateGenderDto { + @ApiProperty({ type: String, format: "uuid" }) + @IsUUID() + genderId: UUID; + + constructor(partial?: Partial) { + Object.assign(this, partial); + } +} diff --git a/src/users/dtos/updateOpenness.dto.ts b/src/users/dtos/updateOpenness.dto.ts index e69de29..9cfabbf 100644 --- a/src/users/dtos/updateOpenness.dto.ts +++ b/src/users/dtos/updateOpenness.dto.ts @@ -0,0 +1,12 @@ +import { IsUUID } from "class-validator"; +import { ApiProperty } from "@nestjs/swagger"; + +export class UpdateOpennessDto { + @ApiProperty({ type: String, format: "uuid" }) + @IsUUID() + opennessId: UUID; + + constructor(partial?: Partial) { + Object.assign(this, partial); + } +} diff --git a/src/users/dtos/updateSexuality.dto.ts b/src/users/dtos/updateSexuality.dto.ts index e69de29..cbb7802 100644 --- a/src/users/dtos/updateSexuality.dto.ts +++ b/src/users/dtos/updateSexuality.dto.ts @@ -0,0 +1,12 @@ +import { IsUUID } from "class-validator"; +import { ApiProperty } from "@nestjs/swagger"; + +export class UpdateSexualityDto { + @ApiProperty({ type: String, format: "uuid" }) + @IsUUID() + sexualityId: UUID; + + constructor(partial?: Partial) { + Object.assign(this, partial); + } +} From b74bd2dda4894d13c1991a60d733a37df5437e0f Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sun, 27 Nov 2022 23:50:12 -0800 Subject: [PATCH 061/153] revise the avatars --- src/users/models/user.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/users/models/user.ts b/src/users/models/user.ts index 162b52a..6432347 100644 --- a/src/users/models/user.ts +++ b/src/users/models/user.ts @@ -30,8 +30,9 @@ import { IsUUID, } from "class-validator"; -export type AvatarUrl = string; -export type AvatarAscii = string; +type AvatarUrl = string; +type AvatarAscii = string; +export type UserAvatar = AvatarUrl | AvatarAscii; @Labels("User") export class User extends Model { @@ -48,7 +49,7 @@ export class User extends Model { @NodeProperty() @IsString() - avatar: AvatarAscii | AvatarUrl; + avatar: UserAvatar; @NodeProperty() @IsString() From 0db7c576128ff048f78bee7a89d7688e68808257 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sun, 27 Nov 2022 23:50:32 -0800 Subject: [PATCH 062/153] add the controllers --- src/users/users.module.ts | 41 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/users/users.module.ts b/src/users/users.module.ts index 2b2f137..f3be481 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -3,6 +3,13 @@ import { UsersRepository } from "./repositories/users/users.repository"; import { UsersController } from "./controllers/users.controller"; import { _$ } from "../_domain/injectableTokens"; import { DatabaseAccessLayerModule } from "../database-access-layer/database-access-layer.module"; +import { ProfileSetupService } from "./services/profileSetup/profileSetup.service"; +import { GenderRepository } from "./repositories/gender/gender.repository"; +import { SexualityRepository } from "./repositories/sexuality/sexuality.repository"; +import { OpennessRepository } from "./repositories/openness/openness.repository"; +import { SexualitiesController } from "./controllers/sexualities.controller"; +import { GendersController } from "./controllers/genders.controller"; +import { OpennessController } from "./controllers/openness.controller"; @Module({ imports: [forwardRef(() => DatabaseAccessLayerModule)], @@ -11,13 +18,45 @@ import { DatabaseAccessLayerModule } from "../database-access-layer/database-acc provide: _$.IUsersRepository, useClass: UsersRepository, }, + { + provide: _$.IProfileSetupService, + useClass: ProfileSetupService, + }, + { + provide: _$.IGenderRepository, + useClass: GenderRepository, + }, + { + provide: _$.ISexualityRepository, + useClass: SexualityRepository, + }, + { + provide: _$.IOpennessRepository, + useClass: OpennessRepository, + }, ], exports: [ { provide: _$.IUsersRepository, useClass: UsersRepository, }, + { + provide: _$.IProfileSetupService, + useClass: ProfileSetupService, + }, + { + provide: _$.IGenderRepository, + useClass: GenderRepository, + }, + { + provide: _$.ISexualityRepository, + useClass: SexualityRepository, + }, + { + provide: _$.IOpennessRepository, + useClass: OpennessRepository, + }, ], - controllers: [UsersController], + controllers: [UsersController, SexualitiesController, GendersController, OpennessController], }) export class UsersModule {} From 382717662385170b1aba88b67c688a79b651942a Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sun, 27 Nov 2022 23:50:53 -0800 Subject: [PATCH 063/153] make controllers for sexuality, gender, and opneness --- src/users/controllers/genders.controller.ts | 37 +++++++++++++++++++ src/users/controllers/openness.controller.ts | 28 ++++++++++++++ .../controllers/sexualities.controller.ts | 28 ++++++++++++++ src/users/controllers/users.controller.ts | 17 ++++++++- 4 files changed, 108 insertions(+), 2 deletions(-) diff --git a/src/users/controllers/genders.controller.ts b/src/users/controllers/genders.controller.ts index e69de29..20315d1 100644 --- a/src/users/controllers/genders.controller.ts +++ b/src/users/controllers/genders.controller.ts @@ -0,0 +1,37 @@ +import { + ClassSerializerInterceptor, + Controller, + Get, + Inject, + Param, + ParseUUIDPipe, + UseInterceptors, +} from "@nestjs/common"; +import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; +import { IGenderRepository } from "../repositories/gender/gender.repository.interface"; +import { _$ } from "../../_domain/injectableTokens"; +import { Gender } from "../models"; + +@UseInterceptors(ClassSerializerInterceptor) +@ApiTags("genders") +@ApiBearerAuth() +@Controller("genders") +export class GendersController { + private readonly _genderRepository: IGenderRepository; + + constructor(@Inject(_$.IGenderRepository) genderRepository: IGenderRepository) { + this._genderRepository = genderRepository; + } + + @Get("/") + public async getGenders(): Promise { + return await this._genderRepository.findAll(); + } + + @Get("/:genderId") + public async getGenderById( + @Param("genderId", new ParseUUIDPipe()) genderId: UUID + ): Promise { + return await this._genderRepository.findGenderById(genderId); + } +} diff --git a/src/users/controllers/openness.controller.ts b/src/users/controllers/openness.controller.ts index e69de29..32abe13 100644 --- a/src/users/controllers/openness.controller.ts +++ b/src/users/controllers/openness.controller.ts @@ -0,0 +1,28 @@ +import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; +import { Controller, Get, Inject, Param, ParseUUIDPipe } from "@nestjs/common"; +import { IOpennessRepository } from "../repositories/openness/openness.repository.interface"; +import { _$ } from "../../_domain/injectableTokens"; +import { Openness } from "../models"; + +@ApiTags("openness") +@ApiBearerAuth() +@Controller("openness") +export class OpennessController { + private readonly _opennessRepository: IOpennessRepository; + + constructor(@Inject(_$.IOpennessRepository) opennessRepository: IOpennessRepository) { + this._opennessRepository = opennessRepository; + } + + @Get("/") + public async getOpennesses(): Promise { + return await this._opennessRepository.findAll(); + } + + @Get("/:opennessId") + public async getOpennessById( + @Param("opennessId", new ParseUUIDPipe()) opennessId: UUID + ): Promise { + return await this._opennessRepository.findOpennessById(opennessId); + } +} diff --git a/src/users/controllers/sexualities.controller.ts b/src/users/controllers/sexualities.controller.ts index e69de29..5da8840 100644 --- a/src/users/controllers/sexualities.controller.ts +++ b/src/users/controllers/sexualities.controller.ts @@ -0,0 +1,28 @@ +import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; +import { Controller, Get, Inject, Param, ParseUUIDPipe } from "@nestjs/common"; +import { ISexualityRepository } from "../repositories/sexuality/sexuality.repository.interface"; +import { _$ } from "../../_domain/injectableTokens"; +import { Sexuality } from "../models"; + +@ApiTags("sexualities") +@ApiBearerAuth() +@Controller("sexualities") +export class SexualitiesController { + private readonly _sexualityRepository: ISexualityRepository; + + constructor(@Inject(_$.ISexualityRepository) sexualityRepository: ISexualityRepository) { + this._sexualityRepository = sexualityRepository; + } + + @Get("/") + public async getSexualities(): Promise { + return await this._sexualityRepository.findAll(); + } + + @Get("/:sexualityId") + public async getSexualityById( + @Param("sexualityId", new ParseUUIDPipe()) sexualityId: UUID + ): Promise { + return await this._sexualityRepository.findSexualityById(sexualityId); + } +} diff --git a/src/users/controllers/users.controller.ts b/src/users/controllers/users.controller.ts index e833348..f9d8680 100644 --- a/src/users/controllers/users.controller.ts +++ b/src/users/controllers/users.controller.ts @@ -8,6 +8,7 @@ import { Param, ParseUUIDPipe, Patch, + Put, UseGuards, UseInterceptors, } from "@nestjs/common"; @@ -21,7 +22,8 @@ import { IUsersRepository } from "../repositories/users/users.repository.interfa import { ModerationPayloadDto } from "../../moderation/dtos/moderatorActions"; import { AuthedUser } from "../../auth/decorators/authedUser.param.decorator"; import { OptionalJwtAuthGuard } from "../../auth/guards/optionalJwtAuth.guard"; -import { PublicUserDto } from "../dtos"; +import { PublicUserDto, SetupProfileDto } from "../dtos"; +import { IProfileSetupService } from "../services/profileSetup/profileSetup.service.interface"; @UseInterceptors(ClassSerializerInterceptor) @ApiTags("users") @@ -29,9 +31,14 @@ import { PublicUserDto } from "../dtos"; @Controller("users") export class UsersController { private readonly _usersRepository: IUsersRepository; + private readonly _profileSetup: IProfileSetupService; - constructor(@Inject(_$.IUsersRepository) usersRepository: IUsersRepository) { + constructor( + @Inject(_$.IUsersRepository) usersRepository: IUsersRepository, + @Inject(_$.IProfileSetupService) profileSetupService: IProfileSetupService + ) { this._usersRepository = usersRepository; + this._profileSetup = profileSetupService; } @Get() @@ -68,6 +75,12 @@ export class UsersController { return PublicUserDto.fromUser(await user.toJSON()); } + @Put("/profileSetup") + @UseGuards(AuthGuard("jwt")) + public async profileSetupSubmit(@Body() setupProfileDto: SetupProfileDto): Promise { + await this._profileSetup.setupProfile(setupProfileDto); + } + @Patch("/ban") @Roles(Role.MODERATOR) @UseGuards(AuthGuard("jwt"), RolesGuard) From cbf1c288e380ec40c08132edf64604085b8b4408 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Sun, 27 Nov 2022 23:51:16 -0800 Subject: [PATCH 064/153] fix some dependency injection stuff --- src/_domain/injectableTokens.ts | 1 + src/database-access-layer/databaseContext.ts | 4 ++++ src/posts/posts.module.ts | 18 ------------------ 3 files changed, 5 insertions(+), 18 deletions(-) diff --git a/src/_domain/injectableTokens.ts b/src/_domain/injectableTokens.ts index 1a693ab..ba22414 100644 --- a/src/_domain/injectableTokens.ts +++ b/src/_domain/injectableTokens.ts @@ -10,6 +10,7 @@ const injectableTokens = { ISexualityRepository: Symbol("ISexualityRepository"), IOpennessRepository: Symbol("IOpennessRepository"), IGenderRepository: Symbol("IGenderRepository"), + IProfileSetupService: Symbol("IProfileSetupService"), // Posts Module IPostsRepository: Symbol("IPostsRepository"), diff --git a/src/database-access-layer/databaseContext.ts b/src/database-access-layer/databaseContext.ts index ea7daee..601b790 100644 --- a/src/database-access-layer/databaseContext.ts +++ b/src/database-access-layer/databaseContext.ts @@ -8,6 +8,7 @@ import { IGenderRepository } from "../users/repositories/gender/gender.repositor import { ISexualityRepository } from "../users/repositories/sexuality/sexuality.repository.interface"; import { IUsersRepository } from "../users/repositories/users/users.repository.interface"; import { _$ } from "../_domain/injectableTokens"; +import { IOpennessRepository } from "../users/repositories/openness/openness.repository.interface"; @Injectable() export class DatabaseContext { @@ -21,6 +22,7 @@ export class DatabaseContext { @Inject(_$.IUsersRepository) usersRepository: IUsersRepository, @Inject(_$.ISexualityRepository) sexualityRepository: ISexualityRepository, @Inject(_$.IGenderRepository) genderRepository: IGenderRepository, + @Inject(_$.IOpennessRepository) opennessRepository: IOpennessRepository, @Inject(_$.ICommentsRepository) commentsRepository: ICommentsRepository ) { this.neo4jService = neo4jService; @@ -30,6 +32,7 @@ export class DatabaseContext { this.PostTags = postTagsRepository; this.Users = usersRepository; this.Sexualities = sexualityRepository; + this.Openness = opennessRepository; this.Genders = genderRepository; this.Comments = commentsRepository; } @@ -39,6 +42,7 @@ export class DatabaseContext { public PostTags: IPostTagsRepository; public Users: IUsersRepository; public Sexualities: ISexualityRepository; + public Openness: IOpennessRepository; public Genders: IGenderRepository; public Comments: ICommentsRepository; } diff --git a/src/posts/posts.module.ts b/src/posts/posts.module.ts index 0a0714f..9178996 100644 --- a/src/posts/posts.module.ts +++ b/src/posts/posts.module.ts @@ -1,8 +1,6 @@ import { HttpModule } from "@nestjs/axios"; import { forwardRef, Module } from "@nestjs/common"; import { DatabaseAccessLayerModule } from "../database-access-layer/database-access-layer.module"; -import { GenderRepository } from "../users/repositories/gender/gender.repository"; -import { SexualityRepository } from "../users/repositories/sexuality/sexuality.repository"; import { _$ } from "../_domain/injectableTokens"; import { PostsController } from "./controllers/posts.controller"; import { PostTagsController } from "./controllers/postTags.controller"; @@ -33,14 +31,6 @@ import { ModerationModule } from "../moderation/moderation.module"; provide: _$.IPostTypesRepository, useClass: PostTypesRepository, }, - { - provide: _$.IGenderRepository, - useClass: GenderRepository, - }, - { - provide: _$.ISexualityRepository, - useClass: SexualityRepository, - }, { provide: _$.IPostAwardRepository, useClass: PostAwardRepository, @@ -63,14 +53,6 @@ import { ModerationModule } from "../moderation/moderation.module"; provide: _$.IPostTypesRepository, useClass: PostTypesRepository, }, - { - provide: _$.IGenderRepository, - useClass: GenderRepository, - }, - { - provide: _$.ISexualityRepository, - useClass: SexualityRepository, - }, { provide: _$.IPostAwardRepository, useClass: PostAwardRepository, From b0ce4421a61f91cc9695c0e6f016843fc8c2f721 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Mon, 28 Nov 2022 00:49:33 -0800 Subject: [PATCH 065/153] make queery and story of the day available and ready to go --- src/posts/controllers/posts.controller.ts | 19 +++- .../services/posts/posts.service.interface.ts | 3 +- src/posts/services/posts/posts.service.ts | 88 +++++++++++++++---- 3 files changed, 90 insertions(+), 20 deletions(-) diff --git a/src/posts/controllers/posts.controller.ts b/src/posts/controllers/posts.controller.ts index 717e7a4..223ef8a 100644 --- a/src/posts/controllers/posts.controller.ts +++ b/src/posts/controllers/posts.controller.ts @@ -6,13 +6,12 @@ import { Controller, Delete, Get, - HttpException, Inject, Param, ParseUUIDPipe, Post, UseGuards, - UseInterceptors + UseInterceptors, } from "@nestjs/common"; import { AuthGuard } from "@nestjs/passport"; import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; @@ -62,6 +61,22 @@ export class PostsController { return await Promise.all(decoratedPosts); } + @Get("/queery/ofTheDay") + @UseGuards(OptionalJwtAuthGuard) + @CacheTTL(3600 * 24) + @UseInterceptors(CacheInterceptor) + public async getQueeriesOfTheDay(): Promise { + return await this._postsService.getQueeriesOfTheDay(); + } + + @Get("/story/ofTheDay") + @UseGuards(OptionalJwtAuthGuard) + @CacheTTL(3600 * 24) + @UseInterceptors(CacheInterceptor) + public async getStoriesOfTheDay(): Promise { + return await this._postsService.getStoriesOfTheDay(); + } + @Get("/queery") @UseGuards(OptionalJwtAuthGuard) @CacheTTL(5) diff --git a/src/posts/services/posts/posts.service.interface.ts b/src/posts/services/posts/posts.service.interface.ts index 7df7466..0bf74bd 100644 --- a/src/posts/services/posts/posts.service.interface.ts +++ b/src/posts/services/posts/posts.service.interface.ts @@ -7,7 +7,8 @@ export type postSortCallback = (postA: Post, postB: Post) => number; export interface IPostsService { authorNewPost(postPayload: PostCreationPayloadDto): Promise; - getQueeryOfTheDay(): Promise; + getQueeriesOfTheDay(): Promise; + getStoriesOfTheDay(): Promise; findAllQueeries(): Promise; diff --git a/src/posts/services/posts/posts.service.ts b/src/posts/services/posts/posts.service.ts index ccbe532..330b7c3 100644 --- a/src/posts/services/posts/posts.service.ts +++ b/src/posts/services/posts/posts.service.ts @@ -66,34 +66,88 @@ export class PostsService implements IPostsService { ); } - public async getQueeryOfTheDay(): Promise { - const allPosts = await this._dbContext.Posts.findAll(); - if (allPosts.length === 0) + public async getQueeriesOfTheDay(): Promise { + let user: User = null; + try { + user = this.getUserFromRequest(); + } catch (e) { + // do nothing + } + + const allQueeries = await this._dbContext.Posts.findPostByPostType("queery"); + if (allQueeries.length === 0) throw new HttpException( "No posts found in the database. Please checkout this application's usage tutorials.", 404 ); - const queeryPosts: Post[] = []; - for (const i in allPosts) { - if (!allPosts[i].pending) continue; + const queeries: Post[] = []; + for (const i in allQueeries) { + if (allQueeries[i].pending) continue; - await allPosts[i].getDeletedProps(); - if (allPosts[i].deletedProps !== null) continue; + await allQueeries[i].getDeletedProps(); + if (allQueeries[i].deletedProps !== null) continue; - await allPosts[i].getRestricted(); - if (allPosts[i].restrictedProps !== null) continue; + await allQueeries[i].getRestricted(); + if (allQueeries[i].restrictedProps !== null) continue; - await allPosts[i].getPostType(); - if (allPosts[i].postType.postTypeName === "Queery") { - queeryPosts.push(allPosts[i]); - } + allQueeries[i].totalComments = await this.getTotalComments(allQueeries[i]); + + allQueeries[i] = await allQueeries[i].toJSON({ + authenticatedUserId: user?.userId ?? undefined, + }); + + queeries.push(allQueeries[i]); + } + + queeries.sort( + (postA, postB) => + postA.totalComments + postA.totalVotes - (postB.totalComments + postB.totalVotes) + ); + + return queeries.slice(0, 5); + } + + public async getStoriesOfTheDay(): Promise { + let user: User = null; + try { + user = this.getUserFromRequest(); + } catch (e) { + // do nothing + } + + const allStories = await this._dbContext.Posts.findPostByPostType("story"); + if (allStories.length === 0) + throw new HttpException( + "No posts found in the database. Please checkout this application's usage tutorials.", + 404 + ); + + const stories: Post[] = []; + for (const i in allStories) { + if (allStories[i].pending) continue; + + await allStories[i].getDeletedProps(); + if (allStories[i].deletedProps !== null) continue; + + await allStories[i].getRestricted(); + if (allStories[i].restrictedProps !== null) continue; + + allStories[i].totalComments = await this.getTotalComments(allStories[i]); + + allStories[i] = await allStories[i].toJSON({ + authenticatedUserId: user?.userId ?? undefined, + }); + + stories.push(allStories[i]); } - if (queeryPosts.length === 0) throw new HttpException("No Queery posts found", 404); + stories.sort( + (postA, postB) => + postA.totalComments + postA.totalVotes - (postB.totalComments + postB.totalVotes) + ); - const queeryOfTheDayIndex = Math.floor(Math.random() * queeryPosts.length); - return queeryPosts[queeryOfTheDayIndex]; + return stories.slice(0, 5); } public async findAllQueeries(): Promise { From cc8077985e705f62622fda07b020b2ef79b15d1a Mon Sep 17 00:00:00 2001 From: Sean Ng Date: Mon, 28 Nov 2022 17:46:50 -0800 Subject: [PATCH 066/153] oopsie --- src/app.module.ts | 8 ++++---- src/neo4j/services/neo4j.seed.service.ts | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index f4b044a..ebdc261 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -62,9 +62,9 @@ export class AppModule { } onModuleInit() { - // this._neo4jSeedService - // .seed() - // .then(() => this._logger.log("Neo4j database seeded ✅")) - // .catch(error => this._logger.error(error)); + this._neo4jSeedService + .seed() + .then(() => this._logger.log("Neo4j database seeded ✅")) + .catch(error => this._logger.error(error)); } } diff --git a/src/neo4j/services/neo4j.seed.service.ts b/src/neo4j/services/neo4j.seed.service.ts index 21f95d1..02270a1 100644 --- a/src/neo4j/services/neo4j.seed.service.ts +++ b/src/neo4j/services/neo4j.seed.service.ts @@ -488,7 +488,7 @@ export class Neo4jSeedService { }, }), new User({ - userId: "3109f9e2-a262-4aef-b648-90d86d6fbf6c", + userId: "dc83daa3-d26b-4063-87b1-2b719069654e", createdAt: new Date().getTime(), updatedAt: new Date().getTime(), avatar: "^_^", @@ -580,11 +580,11 @@ export class Neo4jSeedService { postTags: (await this.getPostTags()).slice(0, 1), restrictedProps: new RestrictedProps({ restrictedAt: 1665780000, - moderatorId: "3109f9e2-a262-4aef-b648-90d86d6fbf6c", + moderatorId: "8f0c1ecf-6853-4642-9199-6e8244b89312", reason: "The moderator thinks there is profanity in this post", }), authorUser: new User({ - userId: "3109f9e2-a262-4aef-b648-90d86d6fbf6c", + userId: "dc83daa3-d26b-4063-87b1-2b719069654e", }), pending: true, totalVotes: 3, @@ -640,7 +640,7 @@ export class Neo4jSeedService { createdAt: 1666990000, updatedAt: 1666990000, authorUser: new User({ - userId: "3109f9e2-a262-4aef-b648-90d86d6fbf6c", + userId: "dc83daa3-d26b-4063-87b1-2b719069654e", }), pinned: false, pending: true, @@ -659,7 +659,7 @@ export class Neo4jSeedService { pending: true, restrictedProps: new RestrictedProps({ restrictedAt: 1667000001, - moderatorId: "3109f9e2-a262-4aef-b648-90d86d6fbf6c", + moderatorId: "8f0c1ecf-6853-4642-9199-6e8244b89312", reason: "The moderator died of cringe", }), childComments: [], From 29995bb1889dfa9f6cfda35974309d9f515166a4 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Mon, 28 Nov 2022 17:49:15 -0800 Subject: [PATCH 067/153] fix a few bugs --- src/auth/dtos/changePasswordUser.dto.ts | 5 +-- src/auth/guards/roles.guard.ts | 6 ++-- src/neo4j/services/neo4j.seed.service.ts | 31 ++++++++++++++++++- .../postReport/postsReport.service.ts | 2 +- src/posts/services/posts/posts.service.ts | 6 ++-- 5 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/auth/dtos/changePasswordUser.dto.ts b/src/auth/dtos/changePasswordUser.dto.ts index c225ed0..c55b654 100644 --- a/src/auth/dtos/changePasswordUser.dto.ts +++ b/src/auth/dtos/changePasswordUser.dto.ts @@ -1,6 +1,5 @@ -import { IsInstance, IsOptional, IsString } from "class-validator"; +import { IsString } from "class-validator"; import { ApiProperty } from "@nestjs/swagger"; -import { Exclude } from "class-transformer"; import { User } from "../../users/models"; export class ChangePasswordUserDto { @@ -12,8 +11,6 @@ export class ChangePasswordUserDto { @IsString() newPassword: string; - @IsInstance(User) - @Exclude() user: User; constructor(partial?: Partial) { diff --git a/src/auth/guards/roles.guard.ts b/src/auth/guards/roles.guard.ts index bd172fa..df40833 100644 --- a/src/auth/guards/roles.guard.ts +++ b/src/auth/guards/roles.guard.ts @@ -2,7 +2,7 @@ import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common"; import { Observable } from "rxjs"; import { Reflector } from "@nestjs/core"; import { ROLES_KEY } from "../decorators/roles.decorator"; -import { Role } from "../../users/models"; +import { Role, User } from "../../users/models"; @Injectable() export class RolesGuard implements CanActivate { @@ -18,7 +18,9 @@ export class RolesGuard implements CanActivate { return true; } - const { user } = context.switchToHttp().getRequest(); + const request = context.switchToHttp().getRequest(); + const user: User = request.user; + console.log(user); if (!user) { return false; } diff --git a/src/neo4j/services/neo4j.seed.service.ts b/src/neo4j/services/neo4j.seed.service.ts index 21f95d1..cfe78e8 100644 --- a/src/neo4j/services/neo4j.seed.service.ts +++ b/src/neo4j/services/neo4j.seed.service.ts @@ -758,6 +758,12 @@ export class Neo4jSeedService { public async getGenders(): Promise { return new Array( + new Gender({ + genderId: "d2945763-d1fb-46aa-b896-7f701b4ca699", + genderName: "NA", + genderPronouns: "NA", + genderFlagSvg: ``, + }), new Gender({ genderId: "d2945763-d1fb-46aa-b896-7f701b4ca699", genderName: "Female", @@ -768,13 +774,31 @@ export class Neo4jSeedService { genderId: "585d31aa-d5b3-4b8d-9690-ffcd57ce2862", genderName: "Male", genderPronouns: "He/Him", - genderFlagSvg: "", + genderFlagSvg: ``, }), new Gender({ genderId: "23907da4-c3f2-4e96-a73d-423e64f18a21", genderName: "Non-binary", genderPronouns: "They/Them", genderFlagSvg: "", + }), + new Gender({ + genderId: "3af72545-99d4-4715-812b-c935fbf57f22", + genderName: "Non-binary", + genderPronouns: "Ve/Ver", + genderFlagSvg: "", + }), + new Gender({ + genderId: "23907da4-c3f2-4e96-a73d-423e64f18a21", + genderName: "Non-binary", + genderPronouns: "Xe/Xem", + genderFlagSvg: "", + }), + new Gender({ + genderId: "23907da4-c3f2-4e96-a73d-423e64f18a21", + genderName: "Non-binary", + genderPronouns: "Ze/Zir", + genderFlagSvg: "", }) ); } @@ -805,6 +829,11 @@ export class Neo4jSeedService { opennessId: "d5c97584-cd1b-4aa6-82ad-b5ddd3577bee", opennessLevel: 3, opennessDescription: "Fully Out", + }), + new Openness({ + opennessId: "77ec4978-6775-4b2f-9e91-ea60bb3742a5", + opennessLevel: 4, + opennessDescription: "Ally", }) ); } diff --git a/src/posts/services/postReport/postsReport.service.ts b/src/posts/services/postReport/postsReport.service.ts index a24d096..81efe85 100644 --- a/src/posts/services/postReport/postsReport.service.ts +++ b/src/posts/services/postReport/postsReport.service.ts @@ -26,7 +26,7 @@ export class PostsReportService implements IPostsReportService { const user = this.getUserFromRequest(); const post = await this._dbContext.Posts.findPostById(reportPostPayload.postId); - if (post === undefined) throw new Error("Post not found"); + if (!post) throw new Error("Post not found"); if (post.pending || post.restrictedProps !== null) { throw new HttpException( diff --git a/src/posts/services/posts/posts.service.ts b/src/posts/services/posts/posts.service.ts index 330b7c3..ffd9ee7 100644 --- a/src/posts/services/posts/posts.service.ts +++ b/src/posts/services/posts/posts.service.ts @@ -188,7 +188,7 @@ export class PostsService implements IPostsService { public async findPostById(postId: UUID): Promise { let foundPost = await this._dbContext.Posts.findPostById(postId); - if (foundPost === null) throw new HttpException("Post not found", 404); + if (!foundPost) throw new HttpException("Post not found", 404); if (foundPost.pending) throw new HttpException( @@ -208,7 +208,7 @@ export class PostsService implements IPostsService { nestedLevel: number ): Promise { const foundPost = await this._dbContext.Posts.findPostById(postId); - if (foundPost === null) throw new HttpException("Post not found", 404); + if (!foundPost) throw new HttpException("Post not found", 404); // level 0 means no nesting const comments = await foundPost.getComments(topLevelLimit); @@ -253,7 +253,7 @@ export class PostsService implements IPostsService { const user = this.getUserFromRequest(); const post = await this._dbContext.Posts.findPostById(votePostPayload.postId); - if (post === undefined) throw new HttpException("Post not found", 404); + if (!post) throw new HttpException("Post not found", 404); const queryResult = await this._dbContext.neo4jService.tryReadAsync( ` From c0ab3cdf1aabc75d059437b4bda5870f885662b6 Mon Sep 17 00:00:00 2001 From: Sean Ng Date: Mon, 28 Nov 2022 18:47:49 -0800 Subject: [PATCH 068/153] Update neo4j.seed.service.ts --- src/neo4j/services/neo4j.seed.service.ts | 218 ++++++++++++++++++++++- 1 file changed, 210 insertions(+), 8 deletions(-) diff --git a/src/neo4j/services/neo4j.seed.service.ts b/src/neo4j/services/neo4j.seed.service.ts index 02270a1..3403956 100644 --- a/src/neo4j/services/neo4j.seed.service.ts +++ b/src/neo4j/services/neo4j.seed.service.ts @@ -447,14 +447,96 @@ export class Neo4jSeedService { return new Array( new User({ - userId: "c612c987-6825-473c-882f-129f17d906b4", + userId: "71120d45-7a75-43fd-b79c-54b06e7868af", + createdAt: new Date().getTime(), + updatedAt: new Date().getTime(), + avatar: "(づ ̄ 3 ̄)づ", + bio: "My name is Wesley.", + username: "wesley", + normalizedUsername: "WESLEY", + passwordHash: "", + phoneNumber: null, + phoneNumberVerified: false, + email: "wesley@domain.com", + emailVerified: false, + level: 0, + roles: [Role.USER], + gender: new Gender({ + genderId: "585d31aa-d5b3-4b8d-9690-ffcd57ce2862", + }), + isGenderPrivate: false, + sexuality: new Sexuality({ + sexualityId: "9164d89b-8d71-4fd1-af61-155d1d7ffe53", + }), + isSexualityPrivate: false, + openness: new Openness({ + opennessId: "db27c417-a8a5-4703-9b35-9dc76e98fc95", + }), + isOpennessPrivate: false, + posts: { + [UserToPostRelTypes.AUTHORED]: { + records: (await this.getPosts()).slice(2, 3).map(post => ({ + entity: post, + relProps: new AuthoredProps({ + authoredAt: new Date("2022-09-13").getTime(), + anonymously: false, + }), + })), + relType: UserToPostRelTypes.AUTHORED, + }, + ...onlyAuthoredPosts, + }, + }), + new User({ + userId: "5e520efd-f78e-4cb0-8903-5c99197d4b8e", + createdAt: new Date().getTime(), + updatedAt: new Date().getTime(), + avatar: "...(* ̄0 ̄)ノ", + bio: "My name is Gaius.", + username: "gaius", + normalizedUsername: "GAIUS", + passwordHash: "", + phoneNumber: null, + phoneNumberVerified: false, + email: "gaius@rome.it", + emailVerified: false, + level: 0, + roles: [Role.USER], + gender: new Gender({ + genderId: "3af72545-99d4-4715-812b-c935fbf57f22", + }), + isGenderPrivate: false, + sexuality: new Sexuality({ + sexualityId: "5bc9535e-cc50-4112-91ad-717dc2de9492", + }), + isSexualityPrivate: false, + openness: new Openness({ + opennessId: "842b5bd7-1da1-4a95-9564-1fc3b97b3655", + }), + isOpennessPrivate: false, + posts: { + [UserToPostRelTypes.AUTHORED]: { + records: (await this.getPosts()).slice(3, 4).map(post => ({ + entity: post, + relProps: new AuthoredProps({ + authoredAt: new Date("10/26/2022").getTime(), + anonymously: false, + }), + })), + relType: UserToPostRelTypes.AUTHORED, + }, + ...onlyAuthoredPosts, + }, + }), + new User({ + userId: "a59437f4-ea62-4a15-a4e6-621b04af74d6", createdAt: new Date().getTime(), updatedAt: new Date().getTime(), avatar: ":^)", bio: "My name is gabriel.", username: "gabriel", normalizedUsername: "GABRIEL", - passwordHash: "someotherpassword", + passwordHash: "", phoneNumber: null, phoneNumberVerified: false, email: "email@domain.com", @@ -475,7 +557,7 @@ export class Neo4jSeedService { isOpennessPrivate: false, posts: { [UserToPostRelTypes.AUTHORED]: { - records: (await this.getPosts()).slice(0, 2).map(post => ({ + records: (await this.getPosts()).slice(0, 1).map(post => ({ entity: post, relProps: new AuthoredProps({ authoredAt: new Date("10/11/2022").getTime(), @@ -495,7 +577,7 @@ export class Neo4jSeedService { bio: "My name is alphonse.", username: "alphonse", normalizedUsername: "ALPHONSE", - passwordHash: "num3r1ca1pa55w0rd", + passwordHash: "", phoneNumber: null, phoneNumberVerified: false, email: "admin@domain.com", @@ -516,10 +598,10 @@ export class Neo4jSeedService { isOpennessPrivate: false, posts: { [UserToPostRelTypes.AUTHORED]: { - records: (await this.getPosts()).slice(1, 3).map(post => ({ + records: (await this.getPosts()).slice(1, 2).map(post => ({ entity: post, relProps: new AuthoredProps({ - authoredAt: new Date("09/6/2022").getTime(), + authoredAt: new Date("2022-09-06").getTime(), anonymously: false, }), })), @@ -545,7 +627,7 @@ export class Neo4jSeedService { postTags: (await this.getPostTags()).slice(-1), restrictedProps: null, authorUser: new User({ - userId: "c612c987-6825-473c-882f-129f17d906b4", + userId: "a59437f4-ea62-4a15-a4e6-621b04af74d6", }), pending: false, totalVotes: 0, @@ -599,6 +681,63 @@ export class Neo4jSeedService { relType: PostToAwardRelTypes.HAS_AWARD, }, }, + }), + new Post({ + postId: "806ca5f3-f80c-47fc-9e4d-00434dd18358", + postTitle: "Coming out to my brother", + postContent: `When I was 17 my girlfriend was over my house and my brother was home from college. + She asks me "does your brother know we're dating?"I say "good question." Then I yell to the other + side of the house "[Brother]! Did you know that I'm dating [girlfriend]?" I think he said + something like "I do now!"`, + updatedAt: 1663095600, + postType: (await this.getPostTypes())[1], + postTags: (await this.getPostTags()).slice(-1), + restrictedProps: null, + authorUser: new User({ + userId: "71120d45-7a75-43fd-b79c-54b06e7868af", + }), + pending: false, + totalVotes: 0, + awards: { + [PostToAwardRelTypes.HAS_AWARD]: { + records: [], + relType: PostToAwardRelTypes.HAS_AWARD, + }, + }, + }), + new Post({ + postId: "6326079f-fd2f-4b81-83fe-487daee459bc", + postTitle: "Coming out as GNC on my birthday", + postContent: `This is the uncomfortable story of how I came out to my mom and younger brother (with whom I live). + \n\nLeading up to my birthday last year I had realized I'm gnc (gender nonconforming) and had been starting to feel + very uncomfortable with masculine pronouns. I was already looking for a replacement name at the time and knew I + needed to do something about the way I was being referred to for my sanity. I've never been particularly fond of + holidays (my birthday especially) because something always seems to go awry when family is involved. None the less + I decided to take control of the situation and hope for the best. \n\nI custom ordered my cake and bought all the + ingredients for the meal and cooked it myself. I even did my makeup. Not something I do often, but it was my birthday + and I wanted to feel pretty. Before unveiling the cake (decorated with a nonbinary flag) I went through the whole + spiel about being nb and what that meant for me, as well as explaining pronouns and what not. \n\nWhen I finished they + were silent for a minute or two. My brother spoke first, saying something to the tune of: My dad says gnc people go + to hell. Obviously not the first thing you want to hear after such a tense interaction. Long story short dinner resolved + peacefully but it didn't seem I got through to them as the next couple weeks were filled with pronoun related arguments. + \n\nMuch headbutting later they've come around on my pronouns and the name I've chosen. As you can imagine though, for a + time I felt extremely unwelcome in the home, and after all the effort I put in I was pretty devastated my coming out + transpired so poorly.`, + updatedAt: 1666810800, + postType: (await this.getPostTypes())[1], + postTags: (await this.getPostTags()).slice(0, 1), + restrictedProps: null, + authorUser: new User({ + userId: "5e520efd-f78e-4cb0-8903-5c99197d4b8e", + }), + pending: false, + totalVotes: 0, + awards: { + [PostToAwardRelTypes.HAS_AWARD]: { + records: [], + relType: PostToAwardRelTypes.HAS_AWARD, + }, + }, }) ); } @@ -635,7 +774,7 @@ export class Neo4jSeedService { childComments: [ new Comment({ commentId: "3ee2801a-998d-437a-a49e-3974919f35c1", - parentId: "be9ab5e4-eb7c-469b-a1e3-592dca2a00d0", + parentId: "806ca5f3-f80c-47fc-9e4d-00434dd18358", commentContent: `Wow that's so scummy of your cousin to do that to you! I would have never forgiven her.`, createdAt: 1666990000, updatedAt: 1666990000, @@ -667,6 +806,64 @@ export class Neo4jSeedService { ], }), ], + }), + new Comment({ + commentId: "5a11c2af-7716-4b67-b00f-e23df9f0c740", + parentId: "806ca5f3-f80c-47fc-9e4d-00434dd18358", + commentContent: `That story is so funny! What a supportive brother.`, + createdAt: 1666890000, + updatedAt: 1666890000, + authorUser: new User({ + userId: "6bd7b8a2-bf8c-49e6-9c28-ee3d89be2453", + }), + pinned: false, + pending: true, + restrictedProps: null, + childComments: [ + new Comment({ + commentId: "9e55090e-2ebf-4679-a912-6542e78f4905", + parentId: "5a11c2af-7716-4b67-b00f-e23df9f0c740", + commentContent: `Same! I wish my brother was like that.`, + createdAt: 1666990000, + updatedAt: 1666990000, + authorUser: new User({ + userId: "dc83daa3-d26b-4063-87b1-2b719069654e", + }), + pinned: false, + pending: true, + restrictedProps: null, + childComments: [], + }), + ], + }), + new Comment({ + commentId: "773c1b6d-9d0a-43cb-94e5-2da1bac633c0", + parentId: "6326079f-fd2f-4b81-83fe-487daee459bc", + commentContent: + "Wow I can't even imagine how hard that must have been. Thankfully your family now understands what you're going through.", + createdAt: 1666990000, + updatedAt: 1666990000, + authorUser: new User({ + userId: "71120d45-7a75-43fd-b79c-54b06e7868af", + }), + pinned: false, + pending: false, + restrictedProps: null, + childComments: [], + }), + new Comment({ + commentId: "84213582-a148-46b7-878d-c30a9cd02231", + parentId: "6326079f-fd2f-4b81-83fe-487daee459bc", + commentContent: "You dad sounds exhausting. I'm glad you're doing better now.", + createdAt: 1666990000, + updatedAt: 1666990000, + authorUser: new User({ + userId: "dc83daa3-d26b-4063-87b1-2b719069654e", + }), + pinned: false, + pending: false, + restrictedProps: null, + childComments: [], }) ); } @@ -752,6 +949,11 @@ export class Neo4jSeedService { sexualityId: "2d32c4d3-4aca-4b03-bf68-ba104656183f", sexualityName: "Asexual", sexualityFlagSvg: "", + }), + new Sexuality({ + sexualityId: "5bc9535e-cc50-4112-91ad-717dc2de9492", + sexualityName: "Bisexual", + sexualityFlagSvg: "", }) ); } From 458cb8082cae7bab3584dae4d702781c151862cb Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Mon, 28 Nov 2022 19:00:50 -0800 Subject: [PATCH 069/153] fix --- src/neo4j/services/neo4j.seed.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/neo4j/services/neo4j.seed.service.ts b/src/neo4j/services/neo4j.seed.service.ts index dd4b9f4..1b4b8f4 100644 --- a/src/neo4j/services/neo4j.seed.service.ts +++ b/src/neo4j/services/neo4j.seed.service.ts @@ -991,13 +991,13 @@ export class Neo4jSeedService { genderFlagSvg: "", }), new Gender({ - genderId: "23907da4-c3f2-4e96-a73d-423e64f18a21", + genderId: "6d67e992-c6d1-45be-8316-7a839894bf36", genderName: "Non-binary", genderPronouns: "Xe/Xem", genderFlagSvg: "", }), new Gender({ - genderId: "23907da4-c3f2-4e96-a73d-423e64f18a21", + genderId: "16c10474-9fa6-4eac-aac2-a63423edb757", genderName: "Non-binary", genderPronouns: "Ze/Zir", genderFlagSvg: "", From 26b06e24ac3157e15ebb6361a0ba5f62cd7bc918 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Mon, 28 Nov 2022 19:13:06 -0800 Subject: [PATCH 070/153] fix the typo --- src/neo4j/services/neo4j.seed.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/neo4j/services/neo4j.seed.service.ts b/src/neo4j/services/neo4j.seed.service.ts index 1b4b8f4..12d9831 100644 --- a/src/neo4j/services/neo4j.seed.service.ts +++ b/src/neo4j/services/neo4j.seed.service.ts @@ -999,7 +999,7 @@ export class Neo4jSeedService { new Gender({ genderId: "16c10474-9fa6-4eac-aac2-a63423edb757", genderName: "Non-binary", - genderPronouns: "Ze/Zir", + genderPronouns: "Ze/Zie", genderFlagSvg: "", }) ); From 857a13fe6637691a54eac55a7eba5edd94842990 Mon Sep 17 00:00:00 2001 From: Sean Ng Date: Tue, 29 Nov 2022 00:57:58 -0800 Subject: [PATCH 071/153] Implemented ban and unban methods --- .../moderatorActions.service.interface.ts | 4 +- .../moderatorActions.service.ts | 29 ++++++++++++-- src/users/controllers/users.controller.ts | 17 ++++---- src/users/models/toSelf/index.ts | 1 + .../users/users.repository.interface.ts | 7 ++++ .../repositories/users/users.repository.ts | 39 +++++++++++++++++++ 6 files changed, 84 insertions(+), 13 deletions(-) diff --git a/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts b/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts index 3df3ee0..de931e0 100644 --- a/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts +++ b/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts @@ -76,7 +76,7 @@ export interface IModeratorActionsService { * - This will be a stretch goal. For now, we will just ban the user by adding a self relationship to the user node. * @param payload */ - banUser(payload: ModerationPayloadDto): Promise; + banUser(payload: ModerationPayloadDto): Promise; /** * Unbans a user. * TODO: @@ -85,5 +85,5 @@ export interface IModeratorActionsService { * - This will be a stretch goal. For now, we will just unban the user by removing the self relationship to the user node. * @param userId */ - unbanUser(userId: UUID): Promise; + unbanUser(userId: UUID): Promise; } diff --git a/src/moderation/services/moderatorActions/moderatorActions.service.ts b/src/moderation/services/moderatorActions/moderatorActions.service.ts index c8258bc..d4fb62c 100644 --- a/src/moderation/services/moderatorActions/moderatorActions.service.ts +++ b/src/moderation/services/moderatorActions/moderatorActions.service.ts @@ -7,6 +7,7 @@ import { Comment } from "../../../comments/models"; import { Post } from "../../../posts/models"; import { ModerationPayloadDto } from "../../dtos/moderatorActions"; import { User } from "../../../users/models"; +import { GotBannedProps } from "src/users/models/toSelf"; /** * This service is responsible for moderating posts and comments. @@ -22,11 +23,31 @@ export class ModeratorActionsService implements IModeratorActionsService { this._dbContext = dbContext; } - public async banUser(payload: ModerationPayloadDto): Promise { - throw new Error("Method not implemented."); + public async banUser(payload: ModerationPayloadDto): Promise { + const banProps = new GotBannedProps({ + bannedAt: Date.now(), + moderatorId: payload.moderatorId, + reason: payload.reason, + }); + await this._dbContext.Users.banUser(payload.id, banProps); + return; } - public async unbanUser(userId: UUID): Promise { - throw new Error("Method not implemented."); + + public async unbanUser(userId: UUID): Promise { + const user = await this._dbContext.Users.findUserById(userId); + if (!user) { + throw new HttpException("User not found", 404); + } + + await user.getGotBannedProps(); + if (!user.gotBannedProps) { + throw new HttpException("User is not banned", 400); + } + + await this._dbContext.Users.addPreviouslyBanned(userId, user.gotBannedProps); + + await this._dbContext.Users.unbanUser(userId); + return; } public async unrestrictComment(commentId: UUID): Promise { diff --git a/src/users/controllers/users.controller.ts b/src/users/controllers/users.controller.ts index f9d8680..f93e4ef 100644 --- a/src/users/controllers/users.controller.ts +++ b/src/users/controllers/users.controller.ts @@ -24,6 +24,7 @@ import { AuthedUser } from "../../auth/decorators/authedUser.param.decorator"; import { OptionalJwtAuthGuard } from "../../auth/guards/optionalJwtAuth.guard"; import { PublicUserDto, SetupProfileDto } from "../dtos"; import { IProfileSetupService } from "../services/profileSetup/profileSetup.service.interface"; +import { IModeratorActionsService } from "src/moderation/services/moderatorActions/moderatorActions.service.interface"; @UseInterceptors(ClassSerializerInterceptor) @ApiTags("users") @@ -32,13 +33,16 @@ import { IProfileSetupService } from "../services/profileSetup/profileSetup.serv export class UsersController { private readonly _usersRepository: IUsersRepository; private readonly _profileSetup: IProfileSetupService; + private readonly _moderationActions: IModeratorActionsService; constructor( @Inject(_$.IUsersRepository) usersRepository: IUsersRepository, - @Inject(_$.IProfileSetupService) profileSetupService: IProfileSetupService + @Inject(_$.IProfileSetupService) profileSetupService: IProfileSetupService, + @Inject(_$.IModeratorActionsService) moderatorActionsService: IModeratorActionsService ) { this._usersRepository = usersRepository; this._profileSetup = profileSetupService; + this._moderationActions = moderatorActionsService; } @Get() @@ -87,9 +91,9 @@ export class UsersController { public async banUser( @AuthedUser() authedUser: User, @Body() moderationPayloadDto: ModerationPayloadDto - ): Promise { + ): Promise { moderationPayloadDto.moderatorId = authedUser.userId; - throw new HttpException("Not implemented", 501); + await this._moderationActions.banUser(moderationPayloadDto); } @Patch("/unban/:userId") @@ -97,9 +101,8 @@ export class UsersController { @UseGuards(AuthGuard("jwt"), RolesGuard) public async unbanUser( @AuthedUser() authedUser: User, - @Body() moderationPayloadDto: ModerationPayloadDto - ): Promise { - moderationPayloadDto.moderatorId = authedUser.userId; - throw new HttpException("Not implemented", 501); + @Param("userId") userId: string + ): Promise { + await this._moderationActions.unbanUser(userId); } } diff --git a/src/users/models/toSelf/index.ts b/src/users/models/toSelf/index.ts index 39fccd2..0f4c650 100644 --- a/src/users/models/toSelf/index.ts +++ b/src/users/models/toSelf/index.ts @@ -4,4 +4,5 @@ export { GotBannedProps } from "./gotBanned.props"; export enum UserToSelfRelTypes { WAS_OFFENDING = "WAS_OFFENDING", GOT_BANNED = "GOT_BANNED", + PREVIOUSLY_BANNED = "PREVIOUSLY_BANNED", } diff --git a/src/users/repositories/users/users.repository.interface.ts b/src/users/repositories/users/users.repository.interface.ts index 86a3be2..61d1dd9 100644 --- a/src/users/repositories/users/users.repository.interface.ts +++ b/src/users/repositories/users/users.repository.interface.ts @@ -1,4 +1,5 @@ import { User } from "../../models"; +import { GotBannedProps } from "src/users/models/toSelf"; export interface IUsersRepository { findAll(): Promise; @@ -14,4 +15,10 @@ export interface IUsersRepository { updateUser(user: User): Promise; deleteUser(userId: UUID): Promise; + + banUser(userId: UUID, banProps: GotBannedProps): Promise; + + unbanUser(userId: UUID): Promise; + + addPreviouslyBanned(userId: UUID, banProps: GotBannedProps): Promise; } diff --git a/src/users/repositories/users/users.repository.ts b/src/users/repositories/users/users.repository.ts index 910946f..4b475d7 100644 --- a/src/users/repositories/users/users.repository.ts +++ b/src/users/repositories/users/users.repository.ts @@ -5,6 +5,8 @@ import { Neo4jService } from "../../../neo4j/services/neo4j.service"; import { UserToSexualityRelTypes } from "../../models/toSexuality"; import { UserToGenderRelTypes } from "../../models/toGender"; import { UserToOpennessRelTypes } from "../../models/toOpenness"; +import { UserToSelfRelTypes } from "../../models/toSelf"; +import { GotBannedProps } from "src/users/models/toSelf"; @Injectable() export class UsersRepository implements IUsersRepository { @@ -180,4 +182,41 @@ export class UsersRepository implements IUsersRepository { userId: userId, }); } + + public async banUser(userId: UUID, banProps: GotBannedProps): Promise { + await this._neo4jService.tryWriteAsync( + `MATCH (u:User {userId: $userId}) + CREATE (u)-[:${UserToSelfRelTypes.GOT_BANNED} {bannedAt: $bannedAt, moderatorId: $moderatorId, reason: $reason}]->(u) + `, + { + userId: userId, + bannedAt: banProps.bannedAt, + moderatorId: banProps.moderatorId, + reason: banProps.reason, + } + ); + } + + public async unbanUser(userId: UUID): Promise { + await this._neo4jService.tryWriteAsync( + `MATCH (u:User {userId: $userId})-[r:${UserToSelfRelTypes.GOT_BANNED}]->(u) DELETE r`, + { + userId: userId, + } + ); + } + + public async addPreviouslyBanned(userId: UUID, banProps: GotBannedProps): Promise { + await this._neo4jService.tryWriteAsync( + `MATCH (u:User {userId: $userId}) + CREATE (u)-[:${UserToSelfRelTypes.PREVIOUSLY_BANNED} {bannedAt: $bannedAt, moderatorId: $moderatorId, reason: $reason}]->(u) + `, + { + userId: userId, + bannedAt: banProps.bannedAt, + moderatorId: banProps.moderatorId, + reason: banProps.reason, + } + ); + } } From aaf3ade71ce311ed2ae2f9f3e545cbbbf7f31a77 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Tue, 29 Nov 2022 01:11:55 -0800 Subject: [PATCH 072/153] fixed a small bug --- src/_domain/injectableTokens.ts | 3 + .../post/posts.repository.interface.ts | 2 + .../repositories/post/posts.repository.ts | 14 ++ src/users/controllers/users.controller.ts | 31 ++- src/users/dtos/index.ts | 5 - src/users/dtos/updateAvatar.dto.ts | 16 -- src/users/dtos/updateBio.dto.ts | 12 -- src/users/dtos/updateGender.dto.ts | 12 -- src/users/dtos/updateOpenness.dto.ts | 12 -- src/users/dtos/updateSexuality.dto.ts | 12 -- .../repositories/gender/gender.repository.ts | 6 +- .../users/users.repository.interface.ts | 33 ++++ .../repositories/users/users.repository.ts | 148 +++++++++++++- .../profileSetup.service.interface.ts | 19 +- .../profileSetup/profileSetup.service.ts | 186 +++++++----------- .../userHistory.service.interface.ts | 10 + .../userHistory/userHistory.service.ts | 29 +++ src/users/users.module.ts | 9 + 18 files changed, 345 insertions(+), 214 deletions(-) delete mode 100644 src/users/dtos/updateAvatar.dto.ts delete mode 100644 src/users/dtos/updateBio.dto.ts delete mode 100644 src/users/dtos/updateGender.dto.ts delete mode 100644 src/users/dtos/updateOpenness.dto.ts delete mode 100644 src/users/dtos/updateSexuality.dto.ts create mode 100644 src/users/services/userHistory/userHistory.service.interface.ts create mode 100644 src/users/services/userHistory/userHistory.service.ts diff --git a/src/_domain/injectableTokens.ts b/src/_domain/injectableTokens.ts index ba22414..ab66175 100644 --- a/src/_domain/injectableTokens.ts +++ b/src/_domain/injectableTokens.ts @@ -1,3 +1,5 @@ +import { IUserHistoryService } from "../users/services/userHistory/userHistory.service.interface"; + const injectableTokens = { // Database Context IDatabaseContext: Symbol.for("IDatabaseContext"), @@ -11,6 +13,7 @@ const injectableTokens = { IOpennessRepository: Symbol("IOpennessRepository"), IGenderRepository: Symbol("IGenderRepository"), IProfileSetupService: Symbol("IProfileSetupService"), + IUserHistoryService: Symbol("IUserHistoryService"), // Posts Module IPostsRepository: Symbol("IPostsRepository"), diff --git a/src/posts/repositories/post/posts.repository.interface.ts b/src/posts/repositories/post/posts.repository.interface.ts index cf23a6f..c9d52a4 100644 --- a/src/posts/repositories/post/posts.repository.interface.ts +++ b/src/posts/repositories/post/posts.repository.interface.ts @@ -8,6 +8,8 @@ export interface IPostsRepository { findPostById(postId: string): Promise; + getPostHistoryByUserId(userId: UUID): Promise; + addPost(post: Post, anonymous: boolean): Promise; updatePost(post: Post): Promise; diff --git a/src/posts/repositories/post/posts.repository.ts b/src/posts/repositories/post/posts.repository.ts index 1a71058..c71c20f 100644 --- a/src/posts/repositories/post/posts.repository.ts +++ b/src/posts/repositories/post/posts.repository.ts @@ -40,6 +40,20 @@ export class PostsRepository implements IPostsRepository { return new Post(post.records[0].get("p").properties, this._neo4jService); } + public async getPostHistoryByUserId(userId: UUID): Promise { + const userPosts = await this._neo4jService.tryReadAsync( + `MATCH (u:User { userId: $userId })-[:${UserToPostRelTypes.AUTHORED}]->(p:Post) + RETURN p`, + { + userId, + } + ); + if (userPosts.records.length === 0) return []; + return userPosts.records.map( + record => new Post(record.get("p").properties, this._neo4jService) + ); + } + public async addPost(post: Post, anonymous: boolean): Promise { if (post.postId === undefined) { post.postId = this._neo4jService.generateId(); diff --git a/src/users/controllers/users.controller.ts b/src/users/controllers/users.controller.ts index f9d8680..950c2e9 100644 --- a/src/users/controllers/users.controller.ts +++ b/src/users/controllers/users.controller.ts @@ -18,26 +18,30 @@ import { Roles } from "../../auth/decorators/roles.decorator"; import { RolesGuard } from "../../auth/guards/roles.guard"; import { _$ } from "../../_domain/injectableTokens"; import { Role, User } from "../models"; -import { IUsersRepository } from "../repositories/users/users.repository.interface"; import { ModerationPayloadDto } from "../../moderation/dtos/moderatorActions"; import { AuthedUser } from "../../auth/decorators/authedUser.param.decorator"; import { OptionalJwtAuthGuard } from "../../auth/guards/optionalJwtAuth.guard"; import { PublicUserDto, SetupProfileDto } from "../dtos"; import { IProfileSetupService } from "../services/profileSetup/profileSetup.service.interface"; +import { DatabaseContext } from "../../database-access-layer/databaseContext"; +import { IUserHistoryService } from "../services/userHistory/userHistory.service.interface"; @UseInterceptors(ClassSerializerInterceptor) @ApiTags("users") @ApiBearerAuth() @Controller("users") export class UsersController { - private readonly _usersRepository: IUsersRepository; + private readonly _dbContext: DatabaseContext; + private readonly _userHistoryService: IUserHistoryService; private readonly _profileSetup: IProfileSetupService; constructor( - @Inject(_$.IUsersRepository) usersRepository: IUsersRepository, + @Inject(_$.IDatabaseContext) dbContext: DatabaseContext, + @Inject(_$.IUserHistoryService) userHistoryService: IUserHistoryService, @Inject(_$.IProfileSetupService) profileSetupService: IProfileSetupService ) { - this._usersRepository = usersRepository; + this._dbContext = dbContext; + this._userHistoryService = userHistoryService; this._profileSetup = profileSetupService; } @@ -45,7 +49,7 @@ export class UsersController { @Roles(Role.ADMIN) @UseGuards(AuthGuard("jwt"), RolesGuard) public async index(): Promise { - const users = await this._usersRepository.findAll(); + const users = await this._dbContext.Users.findAll(); const decoratedUsers = users.map(user => user.toJSON()); return await Promise.all(decoratedUsers); } @@ -54,7 +58,7 @@ export class UsersController { @Roles(Role.ADMIN) @UseGuards(AuthGuard("jwt"), RolesGuard) public async getUserById(@Param("userId", new ParseUUIDPipe()) userId: string): Promise { - const user = await this._usersRepository.findUserById(userId); + const user = await this._dbContext.Users.findUserById(userId); if (user === undefined) throw new HttpException("User not found", 404); return user; } @@ -65,7 +69,7 @@ export class UsersController { @Param("username") username: string, @AuthedUser() authedUser?: User ): Promise { - const user = await this._usersRepository.findUserByUsername(username); + const user = await this._dbContext.Users.findUserByUsername(username); if (user === undefined) throw new HttpException("User not found", 404); // if the found user is the same as the authed user, return the full user object @@ -75,6 +79,19 @@ export class UsersController { return PublicUserDto.fromUser(await user.toJSON()); } + @Get("/:username/history/posts") + @UseGuards(OptionalJwtAuthGuard) + public async getPostHistoryOfUserByUsername( + @Param("username") username: string, + @AuthedUser() authedUser?: User + ) { + const posts = await this._userHistoryService.getPostsHistoryByUsername(username); + const decoratedPosts = posts.map(post => + post.toJSON({ authenticatedUserId: authedUser?.userId ?? undefined }) + ); + return await Promise.all(decoratedPosts); + } + @Put("/profileSetup") @UseGuards(AuthGuard("jwt")) public async profileSetupSubmit(@Body() setupProfileDto: SetupProfileDto): Promise { diff --git a/src/users/dtos/index.ts b/src/users/dtos/index.ts index 9c878ec..bc5a050 100644 --- a/src/users/dtos/index.ts +++ b/src/users/dtos/index.ts @@ -1,7 +1,2 @@ export { PublicUserDto } from "./publicUser.dto"; -export { UpdateBioDto } from "./updateBio.dto"; -export { UpdateAvatarDto } from "./updateAvatar.dto"; -export { UpdateGenderDto } from "./updateGender.dto"; -export { UpdateOpennessDto } from "./updateOpenness.dto"; -export { UpdateSexualityDto } from "./updateSexuality.dto"; export { SetupProfileDto } from "./setupProfile.dto"; diff --git a/src/users/dtos/updateAvatar.dto.ts b/src/users/dtos/updateAvatar.dto.ts deleted file mode 100644 index fe5211e..0000000 --- a/src/users/dtos/updateAvatar.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsString } from "class-validator"; -import { UserAvatar } from "../models/user"; - -export class UpdateAvatarDto { - @ApiProperty({ - type: String, - description: "Can be an Ascii, an URL pointing to an image, or an SVG.", - }) - @IsString() - avatar: UserAvatar; - - constructor(partial?: Partial) { - Object.assign(this, partial); - } -} diff --git a/src/users/dtos/updateBio.dto.ts b/src/users/dtos/updateBio.dto.ts deleted file mode 100644 index 237f1a8..0000000 --- a/src/users/dtos/updateBio.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { IsString } from "class-validator"; -import { ApiProperty } from "@nestjs/swagger"; - -export class UpdateBioDto { - @ApiProperty({ type: String }) - @IsString() - bio: string; - - constructor(partial?: Partial) { - Object.assign(this, partial); - } -} diff --git a/src/users/dtos/updateGender.dto.ts b/src/users/dtos/updateGender.dto.ts deleted file mode 100644 index a9b337d..0000000 --- a/src/users/dtos/updateGender.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { IsUUID } from "class-validator"; -import { ApiProperty } from "@nestjs/swagger"; - -export class UpdateGenderDto { - @ApiProperty({ type: String, format: "uuid" }) - @IsUUID() - genderId: UUID; - - constructor(partial?: Partial) { - Object.assign(this, partial); - } -} diff --git a/src/users/dtos/updateOpenness.dto.ts b/src/users/dtos/updateOpenness.dto.ts deleted file mode 100644 index 9cfabbf..0000000 --- a/src/users/dtos/updateOpenness.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { IsUUID } from "class-validator"; -import { ApiProperty } from "@nestjs/swagger"; - -export class UpdateOpennessDto { - @ApiProperty({ type: String, format: "uuid" }) - @IsUUID() - opennessId: UUID; - - constructor(partial?: Partial) { - Object.assign(this, partial); - } -} diff --git a/src/users/dtos/updateSexuality.dto.ts b/src/users/dtos/updateSexuality.dto.ts deleted file mode 100644 index cbb7802..0000000 --- a/src/users/dtos/updateSexuality.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { IsUUID } from "class-validator"; -import { ApiProperty } from "@nestjs/swagger"; - -export class UpdateSexualityDto { - @ApiProperty({ type: String, format: "uuid" }) - @IsUUID() - sexualityId: UUID; - - constructor(partial?: Partial) { - Object.assign(this, partial); - } -} diff --git a/src/users/repositories/gender/gender.repository.ts b/src/users/repositories/gender/gender.repository.ts index de0cf8e..cf2d0d9 100644 --- a/src/users/repositories/gender/gender.repository.ts +++ b/src/users/repositories/gender/gender.repository.ts @@ -20,7 +20,7 @@ export class GenderRepository implements IGenderRepository { { genderId: genderId } ); if (gender.records.length === 0) return undefined; - return new Gender(gender.records[0].get("s").properties); + return new Gender(gender.records[0].get("g").properties); } public async addGender(gender: Gender): Promise { @@ -46,9 +46,7 @@ export class GenderRepository implements IGenderRepository { } ); - const addedGender = await this.findGenderById(gender.genderId ?? genderId); - - return addedGender; + return await this.findGenderById(gender.genderId ?? genderId); } public async updateGender(gender: Gender): Promise { diff --git a/src/users/repositories/users/users.repository.interface.ts b/src/users/repositories/users/users.repository.interface.ts index 86a3be2..38d7d2a 100644 --- a/src/users/repositories/users/users.repository.interface.ts +++ b/src/users/repositories/users/users.repository.interface.ts @@ -1,4 +1,7 @@ import { User } from "../../models"; +import { HasGenderProps } from "../../models/toGender"; +import { HasOpennessProps } from "../../models/toOpenness"; +import { HasSexualityProps } from "../../models/toSexuality"; export interface IUsersRepository { findAll(): Promise; @@ -14,4 +17,34 @@ export interface IUsersRepository { updateUser(user: User): Promise; deleteUser(userId: UUID): Promise; + + connectUserWithSexuality( + userId: UUID, + sexualityId: UUID, + hasSexualityProps: HasSexualityProps + ): Promise; + detachUserWithSexuality(userId: UUID): Promise; + updateRelationshipPropsOfHasSexuality( + userId: UUID, + hasSexualityProps: HasSexualityProps + ): Promise; + + connectUserWithGender( + userId: UUID, + genderId: UUID, + hasGenderProps: HasGenderProps + ): Promise; + detachUserWithGender(userId: UUID): Promise; + updateRelationshipPropsOfHasGender(userId: UUID, hasGenderProps: HasGenderProps): Promise; + + connectUserWithOpenness( + userId: UUID, + opennessId: UUID, + hasOpennessProps: HasOpennessProps + ): Promise; + detachUserWithOpenness(userId: UUID): Promise; + updateRelationshipPropsOfHasOpenness( + userId: UUID, + hasGenderProps: HasGenderProps + ): Promise; } diff --git a/src/users/repositories/users/users.repository.ts b/src/users/repositories/users/users.repository.ts index 910946f..f588f66 100644 --- a/src/users/repositories/users/users.repository.ts +++ b/src/users/repositories/users/users.repository.ts @@ -2,9 +2,9 @@ import { Inject, Injectable } from "@nestjs/common"; import { Role, User } from "../../models"; import { IUsersRepository } from "./users.repository.interface"; import { Neo4jService } from "../../../neo4j/services/neo4j.service"; -import { UserToSexualityRelTypes } from "../../models/toSexuality"; -import { UserToGenderRelTypes } from "../../models/toGender"; -import { UserToOpennessRelTypes } from "../../models/toOpenness"; +import { HasSexualityProps, UserToSexualityRelTypes } from "../../models/toSexuality"; +import { HasGenderProps, UserToGenderRelTypes } from "../../models/toGender"; +import { HasOpennessProps, UserToOpennessRelTypes } from "../../models/toOpenness"; @Injectable() export class UsersRepository implements IUsersRepository { @@ -151,6 +151,8 @@ export class UsersRepository implements IUsersRepository { u.phoneNumber = $phoneNumber, u.phoneNumberVerified = $phoneNumberVerified, u.username = $username, + u.bio = $bio, + u.avatar = $avatar, u.normalizedUsername = $normalizedUsername, u.email = $email, u.emailVerified = $emailVerified, @@ -164,6 +166,8 @@ export class UsersRepository implements IUsersRepository { phoneNumber: user.phoneNumber ?? "", phoneNumberVerified: user.phoneNumberVerified, username: user.username, + bio: user.bio ?? "", + avatar: user.avatar ?? "", normalizedUsername: user.username.toUpperCase(), email: user.email, emailVerified: user.emailVerified, @@ -180,4 +184,142 @@ export class UsersRepository implements IUsersRepository { userId: userId, }); } + + public async connectUserWithSexuality( + userId: UUID, + sexualityId: UUID, + hasSexualityProps: HasSexualityProps + ): Promise { + await this._neo4jService.tryWriteAsync( + ` + MATCH (u:User { userId : $userId }), (s:Sexuality { sexualityId: $sexualityId }) + MERGE (u)-[:${UserToSexualityRelTypes.HAS_SEXUALITY} { + isPrivate: $isPrivate + }]->(s) + `, + { + userId, + sexualityId, + isPrivate: hasSexualityProps.isPrivate, + } + ); + } + public async detachUserWithSexuality(userId: UUID): Promise { + await this._neo4jService.tryWriteAsync( + ` + MATCH (u:User { userId: $userId })-[r:${UserToSexualityRelTypes.HAS_SEXUALITY}]->(s:Sexuality) + DELETE r + `, + { + userId, + } + ); + } + public async updateRelationshipPropsOfHasSexuality( + userId: UUID, + hasSexualityProps: HasSexualityProps + ): Promise { + await this._neo4jService.tryWriteAsync( + ` + MATCH (u:User { userId: $userId })-[r:${UserToSexualityRelTypes.HAS_SEXUALITY}]->(s:Sexuality) + SET r.isPrivate = $isPrivate + `, + { + userId, + isPrivate: hasSexualityProps.isPrivate, + } + ); + } + + public async connectUserWithGender( + userId: UUID, + genderId: UUID, + hasGenderProps: HasGenderProps + ): Promise { + await this._neo4jService.tryWriteAsync( + ` + MATCH (u:User { userId : $userId }), (g:Gender { genderId: $genderId }) + MERGE (u)-[:${UserToGenderRelTypes.HAS_GENDER} { + isPrivate: $isPrivate + }]->(g) + `, + { + userId, + genderId, + isPrivate: hasGenderProps.isPrivate, + } + ); + } + public async detachUserWithGender(userId: UUID): Promise { + await this._neo4jService.tryWriteAsync( + ` + MATCH (u:User { userId: $userId })-[r:${UserToGenderRelTypes.HAS_GENDER}]->(g:Gender) + DELETE r + `, + { + userId, + } + ); + } + public async updateRelationshipPropsOfHasGender( + userId: UUID, + hasGenderProps: HasGenderProps + ): Promise { + await this._neo4jService.tryWriteAsync( + ` + MATCH (u:User { userId: $userId })-[r:${UserToGenderRelTypes.HAS_GENDER}]->(g:Gender) + SET r.isPrivate = $isPrivate + `, + { + userId, + isPrivate: hasGenderProps.isPrivate, + } + ); + } + + public async connectUserWithOpenness( + userId: UUID, + opennessId: UUID, + hasOpennessProps: HasOpennessProps + ): Promise { + await this._neo4jService.tryWriteAsync( + ` + MATCH (u:User { userId : $userId }), (o:Openness { opennessId: $opennessId }) + MERGE (u)-[:${UserToOpennessRelTypes.HAS_OPENNESS_LEVEL_OF} { + isPrivate: $isPrivate + }]->(o) + `, + { + userId, + opennessId, + isPrivate: hasOpennessProps.isPrivate, + } + ); + } + public async detachUserWithOpenness(userId: UUID): Promise { + await this._neo4jService.tryWriteAsync( + ` + MATCH (u:User { userId: $userId })-[r:${UserToOpennessRelTypes.HAS_OPENNESS_LEVEL_OF}]->(o:Openness) + DELETE r + `, + { + userId, + } + ); + } + public async updateRelationshipPropsOfHasOpenness( + userId: UUID, + hasOpennessProps: HasOpennessProps + ): Promise { + await this._neo4jService.tryWriteAsync( + ` + MATCH (u:User { userId: $userId })-[r:${UserToOpennessRelTypes.HAS_OPENNESS_LEVEL_OF}]->(o:Openness) + SET r.isPrivate = $isPrivate + `, + { + userId, + isPrivate: hasOpennessProps.isPrivate, + } + ); + } } diff --git a/src/users/services/profileSetup/profileSetup.service.interface.ts b/src/users/services/profileSetup/profileSetup.service.interface.ts index c9ee90a..51cd224 100644 --- a/src/users/services/profileSetup/profileSetup.service.interface.ts +++ b/src/users/services/profileSetup/profileSetup.service.interface.ts @@ -1,22 +1,5 @@ -import { - SetupProfileDto, - UpdateAvatarDto, - UpdateBioDto, - UpdateGenderDto, - UpdateOpennessDto, - UpdateSexualityDto, -} from "../../dtos"; +import { SetupProfileDto } from "../../dtos"; export interface IProfileSetupService { setupProfile(payload: SetupProfileDto): Promise; - - updateBio(payload: UpdateBioDto): Promise; - - updateAvatar(payload: UpdateAvatarDto): Promise; - - updateGender(payload: UpdateGenderDto): Promise; - - updateSexuality(payload: UpdateSexualityDto): Promise; - - updateOpenness(payload: UpdateOpennessDto): Promise; } diff --git a/src/users/services/profileSetup/profileSetup.service.ts b/src/users/services/profileSetup/profileSetup.service.ts index 02033d4..439d644 100644 --- a/src/users/services/profileSetup/profileSetup.service.ts +++ b/src/users/services/profileSetup/profileSetup.service.ts @@ -1,21 +1,14 @@ import { IProfileSetupService } from "./profileSetup.service.interface"; -import { - SetupProfileDto, - UpdateAvatarDto, - UpdateBioDto, - UpdateGenderDto, - UpdateOpennessDto, - UpdateSexualityDto, -} from "../../dtos"; +import { SetupProfileDto } from "../../dtos"; import { HttpException, Inject, Injectable, Scope } from "@nestjs/common"; import { Request } from "express"; import { REQUEST } from "@nestjs/core"; import { User } from "../../models"; import { DatabaseContext } from "../../../database-access-layer/databaseContext"; import { _$ } from "../../../_domain/injectableTokens"; -import { UserToSexualityRelTypes } from "../../models/toSexuality"; -import { UserToGenderRelTypes } from "../../models/toGender"; -import { UserToOpennessRelTypes } from "../../models/toOpenness"; +import { HasSexualityProps } from "../../models/toSexuality"; +import { HasGenderProps } from "../../models/toGender"; +import { HasOpennessProps } from "../../models/toOpenness"; @Injectable({ scope: Scope.DEFAULT }) export class ProfileSetupService implements IProfileSetupService { @@ -38,132 +31,99 @@ export class ProfileSetupService implements IProfileSetupService { await this._dbContext.Users.updateUser(user); const userSexuality = await user.getSexuality(); + const sexuality = await this._dbContext.Sexualities.findSexualityById(payload.sexualityId); + if (!sexuality) throw new HttpException("Sexuality not found.", 404); if (userSexuality === null) { - const sexuality = await this._dbContext.Sexualities.findSexualityById( - payload.sexualityId - ); - if (!sexuality) throw new HttpException("Sexuality not found.", 404); - - await this._dbContext.neo4jService.tryWriteAsync( - ` - MATCH (u:User { userId: $userId }), (s:Sexuality { sexualityId: $sexualityId }) - MERGE (u)-[:${UserToSexualityRelTypes.HAS_SEXUALITY} { - isPrivate: $isPrivate - }]->(s) - `, - { - userId: user.userId, - sexualityId: sexuality.sexualityId, - isPrivate: payload.isSexualityOpen, - } + await this._dbContext.Users.connectUserWithSexuality( + user.userId, + sexuality.sexualityId, + new HasSexualityProps({ isPrivate: !payload.isSexualityOpen }) ); } else { - if (payload.isSexualityOpen !== user.isSexualityPrivate) { - await this._dbContext.neo4jService.tryWriteAsync( - ` - MATCH (u:User { userId: $userId })-[r:${UserToSexualityRelTypes.HAS_SEXUALITY}]->(s:Sexuality) - SET r.isPrivate = $isPrivate - `, - { - userId: user.userId, - isPrivate: payload.isSexualityOpen, - } + // If the sexuality is changed, update it. else, skip. + if (userSexuality.sexualityId !== payload.sexualityId) { + // Delete the relationship first. + await this._dbContext.Users.detachUserWithSexuality(user.userId); + // Connect with the new one. + await this._dbContext.Users.connectUserWithSexuality( + user.userId, + sexuality.sexualityId, + new HasSexualityProps({ isPrivate: !payload.isSexualityOpen }) ); + } else { + // If the privacy rule of sexuality of this user has changed, just update the relationship property + if (!payload.isSexualityOpen !== user.isSexualityPrivate) { + await this._dbContext.Users.updateRelationshipPropsOfHasSexuality( + user.userId, + new HasSexualityProps({ isPrivate: !payload.isSexualityOpen }) + ); + } } } const userGender = await user.getGender(); + const gender = await this._dbContext.Genders.findGenderById(payload.genderId); + if (!gender) throw new HttpException("Gender not found.", 404); if (userGender === null) { - const gender = await this._dbContext.Genders.findGenderById(payload.genderId); - if (!gender) throw new HttpException("Gender not found.", 404); - - await this._dbContext.neo4jService.tryWriteAsync( - ` - MATCH (u:User { userId : $userId }), (g:Gender { genderId: $genderId }) - MERGE (u)-[:${UserToGenderRelTypes.HAS_GENDER} { - isPrivate: $isPrivate - }]->(g) - `, - { - userId: user.userId, - genderId: gender.genderId, - isPrivate: payload.isGenderPrivate, - } + await this._dbContext.Users.connectUserWithGender( + user.userId, + gender.genderId, + new HasGenderProps({ isPrivate: payload.isGenderPrivate }) ); } else { - if (payload.isGenderPrivate !== user.isGenderPrivate) { - await this._dbContext.neo4jService.tryWriteAsync( - ` - MATCH (u:User { userId: $userId })-[r:${UserToGenderRelTypes.HAS_GENDER}]->(g:Gender) - SET r.isPrivate = $isPrivate - `, - { - userId: user.userId, - isPrivate: payload.isSexualityOpen, - } + // If the gender is changed, update it. else, skip. + if (userGender.genderId !== payload.genderId) { + // Delete the relationship first. + await this._dbContext.Users.detachUserWithGender(user.userId); + // Connect with the new one. + await this._dbContext.Users.connectUserWithGender( + user.userId, + gender.genderId, + new HasGenderProps({ isPrivate: payload.isGenderPrivate }) ); + } else { + // If the privacy rule of gender of this user has changed, just update the relationship property + if (payload.isGenderPrivate !== user.isGenderPrivate) { + await this._dbContext.Users.updateRelationshipPropsOfHasGender( + user.userId, + new HasGenderProps({ isPrivate: payload.isGenderPrivate }) + ); + } } } const userOpenness = await user.getOpenness(); + const openness = await this._dbContext.Openness.findOpennessById(payload.opennessId); + if (!openness) throw new HttpException("Openness not found.", 404); if (userOpenness === null) { - const openness = await this._dbContext.Openness.findOpennessById(payload.opennessId); - if (!openness) throw new HttpException("Openness not found.", 404); - - await this._dbContext.neo4jService.tryWriteAsync( - ` - MATCH (u:User { userId : $userId }), (g:Openness { opennessId: $opennessId }) - MERGE (u)-[:${UserToOpennessRelTypes.HAS_OPENNESS_LEVEL_OF} { - isPrivate: $isPrivate - }]->(g) - `, - { - userId: user.userId, - opennessId: openness.opennessId, - isPrivate: payload.isGenderPrivate, - } + await this._dbContext.Users.connectUserWithOpenness( + user.userId, + openness.opennessId, + new HasOpennessProps({ isPrivate: payload.isGenderPrivate }) ); } else { - if (payload.isOpennessPrivate !== user.isOpennessPrivate) { - await this._dbContext.neo4jService.tryWriteAsync( - ` - MATCH (u:User { userId: $userId })-[r:${UserToOpennessRelTypes.HAS_OPENNESS_LEVEL_OF}]->(o:Openness) - SET r.isPrivate = $isPrivate - `, - { - userId: user.userId, - isPrivate: payload.isSexualityOpen, - } + // If the openness is changed, update it. else, skip. + if (userOpenness.opennessId !== payload.opennessId) { + // Delete the relationship first. + await this._dbContext.Users.detachUserWithOpenness(user.userId); + // Connect with the new one. + await this._dbContext.Users.connectUserWithOpenness( + user.userId, + openness.opennessId, + new HasOpennessProps({ isPrivate: payload.isGenderPrivate }) ); + } else { + // If the privacy rule of openness of this user has changed, just update the relationship property + if (payload.isOpennessPrivate !== user.isOpennessPrivate) { + await this._dbContext.Users.updateRelationshipPropsOfHasOpenness( + user.userId, + new HasOpennessProps({ isPrivate: payload.isOpennessPrivate }) + ); + } } } } - public async updateBio(payload: UpdateBioDto): Promise { - const user: User = this.getUserFromRequest(); - throw new Error("Method not implemented."); - } - - public async updateAvatar(payload: UpdateAvatarDto): Promise { - const user: User = this.getUserFromRequest(); - throw new Error("Method not implemented."); - } - - public async updateGender(payload: UpdateGenderDto): Promise { - const user: User = this.getUserFromRequest(); - throw new Error("Method not implemented."); - } - - public async updateSexuality(payload: UpdateSexualityDto): Promise { - const user: User = this.getUserFromRequest(); - throw new Error("Method not implemented."); - } - - public async updateOpenness(payload: UpdateOpennessDto): Promise { - const user: User = this.getUserFromRequest(); - throw new Error("Method not implemented."); - } - private getUserFromRequest(): User { const user = this._request.user as User; if (user === undefined) throw new HttpException("Authentication failed.", 403); diff --git a/src/users/services/userHistory/userHistory.service.interface.ts b/src/users/services/userHistory/userHistory.service.interface.ts new file mode 100644 index 0000000..4a5cd11 --- /dev/null +++ b/src/users/services/userHistory/userHistory.service.interface.ts @@ -0,0 +1,10 @@ +import { Comment } from "../../../comments/models"; +import { Post } from "../../../posts/models"; + +export interface IUserHistoryService { + getPostsHistoryByUsername(username: string): Promise; + + getCommentsHistoryByUsername(username: string): Promise; + + getTotalLikesByUsername(username: string): Promise; +} diff --git a/src/users/services/userHistory/userHistory.service.ts b/src/users/services/userHistory/userHistory.service.ts new file mode 100644 index 0000000..20a66c4 --- /dev/null +++ b/src/users/services/userHistory/userHistory.service.ts @@ -0,0 +1,29 @@ +import { Post } from "../../../posts/models"; +import { IUserHistoryService } from "./userHistory.service.interface"; +import { DatabaseContext } from "../../../database-access-layer/databaseContext"; +import { HttpException, Inject } from "@nestjs/common"; +import { _$ } from "../../../_domain/injectableTokens"; +import { Comment } from "../../../comments/models"; + +export class UserHistoryService implements IUserHistoryService { + private readonly _dbContext: DatabaseContext; + + constructor(@Inject(_$.IDatabaseContext) dbContext: DatabaseContext) { + this._dbContext = dbContext; + } + + public async getPostsHistoryByUsername(username: string): Promise { + const user = await this._dbContext.Users.findUserByUsername(username); + if (!user) throw new HttpException("User does not exist.", 404); + + return await this._dbContext.Posts.getPostHistoryByUserId(user.userId); + } + + public async getCommentsHistoryByUsername(username: string): Promise { + throw new Error("Not implemented"); + } + + public async getTotalLikesByUsername(username: string): Promise { + throw new Error("Not implemented"); + } +} diff --git a/src/users/users.module.ts b/src/users/users.module.ts index f3be481..3a7d587 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -10,6 +10,7 @@ import { OpennessRepository } from "./repositories/openness/openness.repository" import { SexualitiesController } from "./controllers/sexualities.controller"; import { GendersController } from "./controllers/genders.controller"; import { OpennessController } from "./controllers/openness.controller"; +import { UserHistoryService } from "./services/userHistory/userHistory.service"; @Module({ imports: [forwardRef(() => DatabaseAccessLayerModule)], @@ -22,6 +23,10 @@ import { OpennessController } from "./controllers/openness.controller"; provide: _$.IProfileSetupService, useClass: ProfileSetupService, }, + { + provide: _$.IUserHistoryService, + useClass: UserHistoryService, + }, { provide: _$.IGenderRepository, useClass: GenderRepository, @@ -44,6 +49,10 @@ import { OpennessController } from "./controllers/openness.controller"; provide: _$.IProfileSetupService, useClass: ProfileSetupService, }, + { + provide: _$.IUserHistoryService, + useClass: UserHistoryService, + }, { provide: _$.IGenderRepository, useClass: GenderRepository, From c1ac1e14f43b5a03aec970e230c92eab80237bfd Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Tue, 29 Nov 2022 01:19:46 -0800 Subject: [PATCH 073/153] fix some imports --- src/comments/services/comments/comments.service.ts | 1 - .../moderatorActions/moderatorActions.service.interface.ts | 1 - .../services/moderatorActions/moderatorActions.service.ts | 1 - src/posts/services/posts/posts.service.ts | 1 - 4 files changed, 4 deletions(-) diff --git a/src/comments/services/comments/comments.service.ts b/src/comments/services/comments/comments.service.ts index 1bcc4a1..7284d92 100644 --- a/src/comments/services/comments/comments.service.ts +++ b/src/comments/services/comments/comments.service.ts @@ -11,7 +11,6 @@ import { UserToCommentRelTypes } from "../../../users/models/toComment"; import { VoteProps } from "../../../users/models/toPost"; import { _$ } from "../../../_domain/injectableTokens"; import { VoteType } from "../../../_domain/models/enums"; -import { DeletedProps } from "../../../_domain/models/toSelf"; import { CommentCreationPayloadDto, VoteCommentPayloadDto } from "../../dtos"; import { Comment } from "../../models"; import { CommentToSelfRelTypes } from "../../models/toSelf"; diff --git a/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts b/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts index de931e0..3129c24 100644 --- a/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts +++ b/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts @@ -1,7 +1,6 @@ import { Post } from "../../../posts/models"; import { Comment } from "../../../comments/models"; import { ModerationPayloadDto } from "../../dtos/moderatorActions"; -import { User } from "../../../users/models"; export interface IModeratorActionsService { /** diff --git a/src/moderation/services/moderatorActions/moderatorActions.service.ts b/src/moderation/services/moderatorActions/moderatorActions.service.ts index d4fb62c..b8f8fea 100644 --- a/src/moderation/services/moderatorActions/moderatorActions.service.ts +++ b/src/moderation/services/moderatorActions/moderatorActions.service.ts @@ -6,7 +6,6 @@ import { DeletedProps, RestrictedProps } from "../../../_domain/models/toSelf"; import { Comment } from "../../../comments/models"; import { Post } from "../../../posts/models"; import { ModerationPayloadDto } from "../../dtos/moderatorActions"; -import { User } from "../../../users/models"; import { GotBannedProps } from "src/users/models/toSelf"; /** diff --git a/src/posts/services/posts/posts.service.ts b/src/posts/services/posts/posts.service.ts index ffd9ee7..c4b3d42 100644 --- a/src/posts/services/posts/posts.service.ts +++ b/src/posts/services/posts/posts.service.ts @@ -4,7 +4,6 @@ import { Request } from "express"; import { Comment } from "../../../comments/models"; import { DatabaseContext } from "../../../database-access-layer/databaseContext"; import { IAutoModerationService } from "../../../moderation/services/autoModeration/autoModeration.service.interface"; -import { PostToCommentRelTypes } from "../../../posts/models/toComment"; import { User } from "../../../users/models"; import { UserToPostRelTypes, VoteProps } from "../../../users/models/toPost"; import { _$ } from "../../../_domain/injectableTokens"; From b91e2b5c43f40356f798f30a1a2b09a1ab08dbc8 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Tue, 29 Nov 2022 01:28:36 -0800 Subject: [PATCH 074/153] fix bug --- .../services/moderatorActions/moderatorActions.service.ts | 5 +++-- src/users/repositories/users/users.repository.interface.ts | 2 +- src/users/users.module.ts | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/moderation/services/moderatorActions/moderatorActions.service.ts b/src/moderation/services/moderatorActions/moderatorActions.service.ts index b8f8fea..0060229 100644 --- a/src/moderation/services/moderatorActions/moderatorActions.service.ts +++ b/src/moderation/services/moderatorActions/moderatorActions.service.ts @@ -1,12 +1,12 @@ import { IModeratorActionsService } from "./moderatorActions.service.interface"; -import { HttpException, Inject } from "@nestjs/common"; +import { HttpException, Inject, Injectable, Scope } from "@nestjs/common"; import { _$ } from "../../../_domain/injectableTokens"; import { DatabaseContext } from "../../../database-access-layer/databaseContext"; import { DeletedProps, RestrictedProps } from "../../../_domain/models/toSelf"; import { Comment } from "../../../comments/models"; import { Post } from "../../../posts/models"; import { ModerationPayloadDto } from "../../dtos/moderatorActions"; -import { GotBannedProps } from "src/users/models/toSelf"; +import { GotBannedProps } from "../../../users/models/toSelf"; /** * This service is responsible for moderating posts and comments. @@ -15,6 +15,7 @@ import { GotBannedProps } from "src/users/models/toSelf"; * - This service is not responsible to check if the user is a moderator. This is done by the guards used by controllers. * @see src/moderation/controllers/moderatorActions/moderatorActions.controller.ts */ +@Injectable({ scope: Scope.DEFAULT }) export class ModeratorActionsService implements IModeratorActionsService { private readonly _dbContext: DatabaseContext; diff --git a/src/users/repositories/users/users.repository.interface.ts b/src/users/repositories/users/users.repository.interface.ts index a44f49b..16810f0 100644 --- a/src/users/repositories/users/users.repository.interface.ts +++ b/src/users/repositories/users/users.repository.interface.ts @@ -2,7 +2,7 @@ import { User } from "../../models"; import { HasGenderProps } from "../../models/toGender"; import { HasOpennessProps } from "../../models/toOpenness"; import { HasSexualityProps } from "../../models/toSexuality"; -import { GotBannedProps } from "src/users/models/toSelf"; +import { GotBannedProps } from "../../models/toSelf"; export interface IUsersRepository { findAll(): Promise; diff --git a/src/users/users.module.ts b/src/users/users.module.ts index 3a7d587..a8e232a 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -11,9 +11,10 @@ import { SexualitiesController } from "./controllers/sexualities.controller"; import { GendersController } from "./controllers/genders.controller"; import { OpennessController } from "./controllers/openness.controller"; import { UserHistoryService } from "./services/userHistory/userHistory.service"; +import { ModerationModule } from "../moderation/moderation.module"; @Module({ - imports: [forwardRef(() => DatabaseAccessLayerModule)], + imports: [forwardRef(() => DatabaseAccessLayerModule), ModerationModule], providers: [ { provide: _$.IUsersRepository, From f701974b8213300134aa264b0ad8d8f51643712a Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Tue, 29 Nov 2022 14:53:19 -0800 Subject: [PATCH 075/153] fix the entities --- src/neo4j/services/neo4j.seed.service.ts | 347 +++++++++++------------ 1 file changed, 164 insertions(+), 183 deletions(-) diff --git a/src/neo4j/services/neo4j.seed.service.ts b/src/neo4j/services/neo4j.seed.service.ts index 12d9831..dc6fd8b 100644 --- a/src/neo4j/services/neo4j.seed.service.ts +++ b/src/neo4j/services/neo4j.seed.service.ts @@ -79,7 +79,7 @@ export class Neo4jSeedService { ); // Populate post types - const postTypes = await this.getPostTypes(); + const postTypes = Object.values(await this.getPostTypes()); for (const postTypeEntity of postTypes) { await this._neo4jService.tryWriteAsync( `CREATE (n:${this.postTypeLabel} { @@ -92,7 +92,7 @@ export class Neo4jSeedService { } // Populate post tags - const postTags = await this.getPostTags(); + const postTags = Object.values(await this.getPostTags()); for (const postTagEntity of postTags) { await this._neo4jService.tryWriteAsync( `CREATE (n:${this.postTagLabel} { @@ -224,8 +224,8 @@ export class Neo4jSeedService { const posts = await this.getPosts(); const votesUserIds = [ - "5c0f145b-ffad-4881-8ee6-7647c3c1b695", - "3109f9e2-a262-4aef-b648-90d86d6fbf6c", + "dc83daa3-d26b-4063-87b1-2b719069654e", // verified - alphonse + "a59437f4-ea62-4a15-a4e6-621b04af74d6", // verified - gabriel ]; for (const postEntity of posts) { @@ -245,7 +245,7 @@ export class Neo4jSeedService { } const authoredProps = new AuthoredProps({ - authoredAt: 1665770000, + authoredAt: postEntity.updatedAt, anonymously: false, }); await this._neo4jService.tryWriteAsync( @@ -314,7 +314,7 @@ export class Neo4jSeedService { pending: postEntity.pending, // PostType - postTypeName: postEntity.postType.postTypeName.trim().toLowerCase(), + postTypeName: postEntity.postType.postTypeName, // AuthoredProps authoredProps_authoredAt: authoredProps.authoredAt, @@ -342,7 +342,7 @@ export class Neo4jSeedService { } as RestrictedProps; } const authoredProps = new AuthoredProps({ - authoredAt: commentEntity.createdAt, + authoredAt: commentEntity.updatedAt, anonymously: false, }); await this._neo4jService.tryWriteAsync( @@ -422,7 +422,11 @@ export class Neo4jSeedService { } public async getUsers(): Promise { - const onlyAuthoredPosts = { + const authoredPosts = { + [UserToPostRelTypes.AUTHORED]: { + records: [], + relType: UserToPostRelTypes.AUTHORED, + }, [UserToPostRelTypes.READ]: { records: [], relType: UserToPostRelTypes.READ, @@ -447,7 +451,7 @@ export class Neo4jSeedService { return new Array( new User({ - userId: "71120d45-7a75-43fd-b79c-54b06e7868af", + userId: "71120d45-7a75-43fd-b79c-54b06e7868af", // verified createdAt: new Date().getTime(), updatedAt: new Date().getTime(), avatar: "(づ ̄ 3 ̄)づ", @@ -460,35 +464,23 @@ export class Neo4jSeedService { email: "wesley@domain.com", emailVerified: false, level: 0, - roles: [Role.USER], + roles: [Role.USER, Role.MODERATOR], gender: new Gender({ - genderId: "585d31aa-d5b3-4b8d-9690-ffcd57ce2862", + genderId: "585d31aa-d5b3-4b8d-9690-ffcd57ce2862", // verified - Male - He/Him }), isGenderPrivate: false, sexuality: new Sexuality({ - sexualityId: "9164d89b-8d71-4fd1-af61-155d1d7ffe53", + sexualityId: "9164d89b-8d71-4fd1-af61-155d1d7ffe53", // verified - Gay }), isSexualityPrivate: false, openness: new Openness({ - opennessId: "db27c417-a8a5-4703-9b35-9dc76e98fc95", + opennessId: "db27c417-a8a5-4703-9b35-9dc76e98fc95", // verified - Out to Few }), isOpennessPrivate: false, - posts: { - [UserToPostRelTypes.AUTHORED]: { - records: (await this.getPosts()).slice(2, 3).map(post => ({ - entity: post, - relProps: new AuthoredProps({ - authoredAt: new Date("2022-09-13").getTime(), - anonymously: false, - }), - })), - relType: UserToPostRelTypes.AUTHORED, - }, - ...onlyAuthoredPosts, - }, + posts: { ...authoredPosts }, }), new User({ - userId: "5e520efd-f78e-4cb0-8903-5c99197d4b8e", + userId: "5e520efd-f78e-4cb0-8903-5c99197d4b8e", // verified createdAt: new Date().getTime(), updatedAt: new Date().getTime(), avatar: "...(* ̄0 ̄)ノ", @@ -503,33 +495,21 @@ export class Neo4jSeedService { level: 0, roles: [Role.USER], gender: new Gender({ - genderId: "3af72545-99d4-4715-812b-c935fbf57f22", + genderId: "3af72545-99d4-4715-812b-c935fbf57f22", // verified - ve/ver }), isGenderPrivate: false, sexuality: new Sexuality({ - sexualityId: "5bc9535e-cc50-4112-91ad-717dc2de9492", + sexualityId: "5bc9535e-cc50-4112-91ad-717dc2de9492", // verified - Bisexual }), isSexualityPrivate: false, openness: new Openness({ - opennessId: "842b5bd7-1da1-4a95-9564-1fc3b97b3655", + opennessId: "842b5bd7-1da1-4a95-9564-1fc3b97b3655", // verified - Not Out }), isOpennessPrivate: false, - posts: { - [UserToPostRelTypes.AUTHORED]: { - records: (await this.getPosts()).slice(3, 4).map(post => ({ - entity: post, - relProps: new AuthoredProps({ - authoredAt: new Date("10/26/2022").getTime(), - anonymously: false, - }), - })), - relType: UserToPostRelTypes.AUTHORED, - }, - ...onlyAuthoredPosts, - }, + posts: { ...authoredPosts }, }), new User({ - userId: "a59437f4-ea62-4a15-a4e6-621b04af74d6", + userId: "a59437f4-ea62-4a15-a4e6-621b04af74d6", // verified createdAt: new Date().getTime(), updatedAt: new Date().getTime(), avatar: ":^)", @@ -539,38 +519,26 @@ export class Neo4jSeedService { passwordHash: "", phoneNumber: null, phoneNumberVerified: false, - email: "email@domain.com", + email: "gabriel@domain.com", emailVerified: false, level: 0, roles: [Role.USER], gender: new Gender({ - genderId: "d2945763-d1fb-46aa-b896-7f701b4ca699", + genderId: "f97edcdf-9df4-4f4a-9114-fbcd702502af", // verified - NA }), isGenderPrivate: false, sexuality: new Sexuality({ - sexualityId: "55da84cc-5f17-454a-a653-227458763edb", + sexualityId: "2d32c4d3-4aca-4b03-bf68-ba104656183f", // verified - Asexual }), isSexualityPrivate: false, openness: new Openness({ - opennessId: "c8921055-563f-4a54-8773-b408efcfb7ac", + opennessId: "ae90b960-5f00-4298-b509-fac92a59b406", // verified - Not Sure }), isOpennessPrivate: false, - posts: { - [UserToPostRelTypes.AUTHORED]: { - records: (await this.getPosts()).slice(0, 1).map(post => ({ - entity: post, - relProps: new AuthoredProps({ - authoredAt: new Date("10/11/2022").getTime(), - anonymously: false, - }), - })), - relType: UserToPostRelTypes.AUTHORED, - }, - ...onlyAuthoredPosts, - }, + posts: { ...authoredPosts }, }), new User({ - userId: "dc83daa3-d26b-4063-87b1-2b719069654e", + userId: "dc83daa3-d26b-4063-87b1-2b719069654e", // verified createdAt: new Date().getTime(), updatedAt: new Date().getTime(), avatar: "^_^", @@ -585,49 +553,68 @@ export class Neo4jSeedService { level: 0, roles: [Role.USER], gender: new Gender({ - genderId: "585d31aa-d5b3-4b8d-9690-ffcd57ce2862", + genderId: "585d31aa-d5b3-4b8d-9690-ffcd57ce2862", // verified - Male - He/Him }), isGenderPrivate: false, sexuality: new Sexuality({ - sexualityId: "d2945763-d1fb-46aa-b896-7f701b4ca699", + sexualityId: "9164d89b-8d71-4fd1-af61-155d1d7ffe53", // verified - Gay }), isSexualityPrivate: false, openness: new Openness({ - opennessId: "d5c97584-cd1b-4aa6-82ad-b5ddd3577bee", + opennessId: "d5c97584-cd1b-4aa6-82ad-b5ddd3577bee", // verified - Fully Out }), isOpennessPrivate: false, - posts: { - [UserToPostRelTypes.AUTHORED]: { - records: (await this.getPosts()).slice(1, 2).map(post => ({ - entity: post, - relProps: new AuthoredProps({ - authoredAt: new Date("2022-09-06").getTime(), - anonymously: false, - }), - })), - relType: UserToPostRelTypes.AUTHORED, - }, - ...onlyAuthoredPosts, - }, + posts: { ...authoredPosts }, + }), + new User({ + userId: "0daef999-7291-4f0c-a41a-078a6f28aa5e", // verified + createdAt: new Date().getTime(), + updatedAt: new Date().getTime(), + avatar: "^_^", + bio: "My name is christopher.", + username: "christopher", + normalizedUsername: "CHRISTOPHER", + passwordHash: "", + phoneNumber: null, + phoneNumberVerified: false, + email: "christopher@domain.com", + emailVerified: false, + level: 0, + roles: [Role.USER, Role.MODERATOR], + gender: new Gender({ + genderId: "585d31aa-d5b3-4b8d-9690-ffcd57ce2862", // verified - Male - He/Him + }), + isGenderPrivate: true, + sexuality: new Sexuality({ + sexualityId: "9164d89b-8d71-4fd1-af61-155d1d7ffe53", // verified - Gay + }), + isSexualityPrivate: true, + openness: new Openness({ + opennessId: "d5c97584-cd1b-4aa6-82ad-b5ddd3577bee", // verified - Fully Out + }), + isOpennessPrivate: false, + posts: { ...authoredPosts }, }) ); } public async getPosts(): Promise { + const postTags = await this.getPostTags(); + const postTypes = await this.getPostTypes(); return new Array( new Post({ - postId: "bcddeb57-939d-441b-b4ea-71e1d2055f32", + postId: "bcddeb57-939d-441b-b4ea-71e1d2055f32", // verified postTitle: "Sister caught me checking out a guy on a camping trip", postContent: "I was on a camping trip and my sister caught me staring at someone across the site with his \n" + " shirt off, for the the rest of the day she wouldn't stop asking me, even getting the other members \n" + " who came with us to join in, I eventually gave in, she was super kind about it and came out as Bisexual the following months", - updatedAt: 1666690000, - postType: (await this.getPostTypes())[1], - postTags: (await this.getPostTags()).slice(-1), + updatedAt: new Date("202-11-17").getTime(), // verified - NOTE: will be counted as `authoredAt` value. + postType: postTypes.story, // verified - story + postTags: [postTags.Casual, postTags.General, postTags.Trigger], // verified restrictedProps: null, authorUser: new User({ - userId: "a59437f4-ea62-4a15-a4e6-621b04af74d6", + userId: "a59437f4-ea62-4a15-a4e6-621b04af74d6", // verified - gabriel }), pending: false, totalVotes: 0, @@ -639,7 +626,7 @@ export class Neo4jSeedService { }, }), new Post({ - postId: "be9ab5e4-eb7c-469b-a1e3-592dca2a00d0", + postId: "be9ab5e4-eb7c-469b-a1e3-592dca2a00d0", // verified postTitle: "Coming out to my accepting family", postContent: "Growing up my family was really gay friendly. I had two gay uncles and everyone was accepting of them. \n " + @@ -657,16 +644,16 @@ export class Neo4jSeedService { "Today my entire family knows that I am gay and they accept me. It is nice to have such an accepting family and I know that I am \n " + "very fortunate to have a family that loves me unconditionally. I am grateful that my family has never judged me or made me feel \n " + "uncomfortable expressing who I am.", - updatedAt: 1666770000, - postType: (await this.getPostTypes())[1], - postTags: (await this.getPostTags()).slice(0, 1), + updatedAt: new Date("2022-11-20").getTime(), // verified - NOTE: will be counted as `authoredAt` value. + postType: postTypes.story, // verified - story + postTags: [postTags.Serious], // verified - Serious restrictedProps: new RestrictedProps({ - restrictedAt: 1665780000, - moderatorId: "8f0c1ecf-6853-4642-9199-6e8244b89312", + restrictedAt: new Date("202-11-22").getTime(), + moderatorId: "71120d45-7a75-43fd-b79c-54b06e7868af", // verified - moderator - wesley reason: "The moderator thinks there is profanity in this post", }), authorUser: new User({ - userId: "dc83daa3-d26b-4063-87b1-2b719069654e", + userId: "dc83daa3-d26b-4063-87b1-2b719069654e", // verified - alphonse }), pending: true, totalVotes: 3, @@ -675,7 +662,7 @@ export class Neo4jSeedService { records: (await this.getAwards()).slice(-1, 1).map(award => ({ entity: award, relProps: new HasAwardProps({ - awardedBy: "6bd7b8a2-bf8c-49e6-9c28-ee3d89be2453", + awardedBy: "0daef999-7291-4f0c-a41a-078a6f28aa5e", // verified - moderator - christopher }), })), relType: PostToAwardRelTypes.HAS_AWARD, @@ -683,18 +670,18 @@ export class Neo4jSeedService { }, }), new Post({ - postId: "806ca5f3-f80c-47fc-9e4d-00434dd18358", + postId: "806ca5f3-f80c-47fc-9e4d-00434dd18358", // verified postTitle: "Coming out to my brother", postContent: `When I was 17 my girlfriend was over my house and my brother was home from college. She asks me "does your brother know we're dating?"I say "good question." Then I yell to the other side of the house "[Brother]! Did you know that I'm dating [girlfriend]?" I think he said something like "I do now!"`, - updatedAt: 1663095600, - postType: (await this.getPostTypes())[1], - postTags: (await this.getPostTags()).slice(-1), + updatedAt: new Date("2022-09-01").getTime(), // verified - NOTE: will be counted as `authoredAt` value. + postType: postTypes.story, // verified - story + postTags: [postTags.Casual], // verified - Casual restrictedProps: null, authorUser: new User({ - userId: "71120d45-7a75-43fd-b79c-54b06e7868af", + userId: "71120d45-7a75-43fd-b79c-54b06e7868af", // verified - wesley - moderator }), pending: false, totalVotes: 0, @@ -706,7 +693,7 @@ export class Neo4jSeedService { }, }), new Post({ - postId: "6326079f-fd2f-4b81-83fe-487daee459bc", + postId: "6326079f-fd2f-4b81-83fe-487daee459bc", // verified postTitle: "Coming out as GNC on my birthday", postContent: `This is the uncomfortable story of how I came out to my mom and younger brother (with whom I live). \n\nLeading up to my birthday last year I had realized I'm gnc (gender nonconforming) and had been starting to feel @@ -723,12 +710,12 @@ export class Neo4jSeedService { \n\nMuch headbutting later they've come around on my pronouns and the name I've chosen. As you can imagine though, for a time I felt extremely unwelcome in the home, and after all the effort I put in I was pretty devastated my coming out transpired so poorly.`, - updatedAt: 1666810800, - postType: (await this.getPostTypes())[1], - postTags: (await this.getPostTags()).slice(0, 1), + updatedAt: new Date("2022-05-26").getTime(), // verified - NOTE: will be counted as `authoredAt` value. + postType: postTypes.story, // verified - story + postTags: [postTags.Serious], // verified - Serious restrictedProps: null, authorUser: new User({ - userId: "5e520efd-f78e-4cb0-8903-5c99197d4b8e", + userId: "5e520efd-f78e-4cb0-8903-5c99197d4b8e", // verified - gaius }), pending: false, totalVotes: 0, @@ -745,60 +732,56 @@ export class Neo4jSeedService { public async getComments(): Promise { return new Array( new Comment({ - commentId: "f8959b32-5b68-4f68-97bc-59afdc0d09cb", - parentId: "bcddeb57-939d-441b-b4ea-71e1d2055f32", + commentId: "f8959b32-5b68-4f68-97bc-59afdc0d09cb", // verified - no children + parentId: "bcddeb57-939d-441b-b4ea-71e1d2055f32", // verified - Title: Sister caught me checking out a guy on a camping trip. commentContent: "Wow that's so nice to hear!", - createdAt: 1666990000, - updatedAt: 1666990000, + updatedAt: new Date("2020-11-18").getTime(), // verified - a day after Post authored authorUser: new User({ - userId: "6bd7b8a2-bf8c-49e6-9c28-ee3d89be2453", + userId: "0daef999-7291-4f0c-a41a-078a6f28aa5e", // verified - moderator - christopher }), - pinned: true, + pinned: true, // verified pending: false, restrictedProps: null, childComments: [], }), new Comment({ - commentId: "c13c4349-bbf9-45a7-a573-7a04efa66e3c", - parentId: "be9ab5e4-eb7c-469b-a1e3-592dca2a00d0", + commentId: "c13c4349-bbf9-45a7-a573-7a04efa66e3c", // verified + parentId: "be9ab5e4-eb7c-469b-a1e3-592dca2a00d0", // verified - Title: Coming out to my accepting family commentContent: `Congratulations! That's so nice you have such a supportive family. Sorry to hear about your ex-girlfriend and your cousin though. I hope you're doing better now.`, - createdAt: 1666890000, - updatedAt: 1666890000, + updatedAt: new Date("2022-11-21").getTime(), // verified - a day after post authored authorUser: new User({ - userId: "6bd7b8a2-bf8c-49e6-9c28-ee3d89be2453", + userId: "a59437f4-ea62-4a15-a4e6-621b04af74d6", // verified - gabriel }), pinned: false, pending: true, restrictedProps: null, childComments: [ new Comment({ - commentId: "3ee2801a-998d-437a-a49e-3974919f35c1", - parentId: "806ca5f3-f80c-47fc-9e4d-00434dd18358", + commentId: "3ee2801a-998d-437a-a49e-3974919f35c1", // verified + parentId: "c13c4349-bbf9-45a7-a573-7a04efa66e3c", // verified commentContent: `Wow that's so scummy of your cousin to do that to you! I would have never forgiven her.`, - createdAt: 1666990000, - updatedAt: 1666990000, + updatedAt: new Date("2022-11-22").getTime(), // verified - a day after parent authored authorUser: new User({ - userId: "dc83daa3-d26b-4063-87b1-2b719069654e", + userId: "dc83daa3-d26b-4063-87b1-2b719069654e", // verified - alphonse }), pinned: false, pending: true, restrictedProps: null, childComments: [ new Comment({ - commentId: "287ca219-005e-41fa-af40-abf59c2c2caf", - parentId: "3ee2801a-998d-437a-a49e-3974919f35c1", + commentId: "287ca219-005e-41fa-af40-abf59c2c2caf", // verified + parentId: "3ee2801a-998d-437a-a49e-3974919f35c1", // verified commentContent: `I agree! I would have never forgiven her either.`, - createdAt: 1667000000, - updatedAt: 1667000000, + updatedAt: new Date("2022-11-23").getTime(), // verified - a day after parent comment authored authorUser: new User({ - userId: "f0b42305-9513-4fe7-a918-320d2b488e61", + userId: "0daef999-7291-4f0c-a41a-078a6f28aa5e", // verified - christopher }), pinned: false, pending: true, restrictedProps: new RestrictedProps({ - restrictedAt: 1667000001, - moderatorId: "8f0c1ecf-6853-4642-9199-6e8244b89312", + restrictedAt: new Date("2022-11-23").getTime(), + moderatorId: "71120d45-7a75-43fd-b79c-54b06e7868af", // verified - moderator - wesley reason: "The moderator died of cringe", }), childComments: [], @@ -808,43 +791,40 @@ export class Neo4jSeedService { ], }), new Comment({ - commentId: "5a11c2af-7716-4b67-b00f-e23df9f0c740", - parentId: "806ca5f3-f80c-47fc-9e4d-00434dd18358", + commentId: "5a11c2af-7716-4b67-b00f-e23df9f0c740", // verified + parentId: "806ca5f3-f80c-47fc-9e4d-00434dd18358", // verified - top-level comment - Post Title: Coming out to my brother commentContent: `That story is so funny! What a supportive brother.`, - createdAt: 1666890000, - updatedAt: 1666890000, + updatedAt: new Date("2022-09-03").getTime(), // verified - 2 days after post authored authorUser: new User({ - userId: "6bd7b8a2-bf8c-49e6-9c28-ee3d89be2453", + userId: "dc83daa3-d26b-4063-87b1-2b719069654e", // verified - alphonse }), pinned: false, - pending: true, + pending: false, restrictedProps: null, childComments: [ new Comment({ - commentId: "9e55090e-2ebf-4679-a912-6542e78f4905", - parentId: "5a11c2af-7716-4b67-b00f-e23df9f0c740", + commentId: "9e55090e-2ebf-4679-a912-6542e78f4905", // verified + parentId: "5a11c2af-7716-4b67-b00f-e23df9f0c740", // verified commentContent: `Same! I wish my brother was like that.`, - createdAt: 1666990000, - updatedAt: 1666990000, + updatedAt: new Date("2022-09-04").getTime(), authorUser: new User({ - userId: "dc83daa3-d26b-4063-87b1-2b719069654e", + userId: "dc83daa3-d26b-4063-87b1-2b719069654e", // verified - alphonse }), pinned: false, - pending: true, + pending: false, restrictedProps: null, childComments: [], }), ], }), new Comment({ - commentId: "773c1b6d-9d0a-43cb-94e5-2da1bac633c0", - parentId: "6326079f-fd2f-4b81-83fe-487daee459bc", + commentId: "773c1b6d-9d0a-43cb-94e5-2da1bac633c0", // verified + parentId: "6326079f-fd2f-4b81-83fe-487daee459bc", // verified - top-level comment - Post Title: Coming out as GNC on my birthday commentContent: "Wow I can't even imagine how hard that must have been. Thankfully your family now understands what you're going through.", - createdAt: 1666990000, - updatedAt: 1666990000, + updatedAt: new Date("2022-05-27").getTime(), // verified - a day after the post was authored authorUser: new User({ - userId: "71120d45-7a75-43fd-b79c-54b06e7868af", + userId: "71120d45-7a75-43fd-b79c-54b06e7868af", // verified - wesley }), pinned: false, pending: false, @@ -852,13 +832,12 @@ export class Neo4jSeedService { childComments: [], }), new Comment({ - commentId: "84213582-a148-46b7-878d-c30a9cd02231", - parentId: "6326079f-fd2f-4b81-83fe-487daee459bc", + commentId: "84213582-a148-46b7-878d-c30a9cd02231", // verified + parentId: "6326079f-fd2f-4b81-83fe-487daee459bc", // verified - top-level comment - Post Title: Coming out as GNC on my birthday commentContent: "You dad sounds exhausting. I'm glad you're doing better now.", - createdAt: 1666990000, - updatedAt: 1666990000, + updatedAt: new Date("2022-05-27").getTime(), // verified - a day after the post was authored authorUser: new User({ - userId: "dc83daa3-d26b-4063-87b1-2b719069654e", + userId: "dc83daa3-d26b-4063-87b1-2b719069654e", // verified - alphonse }), pinned: false, pending: false, @@ -868,61 +847,63 @@ export class Neo4jSeedService { ); } - public async getPostTypes(): Promise { - return new Array( - new PostType({ + public async getPostTypes(): Promise> { + return { + queery: new PostType({ postTypeName: "queery", }), - new PostType({ + story: new PostType({ postTypeName: "story", - }) - ); + }), + }; } - public async getPostTags(): Promise { - return new Array( - new PostTag({ + public async getPostTags(): Promise< + Record<"Serious" | "Advice" | "Discussion" | "Trigger" | "General" | "Casual", PostTag> + > { + return { + Serious: new PostTag({ tagName: "Serious", tagColor: "#FF758C", }), - new PostTag({ + Advice: new PostTag({ tagName: "Advice", tagColor: "#FF758C", }), - new PostTag({ + Discussion: new PostTag({ tagName: "Discussion", tagColor: "#FF758C", }), - new PostTag({ + Trigger: new PostTag({ tagName: "Trigger", tagColor: "#FF758C", }), - new PostTag({ + General: new PostTag({ tagName: "General", tagColor: "#FF758C", }), - new PostTag({ + Casual: new PostTag({ tagName: "Casual", tagColor: "#FF758C", - }) - ); + }), + }; } public async getAwards(): Promise { return new Array( new Award({ awardId: "032930d2-9994-46cc-ad35-559bb41a9d05", - awardName: "Ian's Mom Award", + awardName: "Saddest Story Award", awardSvg: "", }), new Award({ awardId: "375608ce-ca65-4293-8402-da34cd2c42c7", - awardName: "", + awardName: "Quality Queery Award", awardSvg: "", }), new Award({ awardId: "bf99f8f5-66f7-41ce-8014-5f70e5145174", - awardName: "Ilia's Mom Award", + awardName: "Best Ally Award", awardSvg: "", }) ); @@ -931,27 +912,27 @@ export class Neo4jSeedService { public async getSexualities(): Promise { return new Array( new Sexuality({ - sexualityId: "9164d89b-8d71-4fd1-af61-155d1d7ffe53", + sexualityId: "9164d89b-8d71-4fd1-af61-155d1d7ffe53", // verified sexualityName: "Gay", sexualityFlagSvg: "", }), new Sexuality({ - sexualityId: "1b67cf76-752d-4ea5-9584-a4232998b838", + sexualityId: "1b67cf76-752d-4ea5-9584-a4232998b838", // verified sexualityName: "Lesbian", sexualityFlagSvg: "", }), new Sexuality({ - sexualityId: "df388311-c184-4f09-93f4-645c6175322c", + sexualityId: "df388311-c184-4f09-93f4-645c6175322c", // verified sexualityName: "Homosexual", sexualityFlagSvg: "", }), new Sexuality({ - sexualityId: "2d32c4d3-4aca-4b03-bf68-ba104656183f", + sexualityId: "2d32c4d3-4aca-4b03-bf68-ba104656183f", // verified sexualityName: "Asexual", sexualityFlagSvg: "", }), new Sexuality({ - sexualityId: "5bc9535e-cc50-4112-91ad-717dc2de9492", + sexualityId: "5bc9535e-cc50-4112-91ad-717dc2de9492", // verified sexualityName: "Bisexual", sexualityFlagSvg: "", }) @@ -961,43 +942,43 @@ export class Neo4jSeedService { public async getGenders(): Promise { return new Array( new Gender({ - genderId: "d2945763-d1fb-46aa-b896-7f701b4ca699", + genderId: "f97edcdf-9df4-4f4a-9114-fbcd702502af", // verified genderName: "NA", genderPronouns: "NA", genderFlagSvg: ``, }), new Gender({ - genderId: "d2945763-d1fb-46aa-b896-7f701b4ca699", + genderId: "7351c1c9-50cd-4871-9af0-60c8f99a4627", // verified genderName: "Female", genderPronouns: "She/Her", genderFlagSvg: "", }), new Gender({ - genderId: "585d31aa-d5b3-4b8d-9690-ffcd57ce2862", + genderId: "585d31aa-d5b3-4b8d-9690-ffcd57ce2862", // verified genderName: "Male", genderPronouns: "He/Him", genderFlagSvg: ``, }), new Gender({ - genderId: "23907da4-c3f2-4e96-a73d-423e64f18a21", + genderId: "23907da4-c3f2-4e96-a73d-423e64f18a21", // verified genderName: "Non-binary", genderPronouns: "They/Them", genderFlagSvg: "", }), new Gender({ - genderId: "3af72545-99d4-4715-812b-c935fbf57f22", + genderId: "3af72545-99d4-4715-812b-c935fbf57f22", // verified genderName: "Non-binary", genderPronouns: "Ve/Ver", genderFlagSvg: "", }), new Gender({ - genderId: "6d67e992-c6d1-45be-8316-7a839894bf36", + genderId: "6d67e992-c6d1-45be-8316-7a839894bf36", // verified genderName: "Non-binary", genderPronouns: "Xe/Xem", genderFlagSvg: "", }), new Gender({ - genderId: "16c10474-9fa6-4eac-aac2-a63423edb757", + genderId: "16c10474-9fa6-4eac-aac2-a63423edb757", // verified genderName: "Non-binary", genderPronouns: "Ze/Zie", genderFlagSvg: "", @@ -1008,32 +989,32 @@ export class Neo4jSeedService { public async getOpennessRecords(): Promise { return new Array( new Openness({ - opennessId: "ae90b960-5f00-4298-b509-fac92a59b406", + opennessId: "ae90b960-5f00-4298-b509-fac92a59b406", // verified opennessLevel: -1, opennessDescription: "Not Sure", }), new Openness({ - opennessId: "842b5bd7-1da1-4a95-9564-1fc3b97b3655", + opennessId: "842b5bd7-1da1-4a95-9564-1fc3b97b3655", // verified opennessLevel: 0, opennessDescription: "Not Out", }), new Openness({ - opennessId: "db27c417-a8a5-4703-9b35-9dc76e98fc95", + opennessId: "db27c417-a8a5-4703-9b35-9dc76e98fc95", // verified opennessLevel: 1, opennessDescription: "Out to Few", }), new Openness({ - opennessId: "822b2622-70d6-4d7c-860a-f56e309fe950", + opennessId: "822b2622-70d6-4d7c-860a-f56e309fe950", // verified opennessLevel: 2, opennessDescription: "Semi-Out", }), new Openness({ - opennessId: "d5c97584-cd1b-4aa6-82ad-b5ddd3577bee", + opennessId: "d5c97584-cd1b-4aa6-82ad-b5ddd3577bee", // verified opennessLevel: 3, opennessDescription: "Fully Out", }), new Openness({ - opennessId: "77ec4978-6775-4b2f-9e91-ea60bb3742a5", + opennessId: "77ec4978-6775-4b2f-9e91-ea60bb3742a5", // verified opennessLevel: 4, opennessDescription: "Ally", }) From 6ab1a3604fcb19480e72688e0bd68107522523c1 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Tue, 29 Nov 2022 15:06:20 -0800 Subject: [PATCH 076/153] fix the tests --- src/posts/models/post.spec.ts | 20 +++++++++---------- .../post/posts.repository.spec.ts | 17 ++++++++-------- src/users/models/user.spec.ts | 12 +++++------ .../users/users.repository.test.spec.ts | 4 +++- 4 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/posts/models/post.spec.ts b/src/posts/models/post.spec.ts index 9c1d763..65b74e4 100644 --- a/src/posts/models/post.spec.ts +++ b/src/posts/models/post.spec.ts @@ -9,6 +9,8 @@ import { IPostsRepository } from "../repositories/post/posts.repository.interfac import { Post } from "./post"; import { _$ } from "../../_domain/injectableTokens"; +const postIdToFindInTest = "bcddeb57-939d-441b-b4ea-71e1d2055f32"; + describe("Post Model Unit Test", () => { let postsRepository: IPostsRepository; @@ -43,7 +45,7 @@ describe("Post Model Unit Test", () => { describe("given a post instance", () => { beforeAll(async () => { - post = await postsRepository.findPostById("b73edbf4-ba84-4b11-a91c-e1d8b1366974"); + post = await postsRepository.findPostById(postIdToFindInTest); }); it("post instance must exist", async () => { @@ -67,9 +69,7 @@ describe("Post Model Unit Test", () => { describe("given post.getRestricted() called", () => { describe("given the post is not restricted", () => { beforeEach(async () => { - post = await postsRepository.findPostById( - "b73edbf4-ba84-4b11-a91c-e1d8b1366974" - ); + post = await postsRepository.findPostById(postIdToFindInTest); }); it("should return null", async () => { @@ -80,9 +80,7 @@ describe("Post Model Unit Test", () => { describe("given the post is restricted", () => { beforeEach(async () => { - post = await postsRepository.findPostById( - "596632ac-dd54-4700-a783-688618d99fa9" - ); + post = await postsRepository.findPostById(postIdToFindInTest); }); it("should return an object consisting the proper props", async () => { @@ -97,7 +95,7 @@ describe("Post Model Unit Test", () => { describe("given post.getAuthorUser() called", () => { beforeEach(async () => { - post = await postsRepository.findPostById("b73edbf4-ba84-4b11-a91c-e1d8b1366974"); + post = await postsRepository.findPostById(postIdToFindInTest); }); it("should return an object consisting the proper props", async () => { @@ -117,7 +115,7 @@ describe("Post Model Unit Test", () => { describe("given post.getCreatedAt() called", () => { beforeEach(async () => { - post = await postsRepository.findPostById("b73edbf4-ba84-4b11-a91c-e1d8b1366974"); + post = await postsRepository.findPostById(postIdToFindInTest); }); it("should return a number that represents timestamp", async () => { @@ -128,7 +126,7 @@ describe("Post Model Unit Test", () => { describe("given post.getPostType() called", () => { beforeEach(async () => { - post = await postsRepository.findPostById("b73edbf4-ba84-4b11-a91c-e1d8b1366974"); + post = await postsRepository.findPostById(postIdToFindInTest); }); it("should return an object with proper properties", async () => { @@ -140,7 +138,7 @@ describe("Post Model Unit Test", () => { describe("given post.getPostTags() called", () => { beforeEach(async () => { - post = await postsRepository.findPostById("b73edbf4-ba84-4b11-a91c-e1d8b1366974"); + post = await postsRepository.findPostById(postIdToFindInTest); }); it("should return an array that consists valid objects with proper props", async () => { diff --git a/src/posts/repositories/post/posts.repository.spec.ts b/src/posts/repositories/post/posts.repository.spec.ts index deeac2e..4b93abb 100644 --- a/src/posts/repositories/post/posts.repository.spec.ts +++ b/src/posts/repositories/post/posts.repository.spec.ts @@ -88,7 +88,7 @@ describe("PostsRepository", () => { let post: Post; beforeAll(async () => { - post = await postsRepository.findPostById("b73edbf4-ba84-4b11-a91c-e1d8b1366974"); + post = await postsRepository.findPostById("bcddeb57-939d-441b-b4ea-71e1d2055f32"); }); it("should return a post", async () => { @@ -107,16 +107,17 @@ describe("PostsRepository", () => { describe(".addPost() and .deletePost()", () => { const samplePostId = "64f5ef93-31ac-4c61-b98a-79268d282fc7"; - const sampleAuthorId = "3109f9e2-a262-4aef-b648-90d86d6fbf6c"; - const sampleAwardedByUserId = "5c0f145b-ffad-4881-8ee6-7647c3c1b695"; + const sampleAuthorId = "a59437f4-ea62-4a15-a4e6-621b04af74d6"; + const sampleAwardedByUserId = "0daef999-7291-4f0c-a41a-078a6f28aa5e"; beforeAll(async () => { + const postTags = await neo4jSeedService.getPostTags(); const postToAdd = new Post({ postId: samplePostId, postTitle: "Test Post Title", postContent: "This is a test post. will be removed", - updatedAt: 1665770000, - postType: (await neo4jSeedService.getPostTypes())[0], - postTags: (await neo4jSeedService.getPostTags()).slice(0, 2), + updatedAt: new Date("2020-05-20").getTime(), + postType: (await neo4jSeedService.getPostTypes()).queery, + postTags: [postTags.Serious, postTags.Casual], restrictedProps: null, authorUser: new User({ userId: sampleAuthorId }), pending: false, @@ -151,14 +152,14 @@ describe("PostsRepository", () => { describe(".restrictPost() and .unrestrictPost()", () => { let post: Post; - const postId = "b73edbf4-ba84-4b11-a91c-e1d8b1366974"; + const postId = "bcddeb57-939d-441b-b4ea-71e1d2055f32"; beforeAll(async () => { await postsRepository.restrictPost( postId, new RestrictedProps({ restrictedAt: new Date().getTime(), - moderatorId: "5c0f145b-ffad-4881-8ee6-7647c3c1b695", + moderatorId: "0daef999-7291-4f0c-a41a-078a6f28aa5e", reason: "Test", }) ); diff --git a/src/users/models/user.spec.ts b/src/users/models/user.spec.ts index 139c6cc..5fb45e8 100644 --- a/src/users/models/user.spec.ts +++ b/src/users/models/user.spec.ts @@ -43,7 +43,7 @@ describe("Post Model Unit Test", () => { describe("given a post instance", () => { beforeAll(async () => { - user = await usersRepository.findUserById("5c0f145b-ffad-4881-8ee6-7647c3c1b695"); + user = await usersRepository.findUserById("a59437f4-ea62-4a15-a4e6-621b04af74d6"); }); it("instance must exist", async () => { @@ -52,7 +52,7 @@ describe("Post Model Unit Test", () => { describe("given user.getAuthoredPosts() called", () => { beforeEach(async () => { - user = await usersRepository.findUserById("5c0f145b-ffad-4881-8ee6-7647c3c1b695"); + user = await usersRepository.findUserById("a59437f4-ea62-4a15-a4e6-621b04af74d6"); }); it("should return an array", async () => { @@ -72,7 +72,7 @@ describe("Post Model Unit Test", () => { describe("given user.getFavoritePosts() called", () => { beforeEach(async () => { - user = await usersRepository.findUserById("5c0f145b-ffad-4881-8ee6-7647c3c1b695"); + user = await usersRepository.findUserById("a59437f4-ea62-4a15-a4e6-621b04af74d6"); }); it("should return an array of proper objects", async () => { @@ -94,7 +94,7 @@ describe("Post Model Unit Test", () => { describe("given user.getSexuality() called", () => { beforeEach(async () => { - user = await usersRepository.findUserById("5c0f145b-ffad-4881-8ee6-7647c3c1b695"); + user = await usersRepository.findUserById("a59437f4-ea62-4a15-a4e6-621b04af74d6"); }); it("should return an object with proper props", async () => { @@ -108,7 +108,7 @@ describe("Post Model Unit Test", () => { describe("given user.getGender() called", () => { beforeEach(async () => { - user = await usersRepository.findUserById("5c0f145b-ffad-4881-8ee6-7647c3c1b695"); + user = await usersRepository.findUserById("a59437f4-ea62-4a15-a4e6-621b04af74d6"); }); it("should return an object with proper props", async () => { @@ -123,7 +123,7 @@ describe("Post Model Unit Test", () => { describe("given user.getOpenness() called", () => { beforeEach(async () => { - user = await usersRepository.findUserById("5c0f145b-ffad-4881-8ee6-7647c3c1b695"); + user = await usersRepository.findUserById("a59437f4-ea62-4a15-a4e6-621b04af74d6"); }); it("should return an object with proper props", async () => { diff --git a/src/users/repositories/users/users.repository.test.spec.ts b/src/users/repositories/users/users.repository.test.spec.ts index cd73bc4..1a0a30a 100644 --- a/src/users/repositories/users/users.repository.test.spec.ts +++ b/src/users/repositories/users/users.repository.test.spec.ts @@ -11,6 +11,8 @@ import { Neo4jService } from "../../../neo4j/services/neo4j.service"; import { v4 as uuidv4 } from "uuid"; import { _$ } from "../../../_domain/injectableTokens"; +const userIdToFind = "a59437f4-ea62-4a15-a4e6-621b04af74d6"; + describe("UsersRepository", () => { let usersRepository: IUsersRepository; let neo4jSeedService: Neo4jSeedService; @@ -103,7 +105,7 @@ describe("UsersRepository", () => { let user: User; beforeAll(async () => { - user = await usersRepository.findUserById("3109f9e2-a262-4aef-b648-90d86d6fbf6c"); + user = await usersRepository.findUserById(userIdToFind); }); it("should return a user", async () => { From 0d00a9634b69947397eca78a611f96135037f534 Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Tue, 29 Nov 2022 17:02:29 -0800 Subject: [PATCH 077/153] Moved post report to moderation module --- .../userActions}/reportPostPayload.dto.ts | 26 +-- .../postsReport.service.interface.ts | 8 + .../postReport/postsReport.service.ts | 162 +++++++++--------- .../postsReport.service.interface.ts | 8 - 4 files changed, 102 insertions(+), 102 deletions(-) rename src/{posts/dtos => moderation/dtos/userActions}/reportPostPayload.dto.ts (96%) create mode 100644 src/moderation/services/userActions/postReport/postsReport.service.interface.ts rename src/{posts/services => moderation/services/userActions}/postReport/postsReport.service.ts (87%) delete mode 100644 src/posts/services/postReport/postsReport.service.interface.ts diff --git a/src/posts/dtos/reportPostPayload.dto.ts b/src/moderation/dtos/userActions/reportPostPayload.dto.ts similarity index 96% rename from src/posts/dtos/reportPostPayload.dto.ts rename to src/moderation/dtos/userActions/reportPostPayload.dto.ts index c232f14..d6915d7 100644 --- a/src/posts/dtos/reportPostPayload.dto.ts +++ b/src/moderation/dtos/userActions/reportPostPayload.dto.ts @@ -1,13 +1,13 @@ -import { ApiProperty } from "@nestjs/swagger"; - -export class ReportPostPayloadDto { - @ApiProperty({ type: String, format: "uuid" }) - postId: UUID; - - @ApiProperty({ type: String, minLength: 5, maxLength: 500 }) - reason: string; - - constructor(partial?: Partial) { - Object.assign(this, partial); - } -} +import { ApiProperty } from "@nestjs/swagger"; + +export class ReportPostPayloadDto { + @ApiProperty({ type: String, format: "uuid" }) + postId: UUID; + + @ApiProperty({ type: String, minLength: 5, maxLength: 500 }) + reason: string; + + constructor(partial?: Partial) { + Object.assign(this, partial); + } +} diff --git a/src/moderation/services/userActions/postReport/postsReport.service.interface.ts b/src/moderation/services/userActions/postReport/postsReport.service.interface.ts new file mode 100644 index 0000000..af18dd5 --- /dev/null +++ b/src/moderation/services/userActions/postReport/postsReport.service.interface.ts @@ -0,0 +1,8 @@ +import { ReportPostPayloadDto } from "../../../dtos/userActions"; +import { ReportedProps } from "../../../../users/models/toPost"; + +export interface IPostsReportService { + reportPost(reportPostPayload: ReportPostPayloadDto): Promise; + + getReportsForPost(postId: UUID): Promise; +} diff --git a/src/posts/services/postReport/postsReport.service.ts b/src/moderation/services/userActions/postReport/postsReport.service.ts similarity index 87% rename from src/posts/services/postReport/postsReport.service.ts rename to src/moderation/services/userActions/postReport/postsReport.service.ts index 81efe85..0a26e42 100644 --- a/src/posts/services/postReport/postsReport.service.ts +++ b/src/moderation/services/userActions/postReport/postsReport.service.ts @@ -1,81 +1,81 @@ -import { ReportPostPayloadDto } from "../../dtos"; -import { User } from "../../../users/models"; -import { HttpException, Inject, Injectable, Logger, Scope } from "@nestjs/common"; -import { DatabaseContext } from "../../../database-access-layer/databaseContext"; -import { REQUEST } from "@nestjs/core"; -import { Request } from "express"; -import { _$ } from "../../../_domain/injectableTokens"; -import { ReportedProps, UserToPostRelTypes } from "../../../users/models/toPost"; -import { IPostsReportService } from "./postsReport.service.interface"; - -@Injectable({ scope: Scope.REQUEST }) -export class PostsReportService implements IPostsReportService { - private readonly _logger = new Logger(PostsReportService.name); - private readonly _request: Request; - private readonly _dbContext: DatabaseContext; - - constructor( - @Inject(REQUEST) request: Request, - @Inject(_$.IDatabaseContext) databaseContext: DatabaseContext - ) { - this._request = request; - this._dbContext = databaseContext; - } - - public async reportPost(reportPostPayload: ReportPostPayloadDto): Promise { - const user = this.getUserFromRequest(); - - const post = await this._dbContext.Posts.findPostById(reportPostPayload.postId); - if (!post) throw new Error("Post not found"); - - if (post.pending || post.restrictedProps !== null) { - throw new HttpException( - "Post cannot be reported due to being pending or restricted", - 400 - ); - } - - const reports = await this.getReportsForPost(post.postId); - - if (reports.some(r => r.moderatorId === user.userId)) { - throw new HttpException("Post already reported", 400); - } - - await post.getAuthorUser(); - if (post.authorUser.userId === user.userId) { - throw new HttpException("Post cannot be reported by post author", 400); - } - - await this._dbContext.neo4jService.tryWriteAsync( - ` - MATCH (p:Post { postId: $postId }), (u:User { userId: $userId }) - MERGE (u)-[r:${UserToPostRelTypes.REPORTED}]->(p) - `, - { - postId: post.postId, - userId: user.userId, - } - ); - } - - public async getReportsForPost(postId: string): Promise { - const queryResult = await this._dbContext.neo4jService.tryReadAsync( - ` - MATHC (p:Post { postId: $postId })<-[r:${UserToPostRelTypes.REPORTED}]-(u:User)`, - { - postId: postId, - } - ); - return queryResult.records.map(record => { - const reportedProps = new ReportedProps(record.get("r").properties); - reportedProps.moderatorId = record.get("u").properties.userId; - return reportedProps; - }); - } - - private getUserFromRequest(): User { - const user = this._request.user as User; - if (user === undefined) throw new Error("User not found"); - return user; - } -} +import { ReportPostPayloadDto } from "../../../dtos/userActions"; +import { User } from "../../../../users/models"; +import { HttpException, Inject, Injectable, Logger, Scope } from "@nestjs/common"; +import { DatabaseContext } from "../../../../database-access-layer/databaseContext"; +import { REQUEST } from "@nestjs/core"; +import { Request } from "express"; +import { _$ } from "../../../../_domain/injectableTokens"; +import { ReportedProps, UserToPostRelTypes } from "../../../../users/models/toPost"; +import { IPostsReportService } from "./postsReport.service.interface"; + +@Injectable({ scope: Scope.REQUEST }) +export class PostsReportService implements IPostsReportService { + private readonly _logger = new Logger(PostsReportService.name); + private readonly _request: Request; + private readonly _dbContext: DatabaseContext; + + constructor( + @Inject(REQUEST) request: Request, + @Inject(_$.IDatabaseContext) databaseContext: DatabaseContext + ) { + this._request = request; + this._dbContext = databaseContext; + } + + public async reportPost(reportPostPayload: ReportPostPayloadDto): Promise { + const user = this.getUserFromRequest(); + + const post = await this._dbContext.Posts.findPostById(reportPostPayload.postId); + if (!post) throw new Error("Post not found"); + + if (post.pending || post.restrictedProps !== null) { + throw new HttpException( + "Post cannot be reported due to being pending or restricted", + 400 + ); + } + + const reports = await this.getReportsForPost(post.postId); + + if (reports.some(r => r.moderatorId === user.userId)) { + throw new HttpException("Post already reported", 400); + } + + await post.getAuthorUser(); + if (post.authorUser.userId === user.userId) { + throw new HttpException("Post cannot be reported by post author", 400); + } + + await this._dbContext.neo4jService.tryWriteAsync( + ` + MATCH (p:Post { postId: $postId }), (u:User { userId: $userId }) + MERGE (u)-[r:${UserToPostRelTypes.REPORTED}]->(p) + `, + { + postId: post.postId, + userId: user.userId, + } + ); + } + + public async getReportsForPost(postId: string): Promise { + const queryResult = await this._dbContext.neo4jService.tryReadAsync( + ` + MATHC (p:Post { postId: $postId })<-[r:${UserToPostRelTypes.REPORTED}]-(u:User)`, + { + postId: postId, + } + ); + return queryResult.records.map(record => { + const reportedProps = new ReportedProps(record.get("r").properties); + reportedProps.moderatorId = record.get("u").properties.userId; + return reportedProps; + }); + } + + private getUserFromRequest(): User { + const user = this._request.user as User; + if (user === undefined) throw new Error("User not found"); + return user; + } +} diff --git a/src/posts/services/postReport/postsReport.service.interface.ts b/src/posts/services/postReport/postsReport.service.interface.ts deleted file mode 100644 index a972e9e..0000000 --- a/src/posts/services/postReport/postsReport.service.interface.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ReportPostPayloadDto } from "../../dtos"; -import { ReportedProps } from "../../../users/models/toPost"; - -export interface IPostsReportService { - reportPost(reportPostPayload: ReportPostPayloadDto): Promise; - - getReportsForPost(postId: string): Promise; -} From 7d5071e6931f460cd7b8ec1bbced56a2dd550c58 Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Tue, 29 Nov 2022 17:03:21 -0800 Subject: [PATCH 078/153] Fixed import --- src/posts/dtos/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/posts/dtos/index.ts b/src/posts/dtos/index.ts index 0414651..c07d79f 100644 --- a/src/posts/dtos/index.ts +++ b/src/posts/dtos/index.ts @@ -1,4 +1,3 @@ export { DeletePostPayloadDto } from "./deletePostPayload.dto"; export { VotePostPayloadDto } from "./votePostPayload.dto"; -export { ReportPostPayloadDto } from "./reportPostPayload.dto"; export { PostCreationPayloadDto } from "./postCreationPayload.dto"; From 13b00386d0525a6cfb4176ea8aba0a5e1e4bb53d Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Tue, 29 Nov 2022 17:03:57 -0800 Subject: [PATCH 079/153] Init moderation controller --- src/app.module.ts | 8 ++++---- .../controllers/moderation.controller.spec.ts | 18 ++++++++++++++++++ .../controllers/moderation.controller.ts | 19 +++++++++++++++++++ src/moderation/moderation.module.ts | 2 ++ 4 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 src/moderation/controllers/moderation.controller.spec.ts create mode 100644 src/moderation/controllers/moderation.controller.ts diff --git a/src/app.module.ts b/src/app.module.ts index ebdc261..f4b044a 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -62,9 +62,9 @@ export class AppModule { } onModuleInit() { - this._neo4jSeedService - .seed() - .then(() => this._logger.log("Neo4j database seeded ✅")) - .catch(error => this._logger.error(error)); + // this._neo4jSeedService + // .seed() + // .then(() => this._logger.log("Neo4j database seeded ✅")) + // .catch(error => this._logger.error(error)); } } diff --git a/src/moderation/controllers/moderation.controller.spec.ts b/src/moderation/controllers/moderation.controller.spec.ts new file mode 100644 index 0000000..94e4090 --- /dev/null +++ b/src/moderation/controllers/moderation.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ModerationController } from './moderation.controller'; + +describe('ModerationController', () => { + let controller: ModerationController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ModerationController], + }).compile(); + + controller = module.get(ModerationController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/moderation/controllers/moderation.controller.ts b/src/moderation/controllers/moderation.controller.ts new file mode 100644 index 0000000..499345b --- /dev/null +++ b/src/moderation/controllers/moderation.controller.ts @@ -0,0 +1,19 @@ +import { + Body, + CacheInterceptor, + CacheTTL, + ClassSerializerInterceptor, + Controller, + Delete, + Get, + Inject, + Param, + ParseUUIDPipe, + Post, + UseGuards, + UseInterceptors, +} from "@nestjs/common"; + +@Controller("moderation") +export class ModerationController {} + diff --git a/src/moderation/moderation.module.ts b/src/moderation/moderation.module.ts index 2823c67..6b4c5dd 100644 --- a/src/moderation/moderation.module.ts +++ b/src/moderation/moderation.module.ts @@ -4,6 +4,7 @@ import { ModeratorActionsService } from "./services/moderatorActions/moderatorAc import { _$ } from "../_domain/injectableTokens"; import { HttpModule } from "@nestjs/axios"; import { DatabaseAccessLayerModule } from "../database-access-layer/database-access-layer.module"; +import { ModerationController } from './moderation.controller'; @Module({ imports: [HttpModule, forwardRef(() => DatabaseAccessLayerModule)], @@ -27,5 +28,6 @@ import { DatabaseAccessLayerModule } from "../database-access-layer/database-acc useClass: ModeratorActionsService, }, ], + controllers: [ModerationController], }) export class ModerationModule {} From f106baa10dc20ce31f9b10fdc810d776b67bc554 Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Tue, 29 Nov 2022 17:04:27 -0800 Subject: [PATCH 080/153] Create comments report service --- src/moderation/dtos/userActions/index.ts | 2 + .../userActions/reportCommentPayload.dto.ts | 13 +++ .../commentsReport.service.interface.ts | 8 ++ .../commentReport/commentsReport.service.ts | 83 +++++++++++++++++++ 4 files changed, 106 insertions(+) create mode 100644 src/moderation/dtos/userActions/index.ts create mode 100644 src/moderation/dtos/userActions/reportCommentPayload.dto.ts create mode 100644 src/moderation/services/userActions/commentReport/commentsReport.service.interface.ts create mode 100644 src/moderation/services/userActions/commentReport/commentsReport.service.ts diff --git a/src/moderation/dtos/userActions/index.ts b/src/moderation/dtos/userActions/index.ts new file mode 100644 index 0000000..d0c036d --- /dev/null +++ b/src/moderation/dtos/userActions/index.ts @@ -0,0 +1,2 @@ +export { ReportPostPayloadDto } from "./reportPostPayload.dto"; +export { ReportCommentPayloadDto } from "./reportCommentPayload.dto"; diff --git a/src/moderation/dtos/userActions/reportCommentPayload.dto.ts b/src/moderation/dtos/userActions/reportCommentPayload.dto.ts new file mode 100644 index 0000000..ba6518b --- /dev/null +++ b/src/moderation/dtos/userActions/reportCommentPayload.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from "@nestjs/swagger"; + +export class ReportCommentPayloadDto { + @ApiProperty({ type: String, format: "uuid" }) + commentId: UUID; + + @ApiProperty({ type: String, minLength: 5, maxLength: 500 }) + reason: string; + + constructor(partial?: Partial) { + Object.assign(this, partial); + } +} diff --git a/src/moderation/services/userActions/commentReport/commentsReport.service.interface.ts b/src/moderation/services/userActions/commentReport/commentsReport.service.interface.ts new file mode 100644 index 0000000..81a1223 --- /dev/null +++ b/src/moderation/services/userActions/commentReport/commentsReport.service.interface.ts @@ -0,0 +1,8 @@ +import { ReportCommentPayloadDto } from "../../../dtos/userActions"; +import { ReportedProps } from "../../../../users/models/toComment"; + +export interface ICommentsReportService { + reportComment(reportCommentPayload: ReportCommentPayloadDto): Promise; + + getReportsForComment(commentId: UUID): Promise; +} diff --git a/src/moderation/services/userActions/commentReport/commentsReport.service.ts b/src/moderation/services/userActions/commentReport/commentsReport.service.ts new file mode 100644 index 0000000..cae1355 --- /dev/null +++ b/src/moderation/services/userActions/commentReport/commentsReport.service.ts @@ -0,0 +1,83 @@ +import { ReportCommentPayloadDto } from "../../../dtos/userActions"; +import { User } from "../../../../users/models"; +import { HttpException, Inject, Injectable, Logger, Scope } from "@nestjs/common"; +import { DatabaseContext } from "../../../../database-access-layer/databaseContext"; +import { REQUEST } from "@nestjs/core"; +import { Request } from "express"; +import { _$ } from "../../../../_domain/injectableTokens"; +import { ReportedProps, UserToCommentRelTypes } from "../../../../users/models/toComment"; +import { ICommentsReportService } from "./commentsReport.service.interface"; + +@Injectable({ scope: Scope.REQUEST }) +export class CommentsReportService implements ICommentsReportService { + private readonly _logger = new Logger(CommentsReportService.name); + private readonly _request: Request; + private readonly _dbContext: DatabaseContext; + + constructor( + @Inject(REQUEST) request: Request, + @Inject(_$.IDatabaseContext) databaseContext: DatabaseContext + ) { + this._request = request; + this._dbContext = databaseContext; + } + + public async reportComment(reportCommentPayload: ReportCommentPayloadDto): Promise { + const user = this.getUserFromRequest(); + + const comment = await this._dbContext.Comments.findCommentById( + reportCommentPayload.commentId + ); + if (!comment) throw new Error("Comment not found"); + + if (comment.pending || comment.restrictedProps !== null) { + throw new HttpException( + "Comment cannot be reported due to being pending or restricted", + 400 + ); + } + + const reports = await this.getReportsForComment(comment.commentId); + + if (reports.some(r => r.moderatorId === user.userId)) { + throw new HttpException("Comment already reported", 400); + } + + await comment.getAuthorUser(); + if (comment.authorUser.userId === user.userId) { + throw new HttpException("Comment cannot be reported by comment author", 400); + } + + await this._dbContext.neo4jService.tryWriteAsync( + ` + MATCH (c:Comment { commentId: $commentId }), (u:User { userId: $userId }) + MERGE (u)-[r:${UserToCommentRelTypes.REPORTED}]->(c) + `, + { + commentId: comment.commentId, + userId: user.userId, + } + ); + } + + public async getReportsForComment(commentId: UUID): Promise { + const queryResult = await this._dbContext.neo4jService.tryReadAsync( + ` + MATHC (c:Comment { commentId: $commentId })<-[r:${UserToCommentRelTypes.REPORTED}]-(u:User)`, + { + commentId: commentId, + } + ); + return queryResult.records.map(record => { + const reportedProps = new ReportedProps(record.get("r").properties); + reportedProps.moderatorId = record.get("u").properties.userId; + return reportedProps; + }); + } + + private getUserFromRequest(): User { + const user = this._request.user as User; + if (user === undefined) throw new Error("User not found"); + return user; + } +} From bfb27e968eac3d0536b2d5982054ae8049c30083 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Tue, 29 Nov 2022 17:42:51 -0800 Subject: [PATCH 081/153] fix seeds --- src/neo4j/neo4j.module.ts | 4 +- src/neo4j/services/neo4j.seed.service.ts | 106 ++++++++++++++++------- 2 files changed, 77 insertions(+), 33 deletions(-) diff --git a/src/neo4j/neo4j.module.ts b/src/neo4j/neo4j.module.ts index 7908d8b..8f1c369 100644 --- a/src/neo4j/neo4j.module.ts +++ b/src/neo4j/neo4j.module.ts @@ -1,12 +1,14 @@ -import { DynamicModule, Module, Provider } from "@nestjs/common"; +import { DynamicModule, forwardRef, Module, Provider } from "@nestjs/common"; import { Neo4jService } from "./services/neo4j.service"; import { NEO4J_DRIVER, NEO4J_OPTIONS } from "./neo4j.constants"; import { createDriver } from "./neo4j.utils"; import { Neo4jConfig } from "./neo4jConfig.interface"; import { ConfigModule, ConfigService } from "@nestjs/config"; import { Neo4jSeedService } from "./services/neo4j.seed.service"; +import { DatabaseAccessLayerModule } from "../database-access-layer/database-access-layer.module"; @Module({ + imports: [forwardRef(() => DatabaseAccessLayerModule)], providers: [Neo4jService, Neo4jSeedService], }) export class Neo4jModule { diff --git a/src/neo4j/services/neo4j.seed.service.ts b/src/neo4j/services/neo4j.seed.service.ts index dc6fd8b..9765db2 100644 --- a/src/neo4j/services/neo4j.seed.service.ts +++ b/src/neo4j/services/neo4j.seed.service.ts @@ -14,10 +14,15 @@ import { UserToSexualityRelTypes } from "../../users/models/toSexuality"; import { RestrictedProps, _ToSelfRelTypes } from "../../_domain/models/toSelf"; import { LABELS_DECORATOR_KEY } from "../neo4j.constants"; import { Neo4jService } from "./neo4j.service"; +import { _$ } from "../../_domain/injectableTokens"; +import { DatabaseContext } from "../../database-access-layer/databaseContext"; @Injectable() export class Neo4jSeedService { - constructor(@Inject(Neo4jService) private _neo4jService: Neo4jService) {} + constructor( + @Inject(Neo4jService) private _neo4jService: Neo4jService, + @Inject(_$.IDatabaseContext) private _dbContext: DatabaseContext + ) {} private postTypeLabel = Reflect.get(PostType, LABELS_DECORATOR_KEY)[0]; private postTagLabel = Reflect.get(PostTag, LABELS_DECORATOR_KEY)[0]; @@ -132,28 +137,42 @@ export class Neo4jSeedService { // Populate genders const genders = await this.getGenders(); for (const genderEntity of genders) { - await this._neo4jService.tryWriteAsync( - `CREATE (n:${this.genderLabel} { - genderId: $genderId, - genderName: $genderName, - genderPronouns: $genderPronouns, - genderFlagSvg: $genderFlagSvg - })`, - genderEntity + const foundGenderEntity = await this._dbContext.Genders.findGenderById( + genderEntity.genderId ); + if (!foundGenderEntity) { + await this._neo4jService.tryWriteAsync( + `CREATE (n:${this.genderLabel} { + genderId: $genderId, + genderName: $genderName, + genderPronouns: $genderPronouns, + genderFlagSvg: $genderFlagSvg + })`, + genderEntity + ); + } else { + await this._dbContext.Genders.updateGender(genderEntity); + } } // Populate genders const opennessRecords = await this.getOpennessRecords(); for (const opennessEntity of opennessRecords) { - await this._neo4jService.tryWriteAsync( - `CREATE (n:${this.opennessLabel} { - opennessId: $opennessId, - opennessLevel: $opennessLevel, - opennessDescription: $opennessDescription - })`, - opennessEntity + const foundOpennessEntity = await this._dbContext.Openness.findOpennessById( + opennessEntity.opennessId ); + if (!foundOpennessEntity) { + await this._neo4jService.tryWriteAsync( + `CREATE (n:${this.opennessLabel} { + opennessId: $opennessId, + opennessLevel: $opennessLevel, + opennessDescription: $opennessDescription + })`, + opennessEntity + ); + } else { + await this._dbContext.Openness.updateOpenness(opennessEntity); + } } // Populate users @@ -609,7 +628,7 @@ export class Neo4jSeedService { "I was on a camping trip and my sister caught me staring at someone across the site with his \n" + " shirt off, for the the rest of the day she wouldn't stop asking me, even getting the other members \n" + " who came with us to join in, I eventually gave in, she was super kind about it and came out as Bisexual the following months", - updatedAt: new Date("202-11-17").getTime(), // verified - NOTE: will be counted as `authoredAt` value. + updatedAt: new Date("2022-11-17").getTime(), // verified - NOTE: will be counted as `authoredAt` value. postType: postTypes.story, // verified - story postTags: [postTags.Casual, postTags.General, postTags.Trigger], // verified restrictedProps: null, @@ -648,7 +667,7 @@ export class Neo4jSeedService { postType: postTypes.story, // verified - story postTags: [postTags.Serious], // verified - Serious restrictedProps: new RestrictedProps({ - restrictedAt: new Date("202-11-22").getTime(), + restrictedAt: new Date("2022-11-22").getTime(), moderatorId: "71120d45-7a75-43fd-b79c-54b06e7868af", // verified - moderator - wesley reason: "The moderator thinks there is profanity in this post", }), @@ -735,7 +754,7 @@ export class Neo4jSeedService { commentId: "f8959b32-5b68-4f68-97bc-59afdc0d09cb", // verified - no children parentId: "bcddeb57-939d-441b-b4ea-71e1d2055f32", // verified - Title: Sister caught me checking out a guy on a camping trip. commentContent: "Wow that's so nice to hear!", - updatedAt: new Date("2020-11-18").getTime(), // verified - a day after Post authored + updatedAt: new Date("2022-11-18").getTime(), // verified - a day after Post authored authorUser: new User({ userId: "0daef999-7291-4f0c-a41a-078a6f28aa5e", // verified - moderator - christopher }), @@ -859,32 +878,55 @@ export class Neo4jSeedService { } public async getPostTags(): Promise< - Record<"Serious" | "Advice" | "Discussion" | "Trigger" | "General" | "Casual", PostTag> + Record< + | "Serious" + | "Advice" + | "Discussion" + | "Trigger" + | "General" + | "Casual" + | "Inspiring" + | "Vent" + | "Drama", + PostTag + > > { return { Serious: new PostTag({ - tagName: "Serious", - tagColor: "#FF758C", + tagName: "serious", + tagColor: "#E02947", }), Advice: new PostTag({ - tagName: "Advice", - tagColor: "#FF758C", + tagName: "advice", + tagColor: "#FFB6C3", }), Discussion: new PostTag({ - tagName: "Discussion", - tagColor: "#FF758C", + tagName: "discussion", + tagColor: "#FFB6C3", }), Trigger: new PostTag({ - tagName: "Trigger", - tagColor: "#FF758C", + tagName: "trigger", + tagColor: "#C2ADFF", }), General: new PostTag({ - tagName: "General", - tagColor: "#FF758C", + tagName: "general", + tagColor: "#FFB6C3", }), Casual: new PostTag({ - tagName: "Casual", - tagColor: "#FF758C", + tagName: "casual", + tagColor: "#FFEAD4", + }), + Inspiring: new PostTag({ + tagName: "inspiring", + tagColor: "#C2ADFF", + }), + Vent: new PostTag({ + tagName: "vent", + tagColor: "#C2ADFF", + }), + Drama: new PostTag({ + tagName: "drama", + tagColor: "#C2ADFF", }), }; } From 8640832e554fe83f8c70b3a62f4f939475e0fb3a Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Tue, 29 Nov 2022 17:44:48 -0800 Subject: [PATCH 082/153] Removing duplicate methods --- .../users/users.repository.interface.ts | 4 ---- .../repositories/users/users.repository.ts | 24 +------------------ 2 files changed, 1 insertion(+), 27 deletions(-) diff --git a/src/users/repositories/users/users.repository.interface.ts b/src/users/repositories/users/users.repository.interface.ts index 16810f0..d175f52 100644 --- a/src/users/repositories/users/users.repository.interface.ts +++ b/src/users/repositories/users/users.repository.interface.ts @@ -49,9 +49,5 @@ export interface IUsersRepository { hasGenderProps: HasGenderProps ): Promise; - banUser(userId: UUID, banProps: GotBannedProps): Promise; - - unbanUser(userId: UUID): Promise; - addPreviouslyBanned(userId: UUID, banProps: GotBannedProps): Promise; } diff --git a/src/users/repositories/users/users.repository.ts b/src/users/repositories/users/users.repository.ts index 4b8231a..caf8c5b 100644 --- a/src/users/repositories/users/users.repository.ts +++ b/src/users/repositories/users/users.repository.ts @@ -231,20 +231,6 @@ export class UsersRepository implements IUsersRepository { } ); } - - public async banUser(userId: UUID, banProps: GotBannedProps): Promise { - await this._neo4jService.tryWriteAsync( - `MATCH (u:User {userId: $userId}) - CREATE (u)-[:${UserToSelfRelTypes.GOT_BANNED} {bannedAt: $bannedAt, moderatorId: $moderatorId, reason: $reason}]->(u) - `, - { - userId: userId, - bannedAt: banProps.bannedAt, - moderatorId: banProps.moderatorId, - reason: banProps.reason, - } - ); - } public async connectUserWithGender( userId: UUID, @@ -291,14 +277,6 @@ export class UsersRepository implements IUsersRepository { } ); } - public async unbanUser(userId: UUID): Promise { - await this._neo4jService.tryWriteAsync( - `MATCH (u:User {userId: $userId})-[r:${UserToSelfRelTypes.GOT_BANNED}]->(u) DELETE r`, - { - userId: userId, - } - ); - } public async connectUserWithOpenness( userId: UUID, @@ -345,7 +323,7 @@ export class UsersRepository implements IUsersRepository { } ); } - + public async addPreviouslyBanned(userId: UUID, banProps: GotBannedProps): Promise { await this._neo4jService.tryWriteAsync( `MATCH (u:User {userId: $userId}) From b2d226b742410e53a2983d0451e584c95907e0e7 Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Tue, 29 Nov 2022 18:16:29 -0800 Subject: [PATCH 083/153] Updating report props --- src/moderation/moderation.module.ts | 2 +- src/users/models/toComment/reported.props.ts | 2 +- src/users/models/toPost/reported.props.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/moderation/moderation.module.ts b/src/moderation/moderation.module.ts index 6b4c5dd..315df9a 100644 --- a/src/moderation/moderation.module.ts +++ b/src/moderation/moderation.module.ts @@ -4,7 +4,7 @@ import { ModeratorActionsService } from "./services/moderatorActions/moderatorAc import { _$ } from "../_domain/injectableTokens"; import { HttpModule } from "@nestjs/axios"; import { DatabaseAccessLayerModule } from "../database-access-layer/database-access-layer.module"; -import { ModerationController } from './moderation.controller'; +import { ModerationController } from "./controllers/moderation.controller"; @Module({ imports: [HttpModule, forwardRef(() => DatabaseAccessLayerModule)], diff --git a/src/users/models/toComment/reported.props.ts b/src/users/models/toComment/reported.props.ts index 557cce7..d58bd48 100644 --- a/src/users/models/toComment/reported.props.ts +++ b/src/users/models/toComment/reported.props.ts @@ -3,7 +3,7 @@ import { IsNumber, IsString, IsUUID } from "class-validator"; export class ReportedProps implements RelationshipProps { @IsUUID() - moderatorId: UUID; + userId: UUID; @IsNumber() reportedAt: number; diff --git a/src/users/models/toPost/reported.props.ts b/src/users/models/toPost/reported.props.ts index 557cce7..d58bd48 100644 --- a/src/users/models/toPost/reported.props.ts +++ b/src/users/models/toPost/reported.props.ts @@ -3,7 +3,7 @@ import { IsNumber, IsString, IsUUID } from "class-validator"; export class ReportedProps implements RelationshipProps { @IsUUID() - moderatorId: UUID; + userId: UUID; @IsNumber() reportedAt: number; From 55ff335de8a322802a90c9ed21c4b816847e8238 Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Tue, 29 Nov 2022 18:20:51 -0800 Subject: [PATCH 084/153] Revert "Removing duplicate methods" This reverts commit 8640832e554fe83f8c70b3a62f4f939475e0fb3a. --- .../users/users.repository.interface.ts | 4 ++++ .../repositories/users/users.repository.ts | 24 ++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/users/repositories/users/users.repository.interface.ts b/src/users/repositories/users/users.repository.interface.ts index d175f52..16810f0 100644 --- a/src/users/repositories/users/users.repository.interface.ts +++ b/src/users/repositories/users/users.repository.interface.ts @@ -49,5 +49,9 @@ export interface IUsersRepository { hasGenderProps: HasGenderProps ): Promise; + banUser(userId: UUID, banProps: GotBannedProps): Promise; + + unbanUser(userId: UUID): Promise; + addPreviouslyBanned(userId: UUID, banProps: GotBannedProps): Promise; } diff --git a/src/users/repositories/users/users.repository.ts b/src/users/repositories/users/users.repository.ts index caf8c5b..4b8231a 100644 --- a/src/users/repositories/users/users.repository.ts +++ b/src/users/repositories/users/users.repository.ts @@ -231,6 +231,20 @@ export class UsersRepository implements IUsersRepository { } ); } + + public async banUser(userId: UUID, banProps: GotBannedProps): Promise { + await this._neo4jService.tryWriteAsync( + `MATCH (u:User {userId: $userId}) + CREATE (u)-[:${UserToSelfRelTypes.GOT_BANNED} {bannedAt: $bannedAt, moderatorId: $moderatorId, reason: $reason}]->(u) + `, + { + userId: userId, + bannedAt: banProps.bannedAt, + moderatorId: banProps.moderatorId, + reason: banProps.reason, + } + ); + } public async connectUserWithGender( userId: UUID, @@ -277,6 +291,14 @@ export class UsersRepository implements IUsersRepository { } ); } + public async unbanUser(userId: UUID): Promise { + await this._neo4jService.tryWriteAsync( + `MATCH (u:User {userId: $userId})-[r:${UserToSelfRelTypes.GOT_BANNED}]->(u) DELETE r`, + { + userId: userId, + } + ); + } public async connectUserWithOpenness( userId: UUID, @@ -323,7 +345,7 @@ export class UsersRepository implements IUsersRepository { } ); } - + public async addPreviouslyBanned(userId: UUID, banProps: GotBannedProps): Promise { await this._neo4jService.tryWriteAsync( `MATCH (u:User {userId: $userId}) From 3bdeb15f50a730b0f8fbc227e5d55d5044e969cf Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Tue, 29 Nov 2022 19:15:59 -0800 Subject: [PATCH 085/153] Removing duplicate endpoints --- src/users/controllers/users.controller.ts | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/users/controllers/users.controller.ts b/src/users/controllers/users.controller.ts index 43d4086..bf9ad05 100644 --- a/src/users/controllers/users.controller.ts +++ b/src/users/controllers/users.controller.ts @@ -101,25 +101,4 @@ export class UsersController { public async profileSetupSubmit(@Body() setupProfileDto: SetupProfileDto): Promise { await this._profileSetup.setupProfile(setupProfileDto); } - - @Patch("/ban") - @Roles(Role.MODERATOR) - @UseGuards(AuthGuard("jwt"), RolesGuard) - public async banUser( - @AuthedUser() authedUser: User, - @Body() moderationPayloadDto: ModerationPayloadDto - ): Promise { - moderationPayloadDto.moderatorId = authedUser.userId; - await this._moderationActions.banUser(moderationPayloadDto); - } - - @Patch("/unban/:userId") - @Roles(Role.MODERATOR) - @UseGuards(AuthGuard("jwt"), RolesGuard) - public async unbanUser( - @AuthedUser() authedUser: User, - @Param("userId") userId: string - ): Promise { - await this._moderationActions.unbanUser(userId); - } } From 988fe48433dd8b6270cd050ecb3b89fafd44f0c5 Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Tue, 29 Nov 2022 19:18:04 -0800 Subject: [PATCH 086/153] Name changes for clarity purposes --- .../moderatorActions/moderatorActions.service.interface.ts | 4 ++-- .../services/moderatorActions/moderatorActions.service.ts | 4 ++-- .../userActions/commentReport/commentsReport.service.ts | 4 ++-- .../services/userActions/postReport/postsReport.service.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts b/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts index 3129c24..7d0c8a7 100644 --- a/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts +++ b/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts @@ -54,7 +54,7 @@ export interface IModeratorActionsService { * Removes the mark of "deleted" from the post. * @param postId */ - undeletePost(postId: UUID): Promise; + restorePost(postId: UUID): Promise; /** * Adds the mark of "deleted" to the comment. @@ -65,7 +65,7 @@ export interface IModeratorActionsService { * Removes the mark of deleted from a comment. * @param commentId */ - undeleteComment(commentId: UUID): Promise; + restoreComment(commentId: UUID): Promise; /** * Bans a user. A banned user cannot post, comment, or vote. diff --git a/src/moderation/services/moderatorActions/moderatorActions.service.ts b/src/moderation/services/moderatorActions/moderatorActions.service.ts index 0060229..7b39b17 100644 --- a/src/moderation/services/moderatorActions/moderatorActions.service.ts +++ b/src/moderation/services/moderatorActions/moderatorActions.service.ts @@ -154,7 +154,7 @@ export class ModeratorActionsService implements IModeratorActionsService { return post; } - public async undeleteComment(commentId: UUID): Promise { + public async restoreComment(commentId: UUID): Promise { const comment = await this.acquireComment(commentId); await comment.getDeletedProps(); @@ -168,7 +168,7 @@ export class ModeratorActionsService implements IModeratorActionsService { return comment; } - public async undeletePost(postId: UUID): Promise { + public async restorePost(postId: UUID): Promise { const post = await this.acquirePost(postId); await post.getDeletedProps(); diff --git a/src/moderation/services/userActions/commentReport/commentsReport.service.ts b/src/moderation/services/userActions/commentReport/commentsReport.service.ts index cae1355..51dba1b 100644 --- a/src/moderation/services/userActions/commentReport/commentsReport.service.ts +++ b/src/moderation/services/userActions/commentReport/commentsReport.service.ts @@ -39,7 +39,7 @@ export class CommentsReportService implements ICommentsReportService { const reports = await this.getReportsForComment(comment.commentId); - if (reports.some(r => r.moderatorId === user.userId)) { + if (reports.some(r => r.userId === user.userId)) { throw new HttpException("Comment already reported", 400); } @@ -70,7 +70,7 @@ export class CommentsReportService implements ICommentsReportService { ); return queryResult.records.map(record => { const reportedProps = new ReportedProps(record.get("r").properties); - reportedProps.moderatorId = record.get("u").properties.userId; + reportedProps.userId = record.get("u").properties.userId; return reportedProps; }); } diff --git a/src/moderation/services/userActions/postReport/postsReport.service.ts b/src/moderation/services/userActions/postReport/postsReport.service.ts index 0a26e42..91651f1 100644 --- a/src/moderation/services/userActions/postReport/postsReport.service.ts +++ b/src/moderation/services/userActions/postReport/postsReport.service.ts @@ -37,7 +37,7 @@ export class PostsReportService implements IPostsReportService { const reports = await this.getReportsForPost(post.postId); - if (reports.some(r => r.moderatorId === user.userId)) { + if (reports.some(r => r.userId === user.userId)) { throw new HttpException("Post already reported", 400); } @@ -68,7 +68,7 @@ export class PostsReportService implements IPostsReportService { ); return queryResult.records.map(record => { const reportedProps = new ReportedProps(record.get("r").properties); - reportedProps.moderatorId = record.get("u").properties.userId; + reportedProps.userId = record.get("u").properties.userId; return reportedProps; }); } From 2bc67964beddd212b2f5fd4dcd6a10fc9cbae41a Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Tue, 29 Nov 2022 19:21:20 -0800 Subject: [PATCH 087/153] Moved to moderation service --- .../controllers/comments.controller.ts | 15 --------------- src/posts/controllers/posts.controller.ts | 18 ------------------ 2 files changed, 33 deletions(-) diff --git a/src/comments/controllers/comments.controller.ts b/src/comments/controllers/comments.controller.ts index fcc8e88..6171294 100644 --- a/src/comments/controllers/comments.controller.ts +++ b/src/comments/controllers/comments.controller.ts @@ -130,19 +130,4 @@ export class CommentsController { public async voteComment(@Body() voteCommentPayload: VoteCommentPayloadDto): Promise { await this._commentsService.voteComment(voteCommentPayload); } - - @Post("/report") - @UseGuards(AuthGuard("jwt")) - public async reportComment(@Body() reportCommentPayload: VoteCommentPayloadDto): Promise { - throw new Error("Method not implemented."); - } - - @Post("/allow/:commentId") - @Roles(Role.MODERATOR) - @UseGuards(AuthGuard("jwt"), RolesGuard) - public async allowComment( - @Param("commentId", new ParseUUIDPipe()) commentId: UUID - ): Promise { - await this._moderatorActionsService.allowComment(commentId); - } } diff --git a/src/posts/controllers/posts.controller.ts b/src/posts/controllers/posts.controller.ts index 223ef8a..0eebfb9 100644 --- a/src/posts/controllers/posts.controller.ts +++ b/src/posts/controllers/posts.controller.ts @@ -153,22 +153,4 @@ export class PostsController { await this._postsService.votePost(votePostPayload); return; } - - @Post("/report") - @UseGuards(AuthGuard("jwt")) - public async reportPost( - @AuthedUser() user: User, - @Body() reportPayload: ModerationPayloadDto - ): Promise { - reportPayload.moderatorId = user.userId; - throw new Error("Not implemented"); - } - - @Post("/allow/:postId") - @Roles(Role.MODERATOR) - @UseGuards(AuthGuard("jwt"), RolesGuard) - public async allowPost(@Param("postId", new ParseUUIDPipe()) postId: UUID): Promise { - await this._moderationActionsService.allowPost(postId); - throw new Error("Not implemented"); - } } From 032375a00da8be0d84adeeb146cc682a641ade66 Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Tue, 29 Nov 2022 19:36:52 -0800 Subject: [PATCH 088/153] Reverted commit --- .../dtos}/reportCommentPayload.dto.ts | 0 .../commentReport/commentsReport.service.interface.ts | 4 ++-- .../services}/commentReport/commentsReport.service.ts | 0 .../postReport/postsReport.service.interface.ts | 8 -------- .../userActions/userActions.service.interface.ts | 0 .../services/userActions/userActions.service.ts | 0 src/posts/dtos/index.ts | 1 + .../dtos}/reportPostPayload.dto.ts | 0 .../postReport/postsReport.service.interface.ts | 8 ++++++++ .../services}/postReport/postsReport.service.ts | 11 ++++++----- 10 files changed, 17 insertions(+), 15 deletions(-) rename src/{moderation/dtos/userActions => comments/dtos}/reportCommentPayload.dto.ts (100%) rename src/{moderation/services/userActions => comments/services}/commentReport/commentsReport.service.interface.ts (57%) rename src/{moderation/services/userActions => comments/services}/commentReport/commentsReport.service.ts (100%) delete mode 100644 src/moderation/services/userActions/postReport/postsReport.service.interface.ts create mode 100644 src/moderation/services/userActions/userActions.service.interface.ts create mode 100644 src/moderation/services/userActions/userActions.service.ts rename src/{moderation/dtos/userActions => posts/dtos}/reportPostPayload.dto.ts (100%) create mode 100644 src/posts/services/postReport/postsReport.service.interface.ts rename src/{moderation/services/userActions => posts/services}/postReport/postsReport.service.ts (88%) diff --git a/src/moderation/dtos/userActions/reportCommentPayload.dto.ts b/src/comments/dtos/reportCommentPayload.dto.ts similarity index 100% rename from src/moderation/dtos/userActions/reportCommentPayload.dto.ts rename to src/comments/dtos/reportCommentPayload.dto.ts diff --git a/src/moderation/services/userActions/commentReport/commentsReport.service.interface.ts b/src/comments/services/commentReport/commentsReport.service.interface.ts similarity index 57% rename from src/moderation/services/userActions/commentReport/commentsReport.service.interface.ts rename to src/comments/services/commentReport/commentsReport.service.interface.ts index 81a1223..368bee0 100644 --- a/src/moderation/services/userActions/commentReport/commentsReport.service.interface.ts +++ b/src/comments/services/commentReport/commentsReport.service.interface.ts @@ -1,5 +1,5 @@ -import { ReportCommentPayloadDto } from "../../../dtos/userActions"; -import { ReportedProps } from "../../../../users/models/toComment"; +import { ReportCommentPayloadDto } from "../../../moderation/dtos/userActions"; +import { ReportedProps } from "../../../users/models/toComment"; export interface ICommentsReportService { reportComment(reportCommentPayload: ReportCommentPayloadDto): Promise; diff --git a/src/moderation/services/userActions/commentReport/commentsReport.service.ts b/src/comments/services/commentReport/commentsReport.service.ts similarity index 100% rename from src/moderation/services/userActions/commentReport/commentsReport.service.ts rename to src/comments/services/commentReport/commentsReport.service.ts diff --git a/src/moderation/services/userActions/postReport/postsReport.service.interface.ts b/src/moderation/services/userActions/postReport/postsReport.service.interface.ts deleted file mode 100644 index af18dd5..0000000 --- a/src/moderation/services/userActions/postReport/postsReport.service.interface.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ReportPostPayloadDto } from "../../../dtos/userActions"; -import { ReportedProps } from "../../../../users/models/toPost"; - -export interface IPostsReportService { - reportPost(reportPostPayload: ReportPostPayloadDto): Promise; - - getReportsForPost(postId: UUID): Promise; -} diff --git a/src/moderation/services/userActions/userActions.service.interface.ts b/src/moderation/services/userActions/userActions.service.interface.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/moderation/services/userActions/userActions.service.ts b/src/moderation/services/userActions/userActions.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/posts/dtos/index.ts b/src/posts/dtos/index.ts index c07d79f..dd597d6 100644 --- a/src/posts/dtos/index.ts +++ b/src/posts/dtos/index.ts @@ -1,3 +1,4 @@ export { DeletePostPayloadDto } from "./deletePostPayload.dto"; export { VotePostPayloadDto } from "./votePostPayload.dto"; export { PostCreationPayloadDto } from "./postCreationPayload.dto"; +export { ReportPostPayloadDto } from "./reportPostPayload.dto"; diff --git a/src/moderation/dtos/userActions/reportPostPayload.dto.ts b/src/posts/dtos/reportPostPayload.dto.ts similarity index 100% rename from src/moderation/dtos/userActions/reportPostPayload.dto.ts rename to src/posts/dtos/reportPostPayload.dto.ts diff --git a/src/posts/services/postReport/postsReport.service.interface.ts b/src/posts/services/postReport/postsReport.service.interface.ts new file mode 100644 index 0000000..6ad294f --- /dev/null +++ b/src/posts/services/postReport/postsReport.service.interface.ts @@ -0,0 +1,8 @@ +import { ReportPostPayloadDto } from "../../dtos"; +import { ReportedProps } from "../../../users/models/toPost"; + +export interface IPostsReportService { + reportPost(reportPostPayload: ReportPostPayloadDto): Promise; + + getReportsForPost(postId: string): Promise; +} diff --git a/src/moderation/services/userActions/postReport/postsReport.service.ts b/src/posts/services/postReport/postsReport.service.ts similarity index 88% rename from src/moderation/services/userActions/postReport/postsReport.service.ts rename to src/posts/services/postReport/postsReport.service.ts index 91651f1..262b6cd 100644 --- a/src/moderation/services/userActions/postReport/postsReport.service.ts +++ b/src/posts/services/postReport/postsReport.service.ts @@ -1,11 +1,11 @@ -import { ReportPostPayloadDto } from "../../../dtos/userActions"; -import { User } from "../../../../users/models"; +import { ReportPostPayloadDto } from "../../dtos"; +import { User } from "../../../users/models"; import { HttpException, Inject, Injectable, Logger, Scope } from "@nestjs/common"; -import { DatabaseContext } from "../../../../database-access-layer/databaseContext"; +import { DatabaseContext } from "../../../database-access-layer/databaseContext"; import { REQUEST } from "@nestjs/core"; import { Request } from "express"; -import { _$ } from "../../../../_domain/injectableTokens"; -import { ReportedProps, UserToPostRelTypes } from "../../../../users/models/toPost"; +import { _$ } from "../../../_domain/injectableTokens"; +import { ReportedProps, UserToPostRelTypes } from "../../../users/models/toPost"; import { IPostsReportService } from "./postsReport.service.interface"; @Injectable({ scope: Scope.REQUEST }) @@ -79,3 +79,4 @@ export class PostsReportService implements IPostsReportService { return user; } } + From 6a8168e73c64198ae7eb5d5cf9a2849a77da3dde Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Tue, 29 Nov 2022 19:42:34 -0800 Subject: [PATCH 089/153] Fixing imports --- src/comments/dtos/index.ts | 1 + .../commentReport/commentsReport.service.interface.ts | 2 +- .../services/commentReport/commentsReport.service.ts | 10 +++++----- src/moderation/dtos/userActions/index.ts | 3 +-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/comments/dtos/index.ts b/src/comments/dtos/index.ts index 596fc89..7f94b1b 100644 --- a/src/comments/dtos/index.ts +++ b/src/comments/dtos/index.ts @@ -1,2 +1,3 @@ export { CommentCreationPayloadDto } from "./commentCreationPayload.dto"; export { VoteCommentPayloadDto } from "./voteCommentPayload.dto"; +export { ReportCommentPayloadDto } from "./reportCommentPayload.dto"; diff --git a/src/comments/services/commentReport/commentsReport.service.interface.ts b/src/comments/services/commentReport/commentsReport.service.interface.ts index 368bee0..71a7eea 100644 --- a/src/comments/services/commentReport/commentsReport.service.interface.ts +++ b/src/comments/services/commentReport/commentsReport.service.interface.ts @@ -1,4 +1,4 @@ -import { ReportCommentPayloadDto } from "../../../moderation/dtos/userActions"; +import { ReportCommentPayloadDto } from "../../dtos"; import { ReportedProps } from "../../../users/models/toComment"; export interface ICommentsReportService { diff --git a/src/comments/services/commentReport/commentsReport.service.ts b/src/comments/services/commentReport/commentsReport.service.ts index 51dba1b..c25fa39 100644 --- a/src/comments/services/commentReport/commentsReport.service.ts +++ b/src/comments/services/commentReport/commentsReport.service.ts @@ -1,11 +1,11 @@ -import { ReportCommentPayloadDto } from "../../../dtos/userActions"; -import { User } from "../../../../users/models"; +import { ReportCommentPayloadDto } from "../../dtos"; +import { User } from "../../../users/models"; import { HttpException, Inject, Injectable, Logger, Scope } from "@nestjs/common"; -import { DatabaseContext } from "../../../../database-access-layer/databaseContext"; +import { DatabaseContext } from "../../../database-access-layer/databaseContext"; import { REQUEST } from "@nestjs/core"; import { Request } from "express"; -import { _$ } from "../../../../_domain/injectableTokens"; -import { ReportedProps, UserToCommentRelTypes } from "../../../../users/models/toComment"; +import { _$ } from "../../../_domain/injectableTokens"; +import { ReportedProps, UserToCommentRelTypes } from "../../../users/models/toComment"; import { ICommentsReportService } from "./commentsReport.service.interface"; @Injectable({ scope: Scope.REQUEST }) diff --git a/src/moderation/dtos/userActions/index.ts b/src/moderation/dtos/userActions/index.ts index d0c036d..8b13789 100644 --- a/src/moderation/dtos/userActions/index.ts +++ b/src/moderation/dtos/userActions/index.ts @@ -1,2 +1 @@ -export { ReportPostPayloadDto } from "./reportPostPayload.dto"; -export { ReportCommentPayloadDto } from "./reportCommentPayload.dto"; + From 18eb1b87e2746ceb128eac94090e135ad7e64f69 Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Tue, 29 Nov 2022 19:43:41 -0800 Subject: [PATCH 090/153] Revert "Moved to moderation service" This reverts commit 2bc67964beddd212b2f5fd4dcd6a10fc9cbae41a. --- .../controllers/comments.controller.ts | 15 +++++++++++++++ src/posts/controllers/posts.controller.ts | 18 ++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/comments/controllers/comments.controller.ts b/src/comments/controllers/comments.controller.ts index 6171294..fcc8e88 100644 --- a/src/comments/controllers/comments.controller.ts +++ b/src/comments/controllers/comments.controller.ts @@ -130,4 +130,19 @@ export class CommentsController { public async voteComment(@Body() voteCommentPayload: VoteCommentPayloadDto): Promise { await this._commentsService.voteComment(voteCommentPayload); } + + @Post("/report") + @UseGuards(AuthGuard("jwt")) + public async reportComment(@Body() reportCommentPayload: VoteCommentPayloadDto): Promise { + throw new Error("Method not implemented."); + } + + @Post("/allow/:commentId") + @Roles(Role.MODERATOR) + @UseGuards(AuthGuard("jwt"), RolesGuard) + public async allowComment( + @Param("commentId", new ParseUUIDPipe()) commentId: UUID + ): Promise { + await this._moderatorActionsService.allowComment(commentId); + } } diff --git a/src/posts/controllers/posts.controller.ts b/src/posts/controllers/posts.controller.ts index 0eebfb9..223ef8a 100644 --- a/src/posts/controllers/posts.controller.ts +++ b/src/posts/controllers/posts.controller.ts @@ -153,4 +153,22 @@ export class PostsController { await this._postsService.votePost(votePostPayload); return; } + + @Post("/report") + @UseGuards(AuthGuard("jwt")) + public async reportPost( + @AuthedUser() user: User, + @Body() reportPayload: ModerationPayloadDto + ): Promise { + reportPayload.moderatorId = user.userId; + throw new Error("Not implemented"); + } + + @Post("/allow/:postId") + @Roles(Role.MODERATOR) + @UseGuards(AuthGuard("jwt"), RolesGuard) + public async allowPost(@Param("postId", new ParseUUIDPipe()) postId: UUID): Promise { + await this._moderationActionsService.allowPost(postId); + throw new Error("Not implemented"); + } } From bf3cedd0d98d26b0f163098bab91bda78cf04097 Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Tue, 29 Nov 2022 20:58:54 -0800 Subject: [PATCH 091/153] Changed read -> tryReadAsync --- .../comment/comments.repository.ts | 4 +-- .../repositories/post/posts.repository.ts | 2 +- .../postAward/postAward.repository.ts | 4 +-- .../postTag/postTags.repository.ts | 2 +- .../postType/postTypes.repository.ts | 5 ++- .../postReport/postsReport.service.ts | 34 ++++++++++++++----- src/users/models/toComment/reported.props.ts | 3 -- src/users/models/toPost/reported.props.ts | 3 -- .../repositories/gender/gender.repository.ts | 4 +-- .../openness/openness.repository.ts | 7 ++-- .../sexuality/sexuality.repository.ts | 7 ++-- .../repositories/users/users.repository.ts | 6 ++-- 12 files changed, 51 insertions(+), 30 deletions(-) diff --git a/src/comments/repositories/comment/comments.repository.ts b/src/comments/repositories/comment/comments.repository.ts index 53e7999..ff86308 100644 --- a/src/comments/repositories/comment/comments.repository.ts +++ b/src/comments/repositories/comment/comments.repository.ts @@ -12,14 +12,14 @@ export class CommentsRepository implements ICommentsRepository { constructor(@Inject(Neo4jService) private _neo4jService: Neo4jService) {} public async findAll(): Promise { - const allComments = await this._neo4jService.read(`MATCH (c:Comment) RETURN c`, {}); + const allComments = await this._neo4jService.tryReadAsync(`MATCH (c:Comment) RETURN c`, {}); const records = allComments.records; if (records.length === 0) return []; return records.map(record => new Comment(record.get("c").properties, this._neo4jService)); } public async findCommentById(commentId: string): Promise { - const comment = await this._neo4jService.read( + const comment = await this._neo4jService.tryReadAsync( `MATCH (c:Comment) WHERE c.commentId = $commentId RETURN c`, { commentId: commentId } ); diff --git a/src/posts/repositories/post/posts.repository.ts b/src/posts/repositories/post/posts.repository.ts index c71c20f..8f798bf 100644 --- a/src/posts/repositories/post/posts.repository.ts +++ b/src/posts/repositories/post/posts.repository.ts @@ -32,7 +32,7 @@ export class PostsRepository implements IPostsRepository { } public async findPostById(postId: string): Promise { - const post = await this._neo4jService.read( + const post = await this._neo4jService.tryReadAsync( `MATCH (p:Post) WHERE p.postId = $postId RETURN p`, { postId: postId } ); diff --git a/src/posts/repositories/postAward/postAward.repository.ts b/src/posts/repositories/postAward/postAward.repository.ts index dbf512e..2753308 100644 --- a/src/posts/repositories/postAward/postAward.repository.ts +++ b/src/posts/repositories/postAward/postAward.repository.ts @@ -9,14 +9,14 @@ export class PostAwardRepository implements IPostAwardRepository { constructor(@Inject(Neo4jService) private _neo4jService: Neo4jService) {} public async findAll(): Promise { - const allAwards = await this._neo4jService.read(`MATCH (a:Award) RETURN a`, {}); + const allAwards = await this._neo4jService.tryReadAsync(`MATCH (a:Award) RETURN a`, {}); const records = allAwards.records; if (records.length === 0) return []; return records.map(record => new Award(record.get("a").properties)); } public async findAwardById(awardId: string): Promise { - const award = await this._neo4jService.read( + const award = await this._neo4jService.tryReadAsync( `MATCH (a:Award) WHERE a.awardId = $awardId RETURN a`, { awardId: awardId } ); diff --git a/src/posts/repositories/postTag/postTags.repository.ts b/src/posts/repositories/postTag/postTags.repository.ts index 616d747..98cb0d9 100644 --- a/src/posts/repositories/postTag/postTags.repository.ts +++ b/src/posts/repositories/postTag/postTags.repository.ts @@ -8,7 +8,7 @@ export class PostTagsRepository implements IPostTagsRepository { constructor(@Inject(Neo4jService) private _neo4jService: Neo4jService) {} public async findAll(): Promise { - const allPostTags = await this._neo4jService.read(`MATCH (t:PostTag) RETURN t`, {}); + const allPostTags = await this._neo4jService.tryReadAsync(`MATCH (t:PostTag) RETURN t`, {}); const records = allPostTags.records; if (records.length === 0) return []; return records.map(record => new PostTag(record.get("t").properties)); diff --git a/src/posts/repositories/postType/postTypes.repository.ts b/src/posts/repositories/postType/postTypes.repository.ts index c70f05d..ea1fdc4 100644 --- a/src/posts/repositories/postType/postTypes.repository.ts +++ b/src/posts/repositories/postType/postTypes.repository.ts @@ -8,7 +8,10 @@ export class PostTypesRepository implements IPostTypesRepository { constructor(@Inject(Neo4jService) private _neo4jService: Neo4jService) {} public async findAll(): Promise { - const allPostTypes = await this._neo4jService.read(`MATCH (t:PostType) RETURN t`, {}); + const allPostTypes = await this._neo4jService.tryReadAsync( + `MATCH (t:PostType) RETURN t`, + {} + ); const records = allPostTypes.records; if (records.length === 0) return []; return records.map(record => new PostType(record.get("t").properties)); diff --git a/src/posts/services/postReport/postsReport.service.ts b/src/posts/services/postReport/postsReport.service.ts index 262b6cd..5ff2db0 100644 --- a/src/posts/services/postReport/postsReport.service.ts +++ b/src/posts/services/postReport/postsReport.service.ts @@ -24,7 +24,6 @@ export class PostsReportService implements IPostsReportService { public async reportPost(reportPostPayload: ReportPostPayloadDto): Promise { const user = this.getUserFromRequest(); - const post = await this._dbContext.Posts.findPostById(reportPostPayload.postId); if (!post) throw new Error("Post not found"); @@ -35,10 +34,10 @@ export class PostsReportService implements IPostsReportService { ); } - const reports = await this.getReportsForPost(post.postId); + const report = await this.checkIfUserReportedPost(post.postId, user.userId); - if (reports.some(r => r.userId === user.userId)) { - throw new HttpException("Post already reported", 400); + if (report === true) { + throw new HttpException("You have already reported this post", 400); } await post.getAuthorUser(); @@ -49,30 +48,49 @@ export class PostsReportService implements IPostsReportService { await this._dbContext.neo4jService.tryWriteAsync( ` MATCH (p:Post { postId: $postId }), (u:User { userId: $userId }) - MERGE (u)-[r:${UserToPostRelTypes.REPORTED}]->(p) + MERGE (u)-[r:${UserToPostRelTypes.REPORTED}{reportedAt: $reportedAt, reason: $reason}]->(p) `, { + reportedAt: Date.now(), + reason: reportPostPayload.reason, postId: post.postId, userId: user.userId, } ); } - public async getReportsForPost(postId: string): Promise { + public async getReportsForPost(postId: UUID): Promise { const queryResult = await this._dbContext.neo4jService.tryReadAsync( ` - MATHC (p:Post { postId: $postId })<-[r:${UserToPostRelTypes.REPORTED}]-(u:User)`, + MATCH (p:Post { postId: $postId })<-[r:${UserToPostRelTypes.REPORTED}]-(u:User) + RETURN r, u`, { postId: postId, } ); return queryResult.records.map(record => { const reportedProps = new ReportedProps(record.get("r").properties); - reportedProps.userId = record.get("u").properties.userId; return reportedProps; }); } + private async checkIfUserReportedPost(postId: UUID, userId: UUID): Promise { + const queryResult = await this._dbContext.neo4jService.tryReadAsync( + ` + MATCH (p:Post { postId: $postId })<-[r:${UserToPostRelTypes.REPORTED}]-(u:User { userId: $userId }) + RETURN r + `, + { + postId: postId, + userId: userId, + } + ); + if (queryResult.records.length > 0) { + return true; + } + return false; + } + private getUserFromRequest(): User { const user = this._request.user as User; if (user === undefined) throw new Error("User not found"); diff --git a/src/users/models/toComment/reported.props.ts b/src/users/models/toComment/reported.props.ts index d58bd48..6088f6b 100644 --- a/src/users/models/toComment/reported.props.ts +++ b/src/users/models/toComment/reported.props.ts @@ -2,9 +2,6 @@ import { RelationshipProps } from "../../../neo4j/neo4j.helper.types"; import { IsNumber, IsString, IsUUID } from "class-validator"; export class ReportedProps implements RelationshipProps { - @IsUUID() - userId: UUID; - @IsNumber() reportedAt: number; diff --git a/src/users/models/toPost/reported.props.ts b/src/users/models/toPost/reported.props.ts index d58bd48..6088f6b 100644 --- a/src/users/models/toPost/reported.props.ts +++ b/src/users/models/toPost/reported.props.ts @@ -2,9 +2,6 @@ import { RelationshipProps } from "../../../neo4j/neo4j.helper.types"; import { IsNumber, IsString, IsUUID } from "class-validator"; export class ReportedProps implements RelationshipProps { - @IsUUID() - userId: UUID; - @IsNumber() reportedAt: number; diff --git a/src/users/repositories/gender/gender.repository.ts b/src/users/repositories/gender/gender.repository.ts index cf2d0d9..7d42b98 100644 --- a/src/users/repositories/gender/gender.repository.ts +++ b/src/users/repositories/gender/gender.repository.ts @@ -8,14 +8,14 @@ export class GenderRepository implements IGenderRepository { constructor(@Inject(Neo4jService) private _neo4jService: Neo4jService) {} public async findAll(): Promise { - const allGenders = await this._neo4jService.read(`MATCH (g:Gender) RETURN g`, {}); + const allGenders = await this._neo4jService.tryReadAsync(`MATCH (g:Gender) RETURN g`, {}); const records = allGenders.records; if (records.length === 0) return []; return records.map(record => new Gender(record.get("g").properties)); } public async findGenderById(genderId: UUID): Promise { - const gender = await this._neo4jService.read( + const gender = await this._neo4jService.tryReadAsync( `MATCH (g:Gender) WHERE g.genderId = $genderId RETURN g`, { genderId: genderId } ); diff --git a/src/users/repositories/openness/openness.repository.ts b/src/users/repositories/openness/openness.repository.ts index 070bdc4..c556177 100644 --- a/src/users/repositories/openness/openness.repository.ts +++ b/src/users/repositories/openness/openness.repository.ts @@ -8,14 +8,17 @@ export class OpennessRepository implements IOpennessRepository { constructor(@Inject(Neo4jService) private _neo4jService: Neo4jService) {} public async findAll(): Promise { - const allOpenness = await this._neo4jService.read(`MATCH (o:Openness) RETURN o`, {}); + const allOpenness = await this._neo4jService.tryReadAsync( + `MATCH (o:Openness) RETURN o`, + {} + ); const records = allOpenness.records; if (records.length === 0) return []; return records.map(record => new Openness(record.get("o").properties)); } public async findOpennessById(opennessId: UUID): Promise { - const openness = await this._neo4jService.read( + const openness = await this._neo4jService.tryReadAsync( `MATCH (o:Openness) WHERE o.opennessId = $opennessId RETURN o`, { opennessId: opennessId } ); diff --git a/src/users/repositories/sexuality/sexuality.repository.ts b/src/users/repositories/sexuality/sexuality.repository.ts index e06225b..47aa42c 100644 --- a/src/users/repositories/sexuality/sexuality.repository.ts +++ b/src/users/repositories/sexuality/sexuality.repository.ts @@ -8,14 +8,17 @@ export class SexualityRepository implements ISexualityRepository { constructor(@Inject(Neo4jService) private _neo4jService: Neo4jService) {} public async findAll(): Promise { - const allSexualities = await this._neo4jService.read(`MATCH (s:Sexuality) RETURN s`, {}); + const allSexualities = await this._neo4jService.tryReadAsync( + `MATCH (s:Sexuality) RETURN s`, + {} + ); const records = allSexualities.records; if (records.length === 0) return []; return records.map(record => new Sexuality(record.get("s").properties)); } public async findSexualityById(sexualityId: UUID): Promise { - const sexuality = await this._neo4jService.read( + const sexuality = await this._neo4jService.tryReadAsync( `MATCH (s:Sexuality) WHERE s.sexualityId = $sexualityId RETURN s`, { sexualityId: sexualityId } ); diff --git a/src/users/repositories/users/users.repository.ts b/src/users/repositories/users/users.repository.ts index 4b8231a..9165409 100644 --- a/src/users/repositories/users/users.repository.ts +++ b/src/users/repositories/users/users.repository.ts @@ -43,7 +43,7 @@ export class UsersRepository implements IUsersRepository { } public async findUserById(userId: UUID): Promise { - const queryResult = await this._neo4jService.read( + const queryResult = await this._neo4jService.tryReadAsync( `MATCH (u:User {userId: $userId}) RETURN u`, { userId: userId, @@ -231,7 +231,7 @@ export class UsersRepository implements IUsersRepository { } ); } - + public async banUser(userId: UUID, banProps: GotBannedProps): Promise { await this._neo4jService.tryWriteAsync( `MATCH (u:User {userId: $userId}) @@ -345,7 +345,7 @@ export class UsersRepository implements IUsersRepository { } ); } - + public async addPreviouslyBanned(userId: UUID, banProps: GotBannedProps): Promise { await this._neo4jService.tryWriteAsync( `MATCH (u:User {userId: $userId}) From 44515a42f92c83ffea4c6a1be0c37b192b444542 Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Tue, 29 Nov 2022 20:59:15 -0800 Subject: [PATCH 092/153] Report comment and post service --- src/_domain/injectableTokens.ts | 2 ++ src/comments/comments.module.ts | 9 ++++++ .../controllers/comments.controller.ts | 21 ++++++------- src/comments/dtos/reportCommentPayload.dto.ts | 3 ++ .../commentReport/commentsReport.service.ts | 31 +++++++++++++++---- src/posts/controllers/posts.controller.ts | 24 +++++--------- src/posts/dtos/reportPostPayload.dto.ts | 3 ++ src/posts/posts.module.ts | 9 ++++++ 8 files changed, 68 insertions(+), 34 deletions(-) diff --git a/src/_domain/injectableTokens.ts b/src/_domain/injectableTokens.ts index ab66175..5548bff 100644 --- a/src/_domain/injectableTokens.ts +++ b/src/_domain/injectableTokens.ts @@ -21,10 +21,12 @@ const injectableTokens = { IPostTagsRepository: Symbol("IPostTagsRepository"), IPostsService: Symbol("IPostsService"), IPostAwardRepository: Symbol("IPostAwardRepository"), + IPostsReportService: Symbol("IPostsReportService"), // Comments Module ICommentsRepository: Symbol("ICommentsRepository"), ICommentsService: Symbol("ICommentsService"), + ICommentsReportService: Symbol("ICommentsReportService"), // Neo4j Module INeo4jService: Symbol("INeo4jService"), diff --git a/src/comments/comments.module.ts b/src/comments/comments.module.ts index 304b2d7..ad41f35 100644 --- a/src/comments/comments.module.ts +++ b/src/comments/comments.module.ts @@ -7,6 +7,7 @@ import { forwardRef, Module } from "@nestjs/common"; import { HttpModule } from "@nestjs/axios"; import { ModerationModule } from "../moderation/moderation.module"; import { PostsModule } from "../posts/posts.module"; +import { CommentsReportService } from "./services/commentReport/commentsReport.service"; @Module({ controllers: [CommentsController], @@ -25,6 +26,10 @@ import { PostsModule } from "../posts/posts.module"; provide: _$.ICommentsRepository, useClass: CommentsRepository, }, + { + provide: _$.ICommentsReportService, + useClass: CommentsReportService, + }, ], exports: [ { @@ -35,6 +40,10 @@ import { PostsModule } from "../posts/posts.module"; provide: _$.ICommentsRepository, useClass: CommentsRepository, }, + { + provide: _$.ICommentsReportService, + useClass: CommentsReportService, + }, ], }) export class CommentsModule {} diff --git a/src/comments/controllers/comments.controller.ts b/src/comments/controllers/comments.controller.ts index fcc8e88..87a8824 100644 --- a/src/comments/controllers/comments.controller.ts +++ b/src/comments/controllers/comments.controller.ts @@ -25,9 +25,10 @@ import { ModerationPayloadDto } from "../../moderation/dtos/moderatorActions"; import { IModeratorActionsService } from "../../moderation/services/moderatorActions/moderatorActions.service.interface"; import { Role, User } from "../../users/models"; import { _$ } from "../../_domain/injectableTokens"; -import { CommentCreationPayloadDto, VoteCommentPayloadDto } from "../dtos"; +import { CommentCreationPayloadDto, ReportCommentPayloadDto, VoteCommentPayloadDto } from "../dtos"; import { Comment as CommentModel } from "../models"; import { ICommentsService } from "../services/comments/comments.service.interface"; +import { ICommentsReportService } from "../services/commentReport/commentsReport.service.interface"; @ApiTags("comments") @Controller("comments") @@ -37,15 +38,18 @@ export class CommentsController { private readonly _dbContext: DatabaseContext; private readonly _commentsService: ICommentsService; private readonly _moderatorActionsService: IModeratorActionsService; + private readonly _commentsReportService: ICommentsReportService; constructor( @Inject(_$.IDatabaseContext) dbContext: DatabaseContext, @Inject(_$.ICommentsService) commentsService: ICommentsService, - @Inject(_$.IModeratorActionsService) moderatorActionsService: IModeratorActionsService + @Inject(_$.IModeratorActionsService) moderatorActionsService: IModeratorActionsService, + @Inject(_$.ICommentsReportService) commentsReportService: ICommentsReportService ) { this._dbContext = dbContext; this._commentsService = commentsService; this._moderatorActionsService = moderatorActionsService; + this._commentsReportService = commentsReportService; } @Get() @@ -133,16 +137,9 @@ export class CommentsController { @Post("/report") @UseGuards(AuthGuard("jwt")) - public async reportComment(@Body() reportCommentPayload: VoteCommentPayloadDto): Promise { - throw new Error("Method not implemented."); - } - - @Post("/allow/:commentId") - @Roles(Role.MODERATOR) - @UseGuards(AuthGuard("jwt"), RolesGuard) - public async allowComment( - @Param("commentId", new ParseUUIDPipe()) commentId: UUID + public async reportComment( + @Body() reportCommentPayload: ReportCommentPayloadDto ): Promise { - await this._moderatorActionsService.allowComment(commentId); + await this._commentsReportService.reportComment(reportCommentPayload); } } diff --git a/src/comments/dtos/reportCommentPayload.dto.ts b/src/comments/dtos/reportCommentPayload.dto.ts index ba6518b..27c4b84 100644 --- a/src/comments/dtos/reportCommentPayload.dto.ts +++ b/src/comments/dtos/reportCommentPayload.dto.ts @@ -1,10 +1,13 @@ import { ApiProperty } from "@nestjs/swagger"; +import { IsUUID, IsString } from "class-validator"; export class ReportCommentPayloadDto { @ApiProperty({ type: String, format: "uuid" }) + @IsUUID() commentId: UUID; @ApiProperty({ type: String, minLength: 5, maxLength: 500 }) + @IsString() reason: string; constructor(partial?: Partial) { diff --git a/src/comments/services/commentReport/commentsReport.service.ts b/src/comments/services/commentReport/commentsReport.service.ts index c25fa39..e458748 100644 --- a/src/comments/services/commentReport/commentsReport.service.ts +++ b/src/comments/services/commentReport/commentsReport.service.ts @@ -37,10 +37,10 @@ export class CommentsReportService implements ICommentsReportService { ); } - const reports = await this.getReportsForComment(comment.commentId); + const report = await this.checkIfUserReportedComment(comment.commentId, user.userId); - if (reports.some(r => r.userId === user.userId)) { - throw new HttpException("Comment already reported", 400); + if (report === true) { + throw new HttpException("You have already reported this post", 400); } await comment.getAuthorUser(); @@ -51,9 +51,11 @@ export class CommentsReportService implements ICommentsReportService { await this._dbContext.neo4jService.tryWriteAsync( ` MATCH (c:Comment { commentId: $commentId }), (u:User { userId: $userId }) - MERGE (u)-[r:${UserToCommentRelTypes.REPORTED}]->(c) + MERGE (u)-[r:${UserToCommentRelTypes.REPORTED}{reportedAt: $reportedAt, reason: $reason}]->(c) `, { + reportedAt: Date.now(), + reason: reportCommentPayload.reason, commentId: comment.commentId, userId: user.userId, } @@ -63,18 +65,35 @@ export class CommentsReportService implements ICommentsReportService { public async getReportsForComment(commentId: UUID): Promise { const queryResult = await this._dbContext.neo4jService.tryReadAsync( ` - MATHC (c:Comment { commentId: $commentId })<-[r:${UserToCommentRelTypes.REPORTED}]-(u:User)`, + MATCH (c:Comment { commentId: $commentId })<-[r:${UserToCommentRelTypes.REPORTED}]-(u:User) + RETURN r, u`, { commentId: commentId, } ); return queryResult.records.map(record => { const reportedProps = new ReportedProps(record.get("r").properties); - reportedProps.userId = record.get("u").properties.userId; return reportedProps; }); } + private async checkIfUserReportedComment(commentId: UUID, userId: UUID): Promise { + const queryResult = await this._dbContext.neo4jService.tryReadAsync( + ` + MATCH (p:Comment { commentId: $commentId })<-[r:${UserToCommentRelTypes.REPORTED}]-(u:User { userId: $userId }) + RETURN r + `, + { + commentId: commentId, + userId: userId, + } + ); + if (queryResult.records.length > 0) { + return true; + } + return false; + } + private getUserFromRequest(): User { const user = this._request.user as User; if (user === undefined) throw new Error("User not found"); diff --git a/src/posts/controllers/posts.controller.ts b/src/posts/controllers/posts.controller.ts index 223ef8a..e673c4a 100644 --- a/src/posts/controllers/posts.controller.ts +++ b/src/posts/controllers/posts.controller.ts @@ -25,9 +25,10 @@ import { ModerationPayloadDto } from "../../moderation/dtos/moderatorActions"; import { IModeratorActionsService } from "../../moderation/services/moderatorActions/moderatorActions.service.interface"; import { Role, User } from "../../users/models"; import { _$ } from "../../_domain/injectableTokens"; -import { PostCreationPayloadDto, VotePostPayloadDto } from "../dtos"; +import { PostCreationPayloadDto, ReportPostPayloadDto, VotePostPayloadDto } from "../dtos"; import { Post as PostModel } from "../models"; import { IPostsService } from "../services/posts/posts.service.interface"; +import { IPostsReportService } from "../services/postReport/postsReport.service.interface"; @ApiTags("posts") @Controller("posts") @@ -37,15 +38,18 @@ export class PostsController { private readonly _dbContext: DatabaseContext; private readonly _postsService: IPostsService; private readonly _moderationActionsService: IModeratorActionsService; + private readonly _postsReportService: IPostsReportService; constructor( @Inject(_$.IDatabaseContext) dbContext: DatabaseContext, @Inject(_$.IPostsService) postsService: IPostsService, - @Inject(_$.IModeratorActionsService) moderationActionsService: IModeratorActionsService + @Inject(_$.IModeratorActionsService) moderationActionsService: IModeratorActionsService, + @Inject(_$.IPostsReportService) postsReportService: IPostsReportService ) { this._dbContext = dbContext; this._postsService = postsService; this._moderationActionsService = moderationActionsService; + this._postsReportService = postsReportService; } @Get() @@ -156,19 +160,7 @@ export class PostsController { @Post("/report") @UseGuards(AuthGuard("jwt")) - public async reportPost( - @AuthedUser() user: User, - @Body() reportPayload: ModerationPayloadDto - ): Promise { - reportPayload.moderatorId = user.userId; - throw new Error("Not implemented"); - } - - @Post("/allow/:postId") - @Roles(Role.MODERATOR) - @UseGuards(AuthGuard("jwt"), RolesGuard) - public async allowPost(@Param("postId", new ParseUUIDPipe()) postId: UUID): Promise { - await this._moderationActionsService.allowPost(postId); - throw new Error("Not implemented"); + public async reportPost(@Body() reportPostPayload: ReportPostPayloadDto): Promise { + await this._postsReportService.reportPost(reportPostPayload); } } diff --git a/src/posts/dtos/reportPostPayload.dto.ts b/src/posts/dtos/reportPostPayload.dto.ts index d6915d7..71db43a 100644 --- a/src/posts/dtos/reportPostPayload.dto.ts +++ b/src/posts/dtos/reportPostPayload.dto.ts @@ -1,10 +1,13 @@ import { ApiProperty } from "@nestjs/swagger"; +import { IsUUID, IsString } from "class-validator"; export class ReportPostPayloadDto { @ApiProperty({ type: String, format: "uuid" }) + @IsUUID() postId: UUID; @ApiProperty({ type: String, minLength: 5, maxLength: 500 }) + @IsString() reason: string; constructor(partial?: Partial) { diff --git a/src/posts/posts.module.ts b/src/posts/posts.module.ts index 9178996..f942be6 100644 --- a/src/posts/posts.module.ts +++ b/src/posts/posts.module.ts @@ -11,6 +11,7 @@ import { PostTypesRepository } from "./repositories/postType/postTypes.repositor import { PostAwardRepository } from "./repositories/postAward/postAward.repository"; import { PostTypesController } from "./controllers/postTypes.controller"; import { ModerationModule } from "../moderation/moderation.module"; +import { PostsReportService } from "./services/postReport/postsReport.service"; @Module({ imports: [forwardRef(() => DatabaseAccessLayerModule), HttpModule, ModerationModule], @@ -35,6 +36,10 @@ import { ModerationModule } from "../moderation/moderation.module"; provide: _$.IPostAwardRepository, useClass: PostAwardRepository, }, + { + provide: _$.IPostsReportService, + useClass: PostsReportService, + }, ], exports: [ { @@ -57,6 +62,10 @@ import { ModerationModule } from "../moderation/moderation.module"; provide: _$.IPostAwardRepository, useClass: PostAwardRepository, }, + { + provide: _$.IPostsReportService, + useClass: PostsReportService, + }, ], controllers: [PostsController, PostTagsController, PostTypesController], }) From e19df40e219c3f9c5b646fe2b5e2e70b494577db Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Tue, 29 Nov 2022 21:29:51 -0800 Subject: [PATCH 093/153] Fixed Ilia's typo --- .../services/moderatorActions/moderatorActions.service.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/moderation/services/moderatorActions/moderatorActions.service.ts b/src/moderation/services/moderatorActions/moderatorActions.service.ts index 7b39b17..9d5d5fe 100644 --- a/src/moderation/services/moderatorActions/moderatorActions.service.ts +++ b/src/moderation/services/moderatorActions/moderatorActions.service.ts @@ -7,6 +7,7 @@ import { Comment } from "../../../comments/models"; import { Post } from "../../../posts/models"; import { ModerationPayloadDto } from "../../dtos/moderatorActions"; import { GotBannedProps } from "../../../users/models/toSelf"; +import { Role } from "../../../users/models"; /** * This service is responsible for moderating posts and comments. @@ -24,6 +25,10 @@ export class ModeratorActionsService implements IModeratorActionsService { } public async banUser(payload: ModerationPayloadDto): Promise { + const user = await this._dbContext.Users.findUserById(payload.id); + if (user.roles.some(role => role === Role.ADMIN || role === Role.MODERATOR)) { + throw new HttpException("Unable to ban this user, permission level too high", 400); + } const banProps = new GotBannedProps({ bannedAt: Date.now(), moderatorId: payload.moderatorId, @@ -146,7 +151,7 @@ export class ModeratorActionsService implements IModeratorActionsService { const restrictedProps = new RestrictedProps({ restrictedAt: Date.now(), moderatorId: payload.moderatorId, - reason: payload.moderatorId, + reason: payload.reason, }); await this._dbContext.Posts.restrictPost(payload.id, restrictedProps); post.restrictedProps = restrictedProps; From 780630d1f6f8ce64a06e14b42e756b74dbd61aa1 Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Tue, 29 Nov 2022 21:30:15 -0800 Subject: [PATCH 094/153] Fixed my copy paste typo --- src/comments/repositories/comment/comments.repository.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/comments/repositories/comment/comments.repository.ts b/src/comments/repositories/comment/comments.repository.ts index ff86308..52a28f7 100644 --- a/src/comments/repositories/comment/comments.repository.ts +++ b/src/comments/repositories/comment/comments.repository.ts @@ -32,8 +32,8 @@ export class CommentsRepository implements ICommentsRepository { ` MATCH (c:Comment) WHERE c.commentId = $commentId SET c.updatedAt = $updatedAt, - p.commentContent = $commentContent, - p.pending = $pending + c.commentContent = $commentContent, + c.pending = $pending `, { commentId: comment.commentId, From 5c7ca246ba666ffd638b42c44ccf27fe02829313 Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Tue, 29 Nov 2022 21:31:17 -0800 Subject: [PATCH 095/153] Moderation Controller --- src/comments/models/comment.ts | 7 +- .../controllers/moderation.controller.ts | 129 +++++++++++++++++- src/moderation/dtos/index.ts | 1 + src/moderation/dtos/userActions/index.ts | 1 - .../userActions.service.interface.ts | 0 .../userActions/userActions.service.ts | 0 .../repositories/post/posts.repository.ts | 4 +- 7 files changed, 135 insertions(+), 7 deletions(-) delete mode 100644 src/moderation/dtos/userActions/index.ts delete mode 100644 src/moderation/services/userActions/userActions.service.interface.ts delete mode 100644 src/moderation/services/userActions/userActions.service.ts diff --git a/src/comments/models/comment.ts b/src/comments/models/comment.ts index f5a4eb1..3b40ee0 100644 --- a/src/comments/models/comment.ts +++ b/src/comments/models/comment.ts @@ -7,7 +7,7 @@ import { IsNumber, IsOptional, IsString, - IsUUID + IsUUID, } from "class-validator"; import neo4j from "neo4j-driver"; import { Labels, NodeProperty } from "../../neo4j/neo4j.decorators"; @@ -118,8 +118,9 @@ export class Comment extends Model { public async getChildrenComments(limit = 0): Promise { const queryResult = await this.neo4jService.tryReadAsync( ` - MATCH (c:Comment)-[:${CommentToSelfRelTypes.REPLIED - }]->(p:Comment) WHERE p.commentId = $parentId + MATCH (c:Comment)-[:${ + CommentToSelfRelTypes.REPLIED + }]->(p:Comment) WHERE c.commentId = $parentId RETURN c ${limit > 0 ? `LIMIT $limit` : ""} `, diff --git a/src/moderation/controllers/moderation.controller.ts b/src/moderation/controllers/moderation.controller.ts index 499345b..730a00f 100644 --- a/src/moderation/controllers/moderation.controller.ts +++ b/src/moderation/controllers/moderation.controller.ts @@ -9,11 +9,138 @@ import { Inject, Param, ParseUUIDPipe, + Patch, Post, UseGuards, UseInterceptors, } from "@nestjs/common"; +import { AuthGuard } from "@nestjs/passport"; +import { AuthedUser } from "../../auth/decorators/authedUser.param.decorator"; +import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; +import { Roles } from "../../auth/decorators/roles.decorator"; +import { RolesGuard } from "../../auth/guards/roles.guard"; +import { Role, User } from "../../users/models"; +import { _$ } from "../../_domain/injectableTokens"; +import { ModerationPayloadDto } from "../dtos"; +import { IModeratorActionsService } from "../services/moderatorActions/moderatorActions.service.interface"; +@ApiTags("moderation") @Controller("moderation") -export class ModerationController {} +@ApiBearerAuth() +@UseInterceptors(ClassSerializerInterceptor) +export class ModerationController { + private readonly _moderationActionsService: IModeratorActionsService; + + constructor( + @Inject(_$.IModeratorActionsService) moderationActionsService: IModeratorActionsService + ) { + this._moderationActionsService = moderationActionsService; + } + + @Patch("/post/:postId/allow") + @Roles(Role.MODERATOR) + @UseGuards(AuthGuard("jwt"), RolesGuard) + public async allowPost(@Param("postId", ParseUUIDPipe) postId: UUID): Promise { + await this._moderationActionsService.allowPost(postId); + } + + @Patch("/post/restrict") + @Roles(Role.MODERATOR) + @UseGuards(AuthGuard("jwt"), RolesGuard) + public async restrictPost( + @AuthedUser() user: User, + @Body() moderationPayload: ModerationPayloadDto + ): Promise { + moderationPayload.moderatorId = user.userId; + await this._moderationActionsService.restrictPost(moderationPayload); + } + + @Patch("/post/:postId/unrestrict") + @Roles(Role.MODERATOR) + @UseGuards(AuthGuard("jwt"), RolesGuard) + public async unrestrictPost(@Param("postId", ParseUUIDPipe) postId: UUID): Promise { + await this._moderationActionsService.unrestrictPost(postId); + } + + @Patch("/comment/:commentId/allow") + @Roles(Role.MODERATOR) + @UseGuards(AuthGuard("jwt"), RolesGuard) + public async allowComment(@Param("commentId", ParseUUIDPipe) commentId: UUID): Promise { + await this._moderationActionsService.allowComment(commentId); + } + + @Patch("/comment/restrict") + @Roles(Role.MODERATOR) + @UseGuards(AuthGuard("jwt"), RolesGuard) + public async restrictComment( + @AuthedUser() user: User, + @Body() moderationPayload: ModerationPayloadDto + ): Promise { + moderationPayload.moderatorId = user.userId; + await this._moderationActionsService.restrictComment(moderationPayload); + } + + @Patch("/comment/:commentId/unrestrict") + @Roles(Role.MODERATOR) + @UseGuards(AuthGuard("jwt"), RolesGuard) + public async unrestrictComment( + @Param("commentId", ParseUUIDPipe) commentId: UUID + ): Promise { + await this._moderationActionsService.unrestrictComment(commentId); + } + + @Patch("/post/delete") + @Roles(Role.MODERATOR) + @UseGuards(AuthGuard("jwt"), RolesGuard) + public async deletePost( + @AuthedUser() user: User, + @Body() moderationPayload: ModerationPayloadDto + ): Promise { + moderationPayload.moderatorId = user.userId; + await this._moderationActionsService.deletePost(moderationPayload); + } + + @Patch("/post/:postId/restore") + @Roles(Role.MODERATOR) + @UseGuards(AuthGuard("jwt"), RolesGuard) + public async restorePost(@Param("postId", ParseUUIDPipe) postId: UUID): Promise { + await this._moderationActionsService.restorePost(postId); + } + + @Patch("/comment/delete") + @Roles(Role.MODERATOR) + @UseGuards(AuthGuard("jwt"), RolesGuard) + public async deleteComment( + @AuthedUser() user: User, + @Body() moderationPayload: ModerationPayloadDto + ): Promise { + moderationPayload.moderatorId = user.userId; + await this._moderationActionsService.deleteComment(moderationPayload); + } + + @Patch("/comment/:commentId/restore") + @Roles(Role.MODERATOR) + @UseGuards(AuthGuard("jwt"), RolesGuard) + public async restoreComment(@Param("commentId", ParseUUIDPipe) commentId: UUID): Promise { + await this._moderationActionsService.restoreComment(commentId); + } + + @Patch("/user/:userId/unban") + @Roles(Role.MODERATOR) + @UseGuards(AuthGuard("jwt"), RolesGuard) + public async unbanUser(@Param("userId", ParseUUIDPipe) userId: UUID): Promise { + await this._moderationActionsService.unbanUser(userId); + } + + @Patch("/user/ban") + @Roles(Role.MODERATOR) + @UseGuards(AuthGuard("jwt"), RolesGuard) + public async banUser( + @AuthedUser() user: User, + @Body() moderationPayload: ModerationPayloadDto + ): Promise { + moderationPayload.moderatorId = user.userId; + await this._moderationActionsService.banUser(moderationPayload); + } +} diff --git a/src/moderation/dtos/index.ts b/src/moderation/dtos/index.ts index 91b0013..c1d2e1b 100644 --- a/src/moderation/dtos/index.ts +++ b/src/moderation/dtos/index.ts @@ -1,2 +1,3 @@ export * from "./hateSpeechRequestPayload.dto"; export * from "./hateSpeechResponse.dto"; +export * from "./moderatorActions"; diff --git a/src/moderation/dtos/userActions/index.ts b/src/moderation/dtos/userActions/index.ts deleted file mode 100644 index 8b13789..0000000 --- a/src/moderation/dtos/userActions/index.ts +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/moderation/services/userActions/userActions.service.interface.ts b/src/moderation/services/userActions/userActions.service.interface.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/moderation/services/userActions/userActions.service.ts b/src/moderation/services/userActions/userActions.service.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/posts/repositories/post/posts.repository.ts b/src/posts/repositories/post/posts.repository.ts index 8f798bf..b6da45b 100644 --- a/src/posts/repositories/post/posts.repository.ts +++ b/src/posts/repositories/post/posts.repository.ts @@ -13,7 +13,7 @@ export class PostsRepository implements IPostsRepository { constructor(@Inject(Neo4jService) private _neo4jService: Neo4jService) {} public async findAll(): Promise { - const allPosts = await this._neo4jService.read(`MATCH (p:Post) RETURN p`, {}); + const allPosts = await this._neo4jService.tryReadAsync(`MATCH (p:Post) RETURN p`, {}); const records = allPosts.records; if (records.length === 0) return []; return records.map(record => new Post(record.get("p").properties, this._neo4jService)); @@ -203,7 +203,7 @@ export class PostsRepository implements IPostsRepository { public async restrictPost(postId: string, restrictedProps: RestrictedProps): Promise { await this._neo4jService.tryWriteAsync( - `MATCH (p:Post) WHERE p.postId = $postId + `MATCH (p:Post { postId: $postId }) CREATE (p)-[r:${_ToSelfRelTypes.RESTRICTED} { restrictedAt: $restrictedAt, moderatorId: $moderatorId, From c8714248083a82f73bd987ce7ab62bbdb9ce9967 Mon Sep 17 00:00:00 2001 From: "[Ian Chao]" <90526260+iantelli@users.noreply.github.com> Date: Wed, 30 Nov 2022 01:00:51 -0800 Subject: [PATCH 096/153] Better ban hierarchy Re-ordered the role model to be relative to permissions. --- src/moderation/controllers/moderation.controller.ts | 2 +- .../services/moderatorActions/moderatorActions.service.ts | 8 +++++--- src/users/models/role.ts | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/moderation/controllers/moderation.controller.ts b/src/moderation/controllers/moderation.controller.ts index 730a00f..2f03344 100644 --- a/src/moderation/controllers/moderation.controller.ts +++ b/src/moderation/controllers/moderation.controller.ts @@ -38,7 +38,7 @@ export class ModerationController { } @Patch("/post/:postId/allow") - @Roles(Role.MODERATOR) + @Roles(Role.MODERATOR, Role.ADMIN) @UseGuards(AuthGuard("jwt"), RolesGuard) public async allowPost(@Param("postId", ParseUUIDPipe) postId: UUID): Promise { await this._moderationActionsService.allowPost(postId); diff --git a/src/moderation/services/moderatorActions/moderatorActions.service.ts b/src/moderation/services/moderatorActions/moderatorActions.service.ts index 9d5d5fe..262c3ea 100644 --- a/src/moderation/services/moderatorActions/moderatorActions.service.ts +++ b/src/moderation/services/moderatorActions/moderatorActions.service.ts @@ -7,7 +7,6 @@ import { Comment } from "../../../comments/models"; import { Post } from "../../../posts/models"; import { ModerationPayloadDto } from "../../dtos/moderatorActions"; import { GotBannedProps } from "../../../users/models/toSelf"; -import { Role } from "../../../users/models"; /** * This service is responsible for moderating posts and comments. @@ -26,8 +25,11 @@ export class ModeratorActionsService implements IModeratorActionsService { public async banUser(payload: ModerationPayloadDto): Promise { const user = await this._dbContext.Users.findUserById(payload.id); - if (user.roles.some(role => role === Role.ADMIN || role === Role.MODERATOR)) { - throw new HttpException("Unable to ban this user, permission level too high", 400); + const moderator = await this._dbContext.Users.findUserById(payload.moderatorId); + + // If the moderator role's permissions are higher than the user's role's permissions, the moderator can ban the user. + if (Math.max(...moderator.roles) <= Math.max(...user.roles)) { + throw new HttpException("You cannot ban this user.", 403); } const banProps = new GotBannedProps({ bannedAt: Date.now(), diff --git a/src/users/models/role.ts b/src/users/models/role.ts index 5afb43e..29091a0 100644 --- a/src/users/models/role.ts +++ b/src/users/models/role.ts @@ -1,5 +1,5 @@ export enum Role { USER, - ADMIN, MODERATOR, + ADMIN, } From c9c98ea89e3de2af93cb70cc4dbd28e4400ac580 Mon Sep 17 00:00:00 2001 From: "[Ian Chao]" <90526260+iantelli@users.noreply.github.com> Date: Wed, 30 Nov 2022 01:03:19 -0800 Subject: [PATCH 097/153] Remove unnecessary line --- src/moderation/controllers/moderation.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/moderation/controllers/moderation.controller.ts b/src/moderation/controllers/moderation.controller.ts index 2f03344..730a00f 100644 --- a/src/moderation/controllers/moderation.controller.ts +++ b/src/moderation/controllers/moderation.controller.ts @@ -38,7 +38,7 @@ export class ModerationController { } @Patch("/post/:postId/allow") - @Roles(Role.MODERATOR, Role.ADMIN) + @Roles(Role.MODERATOR) @UseGuards(AuthGuard("jwt"), RolesGuard) public async allowPost(@Param("postId", ParseUUIDPipe) postId: UUID): Promise { await this._moderationActionsService.allowPost(postId); From 6dc2e1dd41c361cc7d8e1a4b49304ac3e41c8cee Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Wed, 30 Nov 2022 02:31:09 -0800 Subject: [PATCH 098/153] install @google-cloud/recaptcha-enterprise --- package-lock.json | 1516 ++++++++++++++++- package.json | 1 + .../captcha.guard.ts | 0 .../captchaConfig.decorator.ts | 0 ...le-cloud-recaptcha-enterprise.constants.ts | 0 ...oogle-cloud-recaptcha-enterprise.module.ts | 4 + ...-recaptcha-enterprise.service.interface.ts | 0 ...ogle-cloud-recaptcha-enterprise.service.ts | 0 .../models/assessment.dto.ts | 15 + .../captchaConfigParameters.interface.ts | 0 .../models/userActions.enum.ts | 7 + 11 files changed, 1477 insertions(+), 66 deletions(-) create mode 100644 src/google-cloud-recaptcha-enterprise/captcha.guard.ts create mode 100644 src/google-cloud-recaptcha-enterprise/captchaConfig.decorator.ts create mode 100644 src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.constants.ts create mode 100644 src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.module.ts create mode 100644 src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.service.interface.ts create mode 100644 src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.service.ts create mode 100644 src/google-cloud-recaptcha-enterprise/models/assessment.dto.ts create mode 100644 src/google-cloud-recaptcha-enterprise/models/captchaConfigParameters.interface.ts create mode 100644 src/google-cloud-recaptcha-enterprise/models/userActions.enum.ts diff --git a/package-lock.json b/package-lock.json index ce2efb2..aaf3651 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "license": "UNLICENSED", "dependencies": { + "@google-cloud/recaptcha-enterprise": "^3.1.1", "@nestjs/axios": "^0.1.0", "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.2.0", @@ -560,7 +561,6 @@ "version": "7.19.3", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.3.tgz", "integrity": "sha512-pJ9xOlNWHiy9+FuFP09DEAFbAn4JskgRsVcc169w2xRBC3FRGuQEwjeIMMND9L2zc0iEhO/tGv4Zq+km+hxNpQ==", - "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -882,6 +882,72 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "node_modules/@google-cloud/recaptcha-enterprise": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@google-cloud/recaptcha-enterprise/-/recaptcha-enterprise-3.1.1.tgz", + "integrity": "sha512-/DeLqdQPQzBENpxsQaZSgjzuCj1wmpTUi7J+EdDTexzn3MynALKm+9mAfrU+xAcoXHSEwLdHgANcNdPdxSbmeQ==", + "dependencies": { + "google-gax": "^3.5.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.3.tgz", + "integrity": "sha512-H9l79u4kJ2PVSxUNA08HMYAnUBLj9v6KjYQ7SQ71hOZcEXhShE/y5iQCesP8+6/Ik/7i2O0a10bPquIcYfufog==", + "dependencies": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.3.tgz", + "integrity": "sha512-5dAvoZwna2Py3Ef96Ux9jIkp3iZ62TUsV00p3wVBPNX5K178UbNi8Q7gQVqwXT1Yq9RejIGG9G2IPEo93T6RcA==", + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "engines": { + "node": ">=10" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.10.5", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.5.tgz", @@ -1944,6 +2010,60 @@ "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6.tgz", "integrity": "sha512-P5v/PuEIJLYXZUZBvOLPqoyCW+m6StNqHdiR6te++gYVODpPdLakks5HVx3JaZIY+LwR02juJWFlwpc9Eog/ug==" }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, "node_modules/@sinclair/typebox": { "version": "0.24.43", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.43.tgz", @@ -2174,6 +2294,30 @@ "@types/node": "*" } }, + "node_modules/@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==" + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, + "node_modules/@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==" + }, "node_modules/@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -2653,6 +2797,17 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -2669,7 +2824,6 @@ "version": "8.8.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", - "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -2690,7 +2844,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -2879,6 +3032,14 @@ "node": ">=8" } }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "engines": { + "node": ">=8" + } + }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -3048,6 +3209,14 @@ "node": ">= 10.0.0" } }, + "node_modules/bignumber.js": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.0.tgz", + "integrity": "sha512-4LwHK4nfDOraBCtst+wOWIHbu1vhvAPJK8g8nROd4iuc3PSEjWif/qwbkh8jwCJz6yDBvtU4KPynETgrfh7y3A==", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -3082,6 +3251,11 @@ "node": ">= 6" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, "node_modules/body-parser": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", @@ -3305,6 +3479,17 @@ } ] }, + "node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/chalk": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", @@ -3463,7 +3648,6 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -3713,8 +3897,7 @@ "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, "node_modules/deepmerge": { "version": "4.2.2", @@ -3849,6 +4032,30 @@ "node": ">=12" } }, + "node_modules/duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "node_modules/duplexify/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -3897,7 +4104,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "dependencies": { "once": "^1.4.0" } @@ -3915,6 +4121,14 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -3934,7 +4148,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, "engines": { "node": ">=6" } @@ -3956,6 +4169,83 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/escodegen/node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/eslint": { "version": "8.22.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.22.0.tgz", @@ -4089,7 +4379,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", - "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -4170,7 +4459,6 @@ "version": "9.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz", "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==", - "dev": true, "dependencies": { "acorn": "^8.8.0", "acorn-jsx": "^5.3.2", @@ -4187,7 +4475,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -4242,7 +4529,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, "engines": { "node": ">=4.0" } @@ -4251,7 +4537,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -4264,6 +4549,14 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -4380,6 +4673,11 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -4431,14 +4729,18 @@ "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" }, + "node_modules/fast-text-encoding": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", + "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==" + }, "node_modules/fastq": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", @@ -4775,6 +5077,32 @@ "node": ">=10" } }, + "node_modules/gaxios": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz", + "integrity": "sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gcp-metadata": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.0.1.tgz", + "integrity": "sha512-jiRJ+Fk7e8FH68Z6TLaqwea307OktJpDjmYnU7/li6ziwvVvU2RlrCyQo5vkdeP94chm0kcSCOOszvmuaioq3g==", + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -4788,7 +5116,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -4899,11 +5226,90 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-auth-library": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.7.0.tgz", + "integrity": "sha512-1M0NG5VDIvJZEnstHbRdckLZESoJwguinwN8Dhae0j2ZKIQFIV63zxm6Fo6nM4xkgqUr2bbMtV5Dgo+Hy6oo0Q==", + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-auth-library/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/google-auth-library/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/google-gax": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-3.5.2.tgz", + "integrity": "sha512-AyP53w0gHcWlzxm+jSgqCR3Xu4Ld7EpSjhtNBnNhzwwWaIUyphH9kBGNIEH+i4UGkTUXOY29K/Re8EiAvkBRGw==", + "dependencies": { + "@grpc/grpc-js": "~1.7.0", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^8.0.2", + "is-stream-ended": "^0.1.4", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^1.0.0", + "protobufjs": "7.1.2", + "protobufjs-cli": "1.0.2", + "retry-request": "^5.0.0" + }, + "bin": { + "compileProtos": "build/tools/compileProtos.js", + "minifyProtoJson": "build/tools/minify.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "dependencies": { + "node-forge": "^1.3.1" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, "node_modules/grapheme-splitter": { "version": "1.0.4", @@ -4911,6 +5317,38 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, + "node_modules/gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "dependencies": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/gtoken/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/gtoken/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -5264,7 +5702,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, "engines": { "node": ">=8" }, @@ -5272,6 +5709,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==" + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -6202,11 +6644,66 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "node_modules/jsdoc": { + "version": "3.6.11", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.11.tgz", + "integrity": "sha512-8UCU0TYeIYD9KeLzEcAu2q8N/mx9O3phAGl32nmHlE0LpaJL71mMkP4d+QE5zWfNt50qheHtOZ0qoxVrsX5TUg==", + "dependencies": { + "@babel/parser": "^7.9.4", + "@types/markdown-it": "^12.2.3", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^12.3.2", + "markdown-it-anchor": "^8.4.1", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "taffydb": "2.6.2", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsdoc/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jsdoc/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, "bin": { "jsesc": "bin/jsesc" }, @@ -6214,6 +6711,14 @@ "node": ">=4" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -6310,6 +6815,14 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -6352,6 +6865,14 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -6381,6 +6902,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -6465,6 +6991,11 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -6537,6 +7068,41 @@ "tmpl": "1.0.5" } }, + "node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it-anchor": { + "version": "8.6.5", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.5.tgz", + "integrity": "sha512-PI1qEHHkTNWT+X6Ip9w+paonfIQ+QZP9sCeMYi47oqhH+EsW8CrJ8J7CzV19QVOj6il8ATGbK2nTECj22ZHGvQ==", + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/marked": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.3.tgz", + "integrity": "sha512-slWRdJkbTZ+PjkyJnE30Uid64eHwbwa1Q25INCAYfZlK4o6ylagBy/Le9eWntqJFoFT93ikUKMv47GZ4gTwHkw==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", @@ -6547,6 +7113,11 @@ "is-buffer": "~1.1.6" } }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -6841,6 +7412,14 @@ } } }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "engines": { + "node": ">= 6.13.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -7392,6 +7971,143 @@ "node": ">= 6" } }, + "node_modules/proto3-json-serializer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.0.tgz", + "integrity": "sha512-SjXwUWe/vANGs/mJJTbw5++7U67nwsymg7qsoPtw6GiXqw3kUy8ByojrlEdVE2efxAdKreX8WkDafxvYW95ZQg==", + "dependencies": { + "protobufjs": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.1.2.tgz", + "integrity": "sha512-4ZPTPkXCdel3+L81yw3dG6+Kq3umdWKh7Dc7GW/CpNk4SX3hK58iPCWeCyhVTDrbkNeKrYNZ7EojM5WDaEWTLQ==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/protobufjs-cli": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.0.2.tgz", + "integrity": "sha512-cz9Pq9p/Zs7okc6avH20W7QuyjTclwJPgqXG11jNaulfS3nbVisID8rC+prfgq0gbZE0w9LBFd1OKFF03kgFzg==", + "dependencies": { + "chalk": "^4.0.0", + "escodegen": "^1.13.0", + "espree": "^9.0.0", + "estraverse": "^5.1.0", + "glob": "^8.0.0", + "jsdoc": "^3.6.3", + "minimist": "^1.2.0", + "semver": "^7.1.2", + "tmp": "^0.2.1", + "uglify-js": "^3.7.7" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "protobufjs": "^7.0.0" + } + }, + "node_modules/protobufjs-cli/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/protobufjs-cli/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/protobufjs-cli/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/protobufjs-cli/node_modules/glob": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", + "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/protobufjs-cli/node_modules/minimatch": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.1.tgz", + "integrity": "sha512-362NP+zlprccbEt/SkxKfRMHnNY85V74mVnpUpNyr3F35covl09Kec7/sEFLt3RA4oXmewtoaanoIf67SE5Y5g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/protobufjs-cli/node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/protobufjs/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -7563,7 +8279,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -7577,6 +8292,14 @@ "node": ">=0.10.0" } }, + "node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "dependencies": { + "lodash": "^4.17.21" + } + }, "node_modules/resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -7646,6 +8369,18 @@ "node": ">=8" } }, + "node_modules/retry-request": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz", + "integrity": "sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ==", + "dependencies": { + "debug": "^4.1.1", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -8011,6 +8746,11 @@ "node": ">= 0.8" } }, + "node_modules/stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -8091,7 +8831,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, "engines": { "node": ">=8" }, @@ -8210,6 +8949,11 @@ "node": ">=0.10" } }, + "node_modules/taffydb": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz", + "integrity": "sha512-y3JaeRSplks6NYQuCOj3ZFMO3j60rTwbuKCvZxsAraGYH2epusatvZ0baZYA01WsGqJBq/Dl6vOrMUJqyMj8kA==" + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -8699,6 +9443,27 @@ "node": ">=4.2.0" } }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" + }, "node_modules/universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", @@ -9013,7 +9778,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -9022,7 +9786,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -9053,6 +9816,11 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -9065,7 +9833,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "engines": { "node": ">=10" } @@ -9518,8 +10285,7 @@ "@babel/parser": { "version": "7.19.3", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.3.tgz", - "integrity": "sha512-pJ9xOlNWHiy9+FuFP09DEAFbAn4JskgRsVcc169w2xRBC3FRGuQEwjeIMMND9L2zc0iEhO/tGv4Zq+km+hxNpQ==", - "dev": true + "integrity": "sha512-pJ9xOlNWHiy9+FuFP09DEAFbAn4JskgRsVcc169w2xRBC3FRGuQEwjeIMMND9L2zc0iEhO/tGv4Zq+km+hxNpQ==" }, "@babel/plugin-syntax-async-generators": { "version": "7.8.4", @@ -9765,6 +10531,56 @@ } } }, + "@google-cloud/recaptcha-enterprise": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@google-cloud/recaptcha-enterprise/-/recaptcha-enterprise-3.1.1.tgz", + "integrity": "sha512-/DeLqdQPQzBENpxsQaZSgjzuCj1wmpTUi7J+EdDTexzn3MynALKm+9mAfrU+xAcoXHSEwLdHgANcNdPdxSbmeQ==", + "requires": { + "google-gax": "^3.5.2" + } + }, + "@grpc/grpc-js": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.3.tgz", + "integrity": "sha512-H9l79u4kJ2PVSxUNA08HMYAnUBLj9v6KjYQ7SQ71hOZcEXhShE/y5iQCesP8+6/Ik/7i2O0a10bPquIcYfufog==", + "requires": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + } + }, + "@grpc/proto-loader": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.3.tgz", + "integrity": "sha512-5dAvoZwna2Py3Ef96Ux9jIkp3iZ62TUsV00p3wVBPNX5K178UbNi8Q7gQVqwXT1Yq9RejIGG9G2IPEo93T6RcA==", + "requires": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^16.2.0" + }, + "dependencies": { + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" + } + } + }, "@humanwhocodes/config-array": { "version": "0.10.5", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.5.tgz", @@ -10527,6 +11343,60 @@ "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6.tgz", "integrity": "sha512-P5v/PuEIJLYXZUZBvOLPqoyCW+m6StNqHdiR6te++gYVODpPdLakks5HVx3JaZIY+LwR02juJWFlwpc9Eog/ug==" }, + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, "@sinclair/typebox": { "version": "0.24.43", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.43.tgz", @@ -10757,6 +11627,30 @@ "@types/node": "*" } }, + "@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==" + }, + "@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, + "@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "requires": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==" + }, "@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -11147,6 +12041,14 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, "accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -11159,8 +12061,7 @@ "acorn": { "version": "8.8.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", - "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", - "dev": true + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==" }, "acorn-import-assertions": { "version": "1.8.0", @@ -11173,7 +12074,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, "requires": {} }, "acorn-walk": { @@ -11310,6 +12210,11 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, + "arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" + }, "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -11436,6 +12341,11 @@ "node-addon-api": "^5.0.0" } }, + "bignumber.js": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.0.tgz", + "integrity": "sha512-4LwHK4nfDOraBCtst+wOWIHbu1vhvAPJK8g8nROd4iuc3PSEjWif/qwbkh8jwCJz6yDBvtU4KPynETgrfh7y3A==" + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -11466,6 +12376,11 @@ } } }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, "body-parser": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", @@ -11625,6 +12540,14 @@ "integrity": "sha512-+TeEIee1gS5bYOiuf+PS/kp2mrXic37Hl66VY6EAfxasIk5fELTktK2oOezYed12H8w7jt3s512PpulQidPjwA==", "dev": true }, + "catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "requires": { + "lodash": "^4.17.15" + } + }, "chalk": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", @@ -11740,7 +12663,6 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, "requires": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -11941,8 +12863,7 @@ "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, "deepmerge": { "version": "4.2.2", @@ -12040,6 +12961,29 @@ "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-8.0.3.tgz", "integrity": "sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg==" }, + "duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -12079,7 +13023,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "requires": { "once": "^1.4.0" } @@ -12094,6 +13037,11 @@ "tapable": "^2.2.0" } }, + "entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==" + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -12112,8 +13060,7 @@ "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" }, "escape-html": { "version": "1.0.3", @@ -12126,6 +13073,61 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true }, + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "requires": { + "prelude-ls": "~1.1.2" + } + } + } + }, "eslint": { "version": "8.22.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.22.0.tgz", @@ -12274,14 +13276,12 @@ "eslint-visitor-keys": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", - "dev": true + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==" }, "espree": { "version": "9.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz", "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==", - "dev": true, "requires": { "acorn": "^8.8.0", "acorn-jsx": "^5.3.2", @@ -12291,8 +13291,7 @@ "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "esquery": { "version": "1.4.0", @@ -12331,20 +13330,23 @@ "estraverse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" }, "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, "events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -12445,6 +13447,11 @@ } } }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -12490,14 +13497,18 @@ "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, "fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" }, + "fast-text-encoding": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", + "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==" + }, "fastq": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", @@ -12747,6 +13758,26 @@ "wide-align": "^1.1.2" } }, + "gaxios": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz", + "integrity": "sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==", + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + } + }, + "gcp-metadata": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.0.1.tgz", + "integrity": "sha512-jiRJ+Fk7e8FH68Z6TLaqwea307OktJpDjmYnU7/li6ziwvVvU2RlrCyQo5vkdeP94chm0kcSCOOszvmuaioq3g==", + "requires": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + } + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -12756,8 +13787,7 @@ "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-intrinsic": { "version": "1.1.3", @@ -12832,11 +13862,76 @@ "slash": "^3.0.0" } }, + "google-auth-library": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.7.0.tgz", + "integrity": "sha512-1M0NG5VDIvJZEnstHbRdckLZESoJwguinwN8Dhae0j2ZKIQFIV63zxm6Fo6nM4xkgqUr2bbMtV5Dgo+Hy6oo0Q==", + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "dependencies": { + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + } + } + }, + "google-gax": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-3.5.2.tgz", + "integrity": "sha512-AyP53w0gHcWlzxm+jSgqCR3Xu4Ld7EpSjhtNBnNhzwwWaIUyphH9kBGNIEH+i4UGkTUXOY29K/Re8EiAvkBRGw==", + "requires": { + "@grpc/grpc-js": "~1.7.0", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^8.0.2", + "is-stream-ended": "^0.1.4", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^1.0.0", + "protobufjs": "7.1.2", + "protobufjs-cli": "1.0.2", + "retry-request": "^5.0.0" + } + }, + "google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "requires": { + "node-forge": "^1.3.1" + } + }, "graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, "grapheme-splitter": { "version": "1.0.4", @@ -12844,6 +13939,37 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, + "gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "requires": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + }, + "dependencies": { + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + } + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -13094,8 +14220,12 @@ "is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, + "is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==" }, "is-unicode-supported": { "version": "0.1.0", @@ -13808,12 +14938,62 @@ "argparse": "^2.0.1" } }, + "js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "requires": { + "xmlcreate": "^2.0.4" + } + }, + "jsdoc": { + "version": "3.6.11", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.11.tgz", + "integrity": "sha512-8UCU0TYeIYD9KeLzEcAu2q8N/mx9O3phAGl32nmHlE0LpaJL71mMkP4d+QE5zWfNt50qheHtOZ0qoxVrsX5TUg==", + "requires": { + "@babel/parser": "^7.9.4", + "@types/markdown-it": "^12.2.3", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^12.3.2", + "markdown-it-anchor": "^8.4.1", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "taffydb": "2.6.2", + "underscore": "~1.13.2" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==" + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + } + } + }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "requires": { + "bignumber.js": "^9.0.0" + } + }, "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -13897,6 +15077,14 @@ "safe-buffer": "^5.0.1" } }, + "klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "requires": { + "graceful-fs": "^4.1.9" + } + }, "kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -13930,6 +15118,14 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "requires": { + "uc.micro": "^1.0.1" + } + }, "loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -13950,6 +15146,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, "lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -14024,6 +15225,11 @@ } } }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -14077,6 +15283,29 @@ "tmpl": "1.0.5" } }, + "markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "requires": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + } + }, + "markdown-it-anchor": { + "version": "8.6.5", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.5.tgz", + "integrity": "sha512-PI1qEHHkTNWT+X6Ip9w+paonfIQ+QZP9sCeMYi47oqhH+EsW8CrJ8J7CzV19QVOj6il8ATGbK2nTECj22ZHGvQ==", + "requires": {} + }, + "marked": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.3.tgz", + "integrity": "sha512-slWRdJkbTZ+PjkyJnE30Uid64eHwbwa1Q25INCAYfZlK4o6ylagBy/Le9eWntqJFoFT93ikUKMv47GZ4gTwHkw==" + }, "md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", @@ -14087,6 +15316,11 @@ "is-buffer": "~1.1.6" } }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -14310,6 +15544,11 @@ "whatwg-url": "^5.0.0" } }, + "node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -14704,6 +15943,109 @@ "sisteransi": "^1.0.5" } }, + "proto3-json-serializer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.0.tgz", + "integrity": "sha512-SjXwUWe/vANGs/mJJTbw5++7U67nwsymg7qsoPtw6GiXqw3kUy8ByojrlEdVE2efxAdKreX8WkDafxvYW95ZQg==", + "requires": { + "protobufjs": "^7.0.0" + } + }, + "protobufjs": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.1.2.tgz", + "integrity": "sha512-4ZPTPkXCdel3+L81yw3dG6+Kq3umdWKh7Dc7GW/CpNk4SX3hK58iPCWeCyhVTDrbkNeKrYNZ7EojM5WDaEWTLQ==", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "dependencies": { + "long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==" + } + } + }, + "protobufjs-cli": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.0.2.tgz", + "integrity": "sha512-cz9Pq9p/Zs7okc6avH20W7QuyjTclwJPgqXG11jNaulfS3nbVisID8rC+prfgq0gbZE0w9LBFd1OKFF03kgFzg==", + "requires": { + "chalk": "^4.0.0", + "escodegen": "^1.13.0", + "espree": "^9.0.0", + "estraverse": "^5.1.0", + "glob": "^8.0.0", + "jsdoc": "^3.6.3", + "minimist": "^1.2.0", + "semver": "^7.1.2", + "tmp": "^0.2.1", + "uglify-js": "^3.7.7" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + }, + "glob": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", + "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "minimatch": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.1.tgz", + "integrity": "sha512-362NP+zlprccbEt/SkxKfRMHnNY85V74mVnpUpNyr3F35covl09Kec7/sEFLt3RA4oXmewtoaanoIf67SE5Y5g==", + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "requires": { + "rimraf": "^3.0.0" + } + } + } + }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -14832,8 +16174,7 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" }, "require-from-string": { "version": "2.0.2", @@ -14841,6 +16182,14 @@ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true }, + "requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "requires": { + "lodash": "^4.17.21" + } + }, "resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -14891,6 +16240,15 @@ "signal-exit": "^3.0.2" } }, + "retry-request": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz", + "integrity": "sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ==", + "requires": { + "debug": "^4.1.1", + "extend": "^3.0.2" + } + }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -15167,6 +16525,11 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" }, + "stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + }, "streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -15230,8 +16593,7 @@ "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" }, "superagent": { "version": "8.0.0", @@ -15316,6 +16678,11 @@ "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", "dev": true }, + "taffydb": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz", + "integrity": "sha512-y3JaeRSplks6NYQuCOj3ZFMO3j60rTwbuKCvZxsAraGYH2epusatvZ0baZYA01WsGqJBq/Dl6vOrMUJqyMj8kA==" + }, "tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -15642,6 +17009,21 @@ "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", "dev": true }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, + "uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==" + }, + "underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" + }, "universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", @@ -15869,14 +17251,12 @@ "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "requires": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -15898,6 +17278,11 @@ "signal-exit": "^3.0.7" } }, + "xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==" + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -15906,8 +17291,7 @@ "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" }, "yallist": { "version": "4.0.0", diff --git a/package.json b/package.json index ca2392d..539ee35 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "test:e2e": "jest --config ./test/jest-e2e.json --detectOpenHandles" }, "dependencies": { + "@google-cloud/recaptcha-enterprise": "^3.1.1", "@nestjs/axios": "^0.1.0", "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.2.0", diff --git a/src/google-cloud-recaptcha-enterprise/captcha.guard.ts b/src/google-cloud-recaptcha-enterprise/captcha.guard.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/google-cloud-recaptcha-enterprise/captchaConfig.decorator.ts b/src/google-cloud-recaptcha-enterprise/captchaConfig.decorator.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.constants.ts b/src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.constants.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.module.ts b/src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.module.ts new file mode 100644 index 0000000..6f6066d --- /dev/null +++ b/src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.module.ts @@ -0,0 +1,4 @@ +import { Module } from '@nestjs/common'; + +@Module({}) +export class GoogleCloudRecaptchaEnterpriseModule {} diff --git a/src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.service.interface.ts b/src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.service.interface.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.service.ts b/src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/google-cloud-recaptcha-enterprise/models/assessment.dto.ts b/src/google-cloud-recaptcha-enterprise/models/assessment.dto.ts new file mode 100644 index 0000000..6c037d8 --- /dev/null +++ b/src/google-cloud-recaptcha-enterprise/models/assessment.dto.ts @@ -0,0 +1,15 @@ +import { IsEnum, IsNotEmpty, IsString } from "class-validator"; +import { UserActionsEnum } from "./userActions.enum"; + +export class AssessmentDto { + @IsString() + @IsNotEmpty() + token: string; + + @IsEnum(UserActionsEnum) + recaptchaAction: UserActionsEnum; + + constructor(partial?: Partial) { + Object.assign(this, partial); + } +} diff --git a/src/google-cloud-recaptcha-enterprise/models/captchaConfigParameters.interface.ts b/src/google-cloud-recaptcha-enterprise/models/captchaConfigParameters.interface.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/google-cloud-recaptcha-enterprise/models/userActions.enum.ts b/src/google-cloud-recaptcha-enterprise/models/userActions.enum.ts new file mode 100644 index 0000000..e8686e5 --- /dev/null +++ b/src/google-cloud-recaptcha-enterprise/models/userActions.enum.ts @@ -0,0 +1,7 @@ +export enum UserActionsEnum { + ChangePassword = "ChangePassword", + CreateComment = "CreateComment", + CreatePost = "CreatePost", + Login = "Login", + SignUp = "SignUp", +} From ba096ab8a7cff021202ae2dc9c831a2e86eb5452 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Wed, 30 Nov 2022 02:31:48 -0800 Subject: [PATCH 099/153] add google-cloud-recaptcha-enterprise module --- .../captcha.guard.ts | 48 +++++++++++ .../captchaConfig.decorator.ts | 6 ++ ...le-cloud-recaptcha-enterprise.constants.ts | 11 +++ ...oogle-cloud-recaptcha-enterprise.module.ts | 25 +++++- ...-recaptcha-enterprise.service.interface.ts | 5 ++ ...ogle-cloud-recaptcha-enterprise.service.ts | 81 +++++++++++++++++++ .../captchaConfigParameters.interface.ts | 3 + 7 files changed, 175 insertions(+), 4 deletions(-) diff --git a/src/google-cloud-recaptcha-enterprise/captcha.guard.ts b/src/google-cloud-recaptcha-enterprise/captcha.guard.ts index e69de29..349aa8a 100644 --- a/src/google-cloud-recaptcha-enterprise/captcha.guard.ts +++ b/src/google-cloud-recaptcha-enterprise/captcha.guard.ts @@ -0,0 +1,48 @@ +import { CanActivate, ExecutionContext, Inject, Injectable } from "@nestjs/common"; +import { _$ } from "../_domain/injectableTokens"; +import { IGoogleCloudRecaptchaEnterpriseService } from "./google-cloud-recaptcha-enterprise.service.interface"; +import { + captchaConfigMetaDataKey, + headerKeys as googleCloudRecaptchaEnterpriseHeaders, +} from "./google-cloud-recaptcha-enterprise.constants"; +import { AssessmentDto } from "./models/assessment.dto"; +import { Reflector } from "@nestjs/core"; +import { CaptchaConfigParameters } from "./models/captchaConfigParameters.interface"; + +@Injectable() +export class CaptchaGuard implements CanActivate { + constructor( + @Inject(_$.IGoogleCloudRecaptchaEnterpriseService) + private readonly _recaptchaEnterpriseService: IGoogleCloudRecaptchaEnterpriseService, + private reflector: Reflector + ) {} + + public async canActivate(context: ExecutionContext): Promise { + const captchaConfig = this.reflector.get( + captchaConfigMetaDataKey, + context.getHandler() + ); + + const request = context.switchToHttp().getRequest(); + + const headers = request.headers; + const token = headers[googleCloudRecaptchaEnterpriseHeaders.token]; + const recaptchaAction = headers[googleCloudRecaptchaEnterpriseHeaders.recaptchaAction]; + + if (!token || !recaptchaAction) { + return false; + } + + const assessment = await this._recaptchaEnterpriseService.createAssessment( + new AssessmentDto({ + token, + recaptchaAction, + }) + ); + if (assessment === null) { + return false; + } + + return assessment > (captchaConfig?.passScore || 0.6); + } +} diff --git a/src/google-cloud-recaptcha-enterprise/captchaConfig.decorator.ts b/src/google-cloud-recaptcha-enterprise/captchaConfig.decorator.ts index e69de29..cf5efbe 100644 --- a/src/google-cloud-recaptcha-enterprise/captchaConfig.decorator.ts +++ b/src/google-cloud-recaptcha-enterprise/captchaConfig.decorator.ts @@ -0,0 +1,6 @@ +import { SetMetadata } from "@nestjs/common"; +import { CaptchaConfigParameters } from "./models/captchaConfigParameters.interface"; +import { captchaConfigMetaDataKey } from "./google-cloud-recaptcha-enterprise.constants"; + +export const CaptchaConfig = (config: CaptchaConfigParameters) => + SetMetadata(captchaConfigMetaDataKey, config); diff --git a/src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.constants.ts b/src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.constants.ts index e69de29..7836e9a 100644 --- a/src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.constants.ts +++ b/src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.constants.ts @@ -0,0 +1,11 @@ +export const envKeys = { + projectId: "GOOGLE_CLOUD_RECAPTCHA_ENTERPRISE_PROJECT_ID", + recaptchaSiteKey: "GOOGLE_CLOUD_RECAPTCHA_ENTERPRISE_RECAPTCHA_SITE_KEY", +}; + +export const headerKeys = { + token: "IGAQ-reCaptcahToken".toLowerCase(), + recaptchaAction: "IGAQ-reCaptcahAction".toLowerCase(), +}; + +export const captchaConfigMetaDataKey = "CAPTCHA_CONFIG"; diff --git a/src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.module.ts b/src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.module.ts index 6f6066d..a1bede7 100644 --- a/src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.module.ts +++ b/src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.module.ts @@ -1,4 +1,21 @@ -import { Module } from '@nestjs/common'; - -@Module({}) -export class GoogleCloudRecaptchaEnterpriseModule {} +import { Module } from "@nestjs/common"; +import { _$ } from "../_domain/injectableTokens"; +import { GoogleCloudRecaptchaEnterpriseService } from "./google-cloud-recaptcha-enterprise.service"; +import { ConfigModule } from "@nestjs/config"; + +@Module({ + imports: [ConfigModule], + providers: [ + { + provide: _$.IGoogleCloudRecaptchaEnterpriseService, + useClass: GoogleCloudRecaptchaEnterpriseService, + }, + ], + exports: [ + { + provide: _$.IGoogleCloudRecaptchaEnterpriseService, + useClass: GoogleCloudRecaptchaEnterpriseService, + }, + ], +}) +export class GoogleCloudRecaptchaEnterpriseModule {} diff --git a/src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.service.interface.ts b/src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.service.interface.ts index e69de29..e4f59bd 100644 --- a/src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.service.interface.ts +++ b/src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.service.interface.ts @@ -0,0 +1,5 @@ +import { AssessmentDto } from "./models/assessment.dto"; + +export interface IGoogleCloudRecaptchaEnterpriseService { + createAssessment(assessmentPayload: AssessmentDto): Promise; +} diff --git a/src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.service.ts b/src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.service.ts index e69de29..bdffd43 100644 --- a/src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.service.ts +++ b/src/google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.service.ts @@ -0,0 +1,81 @@ +import { ConfigService } from "@nestjs/config"; +import { envKeys } from "./google-cloud-recaptcha-enterprise.constants"; +import { AssessmentDto } from "./models/assessment.dto"; +import { RecaptchaEnterpriseServiceClient } from "@google-cloud/recaptcha-enterprise"; +import { IGoogleCloudRecaptchaEnterpriseService } from "./google-cloud-recaptcha-enterprise.service.interface"; +import { Injectable, Scope } from "@nestjs/common"; + +@Injectable({ scope: Scope.REQUEST }) +export class GoogleCloudRecaptchaEnterpriseService + implements IGoogleCloudRecaptchaEnterpriseService +{ + constructor(private _configService: ConfigService) {} + + /** + * Create an assessment to analyze the risk of an UI action. Note that + * this example does set error boundaries and returns `null` for + * exceptions. + * + * projectID: GCloud Project ID + * recaptchaSiteKey: Site key obtained by registering a domain/app to use recaptcha services. + * token: The token obtained from the client on passing the recaptchaSiteKey. + * recaptchaAction: Action name corresponding to the token. + */ + public async createAssessment(assessmentPayload: AssessmentDto): Promise { + const projectID = this._configService.get(envKeys.projectId); + const recaptchaSiteKey = this._configService.get(envKeys.recaptchaSiteKey); + + // Create the reCAPTCHA client & set the project path. There are multiple + // ways to authenticate your client. For more information see: + // https://cloud.google.com/docs/authentication + // TODO: To avoid memory issues, move this client generation outside + // of this example, and cache it (recommended) or call client.close() + // before exiting this method. + const client = new RecaptchaEnterpriseServiceClient(); + const projectPath = client.projectPath(projectID); + + const request = { + assessment: { + event: { + token: assessmentPayload.token, + siteKey: recaptchaSiteKey, + }, + }, + parent: projectPath, + }; + + // client.createAssessment() can return a Promise or take a Callback + const [response] = await client.createAssessment(request); + + // Check if the token is valid. + if (!response.tokenProperties.valid) { + console.log( + "The CreateAssessment call failed because the token was: " + + response.tokenProperties.invalidReason + ); + + return null; + } + + // Check if the expected action was executed. + // The `action` property is set by user client in the + // grecaptcha.enterprise.execute() method. + if (response.tokenProperties.action === assessmentPayload.recaptchaAction) { + // Get the risk score and the reason(s). + // For more information on interpreting the assessment, + // see: https://cloud.google.com/recaptcha-enterprise/docs/interpret-assessment + console.log("The reCAPTCHA score is: " + response.riskAnalysis.score); + + response.riskAnalysis.reasons.forEach(reason => { + console.log(reason); + }); + return response.riskAnalysis.score; + } else { + console.log( + "The action attribute in your reCAPTCHA tag " + + "does not match the action you are expecting to score" + ); + return null; + } + } +} diff --git a/src/google-cloud-recaptcha-enterprise/models/captchaConfigParameters.interface.ts b/src/google-cloud-recaptcha-enterprise/models/captchaConfigParameters.interface.ts index e69de29..1945a32 100644 --- a/src/google-cloud-recaptcha-enterprise/models/captchaConfigParameters.interface.ts +++ b/src/google-cloud-recaptcha-enterprise/models/captchaConfigParameters.interface.ts @@ -0,0 +1,3 @@ +export interface CaptchaConfigParameters { + passScore: number; +} From efd030799146222638f3abbfa2fb1b9aad220266 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Wed, 30 Nov 2022 02:32:06 -0800 Subject: [PATCH 100/153] add google captcha token to the token list --- src/_domain/injectableTokens.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/_domain/injectableTokens.ts b/src/_domain/injectableTokens.ts index 5548bff..2e4dd79 100644 --- a/src/_domain/injectableTokens.ts +++ b/src/_domain/injectableTokens.ts @@ -1,4 +1,5 @@ import { IUserHistoryService } from "../users/services/userHistory/userHistory.service.interface"; +import { IGoogleCloudRecaptchaEnterpriseService } from "../google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.service.interface"; const injectableTokens = { // Database Context @@ -34,5 +35,8 @@ const injectableTokens = { // Moderation Module IAutoModerationService: Symbol("IAutoModerationService"), IModeratorActionsService: Symbol("IModeratorActionsService"), + + // Google Cloud reCAPTCHA Enterprise Module + IGoogleCloudRecaptchaEnterpriseService: Symbol("IGoogleCloudRecaptchaEnterpriseService"), }; export { injectableTokens as _$ }; From 02baffe91c0b40069ee780e0f317a5ecfdbc6f70 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Wed, 30 Nov 2022 02:32:34 -0800 Subject: [PATCH 101/153] add google captcha module to app module --- src/app.module.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app.module.ts b/src/app.module.ts index f4b044a..8282d86 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -13,6 +13,7 @@ import { neo4jCredentials } from "./_domain/constants"; import { ModerationModule } from "./moderation/moderation.module"; import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler"; import { APP_GUARD } from "@nestjs/core"; +import { GoogleCloudRecaptchaEnterpriseModule } from './google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.module'; @Module({ imports: [ @@ -44,6 +45,7 @@ import { APP_GUARD } from "@nestjs/core"; CommentsModule, DatabaseAccessLayerModule, ModerationModule, + GoogleCloudRecaptchaEnterpriseModule, ], providers: [ { From 90b49bffd073fb5f78dc28e5defd6f63fee63762 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Wed, 30 Nov 2022 02:32:46 -0800 Subject: [PATCH 102/153] ignore the google app credentials file --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 821945f..8f37d54 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,5 @@ bun.lockb !.vscode/launch.json !.vscode/extensions.json -.env \ No newline at end of file +.env +igaq-google-application-credentials.json \ No newline at end of file From 597d26117e84c63d3819fe0352638d6119340234 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Wed, 30 Nov 2022 02:33:08 -0800 Subject: [PATCH 103/153] import google captcha module --- src/auth/auth.module.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index bd32ffb..d97a726 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -7,9 +7,15 @@ import { AuthService } from "./services/auth.service"; import { JwtStrategy } from "./strategy"; import { _$ } from "../_domain/injectableTokens"; import { DatabaseAccessLayerModule } from "../database-access-layer/database-access-layer.module"; +import { GoogleCloudRecaptchaEnterpriseModule } from "../google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.module"; @Module({ - imports: [forwardRef(() => DatabaseAccessLayerModule), UsersModule, JwtModule.register({})], + imports: [ + forwardRef(() => DatabaseAccessLayerModule), + UsersModule, + JwtModule.register({}), + GoogleCloudRecaptchaEnterpriseModule, + ], controllers: [AuthController], providers: [ { From 58460efb039bc6761c82d3cf323d8882f99d1f6d Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Wed, 30 Nov 2022 02:33:18 -0800 Subject: [PATCH 104/153] use the captcha on login --- src/auth/controllers/auth.controller.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/auth/controllers/auth.controller.ts b/src/auth/controllers/auth.controller.ts index 8d5407d..19ec75e 100644 --- a/src/auth/controllers/auth.controller.ts +++ b/src/auth/controllers/auth.controller.ts @@ -14,6 +14,7 @@ import { AuthedUser } from "../decorators/authedUser.param.decorator"; import { Role, User } from "../../users/models"; import { Roles } from "../decorators/roles.decorator"; import { RolesGuard } from "../guards/roles.guard"; +import { CaptchaGuard } from "../../google-cloud-recaptcha-enterprise/captcha.guard"; @ApiTags("authentication") @ApiBearerAuth() @@ -27,6 +28,7 @@ export class AuthController { } @Post("signin") + @UseGuards(CaptchaGuard) public signin(@Body() signInPayloadDto: SignInPayloadDto): Promise { return this._authService.signIn(signInPayloadDto); } From 9c85b602e711f2df9ac6a0ad7a7de34c15491fa8 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Wed, 30 Nov 2022 02:40:12 -0800 Subject: [PATCH 105/153] use the captcha guard on the proper endpoints --- src/auth/controllers/auth.controller.ts | 1 + src/comments/comments.module.ts | 2 ++ src/comments/controllers/comments.controller.ts | 4 +++- .../models/userActions.enum.ts | 2 ++ src/posts/controllers/posts.controller.ts | 3 +++ src/posts/posts.module.ts | 8 +++++++- src/users/users.module.ts | 7 ++++++- 7 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/auth/controllers/auth.controller.ts b/src/auth/controllers/auth.controller.ts index 19ec75e..7aa1282 100644 --- a/src/auth/controllers/auth.controller.ts +++ b/src/auth/controllers/auth.controller.ts @@ -23,6 +23,7 @@ export class AuthController { constructor(@Inject(_$.IAuthService) private _authService: IAuthService) {} @Post("signup") + @UseGuards(CaptchaGuard) public signup(@Body() signUpPayloadDto: SignUpPayloadDto): Promise { return this._authService.signup(signUpPayloadDto); } diff --git a/src/comments/comments.module.ts b/src/comments/comments.module.ts index ad41f35..f03864b 100644 --- a/src/comments/comments.module.ts +++ b/src/comments/comments.module.ts @@ -8,6 +8,7 @@ import { HttpModule } from "@nestjs/axios"; import { ModerationModule } from "../moderation/moderation.module"; import { PostsModule } from "../posts/posts.module"; import { CommentsReportService } from "./services/commentReport/commentsReport.service"; +import { GoogleCloudRecaptchaEnterpriseModule } from "../google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.module"; @Module({ controllers: [CommentsController], @@ -16,6 +17,7 @@ import { CommentsReportService } from "./services/commentReport/commentsReport.s HttpModule, ModerationModule, PostsModule, + GoogleCloudRecaptchaEnterpriseModule, ], providers: [ { diff --git a/src/comments/controllers/comments.controller.ts b/src/comments/controllers/comments.controller.ts index 87a8824..864782b 100644 --- a/src/comments/controllers/comments.controller.ts +++ b/src/comments/controllers/comments.controller.ts @@ -21,7 +21,7 @@ import { Roles } from "../../auth/decorators/roles.decorator"; import { OptionalJwtAuthGuard } from "../../auth/guards/optionalJwtAuth.guard"; import { RolesGuard } from "../../auth/guards/roles.guard"; import { DatabaseContext } from "../../database-access-layer/databaseContext"; -import { ModerationPayloadDto } from "../../moderation/dtos/moderatorActions"; +import { ModerationPayloadDto } from "../../moderation/dtos"; import { IModeratorActionsService } from "../../moderation/services/moderatorActions/moderatorActions.service.interface"; import { Role, User } from "../../users/models"; import { _$ } from "../../_domain/injectableTokens"; @@ -29,6 +29,7 @@ import { CommentCreationPayloadDto, ReportCommentPayloadDto, VoteCommentPayloadD import { Comment as CommentModel } from "../models"; import { ICommentsService } from "../services/comments/comments.service.interface"; import { ICommentsReportService } from "../services/commentReport/commentsReport.service.interface"; +import { CaptchaGuard } from "../../google-cloud-recaptcha-enterprise/captcha.guard"; @ApiTags("comments") @Controller("comments") @@ -110,6 +111,7 @@ export class CommentsController { } @Post("create") + @UseGuards(CaptchaGuard) @UseGuards(AuthGuard("jwt")) public async createComment( @Body() commentPayload: CommentCreationPayloadDto diff --git a/src/google-cloud-recaptcha-enterprise/models/userActions.enum.ts b/src/google-cloud-recaptcha-enterprise/models/userActions.enum.ts index e8686e5..7063d88 100644 --- a/src/google-cloud-recaptcha-enterprise/models/userActions.enum.ts +++ b/src/google-cloud-recaptcha-enterprise/models/userActions.enum.ts @@ -4,4 +4,6 @@ export enum UserActionsEnum { CreatePost = "CreatePost", Login = "Login", SignUp = "SignUp", + ReportComment = "ReportComment", + ReportPost = "ReportPost", } diff --git a/src/posts/controllers/posts.controller.ts b/src/posts/controllers/posts.controller.ts index e673c4a..760c0d5 100644 --- a/src/posts/controllers/posts.controller.ts +++ b/src/posts/controllers/posts.controller.ts @@ -29,6 +29,7 @@ import { PostCreationPayloadDto, ReportPostPayloadDto, VotePostPayloadDto } from import { Post as PostModel } from "../models"; import { IPostsService } from "../services/posts/posts.service.interface"; import { IPostsReportService } from "../services/postReport/postsReport.service.interface"; +import { CaptchaGuard } from "../../google-cloud-recaptcha-enterprise/captcha.guard"; @ApiTags("posts") @Controller("posts") @@ -134,6 +135,7 @@ export class PostsController { } @Post("/create") + @UseGuards(CaptchaGuard) @UseGuards(AuthGuard("jwt")) public async createPost(@Body() postPayload: PostCreationPayloadDto): Promise { const post = await this._postsService.authorNewPost(postPayload); @@ -159,6 +161,7 @@ export class PostsController { } @Post("/report") + @UseGuards(CaptchaGuard) @UseGuards(AuthGuard("jwt")) public async reportPost(@Body() reportPostPayload: ReportPostPayloadDto): Promise { await this._postsReportService.reportPost(reportPostPayload); diff --git a/src/posts/posts.module.ts b/src/posts/posts.module.ts index f942be6..4b23135 100644 --- a/src/posts/posts.module.ts +++ b/src/posts/posts.module.ts @@ -12,9 +12,15 @@ import { PostAwardRepository } from "./repositories/postAward/postAward.reposito import { PostTypesController } from "./controllers/postTypes.controller"; import { ModerationModule } from "../moderation/moderation.module"; import { PostsReportService } from "./services/postReport/postsReport.service"; +import { GoogleCloudRecaptchaEnterpriseModule } from "../google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.module"; @Module({ - imports: [forwardRef(() => DatabaseAccessLayerModule), HttpModule, ModerationModule], + imports: [ + forwardRef(() => DatabaseAccessLayerModule), + HttpModule, + ModerationModule, + GoogleCloudRecaptchaEnterpriseModule, + ], providers: [ { provide: _$.IPostsRepository, diff --git a/src/users/users.module.ts b/src/users/users.module.ts index a8e232a..97e99a1 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -12,9 +12,14 @@ import { GendersController } from "./controllers/genders.controller"; import { OpennessController } from "./controllers/openness.controller"; import { UserHistoryService } from "./services/userHistory/userHistory.service"; import { ModerationModule } from "../moderation/moderation.module"; +import { GoogleCloudRecaptchaEnterpriseModule } from "../google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.module"; @Module({ - imports: [forwardRef(() => DatabaseAccessLayerModule), ModerationModule], + imports: [ + forwardRef(() => DatabaseAccessLayerModule), + ModerationModule, + GoogleCloudRecaptchaEnterpriseModule, + ], providers: [ { provide: _$.IUsersRepository, From 286f09c61d032d95f23c1eaa721f8e69fa732def Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Wed, 30 Nov 2022 10:28:01 -0800 Subject: [PATCH 106/153] add pusher package --- package-lock.json | 62 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 63 insertions(+) diff --git a/package-lock.json b/package-lock.json index aaf3651..ceacd69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "passport": "^0.6.0", "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", + "pusher": "^5.1.1-beta", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0", @@ -5613,6 +5614,15 @@ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true }, + "node_modules/is-base64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-base64/-/is-base64-1.1.0.tgz", + "integrity": "sha512-Nlhg7Z2dVC4/PTvIFkgVVNvPHSO2eR/Yd0XzhGiXCXEvWnptXlXa/clQ8aePPiMuxEGcWfzWbGw2Fe3d+Y3v1g==", + "bin": { + "is_base64": "bin/is-base64", + "is-base64": "bin/is-base64" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -8139,6 +8149,21 @@ "node": ">=6" } }, + "node_modules/pusher": { + "version": "5.1.1-beta", + "resolved": "https://registry.npmjs.org/pusher/-/pusher-5.1.1-beta.tgz", + "integrity": "sha512-kGzseVc0MM5kJ6d5TFbtZXeBqDI1enkDq5QjfviEly9/57RSCiY/965ve6IJ5NbMp7T8ZPCqg82FIAkJ+zFxNg==", + "dependencies": { + "abort-controller": "^3.0.0", + "is-base64": "^1.1.0", + "node-fetch": "^2.6.1", + "tweetnacl": "^1.0.0", + "tweetnacl-util": "^0.15.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/qs": { "version": "6.10.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", @@ -9380,6 +9405,16 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + }, + "node_modules/tweetnacl-util": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz", + "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -14156,6 +14191,11 @@ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true }, + "is-base64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-base64/-/is-base64-1.1.0.tgz", + "integrity": "sha512-Nlhg7Z2dVC4/PTvIFkgVVNvPHSO2eR/Yd0XzhGiXCXEvWnptXlXa/clQ8aePPiMuxEGcWfzWbGw2Fe3d+Y3v1g==" + }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -16071,6 +16111,18 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, + "pusher": { + "version": "5.1.1-beta", + "resolved": "https://registry.npmjs.org/pusher/-/pusher-5.1.1-beta.tgz", + "integrity": "sha512-kGzseVc0MM5kJ6d5TFbtZXeBqDI1enkDq5QjfviEly9/57RSCiY/965ve6IJ5NbMp7T8ZPCqg82FIAkJ+zFxNg==", + "requires": { + "abort-controller": "^3.0.0", + "is-base64": "^1.1.0", + "node-fetch": "^2.6.1", + "tweetnacl": "^1.0.0", + "tweetnacl-util": "^0.15.0" + } + }, "qs": { "version": "6.10.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", @@ -16968,6 +17020,16 @@ } } }, + "tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + }, + "tweetnacl-util": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz", + "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==" + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 539ee35..ec46281 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "passport": "^0.6.0", "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", + "pusher": "^5.1.1-beta", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0", From 06a1ddcfb2b4abbb372486ae09145b5e9098d152 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Wed, 30 Nov 2022 10:56:59 -0800 Subject: [PATCH 107/153] add pusher module --- src/app.module.ts | 2 ++ src/pusher/pusher.module.ts | 4 ++++ 2 files changed, 6 insertions(+) create mode 100644 src/pusher/pusher.module.ts diff --git a/src/app.module.ts b/src/app.module.ts index 8282d86..26c9afe 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -14,6 +14,7 @@ import { ModerationModule } from "./moderation/moderation.module"; import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler"; import { APP_GUARD } from "@nestjs/core"; import { GoogleCloudRecaptchaEnterpriseModule } from './google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.module'; +import { PusherModule } from './pusher/pusher.module'; @Module({ imports: [ @@ -46,6 +47,7 @@ import { GoogleCloudRecaptchaEnterpriseModule } from './google-cloud-recaptcha-e DatabaseAccessLayerModule, ModerationModule, GoogleCloudRecaptchaEnterpriseModule, + PusherModule, ], providers: [ { diff --git a/src/pusher/pusher.module.ts b/src/pusher/pusher.module.ts new file mode 100644 index 0000000..874c9fc --- /dev/null +++ b/src/pusher/pusher.module.ts @@ -0,0 +1,4 @@ +import { Module } from '@nestjs/common'; + +@Module({}) +export class PusherModule {} From 4d6f45543e79d9d1502fef3dad0a945a1afc3478 Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Wed, 30 Nov 2022 13:02:46 -0800 Subject: [PATCH 108/153] Fixed comments timestamp --- src/comments/repositories/comment/comments.repository.ts | 6 ++---- .../services/autoModeration/autoModeration.service.ts | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/comments/repositories/comment/comments.repository.ts b/src/comments/repositories/comment/comments.repository.ts index 52a28f7..646755a 100644 --- a/src/comments/repositories/comment/comments.repository.ts +++ b/src/comments/repositories/comment/comments.repository.ts @@ -75,7 +75,7 @@ export class CommentsRepository implements ICommentsRepository { commentContent: $commentContent, pending: $pending })${restrictedQueryString}<-[:${UserToCommentRelTypes.AUTHORED} { - authoredAt: $authoredAt + authoredAt: $authoredProps_authoredAt }]-(u), (c)-[:${CommentToSelfRelTypes.REPLIED}]->(commentParent) `, @@ -84,7 +84,6 @@ export class CommentsRepository implements ICommentsRepository { commentId: comment.commentId, updatedAt: comment.updatedAt, commentContent: comment.commentContent, - authoredAt: authoredProps.authoredAt, // Parent parentId: comment.parentId, @@ -134,7 +133,7 @@ export class CommentsRepository implements ICommentsRepository { commentContent: $commentContent, pending: $pending })${restrictedQueryString}<-[:${UserToCommentRelTypes.AUTHORED} { - authoredAt: $authoredAt + authoredAt: $authoredProps_authoredAt }]-(u), (c)<-[:${PostToCommentRelTypes.HAS_COMMENT}]-(parentPost) `, @@ -143,7 +142,6 @@ export class CommentsRepository implements ICommentsRepository { commentId: comment.commentId, updatedAt: comment.updatedAt, commentContent: comment.commentContent, - authoredAt: authoredProps.authoredAt, pending: comment.pending, diff --git a/src/moderation/services/autoModeration/autoModeration.service.ts b/src/moderation/services/autoModeration/autoModeration.service.ts index 1275445..0175476 100644 --- a/src/moderation/services/autoModeration/autoModeration.service.ts +++ b/src/moderation/services/autoModeration/autoModeration.service.ts @@ -51,7 +51,7 @@ export class AutoModerationService implements IAutoModerationService { // if moderation failed, throw error if (hateSpeechResponseDto.class === "flag") { - if (hateSpeechResponseDto.confidence >= 0.99) { + if (hateSpeechResponseDto.confidence >= 0.92) { // TODO: create a ticket for the admin to review await user.addWasOffendingRecord( From 1e5d16db1ede122f35a7f8f8a72a62f25c7c200d Mon Sep 17 00:00:00 2001 From: Ian Chao <90526260+iantelli@users.noreply.github.com> Date: Wed, 30 Nov 2022 13:23:43 -0800 Subject: [PATCH 109/153] Increasing session time "15m" -> "6h" --- src/auth/services/auth.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth/services/auth.service.ts b/src/auth/services/auth.service.ts index 04f82dd..8091356 100644 --- a/src/auth/services/auth.service.ts +++ b/src/auth/services/auth.service.ts @@ -81,7 +81,7 @@ export class AuthService implements IAuthService { const secret = this._configService.get("JWT_SECRET") || "secret"; return await this._jwtService.signAsync(payload, { - expiresIn: "15m", + expiresIn: "6h", secret: secret, }); } From 8be83cb0b2848c3e6a77f75a65352cdd054d51a6 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Wed, 30 Nov 2022 20:42:26 -0800 Subject: [PATCH 110/153] add pusher module --- src/_domain/utils.ts | 0 src/pusher/controllers/pusher.controller.ts | 38 ++++++ .../models/pusherUserPoolItem.interface.ts | 6 + src/pusher/pusher.constant.ts | 7 ++ src/pusher/pusher.module.ts | 30 ++++- src/pusher/pusher.types.ts | 17 +++ .../pusher/pusher.service.interface.ts | 13 +++ src/pusher/services/pusher/pusher.service.ts | 53 +++++++++ .../pusherUserPool.service.interface.ts | 11 ++ .../pusherUserPool.service.ts | 110 ++++++++++++++++++ 10 files changed, 281 insertions(+), 4 deletions(-) create mode 100644 src/_domain/utils.ts create mode 100644 src/pusher/controllers/pusher.controller.ts create mode 100644 src/pusher/models/pusherUserPoolItem.interface.ts create mode 100644 src/pusher/pusher.constant.ts create mode 100644 src/pusher/pusher.types.ts create mode 100644 src/pusher/services/pusher/pusher.service.interface.ts create mode 100644 src/pusher/services/pusher/pusher.service.ts create mode 100644 src/pusher/services/pusherUserPoolServer/pusherUserPool.service.interface.ts create mode 100644 src/pusher/services/pusherUserPoolServer/pusherUserPool.service.ts diff --git a/src/_domain/utils.ts b/src/_domain/utils.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/pusher/controllers/pusher.controller.ts b/src/pusher/controllers/pusher.controller.ts new file mode 100644 index 0000000..423a45b --- /dev/null +++ b/src/pusher/controllers/pusher.controller.ts @@ -0,0 +1,38 @@ +import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; +import { + ClassSerializerInterceptor, + Controller, + Inject, + Post, + UseGuards, + UseInterceptors, +} from "@nestjs/common"; +import { _$ } from "../../_domain/injectableTokens"; +import { IPusherService } from "../services/pusher/pusher.service.interface"; +import { AuthGuard } from "@nestjs/passport"; +import { AuthedUser } from "../../auth/decorators/authedUser.param.decorator"; +import { User } from "../../users/models"; +import { IPusherUserPoolService } from "../services/pusherUserPoolServer/pusherUserPool.service.interface"; + +@ApiTags("pusher") +@Controller("pusher") +@ApiBearerAuth() +@UseInterceptors(ClassSerializerInterceptor) +export class PusherController { + private readonly _pusherService: IPusherService; + private readonly _pusherUserPoolService: IPusherUserPoolService; + + constructor( + @Inject(_$.IPusherService) pusherService: IPusherService, + @Inject(_$.IPusherUserPoolService) pusherUserPoolService: IPusherUserPoolService + ) { + this._pusherService = pusherService; + this._pusherUserPoolService = pusherUserPoolService; + } + + @Post("/auth") + @UseGuards(AuthGuard("jwt")) + public async authenticate(@AuthedUser() authedUser: User): Promise { + return await this._pusherUserPoolService.addUserToPool(authedUser.userId); // returns poolId + } +} diff --git a/src/pusher/models/pusherUserPoolItem.interface.ts b/src/pusher/models/pusherUserPoolItem.interface.ts new file mode 100644 index 0000000..8ffd9f6 --- /dev/null +++ b/src/pusher/models/pusherUserPoolItem.interface.ts @@ -0,0 +1,6 @@ +export interface PusherUserPoolItem { + userId: UUID; + poolId: string; + lastAccessedAt: number; + createdAt: number; +} diff --git a/src/pusher/pusher.constant.ts b/src/pusher/pusher.constant.ts new file mode 100644 index 0000000..500256a --- /dev/null +++ b/src/pusher/pusher.constant.ts @@ -0,0 +1,7 @@ +export const envKeys = { + appId: "PUSHER_APP_ID", + key: "PUSHER_KEY", + secret: "PUSHER_SECRET", + cluster: "PUSHER_CLUSTER", + useTLS: "PUSHER_USE_TLS", +}; diff --git a/src/pusher/pusher.module.ts b/src/pusher/pusher.module.ts index 874c9fc..68ae747 100644 --- a/src/pusher/pusher.module.ts +++ b/src/pusher/pusher.module.ts @@ -1,4 +1,26 @@ -import { Module } from '@nestjs/common'; - -@Module({}) -export class PusherModule {} +import { Module } from "@nestjs/common"; +import { _$ } from "../_domain/injectableTokens"; +import { PusherService } from "./services/pusher/pusher.service"; +import { PusherController } from "./controllers/pusher.controller"; +import { PusherUserPoolService } from "./services/pusherUserPoolServer/pusherUserPool.service"; + +@Module({ + providers: [ + { + provide: _$.IPusherService, + useClass: PusherService, + }, + { + provide: _$.IPusherUserPoolService, + useClass: PusherUserPoolService, + }, + ], + exports: [ + { + provide: _$.IPusherService, + useClass: PusherService, + }, + ], + controllers: [PusherController], +}) +export class PusherModule {} diff --git a/src/pusher/pusher.types.ts b/src/pusher/pusher.types.ts new file mode 100644 index 0000000..b77acd2 --- /dev/null +++ b/src/pusher/pusher.types.ts @@ -0,0 +1,17 @@ +export enum ChannelTypesEnum { + IGAQ_Notification = "igaq-notification", +} + +export enum EventTypes { + NewCommentOnPost = "new-comment-on-post", + NewCommentOnComment = "new-comment-on-comment", + CommentGotUpVote = "comment-got-up-vote", + CommentGotDownVote = "comment-got-down-vote", + CommentGotRestricted = "comment-got-restricted", + CommentGotApprovedByModerator = "comment-got-approved-by-moderator", + CommentGotPinnedByAuthor = "comment-got-pinned-by-author", + PostGotUpVote = "post-got-up-vote", + PostGotDownVote = "post-got-down-vote", + PostGotRestricted = "post-got-restricted", + PostGotApprovedByModerator = "post-got-approved-by-moderator", +} diff --git a/src/pusher/services/pusher/pusher.service.interface.ts b/src/pusher/services/pusher/pusher.service.interface.ts new file mode 100644 index 0000000..293a391 --- /dev/null +++ b/src/pusher/services/pusher/pusher.service.interface.ts @@ -0,0 +1,13 @@ +import { TriggerParams } from "pusher"; +import { Response } from "node-fetch"; + +export interface IPusherService { + triggerUser(channel: string, event: string, userId: UUID, data: any): Promise; + + trigger( + channel: string | string[], + event: string, + data: any, + params?: TriggerParams + ): Promise; +} diff --git a/src/pusher/services/pusher/pusher.service.ts b/src/pusher/services/pusher/pusher.service.ts new file mode 100644 index 0000000..6b8046a --- /dev/null +++ b/src/pusher/services/pusher/pusher.service.ts @@ -0,0 +1,53 @@ +import { Inject, Injectable, Scope } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import * as Pusher from "pusher"; +import { TriggerParams } from "pusher"; +import { envKeys } from "../../pusher.constant"; +import { IPusherService } from "./pusher.service.interface"; +import { IPusherUserPoolService } from "../pusherUserPoolServer/pusherUserPool.service.interface"; +import { _$ } from "../../../_domain/injectableTokens"; +import { Response } from "node-fetch"; + +@Injectable({ scope: Scope.DEFAULT }) +export class PusherService implements IPusherService { + private readonly pusher: Pusher; + + constructor( + private _configService: ConfigService, + @Inject(_$.IPusherUserPoolService) + private readonly _pusherUserPoolService: IPusherUserPoolService + ) { + console.log("connecting to pusher 🇰🇼"); + this.pusher = new Pusher({ + appId: this._configService.get(envKeys.appId), + key: this._configService.get(envKeys.key), + secret: this._configService.get(envKeys.secret), + cluster: this._configService.get(envKeys.cluster), + useTLS: this._configService.get(envKeys.useTLS) ?? false, + }); + } + + public async triggerUser( + channel: string, + event: string, + userId: UUID, + data: any + ): Promise { + const poolItems = await this._pusherUserPoolService.getPoolItemsByUserId(userId); + return await Promise.all( + poolItems.map(poolItem => { + console.log(poolItem); + return this.pusher.trigger(`${poolItem.poolId}-${channel}`, event, data); + }) + ); + } + + public async trigger( + channel: string | string[], + event: string, + data: any, + params?: TriggerParams + ): Promise { + return await this.pusher.trigger(channel, event, data, params); + } +} diff --git a/src/pusher/services/pusherUserPoolServer/pusherUserPool.service.interface.ts b/src/pusher/services/pusherUserPoolServer/pusherUserPool.service.interface.ts new file mode 100644 index 0000000..7829781 --- /dev/null +++ b/src/pusher/services/pusherUserPoolServer/pusherUserPool.service.interface.ts @@ -0,0 +1,11 @@ +import { PusherUserPoolItem } from "../../models/pusherUserPoolItem.interface"; + +export interface IPusherUserPoolService { + addUserToPool(userId: UUID): Promise; + + getPoolItemsByUserId(userId: UUID): Promise; + + getPoolItemByPoolId(poolId: string): Promise; + + removePoolId(poolId: string, userId?: UUID): Promise; +} diff --git a/src/pusher/services/pusherUserPoolServer/pusherUserPool.service.ts b/src/pusher/services/pusherUserPoolServer/pusherUserPool.service.ts new file mode 100644 index 0000000..8cd3416 --- /dev/null +++ b/src/pusher/services/pusherUserPoolServer/pusherUserPool.service.ts @@ -0,0 +1,110 @@ +import { Injectable, Logger, Scope } from "@nestjs/common"; +import { makeStringId } from "../../../_domain/utils"; +import { IPusherUserPoolService } from "./pusherUserPool.service.interface"; +import { PusherUserPoolItem } from "../../models/pusherUserPoolItem.interface"; + +@Injectable({ scope: Scope.DEFAULT }) +export class PusherUserPoolService implements IPusherUserPoolService { + private readonly _logger = new Logger(PusherUserPoolService.name); + + private readonly pusherUserPool: Map = new Map< + string, + PusherUserPoolItem + >(); + private readonly pusherUserIdToPoolIds: Map = new Map(); + + constructor() { + this.maintainPool(); + } + + public async addUserToPool(userId: UUID): Promise { + const poolId = makeStringId(6); + + // don't wait + setTimeout(() => { + this.pusherUserPool.set(poolId, { + poolId, + userId, + lastAccessedAt: Date.now(), + createdAt: Date.now(), + }); + + // add the poolId to the dictionary of userId->poolIds + const poolIdsOfUser = this.pusherUserIdToPoolIds.get(userId); + if (poolIdsOfUser) { + this.pusherUserIdToPoolIds.set(userId, [...poolIdsOfUser, poolId]); + } else { + this.pusherUserIdToPoolIds.set(userId, [poolId]); + } + }); + + return poolId; + } + + public async getPoolItemsByUserId(userId: UUID): Promise { + const foundPoolIds = this.pusherUserIdToPoolIds.get(userId); + if (!foundPoolIds) { + return []; + } + + return ( + await Promise.all(foundPoolIds.map(async poolId => this.getPoolItemByPoolId(poolId))) + ).filter(i => i !== undefined); + } + + public async getPoolItemByPoolId(poolId: string): Promise { + const foundPoolItem = this.pusherUserPool.get(poolId); + if (!foundPoolItem) { + return undefined; + } + + // touch the pool item and update its `lastAccessedAt` for future maintenance. + setTimeout(() => { + this.pusherUserPool.set(poolId, { + poolId, + userId: foundPoolItem.userId, + lastAccessedAt: Date.now(), + createdAt: foundPoolItem.createdAt, + }); + }); + + return foundPoolItem; + } + + public async removePoolId(poolId: string, userId?: UUID): Promise { + // remove it from the userId->poolIds dictionary first. + if (userId) { + const foundPoolIds = this.pusherUserIdToPoolIds.get(userId); + if (foundPoolIds && foundPoolIds.includes(poolId)) { + this.pusherUserIdToPoolIds.set(userId, [ + ...foundPoolIds.filter(pi => pi !== poolId), + ]); + } + } else { + for (const poolIds of this.pusherUserIdToPoolIds.values()) { + if (poolIds.includes(poolId)) { + this.pusherUserIdToPoolIds.set(userId, [ + ...poolIds.filter(pi => pi !== poolId), + ]); + } + } + } + + // then remove it from UserPool + this.pusherUserPool.delete(poolId); + } + + private async maintainPool(): Promise { + setTimeout(async () => { + this.pusherUserPool.forEach((poolItem, poolId) => { + if (Date.now() - poolItem.lastAccessedAt > 30 * 60) { + this._logger.verbose( + `PusherUserPool Maintenance: Removing inactive pool item -> poolId = ${poolItem.poolId}. PoolItem: `, + poolItem + ); + this.removePoolId(poolId); + } + }); + }, 10000); + } +} From 4ed52c7aa2d8f5df807fafd20c82286850ad0be9 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Wed, 30 Nov 2022 20:44:11 -0800 Subject: [PATCH 111/153] use pusher module --- src/comments/comments.module.ts | 2 + .../services/comments/comments.service.ts | 95 ++++++++++++++++- src/moderation/moderation.module.ts | 3 +- .../moderatorActions.service.ts | 100 +++++++++++++++++- src/posts/posts.module.ts | 2 + src/posts/services/posts/posts.service.ts | 25 ++++- 6 files changed, 215 insertions(+), 12 deletions(-) diff --git a/src/comments/comments.module.ts b/src/comments/comments.module.ts index f03864b..0fd93f2 100644 --- a/src/comments/comments.module.ts +++ b/src/comments/comments.module.ts @@ -9,6 +9,7 @@ import { ModerationModule } from "../moderation/moderation.module"; import { PostsModule } from "../posts/posts.module"; import { CommentsReportService } from "./services/commentReport/commentsReport.service"; import { GoogleCloudRecaptchaEnterpriseModule } from "../google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.module"; +import { PusherModule } from "../pusher/pusher.module"; @Module({ controllers: [CommentsController], @@ -18,6 +19,7 @@ import { GoogleCloudRecaptchaEnterpriseModule } from "../google-cloud-recaptcha- ModerationModule, PostsModule, GoogleCloudRecaptchaEnterpriseModule, + PusherModule, ], providers: [ { diff --git a/src/comments/services/comments/comments.service.ts b/src/comments/services/comments/comments.service.ts index 7284d92..f41d13e 100644 --- a/src/comments/services/comments/comments.service.ts +++ b/src/comments/services/comments/comments.service.ts @@ -1,4 +1,4 @@ -import { HttpException, Inject, Injectable, Scope } from "@nestjs/common"; +import { HttpException, Inject, Injectable, Logger, Scope } from "@nestjs/common"; import { REQUEST } from "@nestjs/core"; import { Request } from "express"; import { DatabaseContext } from "../../../database-access-layer/databaseContext"; @@ -15,24 +15,31 @@ import { CommentCreationPayloadDto, VoteCommentPayloadDto } from "../../dtos"; import { Comment } from "../../models"; import { CommentToSelfRelTypes } from "../../models/toSelf"; import { ICommentsService } from "./comments.service.interface"; +import { IPusherService } from "../../../pusher/services/pusher/pusher.service.interface"; +import { ChannelTypesEnum, EventTypes } from "../../../pusher/pusher.types"; @Injectable({ scope: Scope.REQUEST }) export class CommentsService implements ICommentsService { + private readonly _logger = new Logger(CommentsService.name); + private readonly _request: Request; private readonly _dbContext: DatabaseContext; private readonly _autoModerationService: IAutoModerationService; private readonly _postService: IPostsService; + private readonly _pusherService: IPusherService; constructor( @Inject(REQUEST) request: Request, @Inject(_$.IDatabaseContext) databaseContext: DatabaseContext, @Inject(_$.IAutoModerationService) autoModerationService: IAutoModerationService, - @Inject(_$.IPostsService) postsService: IPostsService + @Inject(_$.IPostsService) postsService: IPostsService, + @Inject(_$.IPusherService) pusherService: IPusherService ) { this._request = request; this._dbContext = databaseContext; this._autoModerationService = autoModerationService; this._postService = postsService; + this._pusherService = pusherService; } public async authorNewComment(commentPayload: CommentCreationPayloadDto): Promise { @@ -45,18 +52,43 @@ export class CommentsService implements ICommentsService { // if moderation passed, create comment and return it. if (commentPayload.isPost) { - return await this._dbContext.Comments.addCommentToPost( + const foundPost = await this._postService.findPostById(commentPayload.parentId); + const createdComment = await this._dbContext.Comments.addCommentToPost( new Comment({ commentContent: commentPayload.commentContent, authorUser: user, pending: wasOffending, updatedAt: new Date().getTime(), - parentId: commentPayload.parentId, + parentId: foundPost.postId, }) ); + await foundPost.getAuthorUser(); + await foundPost.getPostType(); + this._pusherService + .triggerUser( + ChannelTypesEnum.IGAQ_Notification, + EventTypes.NewCommentOnPost, + foundPost.authorUser.userId, + { + username: user.username, + avatar: user.avatar, + commentContent: createdComment.commentContent, + postTypeName: foundPost.postType.postTypeName, + } + ) + .then(() => + this._logger.verbose( + `Event ${EventTypes.NewCommentOnPost} got pushed to ${user.username}` + ) + ) + .catch(e => + this._logger.error(`Event ${EventTypes.NewCommentOnPost} ERRORED: `, e) + ); + return createdComment; } - return await this._dbContext.Comments.addCommentToComment( + const foundParentComment = await this.findCommentById(commentPayload.parentId); + const createdComment = await this._dbContext.Comments.addCommentToComment( new Comment({ commentContent: commentPayload.commentContent, authorUser: user, @@ -65,6 +97,24 @@ export class CommentsService implements ICommentsService { parentId: commentPayload.parentId, }) ); + await foundParentComment.getAuthorUser(); + this._pusherService + .triggerUser( + ChannelTypesEnum.IGAQ_Notification, + EventTypes.NewCommentOnComment, + foundParentComment.authorUser.userId, + { + username: user.username, + avatar: user.avatar, + commentContent: createdComment.commentContent, + } + ) + .then(() => + this._logger.verbose( + `Event ${EventTypes.NewCommentOnComment} got pushed to ${user.username}` + ) + ) + .catch(e => this._logger.error(`Event ${EventTypes.NewCommentOnComment} ERRORED: `, e)); } public async findCommentById(commentId: UUID): Promise { @@ -170,6 +220,20 @@ export class CommentsService implements ICommentsService { votedAt: voteProps.votedAt, } ); + + const eventType = + voteCommentPayload.voteType === VoteType.UPVOTES + ? EventTypes.CommentGotUpVote + : EventTypes.CommentGotDownVote; + + this._pusherService + .triggerUser(ChannelTypesEnum.IGAQ_Notification, eventType, user.userId, { + commentId: comment.commentId, + username: user.username, + avatar: user.avatar, + }) + .then(() => this._logger.verbose(`Event ${eventType} got pushed to ${user.username}`)) + .catch(e => this._logger.error(`Event ${eventType} ERRORED: `, e)); } public async markAsPinned(commentId: UUID): Promise { @@ -200,6 +264,27 @@ export class CommentsService implements ICommentsService { commentId: commentId, } ); + + this._pusherService + .triggerUser( + ChannelTypesEnum.IGAQ_Notification, + EventTypes.CommentGotPinnedByAuthor, + user.userId, + { + commentId, + postId: parentPost.postId, + username: user.username, + avatar: user.avatar, + } + ) + .then(() => + this._logger.verbose( + `Event ${EventTypes.CommentGotPinnedByAuthor} got pushed to ${user.username}` + ) + ) + .catch(e => + this._logger.error(`Event ${EventTypes.CommentGotPinnedByAuthor} ERRORED: `, e) + ); } public async markAsUnpinned(commentId: UUID): Promise { diff --git a/src/moderation/moderation.module.ts b/src/moderation/moderation.module.ts index 315df9a..b72c816 100644 --- a/src/moderation/moderation.module.ts +++ b/src/moderation/moderation.module.ts @@ -5,9 +5,10 @@ import { _$ } from "../_domain/injectableTokens"; import { HttpModule } from "@nestjs/axios"; import { DatabaseAccessLayerModule } from "../database-access-layer/database-access-layer.module"; import { ModerationController } from "./controllers/moderation.controller"; +import { PusherModule } from "../pusher/pusher.module"; @Module({ - imports: [HttpModule, forwardRef(() => DatabaseAccessLayerModule)], + imports: [HttpModule, forwardRef(() => DatabaseAccessLayerModule), PusherModule], providers: [ { provide: _$.IAutoModerationService, diff --git a/src/moderation/services/moderatorActions/moderatorActions.service.ts b/src/moderation/services/moderatorActions/moderatorActions.service.ts index 262c3ea..928d20c 100644 --- a/src/moderation/services/moderatorActions/moderatorActions.service.ts +++ b/src/moderation/services/moderatorActions/moderatorActions.service.ts @@ -1,12 +1,14 @@ import { IModeratorActionsService } from "./moderatorActions.service.interface"; -import { HttpException, Inject, Injectable, Scope } from "@nestjs/common"; +import { HttpException, Inject, Injectable, Logger, Scope } from "@nestjs/common"; import { _$ } from "../../../_domain/injectableTokens"; import { DatabaseContext } from "../../../database-access-layer/databaseContext"; import { DeletedProps, RestrictedProps } from "../../../_domain/models/toSelf"; import { Comment } from "../../../comments/models"; import { Post } from "../../../posts/models"; -import { ModerationPayloadDto } from "../../dtos/moderatorActions"; +import { ModerationPayloadDto } from "../../dtos"; import { GotBannedProps } from "../../../users/models/toSelf"; +import { IPusherService } from "../../../pusher/services/pusher/pusher.service.interface"; +import { ChannelTypesEnum, EventTypes } from "../../../pusher/pusher.types"; /** * This service is responsible for moderating posts and comments. @@ -17,10 +19,16 @@ import { GotBannedProps } from "../../../users/models/toSelf"; */ @Injectable({ scope: Scope.DEFAULT }) export class ModeratorActionsService implements IModeratorActionsService { + private readonly _logger = new Logger(ModeratorActionsService.name); private readonly _dbContext: DatabaseContext; + private readonly _pusherService: IPusherService; - constructor(@Inject(_$.IDatabaseContext) dbContext: DatabaseContext) { + constructor( + @Inject(_$.IDatabaseContext) dbContext: DatabaseContext, + @Inject(_$.IPusherService) pusherService: IPusherService + ) { this._dbContext = dbContext; + this._pusherService = pusherService; } public async banUser(payload: ModerationPayloadDto): Promise { @@ -134,11 +142,33 @@ export class ModeratorActionsService implements IModeratorActionsService { const restrictedProps = new RestrictedProps({ restrictedAt: Date.now(), moderatorId: payload.moderatorId, - reason: payload.moderatorId, + reason: payload.reason, }); await this._dbContext.Comments.restrictComment(payload.id, restrictedProps); comment.restrictedProps = restrictedProps; + await comment.getAuthorUser(); + this._pusherService + .triggerUser( + ChannelTypesEnum.IGAQ_Notification, + EventTypes.CommentGotRestricted, + comment.authorUser.userId, + { + commentId: comment.commentId, + reason: payload.reason, + username: comment.authorUser.username, + avatar: comment.authorUser.avatar, + } + ) + .then(() => + this._logger.verbose( + `Event ${EventTypes.CommentGotRestricted} got pushed to ${comment.authorUser.username}` + ) + ) + .catch(e => + this._logger.error(`Event ${EventTypes.CommentGotRestricted} ERRORED: `, e) + ); + return comment; } @@ -158,6 +188,26 @@ export class ModeratorActionsService implements IModeratorActionsService { await this._dbContext.Posts.restrictPost(payload.id, restrictedProps); post.restrictedProps = restrictedProps; + await post.getAuthorUser(); + this._pusherService + .triggerUser( + ChannelTypesEnum.IGAQ_Notification, + EventTypes.PostGotRestricted, + post.authorUser.userId, + { + postId: post.postId, + reason: payload.reason, + username: post.authorUser.username, + avatar: post.authorUser.avatar, + } + ) + .then(() => + this._logger.verbose( + `Event ${EventTypes.PostGotRestricted} got pushed to ${post.authorUser.username}` + ) + ) + .catch(e => this._logger.error(`Event ${EventTypes.PostGotRestricted} ERRORED: `, e)); + return post; } @@ -199,6 +249,27 @@ export class ModeratorActionsService implements IModeratorActionsService { comment.pending = false; await this._dbContext.Comments.updateComment(comment); + await comment.getAuthorUser(); + this._pusherService + .triggerUser( + ChannelTypesEnum.IGAQ_Notification, + EventTypes.PostGotApprovedByModerator, + comment.authorUser.userId, + { + commentId: comment.commentId, + username: comment.authorUser.username, + avatar: comment.authorUser.avatar, + } + ) + .then(() => + this._logger.verbose( + `Event ${EventTypes.CommentGotApprovedByModerator} got pushed to ${comment.authorUser.username}` + ) + ) + .catch(e => + this._logger.error(`Event ${EventTypes.CommentGotApprovedByModerator} ERRORED: `, e) + ); + return comment; } @@ -220,6 +291,27 @@ export class ModeratorActionsService implements IModeratorActionsService { ); post.pending = false; + await post.getAuthorUser(); + this._pusherService + .triggerUser( + ChannelTypesEnum.IGAQ_Notification, + EventTypes.PostGotApprovedByModerator, + post.authorUser.userId, + { + postId: post.postId, + username: post.authorUser.username, + avatar: post.authorUser.avatar, + } + ) + .then(() => + this._logger.verbose( + `Event ${EventTypes.PostGotApprovedByModerator} got pushed to ${post.authorUser.username}` + ) + ) + .catch(e => + this._logger.error(`Event ${EventTypes.PostGotApprovedByModerator} ERRORED: `, e) + ); + return post; } diff --git a/src/posts/posts.module.ts b/src/posts/posts.module.ts index 4b23135..5bd1532 100644 --- a/src/posts/posts.module.ts +++ b/src/posts/posts.module.ts @@ -13,6 +13,7 @@ import { PostTypesController } from "./controllers/postTypes.controller"; import { ModerationModule } from "../moderation/moderation.module"; import { PostsReportService } from "./services/postReport/postsReport.service"; import { GoogleCloudRecaptchaEnterpriseModule } from "../google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.module"; +import { PusherModule } from "../pusher/pusher.module"; @Module({ imports: [ @@ -20,6 +21,7 @@ import { GoogleCloudRecaptchaEnterpriseModule } from "../google-cloud-recaptcha- HttpModule, ModerationModule, GoogleCloudRecaptchaEnterpriseModule, + PusherModule, ], providers: [ { diff --git a/src/posts/services/posts/posts.service.ts b/src/posts/services/posts/posts.service.ts index c4b3d42..86eff25 100644 --- a/src/posts/services/posts/posts.service.ts +++ b/src/posts/services/posts/posts.service.ts @@ -1,4 +1,4 @@ -import { HttpException, Inject, Injectable, Scope } from "@nestjs/common"; +import { HttpException, Inject, Injectable, Logger, Scope } from "@nestjs/common"; import { REQUEST } from "@nestjs/core"; import { Request } from "express"; import { Comment } from "../../../comments/models"; @@ -11,21 +11,28 @@ import { VoteType } from "../../../_domain/models/enums"; import { PostCreationPayloadDto, VotePostPayloadDto } from "../../dtos"; import { Post, PostTag } from "../../models"; import { IPostsService, postSortCallback } from "./posts.service.interface"; +import { IPusherService } from "../../../pusher/services/pusher/pusher.service.interface"; +import { ChannelTypesEnum, EventTypes } from "../../../pusher/pusher.types"; @Injectable({ scope: Scope.REQUEST }) export class PostsService implements IPostsService { + private readonly _logger = new Logger(PostsService.name); + private readonly _request: Request; private readonly _dbContext: DatabaseContext; private readonly _autoModerationService: IAutoModerationService; + private readonly _pusherService: IPusherService; constructor( @Inject(REQUEST) request: Request, @Inject(_$.IDatabaseContext) databaseContext: DatabaseContext, - @Inject(_$.IAutoModerationService) autoModerationService: IAutoModerationService + @Inject(_$.IAutoModerationService) autoModerationService: IAutoModerationService, + @Inject(_$.IPusherService) pusherService: IPusherService ) { this._request = request; this._dbContext = databaseContext; this._autoModerationService = autoModerationService; + this._pusherService = pusherService; } public async authorNewPost(postPayload: PostCreationPayloadDto): Promise { @@ -308,6 +315,20 @@ export class PostsService implements IPostsService { votedAt: voteProps.votedAt, } ); + + const eventType = + votePostPayload.voteType === VoteType.UPVOTES + ? EventTypes.PostGotUpVote + : EventTypes.PostGotDownVote; + + this._pusherService + .triggerUser(ChannelTypesEnum.IGAQ_Notification, eventType, user.userId, { + postId: post.postId, + username: user.username, + avatar: user.avatar, + }) + .then(() => this._logger.verbose(`Event ${eventType} got pushed to ${user.username}`)) + .catch(e => this._logger.error(`Event ${eventType} ERRORED: `, e)); } private getUserFromRequest(): User { From b71d2014ae35956b5a1ed999c93362cbd4f4d569 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Wed, 30 Nov 2022 20:44:34 -0800 Subject: [PATCH 112/153] integrate pusher related stuff --- src/_domain/injectableTokens.ts | 5 +++++ src/_domain/utils.ts | 9 +++++++++ src/app.module.ts | 4 ++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/_domain/injectableTokens.ts b/src/_domain/injectableTokens.ts index 2e4dd79..c9ecb26 100644 --- a/src/_domain/injectableTokens.ts +++ b/src/_domain/injectableTokens.ts @@ -1,5 +1,6 @@ import { IUserHistoryService } from "../users/services/userHistory/userHistory.service.interface"; import { IGoogleCloudRecaptchaEnterpriseService } from "../google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.service.interface"; +import { IPusherUserPoolService } from "../pusher/services/pusherUserPoolServer/pusherUserPool.service.interface"; const injectableTokens = { // Database Context @@ -38,5 +39,9 @@ const injectableTokens = { // Google Cloud reCAPTCHA Enterprise Module IGoogleCloudRecaptchaEnterpriseService: Symbol("IGoogleCloudRecaptchaEnterpriseService"), + + // Pusher + IPusherService: Symbol("IPusherService"), + IPusherUserPoolService: Symbol("IPusherUserPoolService"), }; export { injectableTokens as _$ }; diff --git a/src/_domain/utils.ts b/src/_domain/utils.ts index e69de29..2620293 100644 --- a/src/_domain/utils.ts +++ b/src/_domain/utils.ts @@ -0,0 +1,9 @@ +export function makeStringId(length) { + let result = ""; + const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const charactersLength = characters.length; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; +} diff --git a/src/app.module.ts b/src/app.module.ts index 26c9afe..2a96897 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -13,8 +13,8 @@ import { neo4jCredentials } from "./_domain/constants"; import { ModerationModule } from "./moderation/moderation.module"; import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler"; import { APP_GUARD } from "@nestjs/core"; -import { GoogleCloudRecaptchaEnterpriseModule } from './google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.module'; -import { PusherModule } from './pusher/pusher.module'; +import { GoogleCloudRecaptchaEnterpriseModule } from "./google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.module"; +import { PusherModule } from "./pusher/pusher.module"; @Module({ imports: [ From 96c3c11a0b093abef34897915b06ce8b507ae5a0 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Wed, 30 Nov 2022 22:14:01 -0800 Subject: [PATCH 113/153] give the postId as well --- .../services/comments/comments.service.ts | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/comments/services/comments/comments.service.ts b/src/comments/services/comments/comments.service.ts index f41d13e..ff41263 100644 --- a/src/comments/services/comments/comments.service.ts +++ b/src/comments/services/comments/comments.service.ts @@ -226,14 +226,21 @@ export class CommentsService implements ICommentsService { ? EventTypes.CommentGotUpVote : EventTypes.CommentGotDownVote; - this._pusherService - .triggerUser(ChannelTypesEnum.IGAQ_Notification, eventType, user.userId, { - commentId: comment.commentId, - username: user.username, - avatar: user.avatar, - }) - .then(() => this._logger.verbose(`Event ${eventType} got pushed to ${user.username}`)) - .catch(e => this._logger.error(`Event ${eventType} ERRORED: `, e)); + // don't wait for the push notification. + setTimeout(async () => { + const parentPost = await this.findParentPost(comment.commentId); + this._pusherService + .triggerUser(ChannelTypesEnum.IGAQ_Notification, eventType, user.userId, { + postId: parentPost.postId, + commentId: comment.commentId, + username: user.username, + avatar: user.avatar, + }) + .then(() => + this._logger.verbose(`Event ${eventType} got pushed to ${user.username}`) + ) + .catch(e => this._logger.error(`Event ${eventType} ERRORED: `, e)); + }); } public async markAsPinned(commentId: UUID): Promise { From 177dec5d1b074fcf0ad068b4d0fa982ccc00b9cc Mon Sep 17 00:00:00 2001 From: iantelli <2ndex@duck.com> Date: Wed, 30 Nov 2022 22:57:56 -0800 Subject: [PATCH 114/153] Added getPendingPosts method for moderator --- .../controllers/moderation.controller.ts | 10 +++++++++- .../moderatorActions.service.interface.ts | 2 ++ .../moderatorActions/moderatorActions.service.ts | 5 +++++ .../post/posts.repository.interface.ts | 2 ++ src/posts/repositories/post/posts.repository.ts | 15 +++++++++++++++ 5 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/moderation/controllers/moderation.controller.ts b/src/moderation/controllers/moderation.controller.ts index 730a00f..cd4d320 100644 --- a/src/moderation/controllers/moderation.controller.ts +++ b/src/moderation/controllers/moderation.controller.ts @@ -142,5 +142,13 @@ export class ModerationController { moderationPayload.moderatorId = user.userId; await this._moderationActionsService.banUser(moderationPayload); } -} + @Get("/pendingPosts") + @Roles(Role.MODERATOR) + @UseGuards(AuthGuard("jwt"), RolesGuard) + public async getPendingPosts() { + const posts = await this._moderationActionsService.getPendingPosts(); + const decoratedPosts = posts.map(post => post.toJSON()); + return await Promise.all(decoratedPosts); + } +} diff --git a/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts b/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts index 7d0c8a7..5f6c65d 100644 --- a/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts +++ b/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts @@ -85,4 +85,6 @@ export interface IModeratorActionsService { * @param userId */ unbanUser(userId: UUID): Promise; + + getPendingPosts(): Promise; } diff --git a/src/moderation/services/moderatorActions/moderatorActions.service.ts b/src/moderation/services/moderatorActions/moderatorActions.service.ts index 928d20c..e830720 100644 --- a/src/moderation/services/moderatorActions/moderatorActions.service.ts +++ b/src/moderation/services/moderatorActions/moderatorActions.service.ts @@ -315,6 +315,11 @@ export class ModeratorActionsService implements IModeratorActionsService { return post; } + public async getPendingPosts(): Promise { + const posts = await this._dbContext.Posts.getPendingPosts(); + return posts; + } + /** * @description * This method is to find a comment from the database and throw an error if it does not exist. if it does exist, it will return the comment. diff --git a/src/posts/repositories/post/posts.repository.interface.ts b/src/posts/repositories/post/posts.repository.interface.ts index c9d52a4..5b5947c 100644 --- a/src/posts/repositories/post/posts.repository.interface.ts +++ b/src/posts/repositories/post/posts.repository.interface.ts @@ -23,4 +23,6 @@ export interface IPostsRepository { restrictPost(postId: string, restrictedProps: RestrictedProps): Promise; unrestrictPost(postId: string): Promise; + + getPendingPosts(): Promise; } diff --git a/src/posts/repositories/post/posts.repository.ts b/src/posts/repositories/post/posts.repository.ts index b6da45b..3647cba 100644 --- a/src/posts/repositories/post/posts.repository.ts +++ b/src/posts/repositories/post/posts.repository.ts @@ -226,4 +226,19 @@ export class PostsRepository implements IPostsRepository { } ); } + + public async getPendingPosts(): Promise { + const queryResult = await this._neo4jService.tryReadAsync( + ` + MATCH (p:Post { pending: $pending }) + RETURN p + `, + { + pending: true, + } + ); + const records = queryResult.records; + if (records.length === 0) return []; + return records.map(record => new Post(record.get("p").properties, this._neo4jService)); + } } From d8ce608e24b5b61ff9b7dd6b21d1bfcd9cb08f54 Mon Sep 17 00:00:00 2001 From: iantelli <2ndex@duck.com> Date: Thu, 1 Dec 2022 01:50:38 -0800 Subject: [PATCH 115/153] Updating moderator permissions --- src/users/controllers/users.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/users/controllers/users.controller.ts b/src/users/controllers/users.controller.ts index bf9ad05..c0da0a3 100644 --- a/src/users/controllers/users.controller.ts +++ b/src/users/controllers/users.controller.ts @@ -50,7 +50,7 @@ export class UsersController { } @Get() - @Roles(Role.ADMIN) + @Roles(Role.MODERATOR) @UseGuards(AuthGuard("jwt"), RolesGuard) public async index(): Promise { const users = await this._dbContext.Users.findAll(); @@ -59,7 +59,7 @@ export class UsersController { } @Get(":userId") - @Roles(Role.ADMIN) + @Roles(Role.MODERATOR) @UseGuards(AuthGuard("jwt"), RolesGuard) public async getUserById(@Param("userId", new ParseUUIDPipe()) userId: string): Promise { const user = await this._dbContext.Users.findUserById(userId); From 5e91192f2925b83d8eae1d5ce2901eb19d6ac4d6 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Thu, 1 Dec 2022 03:23:11 -0800 Subject: [PATCH 116/153] add event emitter --- package-lock.json | 32 +++++++++++++++++++ package.json | 1 + src/_domain/_domain.module.ts | 0 src/_domain/eventTypes.ts | 0 .../listeners /commentEvents.listener.ts | 0 src/_domain/listeners /postEvents.listener.ts | 0 .../events/commentGotPinnedByAuthor.event.ts | 0 src/comments/events/commentGotVote.event.ts | 0 src/comments/events/index.ts | 0 src/comments/events/newComment.event.ts | 0 .../commentGotApprovedByModerator.event.ts | 13 ++++++++ .../events/commentGotRestricted.event.ts | 15 +++++++++ src/moderation/events/index.ts | 0 .../postGotApprovedByModerator.event.ts | 0 .../events/postGotRestricted.event.ts | 0 src/posts/events/index.ts | 0 src/posts/events/postGotVote.event.ts | 5 +++ .../notificationStashPoolItem.interface.ts | 0 ...ificationMessageMaker.service.interface.ts | 0 .../notificationMessageMaker.service.ts | 0 ...notificationStashPool.service.interface.ts | 0 .../notificationStashPool.service.ts | 0 22 files changed, 66 insertions(+) create mode 100644 src/_domain/_domain.module.ts create mode 100644 src/_domain/eventTypes.ts create mode 100644 src/_domain/listeners /commentEvents.listener.ts create mode 100644 src/_domain/listeners /postEvents.listener.ts create mode 100644 src/comments/events/commentGotPinnedByAuthor.event.ts create mode 100644 src/comments/events/commentGotVote.event.ts create mode 100644 src/comments/events/index.ts create mode 100644 src/comments/events/newComment.event.ts create mode 100644 src/moderation/events/commentGotApprovedByModerator.event.ts create mode 100644 src/moderation/events/commentGotRestricted.event.ts create mode 100644 src/moderation/events/index.ts create mode 100644 src/moderation/events/postGotApprovedByModerator.event.ts create mode 100644 src/moderation/events/postGotRestricted.event.ts create mode 100644 src/posts/events/index.ts create mode 100644 src/posts/events/postGotVote.event.ts create mode 100644 src/pusher/models/notificationStashPoolItem.interface.ts create mode 100644 src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.interface.ts create mode 100644 src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts create mode 100644 src/pusher/services/notificationStashPool/notificationStashPool.service.interface.ts create mode 100644 src/pusher/services/notificationStashPool/notificationStashPool.service.ts diff --git a/package-lock.json b/package-lock.json index ceacd69..fe20034 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.2.0", "@nestjs/core": "^9.0.0", + "@nestjs/event-emitter": "^1.3.1", "@nestjs/jwt": "^9.0.0", "@nestjs/passport": "^9.0.0", "@nestjs/platform-express": "^9.0.0", @@ -1692,6 +1693,19 @@ } } }, + "node_modules/@nestjs/event-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-1.3.1.tgz", + "integrity": "sha512-AmHkPTe/cP1lbQEm15TIe9IDEAszl5VAR8HjMS2TDtNRuSzwyoJgZUVcRnH7Yk9/2DX5qMtmw6a1MHeR8DD+rw==", + "dependencies": { + "eventemitter2": "6.4.6" + }, + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0", + "@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0", + "reflect-metadata": "^0.1.12" + } + }, "node_modules/@nestjs/jwt": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-9.0.0.tgz", @@ -4558,6 +4572,11 @@ "node": ">=6" } }, + "node_modules/eventemitter2": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.6.tgz", + "integrity": "sha512-OHqo4wbHX5VbvlbB6o6eDwhYmiTjrpWACjF8Pmof/GTD6rdBNdZFNck3xlhqOiQFGCOoq3uzHvA0cQpFHIGVAQ==" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -11168,6 +11187,14 @@ "uuid": "9.0.0" } }, + "@nestjs/event-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-1.3.1.tgz", + "integrity": "sha512-AmHkPTe/cP1lbQEm15TIe9IDEAszl5VAR8HjMS2TDtNRuSzwyoJgZUVcRnH7Yk9/2DX5qMtmw6a1MHeR8DD+rw==", + "requires": { + "eventemitter2": "6.4.6" + } + }, "@nestjs/jwt": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-9.0.0.tgz", @@ -13382,6 +13409,11 @@ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" }, + "eventemitter2": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.6.tgz", + "integrity": "sha512-OHqo4wbHX5VbvlbB6o6eDwhYmiTjrpWACjF8Pmof/GTD6rdBNdZFNck3xlhqOiQFGCOoq3uzHvA0cQpFHIGVAQ==" + }, "events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", diff --git a/package.json b/package.json index ec46281..f92313d 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.2.0", "@nestjs/core": "^9.0.0", + "@nestjs/event-emitter": "^1.3.1", "@nestjs/jwt": "^9.0.0", "@nestjs/passport": "^9.0.0", "@nestjs/platform-express": "^9.0.0", diff --git a/src/_domain/_domain.module.ts b/src/_domain/_domain.module.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/_domain/eventTypes.ts b/src/_domain/eventTypes.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/_domain/listeners /commentEvents.listener.ts b/src/_domain/listeners /commentEvents.listener.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/_domain/listeners /postEvents.listener.ts b/src/_domain/listeners /postEvents.listener.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/comments/events/commentGotPinnedByAuthor.event.ts b/src/comments/events/commentGotPinnedByAuthor.event.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/comments/events/commentGotVote.event.ts b/src/comments/events/commentGotVote.event.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/comments/events/index.ts b/src/comments/events/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/comments/events/newComment.event.ts b/src/comments/events/newComment.event.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/moderation/events/commentGotApprovedByModerator.event.ts b/src/moderation/events/commentGotApprovedByModerator.event.ts new file mode 100644 index 0000000..a75f1c8 --- /dev/null +++ b/src/moderation/events/commentGotApprovedByModerator.event.ts @@ -0,0 +1,13 @@ +export class CommentGotApprovedByModeratorEvent { + subscriberId: UUID; + + commentId: UUID; + + username: string; + + avatar: string; + + constructor(partial?: Partial) { + Object.assign(this, partial); + } +} diff --git a/src/moderation/events/commentGotRestricted.event.ts b/src/moderation/events/commentGotRestricted.event.ts new file mode 100644 index 0000000..dbb39eb --- /dev/null +++ b/src/moderation/events/commentGotRestricted.event.ts @@ -0,0 +1,15 @@ +export class CommentGotRestrictedEvent { + subscriberUserId: UUID; + + commentId: UUID; + + reason: string; + + username: string; + + avatar: string; + + constructor(partial?: Partial) { + Object.assign(partial); + } +} diff --git a/src/moderation/events/index.ts b/src/moderation/events/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/moderation/events/postGotApprovedByModerator.event.ts b/src/moderation/events/postGotApprovedByModerator.event.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/moderation/events/postGotRestricted.event.ts b/src/moderation/events/postGotRestricted.event.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/posts/events/index.ts b/src/posts/events/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/posts/events/postGotVote.event.ts b/src/posts/events/postGotVote.event.ts new file mode 100644 index 0000000..f2a17fe --- /dev/null +++ b/src/posts/events/postGotVote.event.ts @@ -0,0 +1,5 @@ +export class PostGotUpvoteEvent { + postId: UUID; + username: string; + avatar: string; +} diff --git a/src/pusher/models/notificationStashPoolItem.interface.ts b/src/pusher/models/notificationStashPoolItem.interface.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.interface.ts b/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.interface.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts b/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/pusher/services/notificationStashPool/notificationStashPool.service.interface.ts b/src/pusher/services/notificationStashPool/notificationStashPool.service.interface.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/pusher/services/notificationStashPool/notificationStashPool.service.ts b/src/pusher/services/notificationStashPool/notificationStashPool.service.ts new file mode 100644 index 0000000..e69de29 From 6cbf9979877a7732990e6c82ae606e27fb36c7ed Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Thu, 1 Dec 2022 03:23:28 -0800 Subject: [PATCH 117/153] add a notification message maker service --- ...ificationMessageMaker.service.interface.ts | 40 ++++++ .../notificationMessageMaker.service.ts | 114 ++++++++++++++++++ 2 files changed, 154 insertions(+) diff --git a/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.interface.ts b/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.interface.ts index e69de29..a6db1ec 100644 --- a/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.interface.ts +++ b/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.interface.ts @@ -0,0 +1,40 @@ +import { EventTypes } from "../../../_domain/eventTypes"; + +export interface INotificationMessageMakerService { + templates: { [key in EventTypes]: (p: object) => string }; + + makeForNewCommentOnPost(p: { + username: string; + postTypeName: string; + commentContent: string; + }): string; + + makeForNewCommentOnComment(p: { username: string; commentContent: string }): string; + + makeForCommentGotUpVote(p: { username: string; postId: UUID; commentId: UUID }): string; + + makeForCommentGotDownVote(p: { username: string; postId: UUID; commentId: UUID }): string; + + makeForCommentGotRestricted(p: { commentContent: string; reason: string }): string; + + makeForCommentGotApprovedByModerator(p: { + commentId: UUID; + postId: UUID; + username: string; + }): string; + + makeForCommentGotPinnedByAuthor(p: { + commentId: UUID; + postId: UUID; + commentContent: string; + username: string; + }): string; + + makeForPostGotUpVote(p: { username: string; postId: UUID }): string; + + makeForPostGotDownVote(p: { username: string; postId: UUID }): string; + + makeForPostGotRestricted(p: { postTitle: string; reason: string }): string; + + makeForPostGotApprovedByModerator(p: { username: string; postId: UUID }): string; +} diff --git a/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts b/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts index e69de29..fd582a6 100644 --- a/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts +++ b/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts @@ -0,0 +1,114 @@ +import { Injectable, Scope } from "@nestjs/common"; +import { INotificationMessageMakerService } from "./notificationMessageMaker.service.interface"; +import { EventTypes } from "../../../_domain/eventTypes"; + +@Injectable({ scope: Scope.DEFAULT }) +export class NotificationMessageMakerService implements INotificationMessageMakerService { + public readonly templates = { + [EventTypes.NewCommentOnPost]: (p: { + username: string; + postTypeName: string; + commentContent: string; + }) => + `${p.username} replied to your ${p.postTypeName} '${ + p.commentContent?.slice(0, 20) ?? "" + }'`, + [EventTypes.NewCommentOnComment]: (p: { username: string; commentContent: string }) => + `${p.username} replied to your comment '${p.commentContent?.slice(0, 20) ?? ""}'`, + [EventTypes.CommentGotUpVote]: (p: { username: string; postId: UUID; commentId: UUID }) => + `${p.username} up voted your comment check it out!`, + [EventTypes.CommentGotDownVote]: (p: { username: string; postId: UUID; commentId: UUID }) => + `${p.username} down voted your comment go to comment`, + [EventTypes.CommentGotRestricted]: (p: { commentContent: string; reason: string }) => + `A Moderator has restricted your comment due to: "${ + p.reason + }". Comment: '${p.commentContent.slice(0, 20)}'"`, + [EventTypes.CommentGotApprovedByModerator]: (p: { + commentId: UUID; + postId: UUID; + username: string; + }) => + `Our moderator, ${p.username}, allowed your comment to be published. go to comment`, + [EventTypes.CommentGotPinnedByAuthor]: (p: { + commentId: UUID; + postId: UUID; + commentContent: string; + username: string; + }) => + `${p.username} pinned your comment '${p.commentContent.slice( + 0, + 20 + )}'. Check it out`, + [EventTypes.PostGotUpVote]: (p: { username: string; postId: UUID }) => + `${p.username} up voted your post check it out!`, + [EventTypes.PostGotDownVote]: (p: { username: string; postId: UUID }) => + `${p.username} down voted your post go to post`, + [EventTypes.PostGotRestricted]: (p: { postTitle: string; reason: string }) => + `A Moderator has restricted your post due to: "${ + p.reason + }". Post Title: '${p.postTitle.slice(0, 20)}'"`, + [EventTypes.PostGotApprovedByModerator]: (p: { username: string; postId: UUID }) => + `Our moderator, ${p.username}, allowed your post to be published. go to post`, + }; + + public makeForNewCommentOnPost(p: { + username: string; + postTypeName: string; + commentContent: string; + }): string { + return this.templates[EventTypes.NewCommentOnPost](p); + } + + public makeForNewCommentOnComment(p: { username: string; commentContent: string }): string { + return this.templates[EventTypes.NewCommentOnComment](p); + } + + public makeForCommentGotUpVote(p: { username: string; postId: UUID; commentId: UUID }): string { + return this.templates[EventTypes.CommentGotUpVote](p); + } + + public makeForCommentGotDownVote(p: { + username: string; + postId: UUID; + commentId: UUID; + }): string { + return this.templates[EventTypes.CommentGotDownVote](p); + } + + public makeForCommentGotRestricted(p: { commentContent: string; reason: string }): string { + return this.templates[EventTypes.CommentGotRestricted](p); + } + + public makeForCommentGotApprovedByModerator(p: { + commentId: UUID; + postId: UUID; + username: string; + }): string { + return this.templates[EventTypes.CommentGotApprovedByModerator](p); + } + + public makeForCommentGotPinnedByAuthor(p: { + commentId: UUID; + postId: UUID; + commentContent: string; + username: string; + }): string { + return this.templates[EventTypes.CommentGotPinnedByAuthor](p); + } + + public makeForPostGotUpVote(p: { username: string; postId: UUID }): string { + return this.templates[EventTypes.PostGotUpVote](p); + } + + public makeForPostGotDownVote(p: { username: string; postId: UUID }): string { + return this.templates[EventTypes.PostGotDownVote](p); + } + + public makeForPostGotRestricted(p: { postTitle: string; reason: string }): string { + return this.templates[EventTypes.PostGotRestricted](p); + } + + public makeForPostGotApprovedByModerator(p: { username: string; postId: UUID }): string { + return this.templates[EventTypes.PostGotApprovedByModerator](p); + } +} From b42792bab1d2511ed2bd69e24b61d8f157b8994e Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Thu, 1 Dec 2022 03:23:49 -0800 Subject: [PATCH 118/153] add notification stash pool --- ...notificationStashPool.service.interface.ts | 14 ++++++ .../notificationStashPool.service.ts | 47 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/src/pusher/services/notificationStashPool/notificationStashPool.service.interface.ts b/src/pusher/services/notificationStashPool/notificationStashPool.service.interface.ts index e69de29..838c2d2 100644 --- a/src/pusher/services/notificationStashPool/notificationStashPool.service.interface.ts +++ b/src/pusher/services/notificationStashPool/notificationStashPool.service.interface.ts @@ -0,0 +1,14 @@ +import { NotificationStashPoolItem } from "../../models/notificationStashPoolItem.interface"; + +export interface INotificationStashPoolService { + stashNotification( + userId: UUID, + message: string, + avatar?: string, + username?: string + ): Promise; + + popStashNotifications(userId: UUID): Promise; + + dropStashNotification(userId: UUID): Promise; +} diff --git a/src/pusher/services/notificationStashPool/notificationStashPool.service.ts b/src/pusher/services/notificationStashPool/notificationStashPool.service.ts index e69de29..164a113 100644 --- a/src/pusher/services/notificationStashPool/notificationStashPool.service.ts +++ b/src/pusher/services/notificationStashPool/notificationStashPool.service.ts @@ -0,0 +1,47 @@ +import { INotificationStashPoolService } from "./notificationStashPool.service.interface"; +import { Injectable, Scope } from "@nestjs/common"; +import { NotificationStashPoolItem } from "../../models/notificationStashPoolItem.interface"; +import { generateUUID } from "../../../_domain/utils"; + +@Injectable({ scope: Scope.DEFAULT }) +export class NotificationStashPoolService implements INotificationStashPoolService { + private readonly notificationStashPool: Map; + + public async stashNotification( + userId: UUID, + message: string, + avatar?: string, + username?: string + ): Promise { + const createdStash: NotificationStashPoolItem = { + stashToken: generateUUID(), // aka notificationId + message, + avatar, + username, + userId, + pushedAt: Date.now(), + }; + + const foundStash = this.notificationStashPool.get(userId); + if (!foundStash) { + this.notificationStashPool.set(userId, [createdStash]); + return createdStash; + } + + this.notificationStashPool.set(userId, [...foundStash, createdStash]); + } + + public async popStashNotifications(userId: UUID): Promise { + const foundStash = this.notificationStashPool.get(userId); + if (!foundStash) { + return []; + } + + this.notificationStashPool.delete(userId); + return foundStash; + } + + public async dropStashNotification(userId: UUID): Promise { + return this.notificationStashPool.delete(userId); + } +} From 7ecf8d6a6161bff8dc45947fa918ccddc90220bd Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Thu, 1 Dec 2022 03:24:16 -0800 Subject: [PATCH 119/153] update pusher module with the latest changes --- src/pusher/controllers/pusher.controller.ts | 18 +++++++++++++++++- .../notificationStashPoolItem.interface.ts | 12 ++++++++++++ src/pusher/pusher.module.ts | 18 ++++++++++++++++++ src/pusher/pusher.types.ts | 14 ++------------ 4 files changed, 49 insertions(+), 13 deletions(-) diff --git a/src/pusher/controllers/pusher.controller.ts b/src/pusher/controllers/pusher.controller.ts index 423a45b..725556f 100644 --- a/src/pusher/controllers/pusher.controller.ts +++ b/src/pusher/controllers/pusher.controller.ts @@ -2,6 +2,7 @@ import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; import { ClassSerializerInterceptor, Controller, + Get, Inject, Post, UseGuards, @@ -13,6 +14,9 @@ import { AuthGuard } from "@nestjs/passport"; import { AuthedUser } from "../../auth/decorators/authedUser.param.decorator"; import { User } from "../../users/models"; import { IPusherUserPoolService } from "../services/pusherUserPoolServer/pusherUserPool.service.interface"; +import { NotificationStashPoolItem } from "../models/notificationStashPoolItem.interface"; +import { INotificationStashPoolService } from "../services/notificationStashPool/notificationStashPool.service.interface"; +import { auth } from "neo4j-driver"; @ApiTags("pusher") @Controller("pusher") @@ -21,13 +25,17 @@ import { IPusherUserPoolService } from "../services/pusherUserPoolServer/pusherU export class PusherController { private readonly _pusherService: IPusherService; private readonly _pusherUserPoolService: IPusherUserPoolService; + private readonly _notificationStashPoolService: INotificationStashPoolService; constructor( @Inject(_$.IPusherService) pusherService: IPusherService, - @Inject(_$.IPusherUserPoolService) pusherUserPoolService: IPusherUserPoolService + @Inject(_$.IPusherUserPoolService) pusherUserPoolService: IPusherUserPoolService, + @Inject(_$.INotificationStashPoolService) + notificationStashPoolService: INotificationStashPoolService ) { this._pusherService = pusherService; this._pusherUserPoolService = pusherUserPoolService; + this._notificationStashPoolService = notificationStashPoolService; } @Post("/auth") @@ -35,4 +43,12 @@ export class PusherController { public async authenticate(@AuthedUser() authedUser: User): Promise { return await this._pusherUserPoolService.addUserToPool(authedUser.userId); // returns poolId } + + @Get("/notifications/stash") + @UseGuards(AuthGuard("jwt")) + public async getStashedNotifications( + @AuthedUser() authedUser: User + ): Promise { + return await this._notificationStashPoolService.popStashNotifications(authedUser.userId); + } } diff --git a/src/pusher/models/notificationStashPoolItem.interface.ts b/src/pusher/models/notificationStashPoolItem.interface.ts index e69de29..be7504b 100644 --- a/src/pusher/models/notificationStashPoolItem.interface.ts +++ b/src/pusher/models/notificationStashPoolItem.interface.ts @@ -0,0 +1,12 @@ +export interface NotificationStashPoolItem { + userId: UUID; + + username?: string; + avatar?: string; + + stashToken: UUID; + + message: string; + + pushedAt: number; +} diff --git a/src/pusher/pusher.module.ts b/src/pusher/pusher.module.ts index 68ae747..18f6da6 100644 --- a/src/pusher/pusher.module.ts +++ b/src/pusher/pusher.module.ts @@ -3,6 +3,8 @@ import { _$ } from "../_domain/injectableTokens"; import { PusherService } from "./services/pusher/pusher.service"; import { PusherController } from "./controllers/pusher.controller"; import { PusherUserPoolService } from "./services/pusherUserPoolServer/pusherUserPool.service"; +import { NotificationStashPoolService } from "./services/notificationStashPool/notificationStashPool.service"; +import { NotificationMessageMakerService } from "./services/notificationMessageMaker/notificationMessageMaker.service"; @Module({ providers: [ @@ -14,12 +16,28 @@ import { PusherUserPoolService } from "./services/pusherUserPoolServer/pusherUse provide: _$.IPusherUserPoolService, useClass: PusherUserPoolService, }, + { + provide: _$.INotificationStashPoolService, + useClass: NotificationStashPoolService, + }, + { + provide: _$.INotificationMessageMakerService, + useClass: NotificationMessageMakerService, + }, ], exports: [ { provide: _$.IPusherService, useClass: PusherService, }, + { + provide: _$.INotificationStashPoolService, + useClass: NotificationStashPoolService, + }, + { + provide: _$.INotificationMessageMakerService, + useClass: NotificationMessageMakerService, + }, ], controllers: [PusherController], }) diff --git a/src/pusher/pusher.types.ts b/src/pusher/pusher.types.ts index b77acd2..add9086 100644 --- a/src/pusher/pusher.types.ts +++ b/src/pusher/pusher.types.ts @@ -2,16 +2,6 @@ export enum ChannelTypesEnum { IGAQ_Notification = "igaq-notification", } -export enum EventTypes { - NewCommentOnPost = "new-comment-on-post", - NewCommentOnComment = "new-comment-on-comment", - CommentGotUpVote = "comment-got-up-vote", - CommentGotDownVote = "comment-got-down-vote", - CommentGotRestricted = "comment-got-restricted", - CommentGotApprovedByModerator = "comment-got-approved-by-moderator", - CommentGotPinnedByAuthor = "comment-got-pinned-by-author", - PostGotUpVote = "post-got-up-vote", - PostGotDownVote = "post-got-down-vote", - PostGotRestricted = "post-got-restricted", - PostGotApprovedByModerator = "post-got-approved-by-moderator", +export enum PusherEvents { + UserReceivesNotification = "receive-notification", } From 58e3e02bf796701711f79d43e550a1adfcfcf3ff Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Thu, 1 Dec 2022 03:24:39 -0800 Subject: [PATCH 120/153] add events' dtos for posts --- src/posts/events/index.ts | 1 + src/posts/events/postGotVote.event.ts | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/posts/events/index.ts b/src/posts/events/index.ts index e69de29..629d079 100644 --- a/src/posts/events/index.ts +++ b/src/posts/events/index.ts @@ -0,0 +1 @@ +export * from "./postGotVote.event"; diff --git a/src/posts/events/postGotVote.event.ts b/src/posts/events/postGotVote.event.ts index f2a17fe..8a30779 100644 --- a/src/posts/events/postGotVote.event.ts +++ b/src/posts/events/postGotVote.event.ts @@ -1,5 +1,13 @@ -export class PostGotUpvoteEvent { +export class PostGotVoteEvent { + subscriberId: UUID; + postId: UUID; + username: string; + avatar: string; + + constructor(partial?: Partial) { + Object.assign(this, partial); + } } From ddd59d523d7e04e8d086280f334407f9166fb822 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Thu, 1 Dec 2022 03:24:59 -0800 Subject: [PATCH 121/153] revise post service with the event emitter --- src/posts/services/posts/posts.service.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/posts/services/posts/posts.service.ts b/src/posts/services/posts/posts.service.ts index 86eff25..cdc9a6e 100644 --- a/src/posts/services/posts/posts.service.ts +++ b/src/posts/services/posts/posts.service.ts @@ -11,28 +11,29 @@ import { VoteType } from "../../../_domain/models/enums"; import { PostCreationPayloadDto, VotePostPayloadDto } from "../../dtos"; import { Post, PostTag } from "../../models"; import { IPostsService, postSortCallback } from "./posts.service.interface"; -import { IPusherService } from "../../../pusher/services/pusher/pusher.service.interface"; -import { ChannelTypesEnum, EventTypes } from "../../../pusher/pusher.types"; +import { EventEmitter2 } from "@nestjs/event-emitter"; +import { PostGotVoteEvent } from "../../events"; +import { EventTypes } from "../../../_domain/eventTypes"; @Injectable({ scope: Scope.REQUEST }) export class PostsService implements IPostsService { private readonly _logger = new Logger(PostsService.name); + private readonly _eventEmitter: EventEmitter2; private readonly _request: Request; private readonly _dbContext: DatabaseContext; private readonly _autoModerationService: IAutoModerationService; - private readonly _pusherService: IPusherService; constructor( + eventEmitter: EventEmitter2, @Inject(REQUEST) request: Request, @Inject(_$.IDatabaseContext) databaseContext: DatabaseContext, - @Inject(_$.IAutoModerationService) autoModerationService: IAutoModerationService, - @Inject(_$.IPusherService) pusherService: IPusherService + @Inject(_$.IAutoModerationService) autoModerationService: IAutoModerationService ) { + this._eventEmitter = eventEmitter; this._request = request; this._dbContext = databaseContext; this._autoModerationService = autoModerationService; - this._pusherService = pusherService; } public async authorNewPost(postPayload: PostCreationPayloadDto): Promise { @@ -321,14 +322,15 @@ export class PostsService implements IPostsService { ? EventTypes.PostGotUpVote : EventTypes.PostGotDownVote; - this._pusherService - .triggerUser(ChannelTypesEnum.IGAQ_Notification, eventType, user.userId, { + this._eventEmitter.emit( + eventType, + new PostGotVoteEvent({ + subscriberId: user.userId, postId: post.postId, username: user.username, avatar: user.avatar, }) - .then(() => this._logger.verbose(`Event ${eventType} got pushed to ${user.username}`)) - .catch(e => this._logger.error(`Event ${eventType} ERRORED: `, e)); + ); } private getUserFromRequest(): User { From 9bf575114319d6b5875c4c6cea141cc7c4c8613b Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Thu, 1 Dec 2022 03:25:16 -0800 Subject: [PATCH 122/153] make a _domain module --- src/_domain/_domain.module.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/_domain/_domain.module.ts b/src/_domain/_domain.module.ts index e69de29..fc0850c 100644 --- a/src/_domain/_domain.module.ts +++ b/src/_domain/_domain.module.ts @@ -0,0 +1,10 @@ +import { Module } from "@nestjs/common"; +import { PusherModule } from "../pusher/pusher.module"; +import { CommentEventsListener } from "./listeners /commentEvents.listener"; +import { PostEventsListener } from "./listeners /postEvents.listener"; + +@Module({ + imports: [PusherModule], + providers: [CommentEventsListener, PostEventsListener], +}) +export class DomainModule {} From d99dbf3d044b20eeed1d6cf2769902f294413987 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Thu, 1 Dec 2022 03:25:32 -0800 Subject: [PATCH 123/153] add event listeners --- src/_domain/eventTypes.ts | 13 ++ src/_domain/injectableTokens.ts | 6 +- .../listeners /commentEvents.listener.ts | 188 ++++++++++++++++++ src/_domain/listeners /postEvents.listener.ts | 125 ++++++++++++ src/_domain/utils.ts | 6 + 5 files changed, 335 insertions(+), 3 deletions(-) diff --git a/src/_domain/eventTypes.ts b/src/_domain/eventTypes.ts index e69de29..c15c802 100644 --- a/src/_domain/eventTypes.ts +++ b/src/_domain/eventTypes.ts @@ -0,0 +1,13 @@ +export enum EventTypes { + NewCommentOnPost = "new-comment-on-post", + NewCommentOnComment = "new-comment-on-comment", + CommentGotUpVote = "comment-got-up-vote", + CommentGotDownVote = "comment-got-down-vote", + CommentGotRestricted = "comment-got-restricted", + CommentGotApprovedByModerator = "comment-got-approved-by-moderator", + CommentGotPinnedByAuthor = "comment-got-pinned-by-author", + PostGotUpVote = "post-got-up-vote", + PostGotDownVote = "post-got-down-vote", + PostGotRestricted = "post-got-restricted", + PostGotApprovedByModerator = "post-got-approved-by-moderator", +} diff --git a/src/_domain/injectableTokens.ts b/src/_domain/injectableTokens.ts index c9ecb26..1fc22c0 100644 --- a/src/_domain/injectableTokens.ts +++ b/src/_domain/injectableTokens.ts @@ -1,6 +1,4 @@ -import { IUserHistoryService } from "../users/services/userHistory/userHistory.service.interface"; -import { IGoogleCloudRecaptchaEnterpriseService } from "../google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.service.interface"; -import { IPusherUserPoolService } from "../pusher/services/pusherUserPoolServer/pusherUserPool.service.interface"; +import { INotificationMessageMakerService } from "../pusher/services/notificationMessageMaker/notificationMessageMaker.service.interface"; const injectableTokens = { // Database Context @@ -43,5 +41,7 @@ const injectableTokens = { // Pusher IPusherService: Symbol("IPusherService"), IPusherUserPoolService: Symbol("IPusherUserPoolService"), + INotificationStashPoolService: Symbol("INotificationStashPoolService"), + INotificationMessageMakerService: Symbol("INotificationMessageMakerService"), }; export { injectableTokens as _$ }; diff --git a/src/_domain/listeners /commentEvents.listener.ts b/src/_domain/listeners /commentEvents.listener.ts index e69de29..f9e1c2b 100644 --- a/src/_domain/listeners /commentEvents.listener.ts +++ b/src/_domain/listeners /commentEvents.listener.ts @@ -0,0 +1,188 @@ +import { Inject, Injectable, Logger } from "@nestjs/common"; +import { IPusherService } from "../../pusher/services/pusher/pusher.service.interface"; +import { _$ } from "../injectableTokens"; +import { OnEvent } from "@nestjs/event-emitter"; +import { ChannelTypesEnum, PusherEvents } from "../../pusher/pusher.types"; +import { CommentGotApprovedByModeratorEvent } from "../../moderation/events"; +import { CommentGotRestrictedEvent } from "../../moderation/events"; +import { + CommentGotPinnedByAuthorEvent, + CommentGotVoteEvent, + NewCommentEvent, +} from "../../comments/events"; +import { INotificationMessageMakerService } from "../../pusher/services/notificationMessageMaker/notificationMessageMaker.service.interface"; +import { EventTypes } from "../eventTypes"; +import { INotificationStashPoolService } from "../../pusher/services/notificationStashPool/notificationStashPool.service.interface"; + +@Injectable() +export class CommentEventsListener { + private readonly _logger = new Logger(CommentEventsListener.name); + + private readonly _pusherService: IPusherService; + private readonly _notificationMessageMakerService: INotificationMessageMakerService; + private readonly _notificationStashPoolService: INotificationStashPoolService; + + constructor( + @Inject(_$.IPusherService) pusherService: IPusherService, + @Inject(_$.INotificationMessageMakerService) + notificationMessageMakerService: INotificationMessageMakerService, + @Inject(_$.INotificationStashPoolService) + notificationStashPoolService: INotificationStashPoolService + ) { + this._pusherService = pusherService; + this._notificationMessageMakerService = notificationMessageMakerService; + this._notificationStashPoolService = notificationStashPoolService; + } + + @OnEvent(EventTypes.CommentGotUpVote) + public async handleCommentGotUpVote(event: CommentGotVoteEvent): Promise { + const message = this._notificationMessageMakerService.makeForCommentGotUpVote({ + username: event.username, + postId: event.postId, + commentId: event.commentId, + }); + + await this.stashAndPushNotification( + EventTypes.CommentGotUpVote, + event.subscriberId, + event.username, + event.avatar, + message + ); + } + + @OnEvent(EventTypes.CommentGotDownVote) + public async handleCommentGotDownVote(event: CommentGotVoteEvent): Promise { + const message = this._notificationMessageMakerService.makeForCommentGotDownVote({ + username: event.username, + postId: event.postId, + commentId: event.commentId, + }); + + await this.stashAndPushNotification( + EventTypes.CommentGotDownVote, + event.subscriberId, + event.username, + event.avatar, + message + ); + } + + @OnEvent(EventTypes.CommentGotRestricted) + public async handleCommentGotRestricted(event: CommentGotRestrictedEvent): Promise { + const message = this._notificationMessageMakerService.makeForCommentGotRestricted({ + commentContent: event.commentContent, + reason: event.reason, + }); + + await this.stashAndPushNotification( + EventTypes.CommentGotRestricted, + event.subscriberUserId, + event.username, + event.avatar, + message + ); + } + + @OnEvent(EventTypes.CommentGotPinnedByAuthor) + public async handleCommentGotPinnedByAuthor( + event: CommentGotPinnedByAuthorEvent + ): Promise { + const message = this._notificationMessageMakerService.makeForCommentGotPinnedByAuthor({ + commentId: event.commentId, + postId: event.postId, + commentContent: event.commentContent, + username: event.username, + }); + + await this.stashAndPushNotification( + EventTypes.CommentGotPinnedByAuthor, + event.subscriberId, + event.username, + event.avatar, + message + ); + } + + @OnEvent(EventTypes.CommentGotApprovedByModerator) + public async handleCommentGotApprovedByModerator( + event: CommentGotApprovedByModeratorEvent + ): Promise { + const message = this._notificationMessageMakerService.makeForCommentGotApprovedByModerator({ + commentId: event.commentId, + postId: event.postId, + username: event.username, + }); + + await this.stashAndPushNotification( + EventTypes.CommentGotApprovedByModerator, + event.subscriberId, + event.username, + event.avatar, + message + ); + } + + @OnEvent(EventTypes.NewCommentOnComment) + public async handleNewCommentOnComment(event: NewCommentEvent): Promise { + const message = this._notificationMessageMakerService.makeForNewCommentOnComment({ + username: event.username, + commentContent: event.commentContent, + }); + + await this.stashAndPushNotification( + EventTypes.NewCommentOnComment, + event.subscriberId, + event.username, + event.avatar, + message + ); + } + + @OnEvent(EventTypes.NewCommentOnPost) + public async handleNewCommentOnPost(event: NewCommentEvent): Promise { + const message = this._notificationMessageMakerService.makeForNewCommentOnPost({ + username: event.username, + postTypeName: event.postTypeName, + commentContent: event.commentContent, + }); + + await this.stashAndPushNotification( + EventTypes.NewCommentOnPost, + event.subscriberId, + event.username, + event.avatar, + message + ); + } + + private async stashAndPushNotification( + evenType: EventTypes, + subscriberId: UUID, + username: string, + avatar: string, + message: string + ): Promise { + const stashPoolItem = await this._notificationStashPoolService.stashNotification( + subscriberId, + message, + avatar, + username + ); + + this._pusherService + .triggerUser( + ChannelTypesEnum.IGAQ_Notification, + PusherEvents.UserReceivesNotification, + subscriberId, + { + username: username, + avatar: avatar, + composedMessage: message, + stashToken: stashPoolItem.stashToken, + } + ) + .then(() => this._logger.verbose(`Event ${evenType} got pushed to ${username}`)) + .catch(e => this._logger.error(`Event ${evenType} ERRORED: `, e)); + } +} diff --git a/src/_domain/listeners /postEvents.listener.ts b/src/_domain/listeners /postEvents.listener.ts index e69de29..1970060 100644 --- a/src/_domain/listeners /postEvents.listener.ts +++ b/src/_domain/listeners /postEvents.listener.ts @@ -0,0 +1,125 @@ +import { Inject, Injectable, Logger } from "@nestjs/common"; +import { OnEvent } from "@nestjs/event-emitter"; +import { ChannelTypesEnum, PusherEvents } from "../../pusher/pusher.types"; +import { PostGotVoteEvent } from "../../posts/events"; +import { PostGotApprovedByModeratorEvent, PostGotRestrictedEvent } from "../../moderation/events"; +import { _$ } from "../injectableTokens"; +import { IPusherService } from "../../pusher/services/pusher/pusher.service.interface"; +import { INotificationMessageMakerService } from "../../pusher/services/notificationMessageMaker/notificationMessageMaker.service.interface"; +import { EventTypes } from "../eventTypes"; +import { INotificationStashPoolService } from "../../pusher/services/notificationStashPool/notificationStashPool.service.interface"; + +@Injectable() +export class PostEventsListener { + private readonly _logger = new Logger(PostEventsListener.name); + + private readonly _pusherService: IPusherService; + private readonly _notificationMessageMakerService: INotificationMessageMakerService; + private readonly _notificationStashPoolService: INotificationStashPoolService; + + constructor( + @Inject(_$.IPusherService) pusherService: IPusherService, + @Inject(_$.INotificationMessageMakerService) + notificationMessageMakerService: INotificationMessageMakerService, + @Inject(_$.INotificationStashPoolService) + notificationStashPoolService: INotificationStashPoolService + ) { + this._pusherService = pusherService; + this._notificationMessageMakerService = notificationMessageMakerService; + this._notificationStashPoolService = notificationStashPoolService; + } + + @OnEvent(EventTypes.PostGotUpVote) + public async handlePostGotUpVote(event: PostGotVoteEvent): Promise { + const message = this._notificationMessageMakerService.makeForPostGotUpVote({ + username: event.username, + postId: event.postId, + }); + + await this.stashAndPushNotification( + EventTypes.PostGotUpVote, + event.subscriberId, + event.username, + event.avatar, + message + ); + } + + @OnEvent(EventTypes.PostGotDownVote) + public async handlePostGotDownVote(event: PostGotVoteEvent): Promise { + const message = this._notificationMessageMakerService.makeForPostGotDownVote({ + username: event.username, + postId: event.postId, + }); + + await this.stashAndPushNotification( + EventTypes.PostGotDownVote, + event.subscriberId, + event.username, + event.avatar, + message + ); + } + + @OnEvent(EventTypes.PostGotApprovedByModerator) + public async handlePostGotApprovedByModerator( + event: PostGotApprovedByModeratorEvent + ): Promise { + const message = this._notificationMessageMakerService.makeForPostGotApprovedByModerator({ + username: event.username, + postId: event.postId, + }); + + await this.stashAndPushNotification( + EventTypes.PostGotApprovedByModerator, + event.subscriberId, + event.username, + event.avatar, + message + ); + } + + @OnEvent(EventTypes.PostGotRestricted) + public async handlePostGotRestricted(event: PostGotRestrictedEvent): Promise { + const message = this._notificationMessageMakerService.makeForPostGotRestricted({ + postTitle: event.postTitle, + reason: event.reason, + }); + + await this.stashAndPushNotification( + EventTypes.PostGotRestricted, + event.subscriberId, + event.username, + event.avatar, + message + ); + } + + private async stashAndPushNotification( + evenType: EventTypes, + subscriberId: UUID, + username: string, + avatar: string, + message: string + ): Promise { + const stashPoolItem = await this._notificationStashPoolService.stashNotification( + subscriberId, + message + ); + + this._pusherService + .triggerUser( + ChannelTypesEnum.IGAQ_Notification, + PusherEvents.UserReceivesNotification, + subscriberId, + { + username: username, + avatar: avatar, + composedMessage: message, + stashToken: stashPoolItem.stashToken, + } + ) + .then(() => this._logger.verbose(`Event ${evenType} got pushed to ${username}`)) + .catch(e => this._logger.error(`Event ${evenType} ERRORED: `, e)); + } +} diff --git a/src/_domain/utils.ts b/src/_domain/utils.ts index 2620293..30c9bb6 100644 --- a/src/_domain/utils.ts +++ b/src/_domain/utils.ts @@ -1,3 +1,5 @@ +import { v4 as uuidv4 } from "uuid"; + export function makeStringId(length) { let result = ""; const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; @@ -7,3 +9,7 @@ export function makeStringId(length) { } return result; } + +export function generateUUID(): UUID { + return uuidv4(); +} From 1763d4cffdefdd6f34b8201f3898455f1735690f Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Thu, 1 Dec 2022 03:25:50 -0800 Subject: [PATCH 124/153] add comments dto events --- .../events/commentGotPinnedByAuthor.event.ts | 17 +++++++++++++++++ src/comments/events/commentGotVote.event.ts | 15 +++++++++++++++ src/comments/events/index.ts | 3 +++ src/comments/events/newComment.event.ts | 15 +++++++++++++++ 4 files changed, 50 insertions(+) diff --git a/src/comments/events/commentGotPinnedByAuthor.event.ts b/src/comments/events/commentGotPinnedByAuthor.event.ts index e69de29..40fba9d 100644 --- a/src/comments/events/commentGotPinnedByAuthor.event.ts +++ b/src/comments/events/commentGotPinnedByAuthor.event.ts @@ -0,0 +1,17 @@ +export class CommentGotPinnedByAuthorEvent { + subscriberId: UUID; + + commentId: UUID; + + postId: UUID; + + commentContent: string; + + username: string; + + avatar: string; + + constructor(partial?: Partial) { + Object.assign(this, partial); + } +} diff --git a/src/comments/events/commentGotVote.event.ts b/src/comments/events/commentGotVote.event.ts index e69de29..9028033 100644 --- a/src/comments/events/commentGotVote.event.ts +++ b/src/comments/events/commentGotVote.event.ts @@ -0,0 +1,15 @@ +export class CommentGotVoteEvent { + subscriberId: UUID; + + postId: UUID; + + commentId: UUID; + + username: string; + + avatar: string; + + constructor(partial?: Partial) { + Object.assign(this, partial); + } +} diff --git a/src/comments/events/index.ts b/src/comments/events/index.ts index e69de29..5c90b9b 100644 --- a/src/comments/events/index.ts +++ b/src/comments/events/index.ts @@ -0,0 +1,3 @@ +export * from "./newComment.event"; +export * from "./commentGotVote.event"; +export * from "./commentGotPinnedByAuthor.event"; diff --git a/src/comments/events/newComment.event.ts b/src/comments/events/newComment.event.ts index e69de29..03eea54 100644 --- a/src/comments/events/newComment.event.ts +++ b/src/comments/events/newComment.event.ts @@ -0,0 +1,15 @@ +export class NewCommentEvent { + subscriberId: UUID; + + username: string; + + avatar: string; + + commentContent: string; + + postTypeName?: string; + + constructor(partial?: Partial) { + Object.assign(this, partial); + } +} From 53290359b30b1456aaf167b856e4292736b6738f Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Thu, 1 Dec 2022 03:26:07 -0800 Subject: [PATCH 125/153] add moderation events dtos --- .../events/commentGotApprovedByModerator.event.ts | 2 ++ .../events/commentGotRestricted.event.ts | 2 +- src/moderation/events/index.ts | 4 ++++ .../events/postGotApprovedByModerator.event.ts | 13 +++++++++++++ src/moderation/events/postGotRestricted.event.ts | 15 +++++++++++++++ 5 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/moderation/events/commentGotApprovedByModerator.event.ts b/src/moderation/events/commentGotApprovedByModerator.event.ts index a75f1c8..1faf6e1 100644 --- a/src/moderation/events/commentGotApprovedByModerator.event.ts +++ b/src/moderation/events/commentGotApprovedByModerator.event.ts @@ -3,6 +3,8 @@ export class CommentGotApprovedByModeratorEvent { commentId: UUID; + postId: UUID; + username: string; avatar: string; diff --git a/src/moderation/events/commentGotRestricted.event.ts b/src/moderation/events/commentGotRestricted.event.ts index dbb39eb..b07296a 100644 --- a/src/moderation/events/commentGotRestricted.event.ts +++ b/src/moderation/events/commentGotRestricted.event.ts @@ -1,7 +1,7 @@ export class CommentGotRestrictedEvent { subscriberUserId: UUID; - commentId: UUID; + commentContent: string; reason: string; diff --git a/src/moderation/events/index.ts b/src/moderation/events/index.ts index e69de29..41b0b7a 100644 --- a/src/moderation/events/index.ts +++ b/src/moderation/events/index.ts @@ -0,0 +1,4 @@ +export * from "./postGotApprovedByModerator.event"; +export * from "./postGotRestricted.event"; +export * from "./commentGotApprovedByModerator.event"; +export * from "./commentGotRestricted.event"; diff --git a/src/moderation/events/postGotApprovedByModerator.event.ts b/src/moderation/events/postGotApprovedByModerator.event.ts index e69de29..d288ba5 100644 --- a/src/moderation/events/postGotApprovedByModerator.event.ts +++ b/src/moderation/events/postGotApprovedByModerator.event.ts @@ -0,0 +1,13 @@ +export class PostGotApprovedByModeratorEvent { + subscriberId: UUID; + + postId: UUID; + + username: string; + + avatar: string; + + constructor(partial?: Partial) { + Object.assign(this, partial); + } +} diff --git a/src/moderation/events/postGotRestricted.event.ts b/src/moderation/events/postGotRestricted.event.ts index e69de29..3ae0e13 100644 --- a/src/moderation/events/postGotRestricted.event.ts +++ b/src/moderation/events/postGotRestricted.event.ts @@ -0,0 +1,15 @@ +export class PostGotRestrictedEvent { + subscriberId: UUID; + + postTitle: string; + + reason: string; + + username: string; + + avatar: string; + + constructor(partial?: Partial) { + Object.assign(this, partial); + } +} From 80fde92ff1fafe4a981448389e0e27fcf7197804 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Thu, 1 Dec 2022 03:26:24 -0800 Subject: [PATCH 126/153] add the event emimtter and domain module to app module --- src/app.module.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app.module.ts b/src/app.module.ts index 2a96897..b043cfc 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -15,9 +15,11 @@ import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler"; import { APP_GUARD } from "@nestjs/core"; import { GoogleCloudRecaptchaEnterpriseModule } from "./google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.module"; import { PusherModule } from "./pusher/pusher.module"; +import { EventEmitterModule } from "@nestjs/event-emitter"; @Module({ imports: [ + EventEmitterModule.forRoot(), ThrottlerModule.forRoot({ ttl: 69, limit: 42, From 51cabc69743d5cfe1a5969ef0419e58bf81b563f Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Thu, 1 Dec 2022 03:26:54 -0800 Subject: [PATCH 127/153] move the findParentPost from service to repository --- .../comment/comments.repository.interface.ts | 3 + .../comment/comments.repository.ts | 20 ++- .../comments/comments.service.interface.ts | 1 + .../services/comments/comments.service.ts | 132 +++++++----------- 4 files changed, 72 insertions(+), 84 deletions(-) diff --git a/src/comments/repositories/comment/comments.repository.interface.ts b/src/comments/repositories/comment/comments.repository.interface.ts index 2084a3b..f87aed9 100644 --- a/src/comments/repositories/comment/comments.repository.interface.ts +++ b/src/comments/repositories/comment/comments.repository.interface.ts @@ -1,5 +1,6 @@ import { DeletedProps, RestrictedProps } from "../../../_domain/models/toSelf"; import { Comment } from "../../models"; +import { Post } from "../../../posts/models"; export interface ICommentsRepository { findAll(): Promise; @@ -20,4 +21,6 @@ export interface ICommentsRepository { markAsDeleted(commentId: UUID, deletedProps: DeletedProps): Promise; removeDeletedMark(commentId: UUID): Promise; + + findParentPost(commentId: UUID): Promise; } diff --git a/src/comments/repositories/comment/comments.repository.ts b/src/comments/repositories/comment/comments.repository.ts index 646755a..1a64e49 100644 --- a/src/comments/repositories/comment/comments.repository.ts +++ b/src/comments/repositories/comment/comments.repository.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from "@nestjs/common"; +import { HttpException, Inject, Injectable } from "@nestjs/common"; import { Neo4jService } from "../../../neo4j/services/neo4j.service"; import { AuthoredProps, UserToCommentRelTypes } from "../../../users/models/toComment"; import { RestrictedProps, _ToSelfRelTypes, DeletedProps } from "../../../_domain/models/toSelf"; @@ -6,6 +6,7 @@ import { Comment } from "../../models"; import { CommentToSelfRelTypes } from "../../models/toSelf"; import { ICommentsRepository } from "./comments.repository.interface"; import { PostToCommentRelTypes } from "../../../posts/models/toComment"; +import { Post } from "../../../posts/models"; @Injectable() export class CommentsRepository implements ICommentsRepository { @@ -217,4 +218,21 @@ export class CommentsRepository implements ICommentsRepository { } ); } + // throw new HttpException("Post not found", 404); + public async findParentPost(commentId: UUID): Promise { + const parentPost = await this._neo4jService.tryReadAsync( + ` + MATCH (p:Post)-[:${PostToCommentRelTypes.HAS_COMMENT}]->(c:Comment { commentId: $commentId }) + RETURN p + `, + { + commentId, + } + ); + + if (parentPost.records.length === 0) { + return undefined; + } + return new Post(parentPost.records[0].get("p").properties, this._neo4jService); + } } diff --git a/src/comments/services/comments/comments.service.interface.ts b/src/comments/services/comments/comments.service.interface.ts index db1e0b8..543058a 100644 --- a/src/comments/services/comments/comments.service.interface.ts +++ b/src/comments/services/comments/comments.service.interface.ts @@ -1,5 +1,6 @@ import { CommentCreationPayloadDto, VoteCommentPayloadDto } from "../../dtos"; import { Comment } from "../../models"; +import { Post } from "../../../posts/models"; export interface ICommentsService { authorNewComment(commentPayload: CommentCreationPayloadDto): Promise; diff --git a/src/comments/services/comments/comments.service.ts b/src/comments/services/comments/comments.service.ts index ff41263..702b4cd 100644 --- a/src/comments/services/comments/comments.service.ts +++ b/src/comments/services/comments/comments.service.ts @@ -15,31 +15,32 @@ import { CommentCreationPayloadDto, VoteCommentPayloadDto } from "../../dtos"; import { Comment } from "../../models"; import { CommentToSelfRelTypes } from "../../models/toSelf"; import { ICommentsService } from "./comments.service.interface"; -import { IPusherService } from "../../../pusher/services/pusher/pusher.service.interface"; -import { ChannelTypesEnum, EventTypes } from "../../../pusher/pusher.types"; +import { EventEmitter2 } from "@nestjs/event-emitter"; +import { CommentGotPinnedByAuthorEvent, CommentGotVoteEvent, NewCommentEvent } from "../../events"; +import { EventTypes } from "../../../_domain/eventTypes"; @Injectable({ scope: Scope.REQUEST }) export class CommentsService implements ICommentsService { private readonly _logger = new Logger(CommentsService.name); + private readonly _eventEmitter: EventEmitter2; private readonly _request: Request; private readonly _dbContext: DatabaseContext; private readonly _autoModerationService: IAutoModerationService; private readonly _postService: IPostsService; - private readonly _pusherService: IPusherService; constructor( + eventEmitter: EventEmitter2, @Inject(REQUEST) request: Request, @Inject(_$.IDatabaseContext) databaseContext: DatabaseContext, @Inject(_$.IAutoModerationService) autoModerationService: IAutoModerationService, - @Inject(_$.IPostsService) postsService: IPostsService, - @Inject(_$.IPusherService) pusherService: IPusherService + @Inject(_$.IPostsService) postsService: IPostsService ) { + this._eventEmitter = eventEmitter; this._request = request; this._dbContext = databaseContext; this._autoModerationService = autoModerationService; this._postService = postsService; - this._pusherService = pusherService; } public async authorNewComment(commentPayload: CommentCreationPayloadDto): Promise { @@ -64,26 +65,17 @@ export class CommentsService implements ICommentsService { ); await foundPost.getAuthorUser(); await foundPost.getPostType(); - this._pusherService - .triggerUser( - ChannelTypesEnum.IGAQ_Notification, - EventTypes.NewCommentOnPost, - foundPost.authorUser.userId, - { - username: user.username, - avatar: user.avatar, - commentContent: createdComment.commentContent, - postTypeName: foundPost.postType.postTypeName, - } - ) - .then(() => - this._logger.verbose( - `Event ${EventTypes.NewCommentOnPost} got pushed to ${user.username}` - ) - ) - .catch(e => - this._logger.error(`Event ${EventTypes.NewCommentOnPost} ERRORED: `, e) - ); + this._eventEmitter.emit( + EventTypes.NewCommentOnPost, + new NewCommentEvent({ + subscriberId: foundPost.authorUser.userId, + username: user.username, + avatar: user.avatar, + commentContent: createdComment.commentContent, + postTypeName: foundPost.postType.postTypeName, + }) + ); + return createdComment; } @@ -98,23 +90,15 @@ export class CommentsService implements ICommentsService { }) ); await foundParentComment.getAuthorUser(); - this._pusherService - .triggerUser( - ChannelTypesEnum.IGAQ_Notification, - EventTypes.NewCommentOnComment, - foundParentComment.authorUser.userId, - { - username: user.username, - avatar: user.avatar, - commentContent: createdComment.commentContent, - } - ) - .then(() => - this._logger.verbose( - `Event ${EventTypes.NewCommentOnComment} got pushed to ${user.username}` - ) - ) - .catch(e => this._logger.error(`Event ${EventTypes.NewCommentOnComment} ERRORED: `, e)); + this._eventEmitter.emit( + EventTypes.NewCommentOnComment, + new NewCommentEvent({ + subscriberId: foundParentComment.authorUser.userId, + username: user.username, + avatar: user.avatar, + commentContent: createdComment.commentContent, + }) + ); } public async findCommentById(commentId: UUID): Promise { @@ -228,18 +212,17 @@ export class CommentsService implements ICommentsService { // don't wait for the push notification. setTimeout(async () => { - const parentPost = await this.findParentPost(comment.commentId); - this._pusherService - .triggerUser(ChannelTypesEnum.IGAQ_Notification, eventType, user.userId, { + const parentPost = await this.acquireParentPost(comment.commentId); + this._eventEmitter.emit( + eventType, + new CommentGotVoteEvent({ + subscriberId: user.userId, postId: parentPost.postId, commentId: comment.commentId, username: user.username, avatar: user.avatar, }) - .then(() => - this._logger.verbose(`Event ${eventType} got pushed to ${user.username}`) - ) - .catch(e => this._logger.error(`Event ${eventType} ERRORED: `, e)); + ); }); } @@ -272,26 +255,17 @@ export class CommentsService implements ICommentsService { } ); - this._pusherService - .triggerUser( - ChannelTypesEnum.IGAQ_Notification, - EventTypes.CommentGotPinnedByAuthor, - user.userId, - { - commentId, - postId: parentPost.postId, - username: user.username, - avatar: user.avatar, - } - ) - .then(() => - this._logger.verbose( - `Event ${EventTypes.CommentGotPinnedByAuthor} got pushed to ${user.username}` - ) - ) - .catch(e => - this._logger.error(`Event ${EventTypes.CommentGotPinnedByAuthor} ERRORED: `, e) - ); + this._eventEmitter.emit( + EventTypes.CommentGotPinnedByAuthor, + new CommentGotPinnedByAuthorEvent({ + subscriberId: user.userId, + commentId, + commentContent: comment.commentContent, + postId: parentPost.postId, + username: user.username, + avatar: user.avatar, + }) + ); } public async markAsUnpinned(commentId: UUID): Promise { @@ -325,21 +299,13 @@ export class CommentsService implements ICommentsService { } // gets the parent post of any nested comment of the post - private async findParentPost(commentId: UUID): Promise { - const parentPost = await this._dbContext.neo4jService.tryReadAsync( - ` - MATCH (p:Post)-[:${PostToCommentRelTypes.HAS_COMMENT}]->(c:Comment { commentId: $commentId }) - RETURN p - `, - { - commentId, - } - ); + private async acquireParentPost(commentId: UUID): Promise { + const parentPost = await this._dbContext.Comments.findParentPost(commentId); - if (parentPost.records.length === 0) { + if (!parentPost) { throw new HttpException("Post not found", 404); } - return new Post(parentPost.records[0].get("p").properties, this._dbContext.neo4jService); + return parentPost; } // gets the root comment of any nested comment @@ -363,7 +329,7 @@ export class CommentsService implements ICommentsService { ); } else { const rootComment = await this._dbContext.Comments.findCommentById(commentId); - return [await this.findParentPost(rootComment.commentId), isNestedComment]; + return [await this.acquireParentPost(rootComment.commentId), isNestedComment]; } } From ac1cf838dcf09d260a1831e1bd88f2fcf371fc9a Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Thu, 1 Dec 2022 03:27:13 -0800 Subject: [PATCH 128/153] revise moderation module and services with event emitter --- src/moderation/moderation.module.ts | 3 +- .../moderatorActions.service.ts | 130 +++++++----------- 2 files changed, 50 insertions(+), 83 deletions(-) diff --git a/src/moderation/moderation.module.ts b/src/moderation/moderation.module.ts index b72c816..315df9a 100644 --- a/src/moderation/moderation.module.ts +++ b/src/moderation/moderation.module.ts @@ -5,10 +5,9 @@ import { _$ } from "../_domain/injectableTokens"; import { HttpModule } from "@nestjs/axios"; import { DatabaseAccessLayerModule } from "../database-access-layer/database-access-layer.module"; import { ModerationController } from "./controllers/moderation.controller"; -import { PusherModule } from "../pusher/pusher.module"; @Module({ - imports: [HttpModule, forwardRef(() => DatabaseAccessLayerModule), PusherModule], + imports: [HttpModule, forwardRef(() => DatabaseAccessLayerModule)], providers: [ { provide: _$.IAutoModerationService, diff --git a/src/moderation/services/moderatorActions/moderatorActions.service.ts b/src/moderation/services/moderatorActions/moderatorActions.service.ts index e830720..bcaf31f 100644 --- a/src/moderation/services/moderatorActions/moderatorActions.service.ts +++ b/src/moderation/services/moderatorActions/moderatorActions.service.ts @@ -7,8 +7,10 @@ import { Comment } from "../../../comments/models"; import { Post } from "../../../posts/models"; import { ModerationPayloadDto } from "../../dtos"; import { GotBannedProps } from "../../../users/models/toSelf"; -import { IPusherService } from "../../../pusher/services/pusher/pusher.service.interface"; -import { ChannelTypesEnum, EventTypes } from "../../../pusher/pusher.types"; +import { EventEmitter2 } from "@nestjs/event-emitter"; +import { PostGotApprovedByModeratorEvent, PostGotRestrictedEvent } from "../../events"; +import { CommentGotApprovedByModeratorEvent, CommentGotRestrictedEvent } from "../../events"; +import { EventTypes } from "../../../_domain/eventTypes"; /** * This service is responsible for moderating posts and comments. @@ -17,18 +19,17 @@ import { ChannelTypesEnum, EventTypes } from "../../../pusher/pusher.types"; * - This service is not responsible to check if the user is a moderator. This is done by the guards used by controllers. * @see src/moderation/controllers/moderatorActions/moderatorActions.controller.ts */ -@Injectable({ scope: Scope.DEFAULT }) +@Injectable({ scope: Scope.REQUEST }) export class ModeratorActionsService implements IModeratorActionsService { - private readonly _logger = new Logger(ModeratorActionsService.name); + private readonly _eventEmitter: EventEmitter2; private readonly _dbContext: DatabaseContext; - private readonly _pusherService: IPusherService; constructor( - @Inject(_$.IDatabaseContext) dbContext: DatabaseContext, - @Inject(_$.IPusherService) pusherService: IPusherService + eventEmitter: EventEmitter2, + @Inject(_$.IDatabaseContext) dbContext: DatabaseContext ) { + this._eventEmitter = eventEmitter; this._dbContext = dbContext; - this._pusherService = pusherService; } public async banUser(payload: ModerationPayloadDto): Promise { @@ -148,26 +149,16 @@ export class ModeratorActionsService implements IModeratorActionsService { comment.restrictedProps = restrictedProps; await comment.getAuthorUser(); - this._pusherService - .triggerUser( - ChannelTypesEnum.IGAQ_Notification, - EventTypes.CommentGotRestricted, - comment.authorUser.userId, - { - commentId: comment.commentId, - reason: payload.reason, - username: comment.authorUser.username, - avatar: comment.authorUser.avatar, - } - ) - .then(() => - this._logger.verbose( - `Event ${EventTypes.CommentGotRestricted} got pushed to ${comment.authorUser.username}` - ) - ) - .catch(e => - this._logger.error(`Event ${EventTypes.CommentGotRestricted} ERRORED: `, e) - ); + this._eventEmitter.emit( + EventTypes.CommentGotRestricted, + new CommentGotRestrictedEvent({ + subscriberUserId: comment.authorUser.userId, + commentContent: comment.commentContent, + reason: payload.reason, + username: comment.authorUser.username, + avatar: comment.authorUser.avatar, + }) + ); return comment; } @@ -189,24 +180,16 @@ export class ModeratorActionsService implements IModeratorActionsService { post.restrictedProps = restrictedProps; await post.getAuthorUser(); - this._pusherService - .triggerUser( - ChannelTypesEnum.IGAQ_Notification, - EventTypes.PostGotRestricted, - post.authorUser.userId, - { - postId: post.postId, - reason: payload.reason, - username: post.authorUser.username, - avatar: post.authorUser.avatar, - } - ) - .then(() => - this._logger.verbose( - `Event ${EventTypes.PostGotRestricted} got pushed to ${post.authorUser.username}` - ) - ) - .catch(e => this._logger.error(`Event ${EventTypes.PostGotRestricted} ERRORED: `, e)); + this._eventEmitter.emit( + EventTypes.PostGotRestricted, + new PostGotRestrictedEvent({ + subscriberId: post.authorUser.userId, + postTitle: post.postTitle, + reason: payload.reason, + username: post.authorUser.username, + avatar: post.authorUser.avatar, + }) + ); return post; } @@ -249,26 +232,21 @@ export class ModeratorActionsService implements IModeratorActionsService { comment.pending = false; await this._dbContext.Comments.updateComment(comment); - await comment.getAuthorUser(); - this._pusherService - .triggerUser( - ChannelTypesEnum.IGAQ_Notification, - EventTypes.PostGotApprovedByModerator, - comment.authorUser.userId, - { + // don't wait + setTimeout(async () => { + await comment.getAuthorUser(); + const post = await this._dbContext.Comments.findParentPost(comment.commentId); + this._eventEmitter.emit( + EventTypes.CommentGotApprovedByModerator, + new CommentGotApprovedByModeratorEvent({ + subscriberId: comment.authorUser.userId, commentId: comment.commentId, + postId: post.postId, username: comment.authorUser.username, avatar: comment.authorUser.avatar, - } - ) - .then(() => - this._logger.verbose( - `Event ${EventTypes.CommentGotApprovedByModerator} got pushed to ${comment.authorUser.username}` - ) - ) - .catch(e => - this._logger.error(`Event ${EventTypes.CommentGotApprovedByModerator} ERRORED: `, e) + }) ); + }); return comment; } @@ -292,25 +270,15 @@ export class ModeratorActionsService implements IModeratorActionsService { post.pending = false; await post.getAuthorUser(); - this._pusherService - .triggerUser( - ChannelTypesEnum.IGAQ_Notification, - EventTypes.PostGotApprovedByModerator, - post.authorUser.userId, - { - postId: post.postId, - username: post.authorUser.username, - avatar: post.authorUser.avatar, - } - ) - .then(() => - this._logger.verbose( - `Event ${EventTypes.PostGotApprovedByModerator} got pushed to ${post.authorUser.username}` - ) - ) - .catch(e => - this._logger.error(`Event ${EventTypes.PostGotApprovedByModerator} ERRORED: `, e) - ); + this._eventEmitter.emit( + EventTypes.PostGotApprovedByModerator, + new PostGotApprovedByModeratorEvent({ + subscriberId: post.authorUser.userId, + postId: post.postId, + username: post.authorUser.username, + avatar: post.authorUser.avatar, + }) + ); return post; } From 983d695c524d24269837ea0ebff35d2bca450cbd Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Thu, 1 Dec 2022 03:27:35 -0800 Subject: [PATCH 129/153] revise with event emitter --- src/comments/comments.module.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/comments/comments.module.ts b/src/comments/comments.module.ts index 0fd93f2..f03864b 100644 --- a/src/comments/comments.module.ts +++ b/src/comments/comments.module.ts @@ -9,7 +9,6 @@ import { ModerationModule } from "../moderation/moderation.module"; import { PostsModule } from "../posts/posts.module"; import { CommentsReportService } from "./services/commentReport/commentsReport.service"; import { GoogleCloudRecaptchaEnterpriseModule } from "../google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.module"; -import { PusherModule } from "../pusher/pusher.module"; @Module({ controllers: [CommentsController], @@ -19,7 +18,6 @@ import { PusherModule } from "../pusher/pusher.module"; ModerationModule, PostsModule, GoogleCloudRecaptchaEnterpriseModule, - PusherModule, ], providers: [ { From 92ff10b5c0f1e17d2bc2773ba6e1550960c8a3ab Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Thu, 1 Dec 2022 11:38:20 -0800 Subject: [PATCH 130/153] revise the method --- .../notificationStashPool/notificationStashPool.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pusher/services/notificationStashPool/notificationStashPool.service.ts b/src/pusher/services/notificationStashPool/notificationStashPool.service.ts index 164a113..61caa93 100644 --- a/src/pusher/services/notificationStashPool/notificationStashPool.service.ts +++ b/src/pusher/services/notificationStashPool/notificationStashPool.service.ts @@ -37,7 +37,8 @@ export class NotificationStashPoolService implements INotificationStashPoolServi return []; } - this.notificationStashPool.delete(userId); + this.dropStashNotification(userId); + return foundStash; } From b60f4ef06d8e92745c38576cf3905f38fca2c413 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Thu, 1 Dec 2022 12:34:24 -0800 Subject: [PATCH 131/153] fix --- src/_domain/_domain.module.ts | 4 ++-- .../commentEvents.listener.ts | 24 ++++++++++--------- .../postEvents.listener.ts | 15 +++++++----- src/app.module.ts | 4 +++- .../services/comments/comments.service.ts | 5 ++-- src/posts/services/posts/posts.service.ts | 3 ++- .../notificationMessageMaker.service.ts | 12 ++++++---- .../notificationStashPool.service.ts | 5 +++- 8 files changed, 43 insertions(+), 29 deletions(-) rename src/_domain/{listeners => listeners}/commentEvents.listener.ts (88%) rename src/_domain/{listeners => listeners}/postEvents.listener.ts (88%) diff --git a/src/_domain/_domain.module.ts b/src/_domain/_domain.module.ts index fc0850c..9b95f29 100644 --- a/src/_domain/_domain.module.ts +++ b/src/_domain/_domain.module.ts @@ -1,7 +1,7 @@ import { Module } from "@nestjs/common"; import { PusherModule } from "../pusher/pusher.module"; -import { CommentEventsListener } from "./listeners /commentEvents.listener"; -import { PostEventsListener } from "./listeners /postEvents.listener"; +import { CommentEventsListener } from "./listeners/commentEvents.listener"; +import { PostEventsListener } from "./listeners/postEvents.listener"; @Module({ imports: [PusherModule], diff --git a/src/_domain/listeners /commentEvents.listener.ts b/src/_domain/listeners/commentEvents.listener.ts similarity index 88% rename from src/_domain/listeners /commentEvents.listener.ts rename to src/_domain/listeners/commentEvents.listener.ts index f9e1c2b..560c809 100644 --- a/src/_domain/listeners /commentEvents.listener.ts +++ b/src/_domain/listeners/commentEvents.listener.ts @@ -1,10 +1,12 @@ -import { Inject, Injectable, Logger } from "@nestjs/common"; +import { Inject, Injectable, Logger, Scope } from "@nestjs/common"; import { IPusherService } from "../../pusher/services/pusher/pusher.service.interface"; import { _$ } from "../injectableTokens"; import { OnEvent } from "@nestjs/event-emitter"; import { ChannelTypesEnum, PusherEvents } from "../../pusher/pusher.types"; -import { CommentGotApprovedByModeratorEvent } from "../../moderation/events"; -import { CommentGotRestrictedEvent } from "../../moderation/events"; +import { + CommentGotApprovedByModeratorEvent, + CommentGotRestrictedEvent, +} from "../../moderation/events"; import { CommentGotPinnedByAuthorEvent, CommentGotVoteEvent, @@ -14,7 +16,7 @@ import { INotificationMessageMakerService } from "../../pusher/services/notifica import { EventTypes } from "../eventTypes"; import { INotificationStashPoolService } from "../../pusher/services/notificationStashPool/notificationStashPool.service.interface"; -@Injectable() +@Injectable({ scope: Scope.DEFAULT }) export class CommentEventsListener { private readonly _logger = new Logger(CommentEventsListener.name); @@ -34,7 +36,7 @@ export class CommentEventsListener { this._notificationStashPoolService = notificationStashPoolService; } - @OnEvent(EventTypes.CommentGotUpVote) + @OnEvent(EventTypes.CommentGotUpVote, { async: true }) public async handleCommentGotUpVote(event: CommentGotVoteEvent): Promise { const message = this._notificationMessageMakerService.makeForCommentGotUpVote({ username: event.username, @@ -51,7 +53,7 @@ export class CommentEventsListener { ); } - @OnEvent(EventTypes.CommentGotDownVote) + @OnEvent(EventTypes.CommentGotDownVote, { async: true }) public async handleCommentGotDownVote(event: CommentGotVoteEvent): Promise { const message = this._notificationMessageMakerService.makeForCommentGotDownVote({ username: event.username, @@ -68,7 +70,7 @@ export class CommentEventsListener { ); } - @OnEvent(EventTypes.CommentGotRestricted) + @OnEvent(EventTypes.CommentGotRestricted, { async: true }) public async handleCommentGotRestricted(event: CommentGotRestrictedEvent): Promise { const message = this._notificationMessageMakerService.makeForCommentGotRestricted({ commentContent: event.commentContent, @@ -84,7 +86,7 @@ export class CommentEventsListener { ); } - @OnEvent(EventTypes.CommentGotPinnedByAuthor) + @OnEvent(EventTypes.CommentGotPinnedByAuthor, { async: true }) public async handleCommentGotPinnedByAuthor( event: CommentGotPinnedByAuthorEvent ): Promise { @@ -104,7 +106,7 @@ export class CommentEventsListener { ); } - @OnEvent(EventTypes.CommentGotApprovedByModerator) + @OnEvent(EventTypes.CommentGotApprovedByModerator, { async: true }) public async handleCommentGotApprovedByModerator( event: CommentGotApprovedByModeratorEvent ): Promise { @@ -123,7 +125,7 @@ export class CommentEventsListener { ); } - @OnEvent(EventTypes.NewCommentOnComment) + @OnEvent(EventTypes.NewCommentOnComment, { async: true }) public async handleNewCommentOnComment(event: NewCommentEvent): Promise { const message = this._notificationMessageMakerService.makeForNewCommentOnComment({ username: event.username, @@ -139,7 +141,7 @@ export class CommentEventsListener { ); } - @OnEvent(EventTypes.NewCommentOnPost) + @OnEvent(EventTypes.NewCommentOnPost, { async: true }) public async handleNewCommentOnPost(event: NewCommentEvent): Promise { const message = this._notificationMessageMakerService.makeForNewCommentOnPost({ username: event.username, diff --git a/src/_domain/listeners /postEvents.listener.ts b/src/_domain/listeners/postEvents.listener.ts similarity index 88% rename from src/_domain/listeners /postEvents.listener.ts rename to src/_domain/listeners/postEvents.listener.ts index 1970060..e0cd148 100644 --- a/src/_domain/listeners /postEvents.listener.ts +++ b/src/_domain/listeners/postEvents.listener.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable, Logger } from "@nestjs/common"; +import { Inject, Injectable, Logger, Scope } from "@nestjs/common"; import { OnEvent } from "@nestjs/event-emitter"; import { ChannelTypesEnum, PusherEvents } from "../../pusher/pusher.types"; import { PostGotVoteEvent } from "../../posts/events"; @@ -9,7 +9,7 @@ import { INotificationMessageMakerService } from "../../pusher/services/notifica import { EventTypes } from "../eventTypes"; import { INotificationStashPoolService } from "../../pusher/services/notificationStashPool/notificationStashPool.service.interface"; -@Injectable() +@Injectable({ scope: Scope.DEFAULT }) export class PostEventsListener { private readonly _logger = new Logger(PostEventsListener.name); @@ -29,8 +29,9 @@ export class PostEventsListener { this._notificationStashPoolService = notificationStashPoolService; } - @OnEvent(EventTypes.PostGotUpVote) + @OnEvent(EventTypes.PostGotUpVote, { async: true }) public async handlePostGotUpVote(event: PostGotVoteEvent): Promise { + console.log("event listener emitted"); const message = this._notificationMessageMakerService.makeForPostGotUpVote({ username: event.username, postId: event.postId, @@ -45,8 +46,10 @@ export class PostEventsListener { ); } - @OnEvent(EventTypes.PostGotDownVote) + @OnEvent(EventTypes.PostGotDownVote, { async: true }) public async handlePostGotDownVote(event: PostGotVoteEvent): Promise { + console.log("event listener emitted"); + const message = this._notificationMessageMakerService.makeForPostGotDownVote({ username: event.username, postId: event.postId, @@ -61,7 +64,7 @@ export class PostEventsListener { ); } - @OnEvent(EventTypes.PostGotApprovedByModerator) + @OnEvent(EventTypes.PostGotApprovedByModerator, { async: true }) public async handlePostGotApprovedByModerator( event: PostGotApprovedByModeratorEvent ): Promise { @@ -79,7 +82,7 @@ export class PostEventsListener { ); } - @OnEvent(EventTypes.PostGotRestricted) + @OnEvent(EventTypes.PostGotRestricted, { async: true }) public async handlePostGotRestricted(event: PostGotRestrictedEvent): Promise { const message = this._notificationMessageMakerService.makeForPostGotRestricted({ postTitle: event.postTitle, diff --git a/src/app.module.ts b/src/app.module.ts index b043cfc..470fc5a 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -16,10 +16,12 @@ import { APP_GUARD } from "@nestjs/core"; import { GoogleCloudRecaptchaEnterpriseModule } from "./google-cloud-recaptcha-enterprise/google-cloud-recaptcha-enterprise.module"; import { PusherModule } from "./pusher/pusher.module"; import { EventEmitterModule } from "@nestjs/event-emitter"; +import { DomainModule } from "./_domain/_domain.module"; @Module({ imports: [ - EventEmitterModule.forRoot(), + EventEmitterModule.forRoot({}), + DomainModule, ThrottlerModule.forRoot({ ttl: 69, limit: 42, diff --git a/src/comments/services/comments/comments.service.ts b/src/comments/services/comments/comments.service.ts index 702b4cd..2fd3db0 100644 --- a/src/comments/services/comments/comments.service.ts +++ b/src/comments/services/comments/comments.service.ts @@ -216,7 +216,7 @@ export class CommentsService implements ICommentsService { this._eventEmitter.emit( eventType, new CommentGotVoteEvent({ - subscriberId: user.userId, + subscriberId: parentPost.authorUser.userId, postId: parentPost.postId, commentId: comment.commentId, username: user.username, @@ -255,10 +255,11 @@ export class CommentsService implements ICommentsService { } ); + await comment.getAuthorUser(); this._eventEmitter.emit( EventTypes.CommentGotPinnedByAuthor, new CommentGotPinnedByAuthorEvent({ - subscriberId: user.userId, + subscriberId: comment.authorUser.userId, commentId, commentContent: comment.commentContent, postId: parentPost.postId, diff --git a/src/posts/services/posts/posts.service.ts b/src/posts/services/posts/posts.service.ts index cdc9a6e..c979f93 100644 --- a/src/posts/services/posts/posts.service.ts +++ b/src/posts/services/posts/posts.service.ts @@ -322,10 +322,11 @@ export class PostsService implements IPostsService { ? EventTypes.PostGotUpVote : EventTypes.PostGotDownVote; + await post.getAuthorUser(); this._eventEmitter.emit( eventType, new PostGotVoteEvent({ - subscriberId: user.userId, + subscriberId: post.authorUser.userId, postId: post.postId, username: user.username, avatar: user.avatar, diff --git a/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts b/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts index fd582a6..62e0bb7 100644 --- a/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts +++ b/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts @@ -16,9 +16,9 @@ export class NotificationMessageMakerService implements INotificationMessageMake [EventTypes.NewCommentOnComment]: (p: { username: string; commentContent: string }) => `${p.username} replied to your comment '${p.commentContent?.slice(0, 20) ?? ""}'`, [EventTypes.CommentGotUpVote]: (p: { username: string; postId: UUID; commentId: UUID }) => - `${p.username} up voted your comment check it out!`, + `${p.username} up voted your comment (uuid:${this.stashToken}:comment:${p.commentId}:post:${p.postId}:text:check it out!)`, [EventTypes.CommentGotDownVote]: (p: { username: string; postId: UUID; commentId: UUID }) => - `${p.username} down voted your comment go to comment`, + `${p.username} down voted your comment (uuid:${this.stashToken}:comment:${p.commentId}:post:${p.postId}:text:go to comment)`, [EventTypes.CommentGotRestricted]: (p: { commentContent: string; reason: string }) => `A Moderator has restricted your comment due to: "${ p.reason @@ -28,7 +28,7 @@ export class NotificationMessageMakerService implements INotificationMessageMake postId: UUID; username: string; }) => - `Our moderator, ${p.username}, allowed your comment to be published. go to comment`, + `Our moderator, ${p.username}, allowed your comment to be published. (uuid:${this.stashToken}:comment:${p.commentId}:post:${p.postId}:text:go to comment)`, [EventTypes.CommentGotPinnedByAuthor]: (p: { commentId: UUID; postId: UUID; @@ -42,15 +42,17 @@ export class NotificationMessageMakerService implements INotificationMessageMake [EventTypes.PostGotUpVote]: (p: { username: string; postId: UUID }) => `${p.username} up voted your post check it out!`, [EventTypes.PostGotDownVote]: (p: { username: string; postId: UUID }) => - `${p.username} down voted your post go to post`, + `${p.username} down voted your post (uuid:${this.stashToken}:post:${p.postId}:text:go to post)`, [EventTypes.PostGotRestricted]: (p: { postTitle: string; reason: string }) => `A Moderator has restricted your post due to: "${ p.reason }". Post Title: '${p.postTitle.slice(0, 20)}'"`, [EventTypes.PostGotApprovedByModerator]: (p: { username: string; postId: UUID }) => - `Our moderator, ${p.username}, allowed your post to be published. go to post`, + `Our moderator, ${p.username}, allowed your post to be published. (uuid:${this.stashToken}:post:${p.postId}:text:go to post)`, }; + public stashToken: string; + public makeForNewCommentOnPost(p: { username: string; postTypeName: string; diff --git a/src/pusher/services/notificationStashPool/notificationStashPool.service.ts b/src/pusher/services/notificationStashPool/notificationStashPool.service.ts index 61caa93..806a7e2 100644 --- a/src/pusher/services/notificationStashPool/notificationStashPool.service.ts +++ b/src/pusher/services/notificationStashPool/notificationStashPool.service.ts @@ -5,7 +5,10 @@ import { generateUUID } from "../../../_domain/utils"; @Injectable({ scope: Scope.DEFAULT }) export class NotificationStashPoolService implements INotificationStashPoolService { - private readonly notificationStashPool: Map; + private readonly notificationStashPool: Map = new Map< + UUID, + NotificationStashPoolItem[] + >(); public async stashNotification( userId: UUID, From da28007d24ec1cc90c2aee158e71e99283c75a11 Mon Sep 17 00:00:00 2001 From: iantelli <2ndex@duck.com> Date: Thu, 1 Dec 2022 14:25:17 -0800 Subject: [PATCH 132/153] Controller for getting deleted posts --- src/moderation/controllers/moderation.controller.ts | 9 +++++++++ .../moderatorActions.service.interface.ts | 2 ++ .../moderatorActions/moderatorActions.service.ts | 5 +++++ .../repositories/post/posts.repository.interface.ts | 2 ++ src/posts/repositories/post/posts.repository.ts | 13 +++++++++++++ src/posts/services/posts/posts.service.interface.ts | 1 + 6 files changed, 32 insertions(+) diff --git a/src/moderation/controllers/moderation.controller.ts b/src/moderation/controllers/moderation.controller.ts index cd4d320..0128c3b 100644 --- a/src/moderation/controllers/moderation.controller.ts +++ b/src/moderation/controllers/moderation.controller.ts @@ -151,4 +151,13 @@ export class ModerationController { const decoratedPosts = posts.map(post => post.toJSON()); return await Promise.all(decoratedPosts); } + + @Get("/deletedPosts") + @Roles(Role.MODERATOR) + @UseGuards(AuthGuard("jwt"), RolesGuard) + public async getDeletedPosts() { + const posts = await this._moderationActionsService.getDeletedPosts(); + const decoratedPosts = posts.map(post => post.toJSON()); + return await Promise.all(decoratedPosts); + } } diff --git a/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts b/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts index 5f6c65d..abdad0c 100644 --- a/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts +++ b/src/moderation/services/moderatorActions/moderatorActions.service.interface.ts @@ -87,4 +87,6 @@ export interface IModeratorActionsService { unbanUser(userId: UUID): Promise; getPendingPosts(): Promise; + + getDeletedPosts(): Promise; } diff --git a/src/moderation/services/moderatorActions/moderatorActions.service.ts b/src/moderation/services/moderatorActions/moderatorActions.service.ts index bcaf31f..109f9c1 100644 --- a/src/moderation/services/moderatorActions/moderatorActions.service.ts +++ b/src/moderation/services/moderatorActions/moderatorActions.service.ts @@ -288,6 +288,11 @@ export class ModeratorActionsService implements IModeratorActionsService { return posts; } + public async getDeletedPosts(): Promise { + const posts = await this._dbContext.Posts.getDeletedPosts(); + return posts; + } + /** * @description * This method is to find a comment from the database and throw an error if it does not exist. if it does exist, it will return the comment. diff --git a/src/posts/repositories/post/posts.repository.interface.ts b/src/posts/repositories/post/posts.repository.interface.ts index 5b5947c..3a85a5c 100644 --- a/src/posts/repositories/post/posts.repository.interface.ts +++ b/src/posts/repositories/post/posts.repository.interface.ts @@ -25,4 +25,6 @@ export interface IPostsRepository { unrestrictPost(postId: string): Promise; getPendingPosts(): Promise; + + getDeletedPosts(): Promise; } diff --git a/src/posts/repositories/post/posts.repository.ts b/src/posts/repositories/post/posts.repository.ts index 3647cba..ac94ab1 100644 --- a/src/posts/repositories/post/posts.repository.ts +++ b/src/posts/repositories/post/posts.repository.ts @@ -241,4 +241,17 @@ export class PostsRepository implements IPostsRepository { if (records.length === 0) return []; return records.map(record => new Post(record.get("p").properties, this._neo4jService)); } + + public async getDeletedPosts(): Promise { + const queryResult = await this._neo4jService.tryReadAsync( + ` + MATCH (p:Post)-[r:${_ToSelfRelTypes.DELETED}]->(p) + RETURN p + `, + {} + ); + const records = queryResult.records; + if (records.length === 0) return []; + return records.map(record => new Post(record.get("p").properties, this._neo4jService)); + } } diff --git a/src/posts/services/posts/posts.service.interface.ts b/src/posts/services/posts/posts.service.interface.ts index 0bf74bd..39f5846 100644 --- a/src/posts/services/posts/posts.service.interface.ts +++ b/src/posts/services/posts/posts.service.interface.ts @@ -8,6 +8,7 @@ export interface IPostsService { authorNewPost(postPayload: PostCreationPayloadDto): Promise; getQueeriesOfTheDay(): Promise; + getStoriesOfTheDay(): Promise; findAllQueeries(): Promise; From b000b4ca1ed35751e495113c511c2c56d6ecba96 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Thu, 1 Dec 2022 15:39:49 -0800 Subject: [PATCH 133/153] fix the nested comment issue --- src/comments/services/comments/comments.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/comments/services/comments/comments.service.ts b/src/comments/services/comments/comments.service.ts index 2fd3db0..0c4e747 100644 --- a/src/comments/services/comments/comments.service.ts +++ b/src/comments/services/comments/comments.service.ts @@ -89,7 +89,6 @@ export class CommentsService implements ICommentsService { parentId: commentPayload.parentId, }) ); - await foundParentComment.getAuthorUser(); this._eventEmitter.emit( EventTypes.NewCommentOnComment, new NewCommentEvent({ From c62c9de4ae719882c54740e17dc95392f00f9f83 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Thu, 1 Dec 2022 20:26:42 -0800 Subject: [PATCH 134/153] fixed. on god on god. oblitrated --- src/comments/services/comments/comments.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/comments/services/comments/comments.service.ts b/src/comments/services/comments/comments.service.ts index 0c4e747..32b7ef9 100644 --- a/src/comments/services/comments/comments.service.ts +++ b/src/comments/services/comments/comments.service.ts @@ -98,6 +98,8 @@ export class CommentsService implements ICommentsService { commentContent: createdComment.commentContent, }) ); + + return createdComment; } public async findCommentById(commentId: UUID): Promise { From f7469f3194d5245f380a4b5fc95c7a499e2a9dae Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Thu, 1 Dec 2022 20:50:59 -0800 Subject: [PATCH 135/153] fix --- .../services/comments/comments.service.ts | 63 ++++++++------ .../moderatorActions.service.ts | 84 +++++++++++-------- src/posts/services/posts/posts.service.ts | 24 +++--- 3 files changed, 100 insertions(+), 71 deletions(-) diff --git a/src/comments/services/comments/comments.service.ts b/src/comments/services/comments/comments.service.ts index 32b7ef9..a489f2a 100644 --- a/src/comments/services/comments/comments.service.ts +++ b/src/comments/services/comments/comments.service.ts @@ -63,18 +63,22 @@ export class CommentsService implements ICommentsService { parentId: foundPost.postId, }) ); - await foundPost.getAuthorUser(); - await foundPost.getPostType(); - this._eventEmitter.emit( - EventTypes.NewCommentOnPost, - new NewCommentEvent({ - subscriberId: foundPost.authorUser.userId, - username: user.username, - avatar: user.avatar, - commentContent: createdComment.commentContent, - postTypeName: foundPost.postType.postTypeName, - }) - ); + try { + await foundPost.getAuthorUser(); + await foundPost.getPostType(); + this._eventEmitter.emit( + EventTypes.NewCommentOnPost, + new NewCommentEvent({ + subscriberId: foundPost.authorUser.userId, + username: user.username, + avatar: user.avatar, + commentContent: createdComment.commentContent, + postTypeName: foundPost.postType.postTypeName, + }) + ); + } catch (error) { + this._logger.error(error); + } return createdComment; } @@ -212,8 +216,9 @@ export class CommentsService implements ICommentsService { : EventTypes.CommentGotDownVote; // don't wait for the push notification. - setTimeout(async () => { + try { const parentPost = await this.acquireParentPost(comment.commentId); + await parentPost.getAuthorUser(); this._eventEmitter.emit( eventType, new CommentGotVoteEvent({ @@ -224,7 +229,9 @@ export class CommentsService implements ICommentsService { avatar: user.avatar, }) ); - }); + } catch (error) { + this._logger.error(error); + } } public async markAsPinned(commentId: UUID): Promise { @@ -256,18 +263,22 @@ export class CommentsService implements ICommentsService { } ); - await comment.getAuthorUser(); - this._eventEmitter.emit( - EventTypes.CommentGotPinnedByAuthor, - new CommentGotPinnedByAuthorEvent({ - subscriberId: comment.authorUser.userId, - commentId, - commentContent: comment.commentContent, - postId: parentPost.postId, - username: user.username, - avatar: user.avatar, - }) - ); + try { + await comment.getAuthorUser(); + this._eventEmitter.emit( + EventTypes.CommentGotPinnedByAuthor, + new CommentGotPinnedByAuthorEvent({ + subscriberId: comment.authorUser.userId, + commentId, + commentContent: comment.commentContent, + postId: parentPost.postId, + username: user.username, + avatar: user.avatar, + }) + ); + } catch (error) { + this._logger.error(error); + } } public async markAsUnpinned(commentId: UUID): Promise { diff --git a/src/moderation/services/moderatorActions/moderatorActions.service.ts b/src/moderation/services/moderatorActions/moderatorActions.service.ts index 109f9c1..2caa570 100644 --- a/src/moderation/services/moderatorActions/moderatorActions.service.ts +++ b/src/moderation/services/moderatorActions/moderatorActions.service.ts @@ -21,6 +21,7 @@ import { EventTypes } from "../../../_domain/eventTypes"; */ @Injectable({ scope: Scope.REQUEST }) export class ModeratorActionsService implements IModeratorActionsService { + private readonly _logger: Logger = new Logger(ModeratorActionsService.name); private readonly _eventEmitter: EventEmitter2; private readonly _dbContext: DatabaseContext; @@ -148,17 +149,21 @@ export class ModeratorActionsService implements IModeratorActionsService { await this._dbContext.Comments.restrictComment(payload.id, restrictedProps); comment.restrictedProps = restrictedProps; - await comment.getAuthorUser(); - this._eventEmitter.emit( - EventTypes.CommentGotRestricted, - new CommentGotRestrictedEvent({ - subscriberUserId: comment.authorUser.userId, - commentContent: comment.commentContent, - reason: payload.reason, - username: comment.authorUser.username, - avatar: comment.authorUser.avatar, - }) - ); + try { + await comment.getAuthorUser(); + this._eventEmitter.emit( + EventTypes.CommentGotRestricted, + new CommentGotRestrictedEvent({ + subscriberUserId: comment.authorUser.userId, + commentContent: comment.commentContent, + reason: payload.reason, + username: comment.authorUser.username, + avatar: comment.authorUser.avatar, + }) + ); + } catch (error) { + this._logger.error(error); + } return comment; } @@ -179,17 +184,21 @@ export class ModeratorActionsService implements IModeratorActionsService { await this._dbContext.Posts.restrictPost(payload.id, restrictedProps); post.restrictedProps = restrictedProps; - await post.getAuthorUser(); - this._eventEmitter.emit( - EventTypes.PostGotRestricted, - new PostGotRestrictedEvent({ - subscriberId: post.authorUser.userId, - postTitle: post.postTitle, - reason: payload.reason, - username: post.authorUser.username, - avatar: post.authorUser.avatar, - }) - ); + try { + await post.getAuthorUser(); + this._eventEmitter.emit( + EventTypes.PostGotRestricted, + new PostGotRestrictedEvent({ + subscriberId: post.authorUser.userId, + postTitle: post.postTitle, + reason: payload.reason, + username: post.authorUser.username, + avatar: post.authorUser.avatar, + }) + ); + } catch (error) { + this._logger.error(error); + } return post; } @@ -233,7 +242,7 @@ export class ModeratorActionsService implements IModeratorActionsService { await this._dbContext.Comments.updateComment(comment); // don't wait - setTimeout(async () => { + try { await comment.getAuthorUser(); const post = await this._dbContext.Comments.findParentPost(comment.commentId); this._eventEmitter.emit( @@ -246,7 +255,9 @@ export class ModeratorActionsService implements IModeratorActionsService { avatar: comment.authorUser.avatar, }) ); - }); + } catch (error) { + this._logger.error(error); + } return comment; } @@ -269,17 +280,20 @@ export class ModeratorActionsService implements IModeratorActionsService { ); post.pending = false; - await post.getAuthorUser(); - this._eventEmitter.emit( - EventTypes.PostGotApprovedByModerator, - new PostGotApprovedByModeratorEvent({ - subscriberId: post.authorUser.userId, - postId: post.postId, - username: post.authorUser.username, - avatar: post.authorUser.avatar, - }) - ); - + try { + await post.getAuthorUser(); + this._eventEmitter.emit( + EventTypes.PostGotApprovedByModerator, + new PostGotApprovedByModeratorEvent({ + subscriberId: post.authorUser.userId, + postId: post.postId, + username: post.authorUser.username, + avatar: post.authorUser.avatar, + }) + ); + } catch (error) { + this._logger.error(error); + } return post; } diff --git a/src/posts/services/posts/posts.service.ts b/src/posts/services/posts/posts.service.ts index c979f93..861912f 100644 --- a/src/posts/services/posts/posts.service.ts +++ b/src/posts/services/posts/posts.service.ts @@ -322,16 +322,20 @@ export class PostsService implements IPostsService { ? EventTypes.PostGotUpVote : EventTypes.PostGotDownVote; - await post.getAuthorUser(); - this._eventEmitter.emit( - eventType, - new PostGotVoteEvent({ - subscriberId: post.authorUser.userId, - postId: post.postId, - username: user.username, - avatar: user.avatar, - }) - ); + try { + await post.getAuthorUser(); + this._eventEmitter.emit( + eventType, + new PostGotVoteEvent({ + subscriberId: post.authorUser.userId, + postId: post.postId, + username: user.username, + avatar: user.avatar, + }) + ); + } catch (error) { + this._logger.error(error); + } } private getUserFromRequest(): User { From 239db1c72ca28d56549a35e81ac9d3b9c035f248 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Thu, 1 Dec 2022 21:15:22 -0800 Subject: [PATCH 136/153] fix --- .../commentReport/commentsReport.service.ts | 204 +++++++++--------- .../notificationStashPool.service.ts | 1 + 2 files changed, 103 insertions(+), 102 deletions(-) diff --git a/src/comments/services/commentReport/commentsReport.service.ts b/src/comments/services/commentReport/commentsReport.service.ts index e458748..52a89e8 100644 --- a/src/comments/services/commentReport/commentsReport.service.ts +++ b/src/comments/services/commentReport/commentsReport.service.ts @@ -1,102 +1,102 @@ -import { ReportCommentPayloadDto } from "../../dtos"; -import { User } from "../../../users/models"; -import { HttpException, Inject, Injectable, Logger, Scope } from "@nestjs/common"; -import { DatabaseContext } from "../../../database-access-layer/databaseContext"; -import { REQUEST } from "@nestjs/core"; -import { Request } from "express"; -import { _$ } from "../../../_domain/injectableTokens"; -import { ReportedProps, UserToCommentRelTypes } from "../../../users/models/toComment"; -import { ICommentsReportService } from "./commentsReport.service.interface"; - -@Injectable({ scope: Scope.REQUEST }) -export class CommentsReportService implements ICommentsReportService { - private readonly _logger = new Logger(CommentsReportService.name); - private readonly _request: Request; - private readonly _dbContext: DatabaseContext; - - constructor( - @Inject(REQUEST) request: Request, - @Inject(_$.IDatabaseContext) databaseContext: DatabaseContext - ) { - this._request = request; - this._dbContext = databaseContext; - } - - public async reportComment(reportCommentPayload: ReportCommentPayloadDto): Promise { - const user = this.getUserFromRequest(); - - const comment = await this._dbContext.Comments.findCommentById( - reportCommentPayload.commentId - ); - if (!comment) throw new Error("Comment not found"); - - if (comment.pending || comment.restrictedProps !== null) { - throw new HttpException( - "Comment cannot be reported due to being pending or restricted", - 400 - ); - } - - const report = await this.checkIfUserReportedComment(comment.commentId, user.userId); - - if (report === true) { - throw new HttpException("You have already reported this post", 400); - } - - await comment.getAuthorUser(); - if (comment.authorUser.userId === user.userId) { - throw new HttpException("Comment cannot be reported by comment author", 400); - } - - await this._dbContext.neo4jService.tryWriteAsync( - ` - MATCH (c:Comment { commentId: $commentId }), (u:User { userId: $userId }) - MERGE (u)-[r:${UserToCommentRelTypes.REPORTED}{reportedAt: $reportedAt, reason: $reason}]->(c) - `, - { - reportedAt: Date.now(), - reason: reportCommentPayload.reason, - commentId: comment.commentId, - userId: user.userId, - } - ); - } - - public async getReportsForComment(commentId: UUID): Promise { - const queryResult = await this._dbContext.neo4jService.tryReadAsync( - ` - MATCH (c:Comment { commentId: $commentId })<-[r:${UserToCommentRelTypes.REPORTED}]-(u:User) - RETURN r, u`, - { - commentId: commentId, - } - ); - return queryResult.records.map(record => { - const reportedProps = new ReportedProps(record.get("r").properties); - return reportedProps; - }); - } - - private async checkIfUserReportedComment(commentId: UUID, userId: UUID): Promise { - const queryResult = await this._dbContext.neo4jService.tryReadAsync( - ` - MATCH (p:Comment { commentId: $commentId })<-[r:${UserToCommentRelTypes.REPORTED}]-(u:User { userId: $userId }) - RETURN r - `, - { - commentId: commentId, - userId: userId, - } - ); - if (queryResult.records.length > 0) { - return true; - } - return false; - } - - private getUserFromRequest(): User { - const user = this._request.user as User; - if (user === undefined) throw new Error("User not found"); - return user; - } -} +import { ReportCommentPayloadDto } from "../../dtos"; +import { User } from "../../../users/models"; +import { HttpException, Inject, Injectable, Logger, Scope } from "@nestjs/common"; +import { DatabaseContext } from "../../../database-access-layer/databaseContext"; +import { REQUEST } from "@nestjs/core"; +import { Request } from "express"; +import { _$ } from "../../../_domain/injectableTokens"; +import { ReportedProps, UserToCommentRelTypes } from "../../../users/models/toComment"; +import { ICommentsReportService } from "./commentsReport.service.interface"; + +@Injectable({ scope: Scope.REQUEST }) +export class CommentsReportService implements ICommentsReportService { + private readonly _logger = new Logger(CommentsReportService.name); + private readonly _request: Request; + private readonly _dbContext: DatabaseContext; + + constructor( + @Inject(REQUEST) request: Request, + @Inject(_$.IDatabaseContext) databaseContext: DatabaseContext + ) { + this._request = request; + this._dbContext = databaseContext; + } + + public async reportComment(reportCommentPayload: ReportCommentPayloadDto): Promise { + const user = this.getUserFromRequest(); + + const comment = await this._dbContext.Comments.findCommentById( + reportCommentPayload.commentId + ); + if (!comment) throw new Error("Comment not found"); + + if (comment.pending || comment.restrictedProps !== null) { + throw new HttpException( + "Comment cannot be reported due to being pending or restricted", + 400 + ); + } + + const report = await this.checkIfUserReportedComment(comment.commentId, user.userId); + + if (report === true) { + throw new HttpException("You have already reported this post", 400); + } + + await comment.getAuthorUser(); + if (comment.authorUser.userId === user.userId) { + throw new HttpException("Comment cannot be reported by comment author", 400); + } + + await this._dbContext.neo4jService.tryWriteAsync( + ` + MATCH (c:Comment { commentId: $commentId }), (u:User { userId: $userId }) + MERGE (u)-[r:${UserToCommentRelTypes.REPORTED}{reportedAt: $reportedAt, reason: $reason}]->(c) + `, + { + reportedAt: Date.now(), + reason: reportCommentPayload.reason, + commentId: comment.commentId, + userId: user.userId, + } + ); + } + + public async getReportsForComment(commentId: UUID): Promise { + const queryResult = await this._dbContext.neo4jService.tryReadAsync( + ` + MATCH (c:Comment { commentId: $commentId })<-[r:${UserToCommentRelTypes.REPORTED}]-(u:User) + RETURN r, u`, + { + commentId: commentId, + } + ); + return queryResult.records.map(record => { + const reportedProps = new ReportedProps(record.get("r").properties); + return reportedProps; + }); + } + + private async checkIfUserReportedComment(commentId: UUID, userId: UUID): Promise { + const queryResult = await this._dbContext.neo4jService.tryReadAsync( + ` + MATCH (p:Comment { commentId: $commentId })<-[r:${UserToCommentRelTypes.REPORTED}]-(u:User { userId: $userId }) + RETURN r + `, + { + commentId: commentId, + userId: userId, + } + ); + if (queryResult.records.length > 0) { + return true; + } + return false; + } + + private getUserFromRequest(): User { + const user = this._request.user as User; + if (user === undefined) throw new Error("User not found"); + return user; + } +} diff --git a/src/pusher/services/notificationStashPool/notificationStashPool.service.ts b/src/pusher/services/notificationStashPool/notificationStashPool.service.ts index 806a7e2..0694fc6 100644 --- a/src/pusher/services/notificationStashPool/notificationStashPool.service.ts +++ b/src/pusher/services/notificationStashPool/notificationStashPool.service.ts @@ -32,6 +32,7 @@ export class NotificationStashPoolService implements INotificationStashPoolServi } this.notificationStashPool.set(userId, [...foundStash, createdStash]); + return createdStash; } public async popStashNotifications(userId: UUID): Promise { From da7d52bee7fdae82e5b7bab03b13b7b1b34f0467 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Thu, 1 Dec 2022 21:28:36 -0800 Subject: [PATCH 137/153] fix nested comment --- src/comments/models/comment.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/comments/models/comment.ts b/src/comments/models/comment.ts index 3b40ee0..8a4fa7f 100644 --- a/src/comments/models/comment.ts +++ b/src/comments/models/comment.ts @@ -120,7 +120,7 @@ export class Comment extends Model { ` MATCH (c:Comment)-[:${ CommentToSelfRelTypes.REPLIED - }]->(p:Comment) WHERE c.commentId = $parentId + }]->(p:Comment) WHERE p.commentId = $parentId RETURN c ${limit > 0 ? `LIMIT $limit` : ""} `, From 633f16f0a0d68e1e8929239b42e595002633867e Mon Sep 17 00:00:00 2001 From: iantelli <2ndex@duck.com> Date: Thu, 1 Dec 2022 22:16:04 -0800 Subject: [PATCH 138/153] Better error handling --- .../services/moderatorActions/moderatorActions.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/moderation/services/moderatorActions/moderatorActions.service.ts b/src/moderation/services/moderatorActions/moderatorActions.service.ts index 2caa570..8569657 100644 --- a/src/moderation/services/moderatorActions/moderatorActions.service.ts +++ b/src/moderation/services/moderatorActions/moderatorActions.service.ts @@ -299,11 +299,13 @@ export class ModeratorActionsService implements IModeratorActionsService { public async getPendingPosts(): Promise { const posts = await this._dbContext.Posts.getPendingPosts(); + if (!posts) throw new HttpException("Posts not found", 404); return posts; } public async getDeletedPosts(): Promise { const posts = await this._dbContext.Posts.getDeletedPosts(); + if (!posts) throw new HttpException("Posts not found", 404); return posts; } From d4de474f7343ec3c52b5b824aff39295fde779a4 Mon Sep 17 00:00:00 2001 From: iantelli <2ndex@duck.com> Date: Thu, 1 Dec 2022 22:16:18 -0800 Subject: [PATCH 139/153] Adding aliases for UUID --- .../post/posts.repository.interface.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/posts/repositories/post/posts.repository.interface.ts b/src/posts/repositories/post/posts.repository.interface.ts index 3a85a5c..e1a6381 100644 --- a/src/posts/repositories/post/posts.repository.interface.ts +++ b/src/posts/repositories/post/posts.repository.interface.ts @@ -6,7 +6,9 @@ export interface IPostsRepository { findPostByPostType(postTypeName: string): Promise; - findPostById(postId: string): Promise; + findPostById(postId: UUID): Promise; + + findPostsByUserId(userId: UUID): Promise; getPostHistoryByUserId(userId: UUID): Promise; @@ -14,15 +16,15 @@ export interface IPostsRepository { updatePost(post: Post): Promise; - deletePost(postId: string): Promise; + deletePost(postId: UUID): Promise; - markAsDeleted(postId: string, deletedProps: DeletedProps): Promise; + markAsDeleted(postId: UUID, deletedProps: DeletedProps): Promise; - removeDeletedMark(postId: string): Promise; + removeDeletedMark(postId: UUID): Promise; - restrictPost(postId: string, restrictedProps: RestrictedProps): Promise; + restrictPost(postId: UUID, restrictedProps: RestrictedProps): Promise; - unrestrictPost(postId: string): Promise; + unrestrictPost(postId: UUID): Promise; getPendingPosts(): Promise; From 14aad74bb35123372abc37eb4341a567e6265213 Mon Sep 17 00:00:00 2001 From: iantelli <2ndex@duck.com> Date: Thu, 1 Dec 2022 22:16:31 -0800 Subject: [PATCH 140/153] Created getPostsByUserId controller --- src/posts/controllers/posts.controller.ts | 13 +++++++++ .../repositories/post/posts.repository.ts | 27 ++++++++++++++----- .../services/posts/posts.service.interface.ts | 2 ++ src/posts/services/posts/posts.service.ts | 6 +++++ 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/posts/controllers/posts.controller.ts b/src/posts/controllers/posts.controller.ts index 760c0d5..0a53ad4 100644 --- a/src/posts/controllers/posts.controller.ts +++ b/src/posts/controllers/posts.controller.ts @@ -134,6 +134,19 @@ export class PostsController { return await post.toJSON({ authenticatedUserId: user?.userId ?? undefined }); } + @Get("/user/:userId") + @UseGuards(OptionalJwtAuthGuard) + public async getPostsByUserId( + @AuthedUser() user: User, + @Param("userId", new ParseUUIDPipe()) userId: UUID + ): Promise { + const posts = await this._postsService.findPostsByUserId(userId); + const decoratedPosts = posts.map(post => + post.toJSON({ authenticatedUserId: user?.userId ?? undefined }) + ); + return await Promise.all(decoratedPosts); + } + @Post("/create") @UseGuards(CaptchaGuard) @UseGuards(AuthGuard("jwt")) diff --git a/src/posts/repositories/post/posts.repository.ts b/src/posts/repositories/post/posts.repository.ts index ac94ab1..1008fe2 100644 --- a/src/posts/repositories/post/posts.repository.ts +++ b/src/posts/repositories/post/posts.repository.ts @@ -31,7 +31,7 @@ export class PostsRepository implements IPostsRepository { return records.map(record => new Post(record.get("p").properties, this._neo4jService)); } - public async findPostById(postId: string): Promise { + public async findPostById(postId: UUID): Promise { const post = await this._neo4jService.tryReadAsync( `MATCH (p:Post) WHERE p.postId = $postId RETURN p`, { postId: postId } @@ -40,6 +40,21 @@ export class PostsRepository implements IPostsRepository { return new Post(post.records[0].get("p").properties, this._neo4jService); } + public async findPostsByUserId(userId: string): Promise { + const allPosts = await this._neo4jService.tryReadAsync( + ` + MATCH (u:User { userId: $userId})-[r:${UserToPostRelTypes.AUTHORED}]->(p:Post) + RETURN p + `, + { + userId: userId, + } + ); + const records = allPosts.records; + if (records.length === 0) return []; + return records.map(record => new Post(record.get("p").properties, this._neo4jService)); + } + public async getPostHistoryByUserId(userId: UUID): Promise { const userPosts = await this._neo4jService.tryReadAsync( `MATCH (u:User { userId: $userId })-[:${UserToPostRelTypes.AUTHORED}]->(p:Post) @@ -169,13 +184,13 @@ export class PostsRepository implements IPostsRepository { ); } - public async deletePost(postId: string): Promise { + public async deletePost(postId: UUID): Promise { this._neo4jService.write(`MATCH (p:Post) WHERE p.postId = $postId DETACH DELETE p`, { postId: postId, }); } - public async markAsDeleted(postId: string, deletedProps: DeletedProps): Promise { + public async markAsDeleted(postId: UUID, deletedProps: DeletedProps): Promise { await this._neo4jService.tryWriteAsync( ` MATCH (p:Post {postId: $postId}) @@ -189,7 +204,7 @@ export class PostsRepository implements IPostsRepository { ); } - public async removeDeletedMark(postId: string): Promise { + public async removeDeletedMark(postId: UUID): Promise { await this._neo4jService.tryWriteAsync( ` MATCH (p:Post {postId: $postId})-[r:${_ToSelfRelTypes.DELETED}]->(p) @@ -201,7 +216,7 @@ export class PostsRepository implements IPostsRepository { ); } - public async restrictPost(postId: string, restrictedProps: RestrictedProps): Promise { + public async restrictPost(postId: UUID, restrictedProps: RestrictedProps): Promise { await this._neo4jService.tryWriteAsync( `MATCH (p:Post { postId: $postId }) CREATE (p)-[r:${_ToSelfRelTypes.RESTRICTED} { @@ -218,7 +233,7 @@ export class PostsRepository implements IPostsRepository { ); } - public async unrestrictPost(postId: string): Promise { + public async unrestrictPost(postId: UUID): Promise { await this._neo4jService.tryWriteAsync( `MATCH (p:Post)-[r:${_ToSelfRelTypes.RESTRICTED}]->(p) WHERE p.postId = $postId DELETE r`, { diff --git a/src/posts/services/posts/posts.service.interface.ts b/src/posts/services/posts/posts.service.interface.ts index 39f5846..97c67dd 100644 --- a/src/posts/services/posts/posts.service.interface.ts +++ b/src/posts/services/posts/posts.service.interface.ts @@ -17,6 +17,8 @@ export interface IPostsService { findPostById(postId: UUID): Promise; + findPostsByUserId(userId: UUID): Promise; + findNestedCommentsByPostId( postId: UUID, topLevelLimit: number, diff --git a/src/posts/services/posts/posts.service.ts b/src/posts/services/posts/posts.service.ts index 861912f..2721017 100644 --- a/src/posts/services/posts/posts.service.ts +++ b/src/posts/services/posts/posts.service.ts @@ -208,6 +208,12 @@ export class PostsService implements IPostsService { return foundPost; } + public async findPostsByUserId(userId: UUID): Promise { + let foundPosts = await this._dbContext.Posts.findPostsByUserId(userId); + if (!foundPosts) throw new HttpException("Posts not found", 404); + return foundPosts; + } + public async findNestedCommentsByPostId( postId: UUID, topLevelLimit: number, From 36f6a0b7a5a33e4aee1127b7445bde2b30fce483 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Thu, 1 Dec 2022 22:39:55 -0800 Subject: [PATCH 141/153] fix errrors --- src/auth/services/auth.service.ts | 7 +- .../commentReport/commentsReport.service.ts | 4 +- .../services/comments/comments.service.ts | 2 +- .../autoModeration/autoModeration.service.ts | 2 +- .../postReport/postsReport.service.ts | 199 +++++++++--------- src/posts/services/posts/posts.service.ts | 2 +- 6 files changed, 109 insertions(+), 107 deletions(-) diff --git a/src/auth/services/auth.service.ts b/src/auth/services/auth.service.ts index 8091356..39acce3 100644 --- a/src/auth/services/auth.service.ts +++ b/src/auth/services/auth.service.ts @@ -1,4 +1,4 @@ -import { HttpException, Inject, Injectable } from "@nestjs/common"; +import { HttpException, Inject, Injectable, Logger } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { JwtService } from "@nestjs/jwt"; import * as bcrypt from "bcrypt"; @@ -17,6 +17,8 @@ import { _$ } from "../../_domain/injectableTokens"; @Injectable({}) export class AuthService implements IAuthService { + private readonly _logger: Logger = new Logger(AuthService.name); + constructor( @Inject(_$.IUsersRepository) private _usersRepository: IUsersRepository, private _jwtService: JwtService, @@ -51,7 +53,8 @@ export class AuthService implements IAuthService { access_token: token, }); } catch (error) { - throw new Error(error); + this._logger.warn(error); + throw new HttpException("something wrong happened", 500); } } diff --git a/src/comments/services/commentReport/commentsReport.service.ts b/src/comments/services/commentReport/commentsReport.service.ts index 52a89e8..bad79fd 100644 --- a/src/comments/services/commentReport/commentsReport.service.ts +++ b/src/comments/services/commentReport/commentsReport.service.ts @@ -28,7 +28,7 @@ export class CommentsReportService implements ICommentsReportService { const comment = await this._dbContext.Comments.findCommentById( reportCommentPayload.commentId ); - if (!comment) throw new Error("Comment not found"); + if (!comment) throw new HttpException("Comment not found", 404); if (comment.pending || comment.restrictedProps !== null) { throw new HttpException( @@ -96,7 +96,7 @@ export class CommentsReportService implements ICommentsReportService { private getUserFromRequest(): User { const user = this._request.user as User; - if (user === undefined) throw new Error("User not found"); + if (user === undefined) throw new HttpException("User not found", 404); return user; } } diff --git a/src/comments/services/comments/comments.service.ts b/src/comments/services/comments/comments.service.ts index a489f2a..49efb79 100644 --- a/src/comments/services/comments/comments.service.ts +++ b/src/comments/services/comments/comments.service.ts @@ -361,7 +361,7 @@ export class CommentsService implements ICommentsService { private getUserFromRequest(): User { const user = this._request.user as User; - if (user === undefined) throw new Error("User not found"); + if (user === undefined) throw new HttpException("User not found", 404); return user; } } diff --git a/src/moderation/services/autoModeration/autoModeration.service.ts b/src/moderation/services/autoModeration/autoModeration.service.ts index 0175476..3f54037 100644 --- a/src/moderation/services/autoModeration/autoModeration.service.ts +++ b/src/moderation/services/autoModeration/autoModeration.service.ts @@ -92,7 +92,7 @@ export class AutoModerationService implements IAutoModerationService { private getUserFromRequest(): User { const user = this._request.user as User; - if (user === undefined) throw new Error("User not found"); + if (user === undefined) throw new HttpException("User not found", 404); return user; } } diff --git a/src/posts/services/postReport/postsReport.service.ts b/src/posts/services/postReport/postsReport.service.ts index 5ff2db0..234ace2 100644 --- a/src/posts/services/postReport/postsReport.service.ts +++ b/src/posts/services/postReport/postsReport.service.ts @@ -1,100 +1,99 @@ -import { ReportPostPayloadDto } from "../../dtos"; -import { User } from "../../../users/models"; -import { HttpException, Inject, Injectable, Logger, Scope } from "@nestjs/common"; -import { DatabaseContext } from "../../../database-access-layer/databaseContext"; -import { REQUEST } from "@nestjs/core"; -import { Request } from "express"; -import { _$ } from "../../../_domain/injectableTokens"; -import { ReportedProps, UserToPostRelTypes } from "../../../users/models/toPost"; -import { IPostsReportService } from "./postsReport.service.interface"; - -@Injectable({ scope: Scope.REQUEST }) -export class PostsReportService implements IPostsReportService { - private readonly _logger = new Logger(PostsReportService.name); - private readonly _request: Request; - private readonly _dbContext: DatabaseContext; - - constructor( - @Inject(REQUEST) request: Request, - @Inject(_$.IDatabaseContext) databaseContext: DatabaseContext - ) { - this._request = request; - this._dbContext = databaseContext; - } - - public async reportPost(reportPostPayload: ReportPostPayloadDto): Promise { - const user = this.getUserFromRequest(); - const post = await this._dbContext.Posts.findPostById(reportPostPayload.postId); - if (!post) throw new Error("Post not found"); - - if (post.pending || post.restrictedProps !== null) { - throw new HttpException( - "Post cannot be reported due to being pending or restricted", - 400 - ); - } - - const report = await this.checkIfUserReportedPost(post.postId, user.userId); - - if (report === true) { - throw new HttpException("You have already reported this post", 400); - } - - await post.getAuthorUser(); - if (post.authorUser.userId === user.userId) { - throw new HttpException("Post cannot be reported by post author", 400); - } - - await this._dbContext.neo4jService.tryWriteAsync( - ` - MATCH (p:Post { postId: $postId }), (u:User { userId: $userId }) - MERGE (u)-[r:${UserToPostRelTypes.REPORTED}{reportedAt: $reportedAt, reason: $reason}]->(p) - `, - { - reportedAt: Date.now(), - reason: reportPostPayload.reason, - postId: post.postId, - userId: user.userId, - } - ); - } - - public async getReportsForPost(postId: UUID): Promise { - const queryResult = await this._dbContext.neo4jService.tryReadAsync( - ` - MATCH (p:Post { postId: $postId })<-[r:${UserToPostRelTypes.REPORTED}]-(u:User) - RETURN r, u`, - { - postId: postId, - } - ); - return queryResult.records.map(record => { - const reportedProps = new ReportedProps(record.get("r").properties); - return reportedProps; - }); - } - - private async checkIfUserReportedPost(postId: UUID, userId: UUID): Promise { - const queryResult = await this._dbContext.neo4jService.tryReadAsync( - ` - MATCH (p:Post { postId: $postId })<-[r:${UserToPostRelTypes.REPORTED}]-(u:User { userId: $userId }) - RETURN r - `, - { - postId: postId, - userId: userId, - } - ); - if (queryResult.records.length > 0) { - return true; - } - return false; - } - - private getUserFromRequest(): User { - const user = this._request.user as User; - if (user === undefined) throw new Error("User not found"); - return user; - } -} - +import { ReportPostPayloadDto } from "../../dtos"; +import { User } from "../../../users/models"; +import { HttpException, Inject, Injectable, Logger, Scope } from "@nestjs/common"; +import { DatabaseContext } from "../../../database-access-layer/databaseContext"; +import { REQUEST } from "@nestjs/core"; +import { Request } from "express"; +import { _$ } from "../../../_domain/injectableTokens"; +import { ReportedProps, UserToPostRelTypes } from "../../../users/models/toPost"; +import { IPostsReportService } from "./postsReport.service.interface"; + +@Injectable({ scope: Scope.REQUEST }) +export class PostsReportService implements IPostsReportService { + private readonly _logger = new Logger(PostsReportService.name); + private readonly _request: Request; + private readonly _dbContext: DatabaseContext; + + constructor( + @Inject(REQUEST) request: Request, + @Inject(_$.IDatabaseContext) databaseContext: DatabaseContext + ) { + this._request = request; + this._dbContext = databaseContext; + } + + public async reportPost(reportPostPayload: ReportPostPayloadDto): Promise { + const user = this.getUserFromRequest(); + const post = await this._dbContext.Posts.findPostById(reportPostPayload.postId); + if (!post) throw new HttpException("Post not found", 404); + + if (post.pending || post.restrictedProps !== null) { + throw new HttpException( + "Post cannot be reported due to being pending or restricted", + 400 + ); + } + + const report = await this.checkIfUserReportedPost(post.postId, user.userId); + + if (report === true) { + throw new HttpException("You have already reported this post", 400); + } + + await post.getAuthorUser(); + if (post.authorUser.userId === user.userId) { + throw new HttpException("Post cannot be reported by post author", 400); + } + + await this._dbContext.neo4jService.tryWriteAsync( + ` + MATCH (p:Post { postId: $postId }), (u:User { userId: $userId }) + MERGE (u)-[r:${UserToPostRelTypes.REPORTED}{reportedAt: $reportedAt, reason: $reason}]->(p) + `, + { + reportedAt: Date.now(), + reason: reportPostPayload.reason, + postId: post.postId, + userId: user.userId, + } + ); + } + + public async getReportsForPost(postId: UUID): Promise { + const queryResult = await this._dbContext.neo4jService.tryReadAsync( + ` + MATCH (p:Post { postId: $postId })<-[r:${UserToPostRelTypes.REPORTED}]-(u:User) + RETURN r, u`, + { + postId: postId, + } + ); + return queryResult.records.map(record => { + const reportedProps = new ReportedProps(record.get("r").properties); + return reportedProps; + }); + } + + private async checkIfUserReportedPost(postId: UUID, userId: UUID): Promise { + const queryResult = await this._dbContext.neo4jService.tryReadAsync( + ` + MATCH (p:Post { postId: $postId })<-[r:${UserToPostRelTypes.REPORTED}]-(u:User { userId: $userId }) + RETURN r + `, + { + postId: postId, + userId: userId, + } + ); + if (queryResult.records.length > 0) { + return true; + } + return false; + } + + private getUserFromRequest(): User { + const user = this._request.user as User; + if (user === undefined) throw new HttpException("User not found", 404); + return user; + } +} diff --git a/src/posts/services/posts/posts.service.ts b/src/posts/services/posts/posts.service.ts index 2721017..df46c00 100644 --- a/src/posts/services/posts/posts.service.ts +++ b/src/posts/services/posts/posts.service.ts @@ -346,7 +346,7 @@ export class PostsService implements IPostsService { private getUserFromRequest(): User { const user = this._request.user as User; - if (user === undefined) throw new Error("User not found"); + if (user === undefined) throw new HttpException("User not found", 404); return user; } } From aab051363c65e4879ddcc7ea559fcd8f0540272a Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Thu, 1 Dec 2022 22:59:02 -0800 Subject: [PATCH 142/153] fix the report --- .../models/userActions.enum.ts | 3 +-- .../postReport/postsReport.service.interface.ts | 16 ++++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/google-cloud-recaptcha-enterprise/models/userActions.enum.ts b/src/google-cloud-recaptcha-enterprise/models/userActions.enum.ts index 7063d88..65ed95d 100644 --- a/src/google-cloud-recaptcha-enterprise/models/userActions.enum.ts +++ b/src/google-cloud-recaptcha-enterprise/models/userActions.enum.ts @@ -4,6 +4,5 @@ export enum UserActionsEnum { CreatePost = "CreatePost", Login = "Login", SignUp = "SignUp", - ReportComment = "ReportComment", - ReportPost = "ReportPost", + ContentReport = "ContentReport", } diff --git a/src/posts/services/postReport/postsReport.service.interface.ts b/src/posts/services/postReport/postsReport.service.interface.ts index 6ad294f..a972e9e 100644 --- a/src/posts/services/postReport/postsReport.service.interface.ts +++ b/src/posts/services/postReport/postsReport.service.interface.ts @@ -1,8 +1,8 @@ -import { ReportPostPayloadDto } from "../../dtos"; -import { ReportedProps } from "../../../users/models/toPost"; - -export interface IPostsReportService { - reportPost(reportPostPayload: ReportPostPayloadDto): Promise; - - getReportsForPost(postId: string): Promise; -} +import { ReportPostPayloadDto } from "../../dtos"; +import { ReportedProps } from "../../../users/models/toPost"; + +export interface IPostsReportService { + reportPost(reportPostPayload: ReportPostPayloadDto): Promise; + + getReportsForPost(postId: string): Promise; +} From 119f37c5361923bcbb5d2e4bdbf194c526afe3e3 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Fri, 2 Dec 2022 00:50:17 -0800 Subject: [PATCH 143/153] try to optimize the post model toJSON and filter the restricted/deleted/pending posts --- src/posts/models/post.ts | 16 +++++------ src/posts/services/posts/posts.service.ts | 33 ++++++++++++++++++----- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/posts/models/post.ts b/src/posts/models/post.ts index b28ea6e..6ec827f 100644 --- a/src/posts/models/post.ts +++ b/src/posts/models/post.ts @@ -98,14 +98,14 @@ export class Post extends Model { public async toJSON(props: ToJSONProps = {}) { if (this.neo4jService) { await Promise.all([ - this.getPostType(), - this.getPostTags(), - this.getAwards(), - this.getRestricted(), - this.getDeletedProps(), - this.getCreatedAt(), - this.getTotalVotes(), - this.getAuthorUser(), + ...(!this.postType ? [this.getPostType()] : []), + ...(!this.postTags ? [this.getPostTags()] : []), + ...(!this.awards ? [this.getAwards()] : []), + ...(!this.restrictedProps ? [this.getRestricted()] : []), + ...(!this.deletedProps ? [this.getDeletedProps()] : []), + ...(!this.createdAt ? [this.getCreatedAt()] : []), + ...(!this.totalVotes ? [this.getTotalVotes()] : []), + ...(!this.authorUser ? [this.getAuthorUser()] : []), ...(props.authenticatedUserId ? [this.getUserVote(props.authenticatedUserId)] : []), ]); } diff --git a/src/posts/services/posts/posts.service.ts b/src/posts/services/posts/posts.service.ts index df46c00..d60b027 100644 --- a/src/posts/services/posts/posts.service.ts +++ b/src/posts/services/posts/posts.service.ts @@ -55,8 +55,11 @@ export class PostsService implements IPostsService { } // auto-moderation - const wasOffending = await this._autoModerationService.checkForHateSpeech( - postPayload.postTitle + postPayload.postContent + const titleWasOffending = await this._autoModerationService.checkForHateSpeech( + postPayload.postTitle + ); + const contentWasOffending = await this._autoModerationService.checkForHateSpeech( + postPayload.postContent ); // if moderation passed, create post and return it. @@ -67,7 +70,7 @@ export class PostsService implements IPostsService { postTitle: postPayload.postTitle, postContent: postPayload.postContent, authorUser: user, - pending: wasOffending, + pending: titleWasOffending || contentWasOffending, }), postPayload.anonymous ); @@ -158,15 +161,33 @@ export class PostsService implements IPostsService { } public async findAllQueeries(): Promise { - const queeries = await this._dbContext.Posts.findPostByPostType("queery"); + let queeries = await this._dbContext.Posts.findPostByPostType("queery"); + queeries = await this.filterPublicPosts(queeries); return this.decoratePosts(queeries, (postA, postB) => postB.createdAt - postA.createdAt); } public async findAllStories(): Promise { - const stories = await this._dbContext.Posts.findPostByPostType("story"); + let stories = await this._dbContext.Posts.findPostByPostType("story"); + stories = await this.filterPublicPosts(stories); return this.decoratePosts(stories, (postA, postB) => postB.createdAt - postA.createdAt); } + private async filterPublicPosts(posts: Post[]): Promise { + const filteredPosts = []; + for (const post of posts) { + if (post.pending) continue; + + await post.getRestricted(); + if (post.restrictedProps) continue; + + await post.getDeletedProps(); + if (post.deletedProps) continue; + + filteredPosts.push(post); + } + return filteredPosts; + } + private async decoratePosts(posts: Post[], sorted: null | postSortCallback): Promise { posts = await Promise.all( posts.map(async post => { @@ -209,7 +230,7 @@ export class PostsService implements IPostsService { } public async findPostsByUserId(userId: UUID): Promise { - let foundPosts = await this._dbContext.Posts.findPostsByUserId(userId); + const foundPosts = await this._dbContext.Posts.findPostsByUserId(userId); if (!foundPosts) throw new HttpException("Posts not found", 404); return foundPosts; } From 14848dbdf0f8b84ac4fae0282c432f53e83dc03d Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Fri, 2 Dec 2022 02:49:24 -0800 Subject: [PATCH 144/153] fix bugs --- .../listeners/commentEvents.listener.ts | 27 ++++++++++++++++++- src/_domain/listeners/postEvents.listener.ts | 18 ++++++++++++- .../services/comments/comments.service.ts | 2 +- ...ificationMessageMaker.service.interface.ts | 2 ++ .../notificationMessageMaker.service.ts | 15 +++++------ ...notificationStashPool.service.interface.ts | 1 + .../notificationStashPool.service.ts | 4 +-- 7 files changed, 56 insertions(+), 13 deletions(-) diff --git a/src/_domain/listeners/commentEvents.listener.ts b/src/_domain/listeners/commentEvents.listener.ts index 560c809..b1a244a 100644 --- a/src/_domain/listeners/commentEvents.listener.ts +++ b/src/_domain/listeners/commentEvents.listener.ts @@ -15,6 +15,7 @@ import { import { INotificationMessageMakerService } from "../../pusher/services/notificationMessageMaker/notificationMessageMaker.service.interface"; import { EventTypes } from "../eventTypes"; import { INotificationStashPoolService } from "../../pusher/services/notificationStashPool/notificationStashPool.service.interface"; +import { generateUUID } from "../utils"; @Injectable({ scope: Scope.DEFAULT }) export class CommentEventsListener { @@ -38,6 +39,8 @@ export class CommentEventsListener { @OnEvent(EventTypes.CommentGotUpVote, { async: true }) public async handleCommentGotUpVote(event: CommentGotVoteEvent): Promise { + const stashToken = generateUUID(); + this._notificationMessageMakerService.stashToken = stashToken; const message = this._notificationMessageMakerService.makeForCommentGotUpVote({ username: event.username, postId: event.postId, @@ -45,6 +48,7 @@ export class CommentEventsListener { }); await this.stashAndPushNotification( + stashToken, EventTypes.CommentGotUpVote, event.subscriberId, event.username, @@ -55,6 +59,8 @@ export class CommentEventsListener { @OnEvent(EventTypes.CommentGotDownVote, { async: true }) public async handleCommentGotDownVote(event: CommentGotVoteEvent): Promise { + const stashToken = generateUUID(); + this._notificationMessageMakerService.stashToken = stashToken; const message = this._notificationMessageMakerService.makeForCommentGotDownVote({ username: event.username, postId: event.postId, @@ -62,6 +68,7 @@ export class CommentEventsListener { }); await this.stashAndPushNotification( + stashToken, EventTypes.CommentGotDownVote, event.subscriberId, event.username, @@ -72,12 +79,15 @@ export class CommentEventsListener { @OnEvent(EventTypes.CommentGotRestricted, { async: true }) public async handleCommentGotRestricted(event: CommentGotRestrictedEvent): Promise { + const stashToken = generateUUID(); + this._notificationMessageMakerService.stashToken = stashToken; const message = this._notificationMessageMakerService.makeForCommentGotRestricted({ commentContent: event.commentContent, reason: event.reason, }); await this.stashAndPushNotification( + stashToken, EventTypes.CommentGotRestricted, event.subscriberUserId, event.username, @@ -90,6 +100,8 @@ export class CommentEventsListener { public async handleCommentGotPinnedByAuthor( event: CommentGotPinnedByAuthorEvent ): Promise { + const stashToken = generateUUID(); + this._notificationMessageMakerService.stashToken = stashToken; const message = this._notificationMessageMakerService.makeForCommentGotPinnedByAuthor({ commentId: event.commentId, postId: event.postId, @@ -98,6 +110,7 @@ export class CommentEventsListener { }); await this.stashAndPushNotification( + stashToken, EventTypes.CommentGotPinnedByAuthor, event.subscriberId, event.username, @@ -110,6 +123,8 @@ export class CommentEventsListener { public async handleCommentGotApprovedByModerator( event: CommentGotApprovedByModeratorEvent ): Promise { + const stashToken = generateUUID(); + this._notificationMessageMakerService.stashToken = stashToken; const message = this._notificationMessageMakerService.makeForCommentGotApprovedByModerator({ commentId: event.commentId, postId: event.postId, @@ -117,6 +132,7 @@ export class CommentEventsListener { }); await this.stashAndPushNotification( + stashToken, EventTypes.CommentGotApprovedByModerator, event.subscriberId, event.username, @@ -127,12 +143,15 @@ export class CommentEventsListener { @OnEvent(EventTypes.NewCommentOnComment, { async: true }) public async handleNewCommentOnComment(event: NewCommentEvent): Promise { + const stashToken = generateUUID(); + this._notificationMessageMakerService.stashToken = stashToken; const message = this._notificationMessageMakerService.makeForNewCommentOnComment({ username: event.username, commentContent: event.commentContent, }); await this.stashAndPushNotification( + stashToken, EventTypes.NewCommentOnComment, event.subscriberId, event.username, @@ -143,6 +162,8 @@ export class CommentEventsListener { @OnEvent(EventTypes.NewCommentOnPost, { async: true }) public async handleNewCommentOnPost(event: NewCommentEvent): Promise { + const stashToken = generateUUID(); + this._notificationMessageMakerService.stashToken = stashToken; const message = this._notificationMessageMakerService.makeForNewCommentOnPost({ username: event.username, postTypeName: event.postTypeName, @@ -150,6 +171,7 @@ export class CommentEventsListener { }); await this.stashAndPushNotification( + stashToken, EventTypes.NewCommentOnPost, event.subscriberId, event.username, @@ -159,6 +181,7 @@ export class CommentEventsListener { } private async stashAndPushNotification( + stashToken: UUID, evenType: EventTypes, subscriberId: UUID, username: string, @@ -166,6 +189,7 @@ export class CommentEventsListener { message: string ): Promise { const stashPoolItem = await this._notificationStashPoolService.stashNotification( + stashToken, subscriberId, message, avatar, @@ -178,10 +202,11 @@ export class CommentEventsListener { PusherEvents.UserReceivesNotification, subscriberId, { + subscriberId, username: username, avatar: avatar, composedMessage: message, - stashToken: stashPoolItem.stashToken, + stashToken: stashToken, } ) .then(() => this._logger.verbose(`Event ${evenType} got pushed to ${username}`)) diff --git a/src/_domain/listeners/postEvents.listener.ts b/src/_domain/listeners/postEvents.listener.ts index e0cd148..2064d25 100644 --- a/src/_domain/listeners/postEvents.listener.ts +++ b/src/_domain/listeners/postEvents.listener.ts @@ -8,6 +8,7 @@ import { IPusherService } from "../../pusher/services/pusher/pusher.service.inte import { INotificationMessageMakerService } from "../../pusher/services/notificationMessageMaker/notificationMessageMaker.service.interface"; import { EventTypes } from "../eventTypes"; import { INotificationStashPoolService } from "../../pusher/services/notificationStashPool/notificationStashPool.service.interface"; +import { generateUUID } from "../utils"; @Injectable({ scope: Scope.DEFAULT }) export class PostEventsListener { @@ -32,12 +33,15 @@ export class PostEventsListener { @OnEvent(EventTypes.PostGotUpVote, { async: true }) public async handlePostGotUpVote(event: PostGotVoteEvent): Promise { console.log("event listener emitted"); + const stashToken = generateUUID(); + this._notificationMessageMakerService.stashToken = stashToken; const message = this._notificationMessageMakerService.makeForPostGotUpVote({ username: event.username, postId: event.postId, }); await this.stashAndPushNotification( + stashToken, EventTypes.PostGotUpVote, event.subscriberId, event.username, @@ -49,6 +53,8 @@ export class PostEventsListener { @OnEvent(EventTypes.PostGotDownVote, { async: true }) public async handlePostGotDownVote(event: PostGotVoteEvent): Promise { console.log("event listener emitted"); + const stashToken = generateUUID(); + this._notificationMessageMakerService.stashToken = stashToken; const message = this._notificationMessageMakerService.makeForPostGotDownVote({ username: event.username, @@ -56,6 +62,7 @@ export class PostEventsListener { }); await this.stashAndPushNotification( + stashToken, EventTypes.PostGotDownVote, event.subscriberId, event.username, @@ -68,12 +75,15 @@ export class PostEventsListener { public async handlePostGotApprovedByModerator( event: PostGotApprovedByModeratorEvent ): Promise { + const stashToken = generateUUID(); + this._notificationMessageMakerService.stashToken = stashToken; const message = this._notificationMessageMakerService.makeForPostGotApprovedByModerator({ username: event.username, postId: event.postId, }); await this.stashAndPushNotification( + stashToken, EventTypes.PostGotApprovedByModerator, event.subscriberId, event.username, @@ -84,12 +94,15 @@ export class PostEventsListener { @OnEvent(EventTypes.PostGotRestricted, { async: true }) public async handlePostGotRestricted(event: PostGotRestrictedEvent): Promise { + const stashToken = generateUUID(); + this._notificationMessageMakerService.stashToken = stashToken; const message = this._notificationMessageMakerService.makeForPostGotRestricted({ postTitle: event.postTitle, reason: event.reason, }); await this.stashAndPushNotification( + stashToken, EventTypes.PostGotRestricted, event.subscriberId, event.username, @@ -99,6 +112,7 @@ export class PostEventsListener { } private async stashAndPushNotification( + stashToken: UUID, evenType: EventTypes, subscriberId: UUID, username: string, @@ -106,6 +120,7 @@ export class PostEventsListener { message: string ): Promise { const stashPoolItem = await this._notificationStashPoolService.stashNotification( + stashToken, subscriberId, message ); @@ -116,10 +131,11 @@ export class PostEventsListener { PusherEvents.UserReceivesNotification, subscriberId, { + subscriberId, username: username, avatar: avatar, composedMessage: message, - stashToken: stashPoolItem.stashToken, + stashToken: stashToken, } ) .then(() => this._logger.verbose(`Event ${evenType} got pushed to ${username}`)) diff --git a/src/comments/services/comments/comments.service.ts b/src/comments/services/comments/comments.service.ts index 49efb79..c232281 100644 --- a/src/comments/services/comments/comments.service.ts +++ b/src/comments/services/comments/comments.service.ts @@ -217,7 +217,7 @@ export class CommentsService implements ICommentsService { // don't wait for the push notification. try { - const parentPost = await this.acquireParentPost(comment.commentId); + const [parentPost] = await this.findParentCommentRoot(comment.commentId); await parentPost.getAuthorUser(); this._eventEmitter.emit( eventType, diff --git a/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.interface.ts b/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.interface.ts index a6db1ec..dbc13fc 100644 --- a/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.interface.ts +++ b/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.interface.ts @@ -1,6 +1,8 @@ import { EventTypes } from "../../../_domain/eventTypes"; export interface INotificationMessageMakerService { + stashToken: UUID; + templates: { [key in EventTypes]: (p: object) => string }; makeForNewCommentOnPost(p: { diff --git a/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts b/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts index 62e0bb7..c2a30d4 100644 --- a/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts +++ b/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts @@ -16,9 +16,9 @@ export class NotificationMessageMakerService implements INotificationMessageMake [EventTypes.NewCommentOnComment]: (p: { username: string; commentContent: string }) => `${p.username} replied to your comment '${p.commentContent?.slice(0, 20) ?? ""}'`, [EventTypes.CommentGotUpVote]: (p: { username: string; postId: UUID; commentId: UUID }) => - `${p.username} up voted your comment (uuid:${this.stashToken}:comment:${p.commentId}:post:${p.postId}:text:check it out!)`, + `${p.username} up voted your comment (uuid:${this.stashToken}:comm:${p.commentId}:post:${p.postId}:text:check it out!)`, [EventTypes.CommentGotDownVote]: (p: { username: string; postId: UUID; commentId: UUID }) => - `${p.username} down voted your comment (uuid:${this.stashToken}:comment:${p.commentId}:post:${p.postId}:text:go to comment)`, + `${p.username} down voted your comment (uuid:${this.stashToken}:comm:${p.commentId}:post:${p.postId}:text:go to comment)`, [EventTypes.CommentGotRestricted]: (p: { commentContent: string; reason: string }) => `A Moderator has restricted your comment due to: "${ p.reason @@ -28,19 +28,18 @@ export class NotificationMessageMakerService implements INotificationMessageMake postId: UUID; username: string; }) => - `Our moderator, ${p.username}, allowed your comment to be published. (uuid:${this.stashToken}:comment:${p.commentId}:post:${p.postId}:text:go to comment)`, + `Our moderator, ${p.username}, allowed your comment to be published. (uuid:${this.stashToken}:comm:${p.commentId}:post:${p.postId}:text:go to comment)`, [EventTypes.CommentGotPinnedByAuthor]: (p: { commentId: UUID; postId: UUID; commentContent: string; username: string; }) => - `${p.username} pinned your comment '${p.commentContent.slice( - 0, - 20 - )}'. Check it out`, + `${p.username} pinned your comment '${p.commentContent.slice(0, 20)}'. (uuid:${ + this.stashToken + }:post:${p.postId}:comm:${p.commentId}:text:Check it out)`, [EventTypes.PostGotUpVote]: (p: { username: string; postId: UUID }) => - `${p.username} up voted your post check it out!`, + `${p.username} up voted your post (uuid:${this.stashToken}:post:${p.postId}:text:check it out!)`, [EventTypes.PostGotDownVote]: (p: { username: string; postId: UUID }) => `${p.username} down voted your post (uuid:${this.stashToken}:post:${p.postId}:text:go to post)`, [EventTypes.PostGotRestricted]: (p: { postTitle: string; reason: string }) => diff --git a/src/pusher/services/notificationStashPool/notificationStashPool.service.interface.ts b/src/pusher/services/notificationStashPool/notificationStashPool.service.interface.ts index 838c2d2..d0befe2 100644 --- a/src/pusher/services/notificationStashPool/notificationStashPool.service.interface.ts +++ b/src/pusher/services/notificationStashPool/notificationStashPool.service.interface.ts @@ -2,6 +2,7 @@ import { NotificationStashPoolItem } from "../../models/notificationStashPoolIte export interface INotificationStashPoolService { stashNotification( + stashToken: UUID, userId: UUID, message: string, avatar?: string, diff --git a/src/pusher/services/notificationStashPool/notificationStashPool.service.ts b/src/pusher/services/notificationStashPool/notificationStashPool.service.ts index 0694fc6..4317b14 100644 --- a/src/pusher/services/notificationStashPool/notificationStashPool.service.ts +++ b/src/pusher/services/notificationStashPool/notificationStashPool.service.ts @@ -1,7 +1,6 @@ import { INotificationStashPoolService } from "./notificationStashPool.service.interface"; import { Injectable, Scope } from "@nestjs/common"; import { NotificationStashPoolItem } from "../../models/notificationStashPoolItem.interface"; -import { generateUUID } from "../../../_domain/utils"; @Injectable({ scope: Scope.DEFAULT }) export class NotificationStashPoolService implements INotificationStashPoolService { @@ -11,13 +10,14 @@ export class NotificationStashPoolService implements INotificationStashPoolServi >(); public async stashNotification( + stashToken: UUID, userId: UUID, message: string, avatar?: string, username?: string ): Promise { const createdStash: NotificationStashPoolItem = { - stashToken: generateUUID(), // aka notificationId + stashToken, // aka notificationId message, avatar, username, From 00215af054b0d39d2c59dd36db99228703c172c5 Mon Sep 17 00:00:00 2001 From: iantelli <2ndex@duck.com> Date: Fri, 2 Dec 2022 10:36:45 -0800 Subject: [PATCH 145/153] Update posts.service.ts Co-Authored-By: Ilia Abedian <37903573+iliaamiri@users.noreply.github.com> --- src/posts/services/posts/posts.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/posts/services/posts/posts.service.ts b/src/posts/services/posts/posts.service.ts index d60b027..b3da5ae 100644 --- a/src/posts/services/posts/posts.service.ts +++ b/src/posts/services/posts/posts.service.ts @@ -230,9 +230,9 @@ export class PostsService implements IPostsService { } public async findPostsByUserId(userId: UUID): Promise { - const foundPosts = await this._dbContext.Posts.findPostsByUserId(userId); - if (!foundPosts) throw new HttpException("Posts not found", 404); - return foundPosts; + let foundPosts = await this._dbContext.Posts.findPostsByUserId(userId); + foundPosts = await this.filterPublicPosts(foundPosts); + return this.decoratePosts(foundPosts, (postA, postB) => postB.createdAt - postA.createdAt); } public async findNestedCommentsByPostId( From f8803cb31d99334bf014e87499a00d1aee82d205 Mon Sep 17 00:00:00 2001 From: "[Ian Chao]" <90526260+iantelli@users.noreply.github.com> Date: Fri, 2 Dec 2022 11:15:03 -0800 Subject: [PATCH 146/153] Update authguard for getPostsByUserId --- src/posts/controllers/posts.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/posts/controllers/posts.controller.ts b/src/posts/controllers/posts.controller.ts index 0a53ad4..8df494d 100644 --- a/src/posts/controllers/posts.controller.ts +++ b/src/posts/controllers/posts.controller.ts @@ -135,7 +135,7 @@ export class PostsController { } @Get("/user/:userId") - @UseGuards(OptionalJwtAuthGuard) + @UseGuards(AuthGuard("jwt")) public async getPostsByUserId( @AuthedUser() user: User, @Param("userId", new ParseUUIDPipe()) userId: UUID From 2ba4dfaec01e4ce3f6d4841ff7c87a0718f54e5b Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Fri, 2 Dec 2022 13:35:10 -0800 Subject: [PATCH 147/153] fix post tags --- src/posts/models/post.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/posts/models/post.ts b/src/posts/models/post.ts index 6ec827f..7700acc 100644 --- a/src/posts/models/post.ts +++ b/src/posts/models/post.ts @@ -99,7 +99,7 @@ export class Post extends Model { if (this.neo4jService) { await Promise.all([ ...(!this.postType ? [this.getPostType()] : []), - ...(!this.postTags ? [this.getPostTags()] : []), + ...(this.postTags.length === 0 ? [this.getPostTags()] : []), ...(!this.awards ? [this.getAwards()] : []), ...(!this.restrictedProps ? [this.getRestricted()] : []), ...(!this.deletedProps ? [this.getDeletedProps()] : []), From f442e6a775146e40184af160d0a31ebb903a09df Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Fri, 2 Dec 2022 13:53:45 -0800 Subject: [PATCH 148/153] fix comment notification --- .../listeners/commentEvents.listener.ts | 4 +++ src/comments/events/newComment.event.ts | 4 +++ .../services/comments/comments.service.ts | 29 ++++++++++++------- ...ificationMessageMaker.service.interface.ts | 9 +++++- .../notificationMessageMaker.service.ts | 28 ++++++++++++++---- 5 files changed, 57 insertions(+), 17 deletions(-) diff --git a/src/_domain/listeners/commentEvents.listener.ts b/src/_domain/listeners/commentEvents.listener.ts index b1a244a..cfb3b80 100644 --- a/src/_domain/listeners/commentEvents.listener.ts +++ b/src/_domain/listeners/commentEvents.listener.ts @@ -146,6 +146,8 @@ export class CommentEventsListener { const stashToken = generateUUID(); this._notificationMessageMakerService.stashToken = stashToken; const message = this._notificationMessageMakerService.makeForNewCommentOnComment({ + postId: event.postId, + commentId: event.commentId, username: event.username, commentContent: event.commentContent, }); @@ -165,6 +167,8 @@ export class CommentEventsListener { const stashToken = generateUUID(); this._notificationMessageMakerService.stashToken = stashToken; const message = this._notificationMessageMakerService.makeForNewCommentOnPost({ + postId: event.postId, + commentId: event.commentId, username: event.username, postTypeName: event.postTypeName, commentContent: event.commentContent, diff --git a/src/comments/events/newComment.event.ts b/src/comments/events/newComment.event.ts index 03eea54..0d9bc91 100644 --- a/src/comments/events/newComment.event.ts +++ b/src/comments/events/newComment.event.ts @@ -1,6 +1,10 @@ export class NewCommentEvent { subscriberId: UUID; + postId: UUID; + + commentId: UUID; + username: string; avatar: string; diff --git a/src/comments/services/comments/comments.service.ts b/src/comments/services/comments/comments.service.ts index c232281..fe55860 100644 --- a/src/comments/services/comments/comments.service.ts +++ b/src/comments/services/comments/comments.service.ts @@ -70,6 +70,8 @@ export class CommentsService implements ICommentsService { EventTypes.NewCommentOnPost, new NewCommentEvent({ subscriberId: foundPost.authorUser.userId, + postId: foundPost.postId, + commentId: createdComment.commentId, username: user.username, avatar: user.avatar, commentContent: createdComment.commentContent, @@ -83,7 +85,6 @@ export class CommentsService implements ICommentsService { return createdComment; } - const foundParentComment = await this.findCommentById(commentPayload.parentId); const createdComment = await this._dbContext.Comments.addCommentToComment( new Comment({ commentContent: commentPayload.commentContent, @@ -93,15 +94,23 @@ export class CommentsService implements ICommentsService { parentId: commentPayload.parentId, }) ); - this._eventEmitter.emit( - EventTypes.NewCommentOnComment, - new NewCommentEvent({ - subscriberId: foundParentComment.authorUser.userId, - username: user.username, - avatar: user.avatar, - commentContent: createdComment.commentContent, - }) - ); + try { + const foundParentComment = await this.findCommentById(commentPayload.parentId); + const [parentPost] = await this.findParentCommentRoot(foundParentComment.commentId); + this._eventEmitter.emit( + EventTypes.NewCommentOnComment, + new NewCommentEvent({ + subscriberId: foundParentComment.authorUser.userId, + postId: parentPost.postId, + commentId: createdComment.commentId, + username: user.username, + avatar: user.avatar, + commentContent: createdComment.commentContent, + }) + ); + } catch (error) { + this._logger.error(error); + } return createdComment; } diff --git a/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.interface.ts b/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.interface.ts index dbc13fc..e76abf5 100644 --- a/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.interface.ts +++ b/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.interface.ts @@ -6,12 +6,19 @@ export interface INotificationMessageMakerService { templates: { [key in EventTypes]: (p: object) => string }; makeForNewCommentOnPost(p: { + postId: UUID; + commentId: UUID; username: string; postTypeName: string; commentContent: string; }): string; - makeForNewCommentOnComment(p: { username: string; commentContent: string }): string; + makeForNewCommentOnComment(p: { + postId: UUID; + commentId: UUID; + username: string; + commentContent: string; + }): string; makeForCommentGotUpVote(p: { username: string; postId: UUID; commentId: UUID }): string; diff --git a/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts b/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts index c2a30d4..14468ca 100644 --- a/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts +++ b/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts @@ -7,14 +7,23 @@ export class NotificationMessageMakerService implements INotificationMessageMake public readonly templates = { [EventTypes.NewCommentOnPost]: (p: { username: string; + commentId: UUID; + postId: UUID; postTypeName: string; commentContent: string; }) => - `${p.username} replied to your ${p.postTypeName} '${ - p.commentContent?.slice(0, 20) ?? "" - }'`, - [EventTypes.NewCommentOnComment]: (p: { username: string; commentContent: string }) => - `${p.username} replied to your comment '${p.commentContent?.slice(0, 20) ?? ""}'`, + `${p.username} replied to your ${p.postTypeName} '(uuid:${this.stashToken}:post:${ + p.postId + }:comm:${p.commentId}:text:${p.commentContent?.slice(0, 20) ?? ""})'`, + [EventTypes.NewCommentOnComment]: (p: { + postId: UUID; + commentId: UUID; + username: string; + commentContent: string; + }) => + `${p.username} replied to your comment '(uuid:${this.stashToken}:post:${ + p.postId + }:comm:${p.commentId}:text:${p.commentContent?.slice(0, 20) ?? ""})'`, [EventTypes.CommentGotUpVote]: (p: { username: string; postId: UUID; commentId: UUID }) => `${p.username} up voted your comment (uuid:${this.stashToken}:comm:${p.commentId}:post:${p.postId}:text:check it out!)`, [EventTypes.CommentGotDownVote]: (p: { username: string; postId: UUID; commentId: UUID }) => @@ -53,6 +62,8 @@ export class NotificationMessageMakerService implements INotificationMessageMake public stashToken: string; public makeForNewCommentOnPost(p: { + postId: UUID; + commentId: UUID; username: string; postTypeName: string; commentContent: string; @@ -60,7 +71,12 @@ export class NotificationMessageMakerService implements INotificationMessageMake return this.templates[EventTypes.NewCommentOnPost](p); } - public makeForNewCommentOnComment(p: { username: string; commentContent: string }): string { + public makeForNewCommentOnComment(p: { + postId: UUID; + commentId: UUID; + username: string; + commentContent: string; + }): string { return this.templates[EventTypes.NewCommentOnComment](p); } From bfc58f1e7cfa91224111c9a6b6c496f58bfe18c5 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Fri, 2 Dec 2022 14:13:08 -0800 Subject: [PATCH 149/153] fix the message --- .../notificationMessageMaker.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts b/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts index c2a30d4..02e3520 100644 --- a/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts +++ b/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts @@ -14,9 +14,9 @@ export class NotificationMessageMakerService implements INotificationMessageMake p.commentContent?.slice(0, 20) ?? "" }'`, [EventTypes.NewCommentOnComment]: (p: { username: string; commentContent: string }) => - `${p.username} replied to your comment '${p.commentContent?.slice(0, 20) ?? ""}'`, + `someone replied to your comment '${p.commentContent?.slice(0, 20) ?? ""}'`, [EventTypes.CommentGotUpVote]: (p: { username: string; postId: UUID; commentId: UUID }) => - `${p.username} up voted your comment (uuid:${this.stashToken}:comm:${p.commentId}:post:${p.postId}:text:check it out!)`, + `someone up voted your comment (uuid:${this.stashToken}:comm:${p.commentId}:post:${p.postId}:text:check it out!)`, [EventTypes.CommentGotDownVote]: (p: { username: string; postId: UUID; commentId: UUID }) => `${p.username} down voted your comment (uuid:${this.stashToken}:comm:${p.commentId}:post:${p.postId}:text:go to comment)`, [EventTypes.CommentGotRestricted]: (p: { commentContent: string; reason: string }) => From 5ca4dcf9d27e4ea7a1790612cc5f11ad9aca2c14 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Fri, 2 Dec 2022 14:26:52 -0800 Subject: [PATCH 150/153] fix the messagE --- .../notificationMessageMaker.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts b/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts index 849e463..be45450 100644 --- a/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts +++ b/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts @@ -12,7 +12,7 @@ export class NotificationMessageMakerService implements INotificationMessageMake postTypeName: string; commentContent: string; }) => - `someone replied to your ${p.postTypeName} '(uuid:${this.stashToken}:post:${ + `${p.username} replied to your ${p.postTypeName} '(uuid:${this.stashToken}:post:${ p.postId }:comm:${p.commentId}:text:${p.commentContent?.slice(0, 20) ?? ""})'`, [EventTypes.NewCommentOnComment]: (p: { @@ -27,7 +27,7 @@ export class NotificationMessageMakerService implements INotificationMessageMake [EventTypes.CommentGotUpVote]: (p: { username: string; postId: UUID; commentId: UUID }) => `someone up voted your comment (uuid:${this.stashToken}:comm:${p.commentId}:post:${p.postId}:text:check it out!)`, [EventTypes.CommentGotDownVote]: (p: { username: string; postId: UUID; commentId: UUID }) => - `${p.username} down voted your comment (uuid:${this.stashToken}:comm:${p.commentId}:post:${p.postId}:text:go to comment)`, + `someone down voted your comment (uuid:${this.stashToken}:comm:${p.commentId}:post:${p.postId}:text:go to comment)`, [EventTypes.CommentGotRestricted]: (p: { commentContent: string; reason: string }) => `A Moderator has restricted your comment due to: "${ p.reason From 2c03c24706fdca024ae1d098acec3255885f24f9 Mon Sep 17 00:00:00 2001 From: "Ilia A. Amiri" <37903573+iliaamiri@users.noreply.github.com> Date: Fri, 2 Dec 2022 14:33:43 -0800 Subject: [PATCH 151/153] i swear" --- .../notificationMessageMaker.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts b/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts index be45450..f6f971d 100644 --- a/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts +++ b/src/pusher/services/notificationMessageMaker/notificationMessageMaker.service.ts @@ -50,7 +50,7 @@ export class NotificationMessageMakerService implements INotificationMessageMake [EventTypes.PostGotUpVote]: (p: { username: string; postId: UUID }) => `${p.username} up voted your post (uuid:${this.stashToken}:post:${p.postId}:text:check it out!)`, [EventTypes.PostGotDownVote]: (p: { username: string; postId: UUID }) => - `${p.username} down voted your post (uuid:${this.stashToken}:post:${p.postId}:text:go to post)`, + `someone down voted your post (uuid:${this.stashToken}:post:${p.postId}:text:go to post)`, [EventTypes.PostGotRestricted]: (p: { postTitle: string; reason: string }) => `A Moderator has restricted your post due to: "${ p.reason From 67da0c262c52b835850ffcfc8cbe85fad788d974 Mon Sep 17 00:00:00 2001 From: "[Ian Chao]" <90526260+iantelli@users.noreply.github.com> Date: Fri, 2 Dec 2022 14:44:29 -0800 Subject: [PATCH 152/153] Increasing cache size --- .../services/autoModeration/autoModeration.service.ts | 2 +- src/posts/controllers/posts.controller.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/moderation/services/autoModeration/autoModeration.service.ts b/src/moderation/services/autoModeration/autoModeration.service.ts index 3f54037..bd3cfc3 100644 --- a/src/moderation/services/autoModeration/autoModeration.service.ts +++ b/src/moderation/services/autoModeration/autoModeration.service.ts @@ -51,7 +51,7 @@ export class AutoModerationService implements IAutoModerationService { // if moderation failed, throw error if (hateSpeechResponseDto.class === "flag") { - if (hateSpeechResponseDto.confidence >= 0.92) { + if (hateSpeechResponseDto.confidence >= 0.9001) { // TODO: create a ticket for the admin to review await user.addWasOffendingRecord( diff --git a/src/posts/controllers/posts.controller.ts b/src/posts/controllers/posts.controller.ts index 8df494d..5808ad4 100644 --- a/src/posts/controllers/posts.controller.ts +++ b/src/posts/controllers/posts.controller.ts @@ -84,7 +84,7 @@ export class PostsController { @Get("/queery") @UseGuards(OptionalJwtAuthGuard) - @CacheTTL(5) + @CacheTTL(10) @UseInterceptors(CacheInterceptor) public async getAllQueeries(@AuthedUser() user: User): Promise { const queeries = await this._postsService.findAllQueeries(); @@ -96,7 +96,7 @@ export class PostsController { @Get("/story") @UseGuards(OptionalJwtAuthGuard) - @CacheTTL(5) + @CacheTTL(10) @UseInterceptors(CacheInterceptor) public async getAllStories(@AuthedUser() user: User): Promise { const stories = await this._postsService.findAllStories(); @@ -125,6 +125,7 @@ export class PostsController { } @Get("/:postId") + @CacheTTL(10) @UseGuards(OptionalJwtAuthGuard) public async getPostById( @AuthedUser() user: User, @@ -135,6 +136,7 @@ export class PostsController { } @Get("/user/:userId") + @CacheTTL(20) @UseGuards(AuthGuard("jwt")) public async getPostsByUserId( @AuthedUser() user: User, From 86a944a2a5993216aff44de9745c925d2297b8c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Dec 2022 19:27:14 +0000 Subject: [PATCH 153/153] Bump qs and formidable Bumps [qs](https://github.com/ljharb/qs) and [formidable](https://github.com/node-formidable/formidable). These dependencies needed to be updated together. Updates `qs` from 6.9.3 to 6.10.3 - [Release notes](https://github.com/ljharb/qs/releases) - [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md) - [Commits](https://github.com/ljharb/qs/compare/v6.9.3...v6.10.3) Updates `formidable` from 2.0.1 to 2.1.1 - [Release notes](https://github.com/node-formidable/formidable/releases) - [Changelog](https://github.com/node-formidable/formidable/blob/master/CHANGELOG.md) - [Commits](https://github.com/node-formidable/formidable/commits) --- updated-dependencies: - dependency-name: qs dependency-type: indirect - dependency-name: formidable dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 60 ++++++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/package-lock.json b/package-lock.json index fe20034..8f7c71f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3980,9 +3980,9 @@ } }, "node_modules/dezalgo": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", - "integrity": "sha512-K7i4zNfT2kgQz3GylDw40ot9GAE47sFZ9EXHFSPP6zONLgH6kWXE0KWJchkbQJLBkRazq4APwZ4OwiFFlT95OQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", "dev": true, "dependencies": { "asap": "^2.0.0", @@ -4975,25 +4975,28 @@ } }, "node_modules/formidable": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz", - "integrity": "sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.1.tgz", + "integrity": "sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ==", "dev": true, "dependencies": { - "dezalgo": "1.0.3", - "hexoid": "1.0.0", - "once": "1.4.0", - "qs": "6.9.3" + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" }, "funding": { "url": "https://ko-fi.com/tunnckoCore/commissions" } }, "node_modules/formidable/node_modules/qs": { - "version": "6.9.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz", - "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==", + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, "engines": { "node": ">=0.6" }, @@ -12974,9 +12977,9 @@ "dev": true }, "dezalgo": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", - "integrity": "sha512-K7i4zNfT2kgQz3GylDw40ot9GAE47sFZ9EXHFSPP6zONLgH6kWXE0KWJchkbQJLBkRazq4APwZ4OwiFFlT95OQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", "dev": true, "requires": { "asap": "^2.0.0", @@ -13732,22 +13735,25 @@ } }, "formidable": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz", - "integrity": "sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.1.tgz", + "integrity": "sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ==", "dev": true, "requires": { - "dezalgo": "1.0.3", - "hexoid": "1.0.0", - "once": "1.4.0", - "qs": "6.9.3" + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" }, "dependencies": { "qs": { - "version": "6.9.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz", - "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==", - "dev": true + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } } } },