Add events module with dashboard UI, scheduling, signups, and settings updates; extend env/readme.
This commit is contained in:
@@ -1,28 +1,92 @@
|
||||
import { Collection, Message } from 'discord.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { Collection, Message, PermissionFlagsBits } from 'discord.js';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
export interface AutomodConfig {
|
||||
spamThreshold?: number;
|
||||
windowMs?: number;
|
||||
linkWhitelist?: string[];
|
||||
spamTimeoutMinutes?: number;
|
||||
deleteLinks?: boolean;
|
||||
badWordFilter?: boolean;
|
||||
capsFilter?: boolean;
|
||||
customBadwords?: string[];
|
||||
whitelistRoles?: string[];
|
||||
logChannelId?: string;
|
||||
loggingConfig?: {
|
||||
logChannelId?: string;
|
||||
categories?: {
|
||||
automodActions?: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export class AutoModService {
|
||||
private spamTracker = new Collection<string, { count: number; lastMessage: number }>();
|
||||
private spamThreshold = 5;
|
||||
private windowMs = 7000;
|
||||
private defaults: AutomodConfig = {
|
||||
spamThreshold: 5,
|
||||
windowMs: 7000,
|
||||
linkWhitelist: [],
|
||||
spamTimeoutMinutes: 10,
|
||||
deleteLinks: true,
|
||||
badWordFilter: true,
|
||||
capsFilter: false,
|
||||
customBadwords: [],
|
||||
whitelistRoles: []
|
||||
};
|
||||
private defaultBadwords = ['badword', 'spamword'];
|
||||
|
||||
constructor(private linkFilterEnabled = true, private antiSpamEnabled = true) {}
|
||||
|
||||
public checkMessage(message: Message) {
|
||||
public async checkMessage(message: Message, cfg?: AutomodConfig) {
|
||||
if (message.author.bot) return;
|
||||
if (this.linkFilterEnabled && this.containsLink(message.content)) {
|
||||
const config = { ...this.defaults, ...(cfg ?? {}) };
|
||||
const member = message.member;
|
||||
|
||||
if (member?.roles.cache.size && Array.isArray(config.whitelistRoles) && config.whitelistRoles.length) {
|
||||
const allowed = member.roles.cache.some((r) => config.whitelistRoles!.includes(r.id));
|
||||
if (allowed) return;
|
||||
}
|
||||
|
||||
if (this.linkFilterEnabled && config.deleteLinks !== false && this.containsLink(message.content, config.linkWhitelist)) {
|
||||
if (message.member?.permissions.has(PermissionFlagsBits.ManageGuild)) return false;
|
||||
message.delete().catch(() => undefined);
|
||||
message.channel
|
||||
.send({ content: `${message.author}, Links sind hier nicht erlaubt.` })
|
||||
.then((m) => setTimeout(() => m.delete().catch(() => undefined), 5000));
|
||||
logger.info(`Deleted link from ${message.author.tag}`);
|
||||
await this.logAutomodAction(message, config, 'link_filter');
|
||||
return true;
|
||||
}
|
||||
|
||||
if (config.badWordFilter !== false && this.containsBadword(message.content, config.customBadwords)) {
|
||||
if (message.member?.permissions.has(PermissionFlagsBits.ManageGuild)) return false;
|
||||
message.delete().catch(() => undefined);
|
||||
message.channel
|
||||
.send({ content: `${message.author}, bitte auf deine Wortwahl achten.` })
|
||||
.then((m) => setTimeout(() => m.delete().catch(() => undefined), 5000));
|
||||
await this.logAutomodAction(message, config, 'badword', message.content);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (config.capsFilter) {
|
||||
const letters = message.content.replace(/[^a-zA-Z]/g, '');
|
||||
const upper = letters.replace(/[^A-Z]/g, '');
|
||||
if (letters.length >= 10 && upper.length / letters.length > 0.7) {
|
||||
message.delete().catch(() => undefined);
|
||||
message.channel
|
||||
.send({ content: `${message.author}, bitte weniger Capslock nutzen.` })
|
||||
.then((m) => setTimeout(() => m.delete().catch(() => undefined), 5000));
|
||||
await this.logAutomodAction(message, config, 'capslock', message.content);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.antiSpamEnabled) {
|
||||
const now = Date.now();
|
||||
const tracker = this.spamTracker.get(message.author.id) ?? { count: 0, lastMessage: now };
|
||||
if (now - tracker.lastMessage < this.windowMs) {
|
||||
if (now - tracker.lastMessage < (config.windowMs ?? this.windowMs)) {
|
||||
tracker.count += 1;
|
||||
} else {
|
||||
tracker.count = 1;
|
||||
@@ -30,20 +94,52 @@ export class AutoModService {
|
||||
tracker.lastMessage = now;
|
||||
this.spamTracker.set(message.author.id, tracker);
|
||||
|
||||
if (tracker.count >= this.spamThreshold) {
|
||||
message.member?.timeout(10 * 60 * 1000, 'Automod: Spam').catch(() => undefined);
|
||||
const threshold = config.spamThreshold ?? this.spamThreshold;
|
||||
if (tracker.count >= threshold) {
|
||||
const timeoutMs = (config.spamTimeoutMinutes ?? this.defaults.spamTimeoutMinutes!) * 60 * 1000;
|
||||
message.member?.timeout(timeoutMs, 'Automod: Spam').catch(() => undefined);
|
||||
message.channel
|
||||
.send({ content: `${message.author}, bitte langsamer schreiben (Spam-Schutz).` })
|
||||
.then((m) => setTimeout(() => m.delete().catch(() => undefined), 5000));
|
||||
logger.warn(`Timed out ${message.author.tag} for spam`);
|
||||
this.spamTracker.delete(message.author.id);
|
||||
await this.logAutomodAction(message, config, 'spam', `Count ${tracker.count}`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private containsLink(content: string) {
|
||||
return /(https?:\/\/|discord\.gg|www\.)/i.test(content);
|
||||
private containsBadword(content: string, custom: string[] = []) {
|
||||
const combined = [...this.defaultBadwords, ...(custom || [])].filter(Boolean).map((w) => w.toLowerCase());
|
||||
if (!combined.length) return false;
|
||||
const lower = content.toLowerCase();
|
||||
return combined.some((w) => lower.includes(w));
|
||||
}
|
||||
|
||||
private containsLink(content: string, whitelist: string[] = []) {
|
||||
const normalized = whitelist.map((w) => w.toLowerCase()).filter(Boolean);
|
||||
const match = /(https?:\/\/[^\s]+|discord\.gg\/[^\s]+|www\.[^\s]+)/i.exec(content);
|
||||
if (!match) return false;
|
||||
const url = match[0].toLowerCase();
|
||||
return !normalized.some((w) => url.includes(w));
|
||||
}
|
||||
|
||||
private async logAutomodAction(message: Message, config: AutomodConfig, action: string, details?: string) {
|
||||
try {
|
||||
const guild = message.guild;
|
||||
if (!guild) return;
|
||||
const loggingCfg = config.loggingConfig || {};
|
||||
const flags = loggingCfg.categories || {};
|
||||
if (flags.automodActions === false) return;
|
||||
const channelId = loggingCfg.logChannelId || config.logChannelId;
|
||||
if (!channelId) return;
|
||||
const channel = await guild.channels.fetch(channelId).catch(() => null);
|
||||
if (!channel || !channel.isTextBased()) return;
|
||||
const content = `[Automod] ${action} by ${message.author.tag}${details ? ` | ${details}` : ''}`;
|
||||
await channel.send({ content });
|
||||
} catch (err) {
|
||||
logger.error('Automod log failed', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user