Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ const devConfig: Config = {
yesEmojiId: "👍",
noEmojiId: "👎",
},
threatDetection: {
enabled: true,
alertChannel: "1432483525155623063",
scamLinks: {
enabled: true,
blockShorteners: true,
useExternalApi: true,
},
},
modmail: {
pingRole: "1412470653050818724",
archiveChannel: "1412470199495561338",
Expand Down
69 changes: 69 additions & 0 deletions src/config.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,58 @@ import type { Snowflake } from "discord.js";
import type { InformationMessage } from "./modules/information/information.js";
import type { BrandingConfig } from "./util/branding.js";

export interface ThreatDetectionConfig {
enabled: boolean;
alertChannel?: Snowflake;
exemptRoles?: Snowflake[];
scamLinks?: {
enabled: boolean;
useExternalApi?: boolean;
blockShorteners?: boolean;
safeDomains?: string[];
};
spam?: {
enabled: boolean;
maxMessagesPerWindow: number;
windowSeconds: number;
duplicateThreshold: number;
action: "delete" | "mute";
muteDuration?: number;
};
raid?: {
enabled: boolean;
maxJoinsPerWindow: number;
windowSeconds: number;
action: "alert" | "lockdown" | "kick_new";
newAccountThreshold: number;
};
mentionSpam?: {
enabled: boolean;
maxMentionsPerMessage: number;
maxMentionsPerWindow: number;
windowSeconds: number;
action: "delete" | "mute";
};
toxicContent?: {
enabled: boolean;
detectBypasses: boolean;
action: "flag" | "delete";
};
suspiciousAccounts?: {
enabled: boolean;
minAgeDays: number;
flagDefaultAvatar: boolean;
flagSuspiciousNames: boolean;
suspiciousNamePatterns?: string[];
action: "flag" | "kick";
};
escalation?: {
warningsBeforeMute: number;
mutesBeforeKick: number;
scoreDecayRate: number;
};
}

export interface Config {
guildId: string;
clientId: string;
Expand Down Expand Up @@ -60,4 +112,21 @@ export interface Config {
};
branding: BrandingConfig;
informationMessage?: InformationMessage;
threatDetection?: ThreatDetectionConfig;
reputation?: {
enabled: boolean;
warningThresholds: {
muteAt: number;
muteDuration: string;
banAt: number;
};
warningExpiration: {
minor: string;
moderate: string;
severe: string;
};
scoreVisibility: "public" | "mods-only" | "self-only";
allowAppeals: boolean;
appealCooldown: string;
};
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { RolesModule } from "./modules/roles/roles.module.js";
import { ShowcaseModule } from "./modules/showcase.module.js";
import { StarboardModule } from "./modules/starboard/starboard.module.js";
import SuggestModule from "./modules/suggest/suggest.module.js";
import { ThreatDetectionModule } from "./modules/threatDetection/threatDetection.module.js";
import { TokenScannerModule } from "./modules/tokenScanner.module.js";
import { UserModule } from "./modules/user/user.module.js";
import { XpModule } from "./modules/xp/xp.module.js";
Expand Down Expand Up @@ -69,6 +70,7 @@ export const moduleManager = new ModuleManager(
ModmailModule,
LeaderboardModule,
UserModule,
ThreatDetectionModule,
],
);

Expand Down
66 changes: 64 additions & 2 deletions src/modules/moderation/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ export type ModerationLog =
| TempBanExpiredLog
| SoftBanLog
| KickLog
| InviteDeletedLog;
| InviteDeletedLog
| WarningLog
| WarningPardonedLog
| ReputationGrantedLog;

interface BanLog {
kind: "Ban";
Expand Down Expand Up @@ -83,6 +86,35 @@ interface InviteDeletedLog {
matches: string[];
}

interface WarningLog {
kind: "Warning";
moderator: User;
target: UserResolvable;
reason: string;
severity: number;
warningId: number;
warningCount: number;
expiresAt: Date | null;
}

interface WarningPardonedLog {
kind: "WarningPardoned";
moderator: User;
target: UserResolvable;
warningId: number;
reason: string;
}

interface ReputationGrantedLog {
kind: "ReputationGranted";
moderator: User;
target: UserResolvable;
eventType: string;
scoreChange: number;
newScore: number;
reason: string;
}

type ModerationKindMapping<T> = {
[f in ModerationLog["kind"]]: T;
};
Expand All @@ -95,6 +127,9 @@ const embedTitles: ModerationKindMapping<string> = {
TempBan: "Member Tempbanned",
Kick: "Member Kicked",
TempBanEnded: "Tempban Expired",
Warning: "Member Warned",
WarningPardoned: "Warning Pardoned",
ReputationGranted: "Reputation Granted",
};

const embedColors: ModerationKindMapping<keyof typeof Colors> = {
Expand All @@ -105,6 +140,15 @@ const embedColors: ModerationKindMapping<keyof typeof Colors> = {
Unban: "Green",
TempBanEnded: "DarkGreen",
InviteDeleted: "Blurple",
Warning: "Gold",
WarningPardoned: "Aqua",
ReputationGranted: "Green",
};

const SEVERITY_LABELS: Record<number, string> = {
1: "Minor",
2: "Moderate",
3: "Severe",
};

const embedReasons: {
Expand All @@ -120,6 +164,23 @@ const embedReasons: {

TempBan: (tempBan) =>
`**Ban duration**: \`${prettyPrintDuration(tempBan.banDuration)}\``,

Warning: (warning) =>
`**Severity:** ${SEVERITY_LABELS[warning.severity] || "Unknown"}\n` +
`**Warning ID:** #${warning.warningId}\n` +
`**Total Active Warnings:** ${warning.warningCount}\n` +
(warning.expiresAt
? `**Expires:** <t:${Math.floor(warning.expiresAt.getTime() / 1000)}:R>`
: "**Expires:** Never"),

WarningPardoned: (pardon) =>
`**Warning ID:** #${pardon.warningId}\n` +
`**Pardon Reason:** ${pardon.reason}`,

ReputationGranted: (rep) =>
`**Type:** ${rep.eventType}\n` +
`**Score Change:** +${rep.scoreChange}\n` +
`**New Score:** ${rep.newScore >= 0 ? "+" : ""}${rep.newScore}`,
};

export async function logModerationAction(
Expand All @@ -142,7 +203,8 @@ export async function logModerationAction(
embed.setColor(embedColors[action.kind]);

const targetUser = await client.users.fetch(action.target).catch(() => null);
let description = `**Offender**: ${targetUser && fakeMention(targetUser)} ${actualMention(action.target)}\n`;
const targetLabel = action.kind === "ReputationGranted" ? "Recipient" : "Offender";
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's inconsistent terminology in the moderation logs. Line 194-195 uses "Recipient" for reputation grants but "Offender" for other moderation actions. For warnings and other moderation actions, "Target" or "Subject" might be more neutral terms than "Offender", as some actions (like pardons) aren't punitive.

Suggested change
const targetLabel = action.kind === "ReputationGranted" ? "Recipient" : "Offender";
const targetLabel = action.kind === "ReputationGranted" ? "Recipient" : "Target";

Copilot uses AI. Check for mistakes.
let description = `**${targetLabel}**: ${targetUser && fakeMention(targetUser)} ${actualMention(action.target)}\n`;
if ("reason" in action && action.reason) {
description += `**Reason**: ${action.reason}\n`;
}
Expand Down
13 changes: 12 additions & 1 deletion src/modules/moderation/moderation.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@ import { BanCommand } from "./ban.command.js";
import { DeletedMessagesListener } from "./deletedMessages.listener.js";
import { InviteListeners } from "./discordInvitesMonitor.listener.js";
import { KickCommand } from "./kick.command.js";
import { PardonCommand } from "./pardon.command.js";
import { ReputationCommand } from "./reputation.command.js";
import { SoftBanCommand } from "./softBan.command.js";
import { TempBanCommand } from "./tempBan.command.js";
import { TempBanListener } from "./tempBan.listener.js";
import { UnbanCommand } from "./unban.command.js";
import { WarnCommand } from "./warn.command.js";
import { WarningSchedulerListener } from "./warningScheduler.listener.js";
import { WarningsCommand } from "./warnings.command.js";
import { WordlistCommand } from "./wordlist.command.js";
import { ZookeepCommand } from "./zookeep.command.js";

export const ModerationModule: Module = {
Expand All @@ -18,6 +24,11 @@ export const ModerationModule: Module = {
TempBanCommand,
KickCommand,
ZookeepCommand,
WarnCommand,
WarningsCommand,
PardonCommand,
WordlistCommand,
ReputationCommand,
],
listeners: [...InviteListeners, TempBanListener, DeletedMessagesListener],
listeners: [...InviteListeners, TempBanListener, WarningSchedulerListener, DeletedMessagesListener],
};
Loading