Enhance bot functionality with AutoMod, Logging, Welcome, and Verification features

- Implemented AutoMod capabilities including spam filtering, anti-raid, and anti-nuke actions.
- Added Logging module to track moderation actions and events across the bot.
- Introduced Welcome module for customizable welcome messages and autoroles.
- Developed Verification system with captcha and role assignment features.
- Updated Prisma schema to include new models for AutoMod, Logging, Welcome, and Verification.
- Enhanced command localization for new features in both German and English.
- Improved health server to handle captcha verification requests.
- Added new environment variable for public base URL to support captcha links.
This commit is contained in:
smueller
2026-07-22 12:28:42 +02:00
parent a44f4d6641
commit 2518119257
37 changed files with 3002 additions and 108 deletions

View File

@@ -0,0 +1,211 @@
import { z } from 'zod';
export const AutoModRuleTypeSchema = z.enum([
'SPAM',
'MASS_MENTION',
'CAPS',
'INVITE_LINK',
'EXTERNAL_LINK',
'WORD_FILTER',
'DUPLICATE',
'EMOJI_SPAM',
'ZALGO',
'PHISHING'
]);
export type AutoModRuleType = z.infer<typeof AutoModRuleTypeSchema>;
export const AutoModActionSchema = z.enum(['DELETE', 'WARN', 'TIMEOUT', 'KICK', 'BAN']);
export type AutoModAction = z.infer<typeof AutoModActionSchema>;
export const AutoModExceptionsSchema = z.object({
roleIds: z.array(z.string()).default([]),
channelIds: z.array(z.string()).default([])
});
export type AutoModExceptions = z.infer<typeof AutoModExceptionsSchema>;
export const AutoModThresholdSchema = z.object({
maxMentions: z.number().int().positive().optional(),
capsPercent: z.number().min(0).max(100).optional(),
minLength: z.number().int().nonnegative().optional(),
maxMessages: z.number().int().positive().optional(),
windowSeconds: z.number().int().positive().optional(),
maxEmojis: z.number().int().positive().optional(),
duplicateCount: z.number().int().positive().optional(),
linkWhitelist: z.array(z.string()).optional(),
linkBlacklist: z.array(z.string()).optional()
});
export type AutoModThreshold = z.infer<typeof AutoModThresholdSchema>;
export const AutoModWordListSchema = z.object({
words: z.array(z.string()).default([]),
regexPatterns: z.array(z.string()).default([])
});
export type AutoModWordList = z.infer<typeof AutoModWordListSchema>;
export const DEFAULT_AUTOMOD_RULES: Array<{
ruleType: AutoModRuleType;
action: AutoModAction;
threshold?: AutoModThreshold;
wordList?: AutoModWordList;
}> = [
{
ruleType: 'SPAM',
action: 'DELETE',
threshold: { maxMessages: 5, windowSeconds: 5 }
},
{
ruleType: 'MASS_MENTION',
action: 'DELETE',
threshold: { maxMentions: 5 }
},
{
ruleType: 'CAPS',
action: 'DELETE',
threshold: { capsPercent: 70, minLength: 10 }
},
{
ruleType: 'INVITE_LINK',
action: 'DELETE'
},
{
ruleType: 'EXTERNAL_LINK',
action: 'DELETE',
threshold: { linkWhitelist: ['discord.com', 'discord.gg'] }
},
{
ruleType: 'WORD_FILTER',
action: 'DELETE',
wordList: { words: [], regexPatterns: [] }
},
{
ruleType: 'DUPLICATE',
action: 'DELETE',
threshold: { duplicateCount: 3, windowSeconds: 30 }
},
{
ruleType: 'EMOJI_SPAM',
action: 'DELETE',
threshold: { maxEmojis: 10 }
},
{
ruleType: 'ZALGO',
action: 'DELETE'
},
{
ruleType: 'PHISHING',
action: 'DELETE'
}
];
export const LogEventTypeSchema = z.enum([
'MESSAGE_EDIT',
'MESSAGE_DELETE',
'MESSAGE_BULK_DELETE',
'MEMBER_JOIN',
'MEMBER_LEAVE',
'MEMBER_BAN',
'MEMBER_UNBAN',
'MEMBER_UPDATE',
'CHANNEL_CREATE',
'CHANNEL_DELETE',
'CHANNEL_UPDATE',
'VOICE_JOIN',
'VOICE_LEAVE',
'VOICE_MOVE',
'INVITE_CREATE',
'EMOJI_CREATE',
'EMOJI_UPDATE',
'EMOJI_DELETE',
'STICKER_CREATE',
'STICKER_UPDATE',
'STICKER_DELETE',
'THREAD_CREATE',
'THREAD_UPDATE',
'THREAD_DELETE',
'GUILD_UPDATE',
'MOD_ACTION'
]);
export type LogEventType = z.infer<typeof LogEventTypeSchema>;
export const WelcomeMessageTypeSchema = z.enum(['TEXT', 'EMBED', 'IMAGE']);
export type WelcomeMessageType = z.infer<typeof WelcomeMessageTypeSchema>;
export const WelcomeEmbedSchema = z.object({
title: z.string().optional(),
description: z.string().optional(),
color: z.number().int().optional()
});
export type WelcomeEmbed = z.infer<typeof WelcomeEmbedSchema>;
export const VerificationModeSchema = z.enum(['BUTTON', 'CAPTCHA']);
export type VerificationMode = z.infer<typeof VerificationModeSchema>;
export const VerificationFailActionSchema = z.enum(['KICK', 'BAN', 'NONE']);
export type VerificationFailAction = z.infer<typeof VerificationFailActionSchema>;
export const VerificationSetupSchema = z.object({
channel: z.string().min(1),
verifiedRole: z.string().min(1),
unverifiedRole: z.string().optional(),
mode: VerificationModeSchema.default('BUTTON'),
minAccountAgeDays: z.number().int().nonnegative().default(0),
failAction: VerificationFailActionSchema.default('KICK')
});
export type VerificationSetup = z.infer<typeof VerificationSetupSchema>;
export const WELCOME_PLACEHOLDERS = [
'{user}',
'{user.name}',
'{user.tag}',
'{user.id}',
'{server}',
'{memberCount}'
] as const;
export function applyWelcomePlaceholders(
template: string,
vars: Record<string, string | number>
): string {
return Object.entries(vars).reduce(
(acc, [name, value]) => acc.replaceAll(`{${name}}`, String(value)),
template
);
}
export function countEmojis(content: string): number {
const emojiRegex =
/(\p{Extended_Pictographic}|\p{Emoji_Presentation}|<a?:\w+:\d+>)/gu;
return content.match(emojiRegex)?.length ?? 0;
}
export function capsRatio(content: string): number {
const letters = content.replace(/[^a-zA-Z]/g, '');
if (letters.length === 0) {
return 0;
}
const caps = letters.replace(/[^A-Z]/g, '').length;
return (caps / letters.length) * 100;
}
export function hasZalgo(text: string): boolean {
return /[\u0300-\u036f\u0489]/.test(text);
}
export function extractUrls(content: string): string[] {
const urlRegex = /https?:\/\/[^\s<>]+/gi;
return content.match(urlRegex) ?? [];
}
export function isDiscordInvite(content: string): boolean {
return /(?:https?:\/\/)?(?:www\.)?(?:discord\.(?:gg|io|me|li)|discordapp\.com\/invite|discord\.com\/invite)\/[^\s]+/i.test(
content
);
}
export function normalizeHost(url: string): string | null {
try {
return new URL(url).hostname.replace(/^www\./, '').toLowerCase();
} catch {
return null;
}
}