Skip to content

Commit

Permalink
Merge branch 'develop' into ilia-add-return-type-for-auth-controller
Browse files Browse the repository at this point in the history
  • Loading branch information
iliaamiri authored Nov 17, 2022
2 parents 03de92b + 6bc3f91 commit 433764d
Show file tree
Hide file tree
Showing 12 changed files with 276 additions and 164 deletions.
3 changes: 3 additions & 0 deletions src/_domain/injectableTokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,8 @@ const injectableTokens = {

// Neo4j Module
INeo4jService: Symbol("INeo4jService"),

// Moderation Module
IAutoModerationService: Symbol("IAutoModerationService"),
};
export { injectableTokens as _$ };
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ 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';

@Module({
imports: [
Expand All @@ -36,6 +37,7 @@ import { neo4jCredentials } from "./_domain/constants";
PostsModule,
CommentsModule,
DatabaseAccessLayerModule,
ModerationModule,
],
})
export class AppModule {
Expand Down
3 changes: 2 additions & 1 deletion src/comments/comments.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import { CommentsRepository } from "./repositories/comment/comments.repository";
import { DatabaseAccessLayerModule } from "../database-access-layer/database-access-layer.module";
import { forwardRef, Module } from "@nestjs/common";
import { HttpModule } from "@nestjs/axios";
import { ModerationModule } from "../moderation/moderation.module";

@Module({
controllers: [CommentsController],
imports: [forwardRef(() => DatabaseAccessLayerModule), HttpModule],
imports: [forwardRef(() => DatabaseAccessLayerModule), HttpModule, ModerationModule],
providers: [
{
provide: _$.ICommentsService,
Expand Down
11 changes: 10 additions & 1 deletion src/comments/dtos/commentCreationPayload.dto.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsString } from "class-validator";
import { IsBoolean, isNotEmpty, IsNotEmpty, IsString } from "class-validator";

export class CommentCreationPayloadDto {
@ApiProperty({ type: String, minLength: 5, maxLength: 2500 })
@IsString()
@IsNotEmpty()
commentContent: string;

@ApiProperty({ type: String, format: "uuid" })
@IsString()
@IsNotEmpty()
parentId: string;

@ApiProperty({ type: Boolean })
@IsBoolean()
isPost: boolean;

constructor(partial?: Partial<CommentCreationPayloadDto>) {
Object.assign(this, partial);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ export interface ICommentsRepository {

findChildrenComments(parentId: string): Promise<Comment[]>;

addComment(comment: Comment): Promise<Comment>;
addCommentToComment(comment: Comment): Promise<Comment>;

addCommentToPost(comment: Comment): Promise<Comment>;

deleteComment(commentId: string): Promise<void>;

Expand Down
72 changes: 70 additions & 2 deletions src/comments/repositories/comment/comments.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { RestrictedProps, _ToSelfRelTypes } from "../../../_domain/models/toSelf
import { Comment } from "../../models";
import { CommentToSelfRelTypes, RepliedProps } from "../../models/toSelf";
import { ICommentsRepository } from "./comments.repository.interface";
import { PostToCommentRelTypes } from "../../../posts/models/toComment";

@Injectable()
export class CommentsRepository implements ICommentsRepository {
Expand Down Expand Up @@ -36,10 +37,11 @@ export class CommentsRepository implements ICommentsRepository {
return records.map(record => new Comment(record.get("c").properties, this._neo4jService));
}

public async addComment(comment: Comment): Promise<Comment> {
public async addCommentToComment(comment: Comment): Promise<Comment> {
if (comment.commentId === undefined) {
comment.commentId = this._neo4jService.generateId();
}
console.log(comment);
let restrictedQueryString = "";
let restrictedQueryParams = {};
if (comment.restrictedProps !== null) {
Expand All @@ -60,14 +62,16 @@ export class CommentsRepository implements ICommentsRepository {
await this._neo4jService.tryWriteAsync(
`
MATCH (u:User { userId: $userId })
MATCH (commentParent:Comment { commentId: $parentId })
CREATE (c:Comment {
commentId: $commentId,
updatedAt: $updatedAt,
commentContent: $commentContent,
pending: $pending
})${restrictedQueryString}<-[:${UserToCommentRelTypes.AUTHORED} {
authoredAt: $authoredAt
}]-(u)
}]-(u),
(c)-[:${CommentToSelfRelTypes.REPLIED}]->(commentParent)
`,
{
// Comment properties
Expand All @@ -76,6 +80,70 @@ export class CommentsRepository implements ICommentsRepository {
commentContent: comment.commentContent,
authoredAt: authoredProps.authoredAt,

// Parent
parentId: comment.parentId,

pending: comment.pending,
// User properties
userId: comment.authorUser.userId,

// Authored properties
authoredProps_authoredAt: authoredProps.authoredAt ?? new Date().getTime(),

// RestrictedProps (if applicable)
...restrictedQueryParams,
}
);
return await this.findCommentById(comment.commentId);
}

public async addCommentToPost(comment: Comment): Promise<Comment> {
if (comment.commentId === undefined) {
comment.commentId = this._neo4jService.generateId();
}
let restrictedQueryString = "";
let restrictedQueryParams = {};
if (comment.restrictedProps !== null) {
restrictedQueryString = `-[:${_ToSelfRelTypes.RESTRICTED} {
restrictedAt: $restrictedAt,
moderatorId: $moderatorId,
reason: $reason
}]->(c)`;
restrictedQueryParams = {
restrictedAt: comment.restrictedProps.restrictedAt,
moderatorId: comment.restrictedProps.moderatorId,
reason: comment.restrictedProps.reason,
} as RestrictedProps;
}
const authoredProps = new AuthoredProps({
authoredAt: new Date().getTime(),
});
await this._neo4jService.tryWriteAsync(
`
MATCH (u:User { userId: $userId })
MATCH (commentParent:Post { postId: $parentId })
CREATE (c:Comment {
commentId: $commentId,
updatedAt: $updatedAt,
commentContent: $commentContent,
pending: $pending
})${restrictedQueryString}<-[:${UserToCommentRelTypes.AUTHORED} {
authoredAt: $authoredAt
}]-(u),
(c)<-[:${PostToCommentRelTypes.HAS_COMMENT}]-(commentParent)
`,
{
// Comment properties
commentId: comment.commentId,
updatedAt: comment.updatedAt,
commentContent: comment.commentContent,
authoredAt: authoredProps.authoredAt,

pending: comment.pending,

// Parent
parentId: comment.parentId,

// User properties
userId: comment.authorUser.userId,

Expand Down
135 changes: 53 additions & 82 deletions src/comments/services/comments/comments.service.ts
Original file line number Diff line number Diff line change
@@ -1,115 +1,64 @@
import { HttpService } from "@nestjs/axios";
import { HttpException, Inject, Injectable, Logger, Scope } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { HttpException, Inject, Injectable, Scope } from "@nestjs/common";
import { REQUEST } from "@nestjs/core";
import { Request } from "express";
import { catchError, lastValueFrom, map, throwError } from "rxjs";
import { DatabaseContext } from "../../../database-access-layer/databaseContext";
import { Post } from "../../../posts/models";
import { PostToCommentRelTypes } from "../../../posts/models/toComment";
import { User } from "../../../users/models";
import { UserToCommentRelTypes } from "../../../users/models/toComment";
import { WasOffendingProps } from "../../../users/models/toSelf";
import { _$ } from "../../../_domain/injectableTokens";
import {
CommentCreationPayloadDto,
HateSpeechRequestPayloadDto,
HateSpeechResponseDto,
VoteCommentPayloadDto,
VoteType,
} from "../../dtos";
import { CommentCreationPayloadDto, VoteCommentPayloadDto, VoteType } 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";

@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 _httpService: HttpService;
private readonly _configService: ConfigService;
private readonly _autoModerationService: IAutoModerationService;

constructor(
@Inject(REQUEST) request: Request,
@Inject(_$.IDatabaseContext) databaseContext: DatabaseContext,
httpService: HttpService,
configService: ConfigService
@Inject(_$.IAutoModerationService) autoModerationService: IAutoModerationService
) {
this._request = request;
this._dbContext = databaseContext;
this._httpService = httpService;
this._configService = configService;
this._autoModerationService = autoModerationService;
}

public async authorNewComment(commentPayload: CommentCreationPayloadDto): Promise<Comment> {
const user = this.getUserFromRequest();

// auto-moderation
const autoModerationApiKey = this._configService.get<string>("MODERATE_HATESPEECH_API_KEY");
const autoModerationApiUrl = this._configService.get<string>("MODERATE_HATESPEECH_API_URL");

const hateSpeechResponseDto = await lastValueFrom(
this._httpService
.post<HateSpeechResponseDto>(
autoModerationApiUrl,
new HateSpeechRequestPayloadDto({
token: autoModerationApiKey,
text: commentPayload.commentContent,
})
)
.pipe(
map(response => response.data),
catchError(err => {
this._logger.error(err);
return throwError(() => err);
})
)
const wasOffending = await this._autoModerationService.checkForHateSpeech(
commentPayload.commentContent
);

// if moderation failed, throw error
if (hateSpeechResponseDto.class === "flag") {
if (hateSpeechResponseDto.confidence >= 0.8) {
// TODO: create a ticket for the admin to review

await user.addWasOffendingRecord(
new WasOffendingProps({
timestamp: new Date().getTime(),
userContent: commentPayload.commentContent,
autoModConfidenceLevel: hateSpeechResponseDto.confidence,
})
);
throw new HttpException("Hate speech detected", 400);
}
}

// get all records of them offending. And, get all comments that this user authored.
const userOffendingRecords = await user.getWasOffendingRecords();
const userAuthoredComments = await user.getAuthoredComments();

// lazy-query the restriction state of the comments.
for (const i in userAuthoredComments) {
await userAuthoredComments[i].getRestricted();
// if moderation passed, create comment and return it.
if (commentPayload.isPost) {
return await this._dbContext.Comments.addCommentToPost(
new Comment({
commentContent: commentPayload.commentContent,
authorUser: user,
pending: wasOffending,
updatedAt: new Date().getTime(),
parentId: commentPayload.parentId,
})
);
}

// calculate the honour level of the user. (0 < honourLevel < 1)
const numberOfCleanComments = userAuthoredComments.map(
c => !c.pending && c.restrictedProps === null
).length;

const honourGainHardshipCoefficient = 0.1; // 0.1 means: for the user to achieve the full level of honour, they need at least 10 clean comments while not having any offending records.

let honourLevel =
(1 + numberOfCleanComments * honourGainHardshipCoefficient) /
(2 + userOffendingRecords.length);
honourLevel = honourLevel > 1 ? 1 : honourLevel;

// if moderation passed, create comment and return it.
return await this._dbContext.Comments.addComment(
return await this._dbContext.Comments.addCommentToComment(
new Comment({
commentContent: commentPayload.commentContent,
authorUser: user,
pending: honourLevel < 0.4, // the comment will not be in the pending state only if user's honour level is higher than 0.4
pending: wasOffending,
updatedAt: new Date().getTime(),
parentId: commentPayload.parentId,
})
);
}
Expand Down Expand Up @@ -188,7 +137,7 @@ export class CommentsService implements ICommentsService {

const user = this.getUserFromRequest();

const parentPost = await this.findParentPost(commentId);
const [parentPost] = await this.findParentCommentRoot(commentId);

if (parentPost.authorUser.userId !== user.userId) {
throw new HttpException("User is not the author of the post", 403);
Expand Down Expand Up @@ -228,24 +177,44 @@ export class CommentsService implements ICommentsService {

// gets the parent post of any nested comment of the post
private async findParentPost(commentId: string): Promise<Post> {
const parentCommentId = await this.findParentCommentRoot(commentId);

const parentPost = await this._dbContext.neo4jService.tryReadAsync(
`
MATCH (p:Post)-[:${PostToCommentRelTypes.HAS_COMMENT}]->(c:Comment { commentId: $commentId })
RETURN p
`,
{
parentCommentId,
commentId,
}
);

if (parentPost.records.length === 0) {
throw new HttpException("Post not found", 404);
}
return parentPost.records[0].get("p");
}

private async findParentCommentRoot(commentId: string): Promise<Comment> {
// gets the parent comment of any nested comment of the post
private async findComment(commentId: string): Promise<boolean> {
const queryResult = await this._dbContext.neo4jService.tryReadAsync(
`
MATCH (c:Comment { commentId: $commentId })-[:${CommentToSelfRelTypes.REPLIED}]->(commentParent:Comment)
RETURN commentParent
`,
{
commentId,
}
);
return queryResult.records[0].get("commentParent");
}

// gets the root comment of any nested comment
private async findParentCommentRoot(
commentId: string,
isNestedComment = false
): Promise<[Post, boolean]> {
const queryResult = await this._dbContext.neo4jService.tryReadAsync(
`
MATCH (c:Comment { commentId: $commentId })-[:${CommentToSelfRelTypes.REPLIED}]->(c:Comment))
MATCH (c:Comment { commentId: $commentId })-[:${CommentToSelfRelTypes.REPLIED}]->(c:Comment)
RETURN c
`,
{
Expand All @@ -254,10 +223,12 @@ export class CommentsService implements ICommentsService {
);
if (queryResult.records.length > 0) {
return await this.findParentCommentRoot(
queryResult.records[0].get("c").properties.commentId
queryResult.records[0].get("c").properties.commentId,
true
);
} else {
return await this._dbContext.Comments.findCommentById(commentId);
const rootComment = await this._dbContext.Comments.findCommentById(commentId);
return [await this.findParentPost(rootComment.commentId), isNestedComment];
}
}

Expand Down
Loading

0 comments on commit 433764d

Please sign in to comment.