-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #80 from IGAQ/ilia-add-moderation-module
Ilia add moderation module
- Loading branch information
Showing
9 changed files
with
154 additions
and
156 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { Module } from "@nestjs/common"; | ||
import { AutoModerationService } from "./services/autoModeration/autoModeration.service"; | ||
import { _$ } from "../_domain/injectableTokens"; | ||
import { HttpModule } from "@nestjs/axios"; | ||
|
||
@Module({ | ||
imports: [HttpModule], | ||
providers: [ | ||
{ | ||
provide: _$.IAutoModerationService, | ||
useClass: AutoModerationService, | ||
}, | ||
], | ||
exports: [ | ||
{ | ||
provide: _$.IAutoModerationService, | ||
useClass: AutoModerationService, | ||
}, | ||
], | ||
}) | ||
export class ModerationModule {} |
3 changes: 3 additions & 0 deletions
3
src/moderation/services/autoModeration/autoModeration.service.interface.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export interface IAutoModerationService { | ||
checkForHateSpeech(text: string): Promise<boolean>; | ||
} |
98 changes: 98 additions & 0 deletions
98
src/moderation/services/autoModeration/autoModeration.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import { Request } from "express"; | ||
import { REQUEST } from "@nestjs/core"; | ||
import { HttpException, Inject, Logger } from "@nestjs/common"; | ||
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 { WasOffendingProps } from "../../../users/models/toSelf"; | ||
import { User } from "../../../users/models"; | ||
|
||
export class AutoModerationService implements IAutoModerationService { | ||
private readonly _logger = new Logger(AutoModerationService.name); | ||
private readonly _request: Request; | ||
private readonly _configService: ConfigService; | ||
private readonly _httpService: HttpService; | ||
|
||
constructor( | ||
@Inject(REQUEST) request: Request, | ||
configService: ConfigService, | ||
httpService: HttpService | ||
) { | ||
this._request = request; | ||
this._configService = configService; | ||
this._httpService = httpService; | ||
} | ||
|
||
public async checkForHateSpeech(text: string): Promise<boolean> { | ||
const user = this.getUserFromRequest(); | ||
|
||
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, | ||
}) | ||
) | ||
.pipe( | ||
map(response => response.data), | ||
catchError(err => { | ||
this._logger.error(err); | ||
return throwError(() => err); | ||
}) | ||
) | ||
); | ||
|
||
// if moderation failed, throw error | ||
if (hateSpeechResponseDto.class === "flag") { | ||
if (hateSpeechResponseDto.confidence >= 0.9) { | ||
// TODO: create a ticket for the admin to review | ||
|
||
await user.addWasOffendingRecord( | ||
new WasOffendingProps({ | ||
timestamp: new Date().getTime(), | ||
userContent: text, | ||
autoModConfidenceLevel: hateSpeechResponseDto.confidence, | ||
}) | ||
); | ||
throw new HttpException("Hate speech detected", 400); | ||
} | ||
} | ||
|
||
// get all records of them offending. And, get all posts that this user authored. | ||
const userOffendingRecords = await user.getWasOffendingRecords(); | ||
const userAuthoredPosts = await user.getAuthoredPosts(); | ||
|
||
// lazy-query the restriction state of the posts. | ||
for (const i in userAuthoredPosts) { | ||
await userAuthoredPosts[i].getRestricted(); | ||
} | ||
|
||
// calculate the honour level of the user. (0 < honourLevel < 1) | ||
const numberOfCleanPosts = userAuthoredPosts.map( | ||
p => !p.pending && p.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 posts while not having any offending records. | ||
|
||
let honourLevel = | ||
(1 + numberOfCleanPosts * honourGainHardshipCoefficient) / | ||
(2 + userOffendingRecords.length); | ||
honourLevel = honourLevel > 1 ? 1 : honourLevel; | ||
|
||
// the post will not be in the pending state only if user's honour level is higher than 0.4 | ||
return honourLevel < 0.4; | ||
} | ||
|
||
private getUserFromRequest(): User { | ||
const user = this._request.user as User; | ||
if (user === undefined) throw new Error("User not found"); | ||
return user; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.