Skip to content

Commit

Permalink
feat: new and improve anti-spam manager (#167)
Browse files Browse the repository at this point in the history
feat: Integrate AntiSpamManager in spam check logic

fix: Handle error message properly in analyzeImageForNSFW函数

refactor: Remove unused imports and update sendBlacklistNotif usage
  • Loading branch information
dev-737 authored Oct 18, 2024
1 parent 0c3e120 commit 6c8e001
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 111 deletions.
5 changes: 3 additions & 2 deletions dev/docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ services:
- 8000:8000
image: steelcityamir/safe-content-ai:latest

redis:
image: redis
# redis with multithread support
keydb:
image: eqalpha/keydb
ports:
- 127.0.0.1:6379:6379
8 changes: 7 additions & 1 deletion src/core/BaseClient.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import Constants from '#main/config/Constants.js';
import type BaseCommand from '#main/core/BaseCommand.js';
import type { InteractionFunction } from '#main/decorators/Interaction.js';
import AntiSpamManager from '#main/managers/AntiSpamManager.js';
import UserDbManager from '#main/managers/UserDbManager.js';
import CooldownService from '#main/modules/CooldownService.js';
import EventLoader from '#main/modules/Loaders/EventLoader.js';
import Scheduler from '#main/modules/SchedulerService.js';
import type { RemoveMethods } from '#types/index.d.ts';
import { loadCommandFiles, loadInteractions } from '#utils/CommandUtils.js';
import { loadLocales } from '#utils/Locale.js';
import { resolveEval } from '#utils/Utils.js';
import type { RemoveMethods } from '#types/index.d.ts';
import { ClusterClient, getInfo } from 'discord-hybrid-sharding';
import {
type Guild,
Expand Down Expand Up @@ -38,6 +39,11 @@ export default class InterChatClient extends Client {
readonly commandCooldowns = new CooldownService();
public readonly commands = new Collection<string, BaseCommand>();
public readonly interactions = new Collection<string, InteractionFunction>();
public readonly antiSpamManager = new AntiSpamManager({
spamThreshold: 4,
timeWindow: 5000,
spamCountExpirySecs: 60,
});

constructor() {
super({
Expand Down
76 changes: 76 additions & 0 deletions src/managers/AntiSpamManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import getRedis from '#main/utils/Redis.js';
import { Message } from 'discord.js';
import type { Redis } from 'ioredis';

export default class AntiSpamManager {
private config: SpamConfig;
private redis: Redis;

constructor(config: SpamConfig, redis = getRedis()) {
this.redis = redis;
this.config = config;
}

public async handleMessage(message: Message): Promise<UserMessageInfo | undefined> {
const userId = message.author.id;
const currentTime = Date.now();
const key = `spam:${userId}`;

const userInfo = await this.getUserInfo(key);

if (currentTime - userInfo.lastMessage < this.config.timeWindow) {
userInfo.messageCount++;
if (userInfo.messageCount >= this.config.spamThreshold) {
userInfo.lastMessage = currentTime;
await this.incrementSpamCount(message.author.id);
await this.setUserInfo(key, userInfo);
return userInfo;
}
}
else {
userInfo.messageCount = 1;
}

userInfo.lastMessage = currentTime;
await this.setUserInfo(key, userInfo);
}

private async getUserInfo(key: string): Promise<UserMessageInfo> {
const data = await this.redis.hgetall(key);
return {
messageCount: parseInt(data.messageCount || '0', 10),
lastMessage: parseInt(data.lastMessage || '0', 10),
};
}

private async setUserInfo(key: string, info: UserMessageInfo): Promise<void> {
await this.redis.hmset(key, {
messageCount: info.messageCount.toString(),
lastMessage: info.lastMessage.toString(),
});
await this.redis.expire(key, this.config.timeWindow / 1000);
}

private async incrementSpamCount(userId: string): Promise<void> {
const key = `spamcount:${userId}`;
await this.redis.incr(key);
await this.redis.expire(key, this.config.spamCountExpirySecs);
}

public async getSpamCount(userId: string): Promise<number> {
const key = `spamcount:${userId}`;
const count = await this.redis.get(key);
return parseInt(count || '0', 10);
}
}

interface UserMessageInfo {
messageCount: number;
lastMessage: number;
}

interface SpamConfig {
spamThreshold: number;
timeWindow: number;
spamCountExpirySecs: number;
}
8 changes: 5 additions & 3 deletions src/modules/NSFWDetection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const analyzeImageForNSFW = async (imageURL: string): Promise<predictionT
});

const data = await res.json();
if (res.status !== 200) throw new Error(`Failed to analyze image: ${data}`);
if (res.status !== 200) throw new Error('Failed to analyze image:', data);
return data;
};

Expand All @@ -24,5 +24,7 @@ export const analyzeImageForNSFW = async (imageURL: string): Promise<predictionT
* @param predictions The predictions to check
* @returns Whether the predictions are unsafe
*/
export const isImageUnsafe = (prediction: predictionType, minConfidence = 90): boolean =>
prediction.is_nsfw && prediction.confidence_percentage >= minConfidence;
export const isImageUnsafe = (
prediction: predictionType | undefined,
minConfidence = 80,
): boolean => Boolean(prediction?.is_nsfw && prediction.confidence_percentage >= minConfidence);
4 changes: 3 additions & 1 deletion src/types/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import BaseCommand from '#main/core/BaseCommand.js';
import { InteractionFunction } from '#main/decorators/Interaction.ts';
import AntiSpamManager from '#main/managers/AntiSpamManager.js';
import UserDbManager from '#main/managers/UserDbManager.js';
import CooldownService from '#main/modules/CooldownService.js';
import Scheduler from '#main/modules/SchedulerService.js';
import UserDbManager from '#main/managers/UserDbManager.js';
import { ClusterClient } from 'discord-hybrid-sharding';
import {
Collection,
Expand Down Expand Up @@ -31,6 +32,7 @@ declare module 'discord.js' {
readonly reactionCooldowns: Collection<string, number>;
readonly cluster: ClusterClient<Client>;
readonly userManager: UserDbManager;
readonly antiSpamManager: AntiSpamManager;

fetchGuild(guildId: Snowflake): Promise<RemoveMethods<Guild> | undefined>;
getScheduler(): Scheduler;
Expand Down
79 changes: 0 additions & 79 deletions src/utils/network/antiSpam.ts

This file was deleted.

52 changes: 27 additions & 25 deletions src/utils/network/runChecks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,18 @@ import BlacklistManager from '#main/managers/BlacklistManager.js';
import HubSettingsManager from '#main/managers/HubSettingsManager.js';
import UserInfractionManager from '#main/managers/InfractionManager/UserInfractionManager.js';
import { analyzeImageForNSFW, isImageUnsafe } from '#main/modules/NSFWDetection.js';
import { sendBlacklistNotif } from '#main/utils/moderation/blacklistUtils.js';
import db from '#utils/Db.js';
import { isHubMod } from '#utils/hub/utils.js';
import { logBlacklist } from '#utils/HubLogger/ModLogs.js';
import logProfanity from '#utils/HubLogger/Profanity.js';
import { supportedLocaleCodes, t } from '#utils/Locale.js';
import { sendBlacklistNotif } from '#utils/moderation/blacklistUtils.js';
import { createRegexFromWords } from '#utils/moderation/blockedWords.js';
import { sendWelcomeMsg } from '#utils/network/helpers.js';
import { check as checkProfanity } from '#utils/ProfanityUtils.js';
import { containsInviteLinks, replaceLinks } from '#utils/Utils.js';
import { Hub, MessageBlockList } from '@prisma/client';
import { stripIndents } from 'common-tags';
import { Awaitable, EmbedBuilder, Message } from 'discord.js';
import { runAntiSpam } from './antiSpam.js';
import { createRegexFromWords } from '#utils/moderation/blockedWords.js';

interface CheckResult {
passed: boolean;
Expand Down Expand Up @@ -142,26 +140,30 @@ function checkLinks(message: Message<true>, opts: CheckFunctionOpts): CheckResul

async function checkSpam(message: Message<true>, opts: CheckFunctionOpts): Promise<CheckResult> {
const { settings, hub } = opts;
const antiSpamResult = runAntiSpam(message.author, 3);
if (settings.getSetting('SpamFilter') && antiSpamResult && antiSpamResult.infractions >= 3) {
const expiresAt = new Date(Date.now() + 60 * 5000);
const reason = 'Auto-blacklisted for spamming.';
const target = message.author;
const mod = message.client.user;

const blacklistManager = new BlacklistManager(new UserInfractionManager(target.id));
await blacklistManager.addBlacklist({ hubId: hub.id, reason, expiresAt, moderatorId: mod.id });

await logBlacklist(hub.id, message.client, { target, mod, reason, expiresAt }).catch(
() => null,
);

await sendBlacklistNotif('user', message.client, {
target,
hubId: hub.id,
expiresAt,
reason,
}).catch(() => null);
const result = await message.client.antiSpamManager.handleMessage(message);
if (settings.getSetting('SpamFilter') && result) {
if (result.messageCount >= 6) {
const expiresAt = new Date(Date.now() + 60 * 5000);
const reason = 'Auto-blacklisted for spamminag.';
const target = message.author;
const mod = message.client.user;

const blacklistManager = new BlacklistManager(new UserInfractionManager(target.id));
await blacklistManager.addBlacklist({
hubId: hub.id,
reason,
expiresAt,
moderatorId: mod.id,
});

await blacklistManager.log(hub.id, message.client, { mod, reason, expiresAt });
await sendBlacklistNotif('user', message.client, {
target,
hubId: hub.id,
expiresAt,
reason,
}).catch(() => null);
}

await message.react(emojis.timeout).catch(() => null);
return { passed: false };
Expand Down Expand Up @@ -260,7 +262,7 @@ async function checkNSFW(message: Message<true>, opts: CheckFunctionOpts): Promi
const { attachmentURL } = opts;
if (attachmentURL && Constants.Regex.StaticImageUrl.test(attachmentURL)) {
const predictions = await analyzeImageForNSFW(attachmentURL);
if (predictions.length > 0 && isImageUnsafe(predictions[0])) {
if (isImageUnsafe(predictions.at(0))) {
const nsfwEmbed = new EmbedBuilder()
.setColor(Constants.Colors.invisible)
.setDescription(
Expand Down

0 comments on commit 6c8e001

Please sign in to comment.