[deploy] Automod logging reasons und Modul-Fix
All checks were successful
Deploy Discord Bot / deploy (push) Successful in 38s

This commit is contained in:
Pascal Prießnitz
2025-12-04 21:06:33 +01:00
parent c95444feac
commit c18441eb9a
11 changed files with 91 additions and 43 deletions

View File

@@ -23,7 +23,7 @@ const command: SlashCommand = {
await member.ban({ reason }).catch(() => null); await member.ban({ reason }).catch(() => null);
await interaction.reply({ content: `${user.tag} wurde gebannt. Grund: ${reason}` }); await interaction.reply({ content: `${user.tag} wurde gebannt. Grund: ${reason}` });
context.logging.logAction(user, 'Ban', reason); context.logging.logAction(user, 'Ban', reason, interaction.guild);
} }
}; };

View File

@@ -21,7 +21,7 @@ const command: SlashCommand = {
} }
await member.kick(reason); await member.kick(reason);
await interaction.reply({ content: `${user.tag} wurde gekickt.` }); await interaction.reply({ content: `${user.tag} wurde gekickt.` });
context.logging.logAction(user, 'Kick', reason); context.logging.logAction(user, 'Kick', reason, interaction.guild);
} }
}; };

View File

@@ -23,7 +23,7 @@ const command: SlashCommand = {
} }
await member.timeout(minutes * 60 * 1000, reason).catch(() => null); await member.timeout(minutes * 60 * 1000, reason).catch(() => null);
await interaction.reply({ content: `${user.tag} wurde für ${minutes} Minuten gemutet.` }); await interaction.reply({ content: `${user.tag} wurde für ${minutes} Minuten gemutet.` });
context.logging.logAction(user, 'Mute', reason); context.logging.logAction(user, 'Mute', reason, interaction.guild);
} }
}; };

View File

@@ -25,7 +25,7 @@ const command: SlashCommand = {
await member.ban({ reason: `${reason} | ${minutes} Minuten` }); await member.ban({ reason: `${reason} | ${minutes} Minuten` });
await interaction.reply({ content: `${user.tag} wurde für ${minutes} Minuten gebannt.` }); await interaction.reply({ content: `${user.tag} wurde für ${minutes} Minuten gebannt.` });
context.logging.logAction(user, 'Tempban', reason); context.logging.logAction(user, 'Tempban', reason, interaction.guild);
setTimeout(async () => { setTimeout(async () => {
await interaction.guild?.members.unban(user.id, 'Tempban abgelaufen').catch(() => null); await interaction.guild?.members.unban(user.id, 'Tempban abgelaufen').catch(() => null);

View File

@@ -23,7 +23,7 @@ const command: SlashCommand = {
} }
await member.timeout(minutes * 60 * 1000, reason).catch(() => null); await member.timeout(minutes * 60 * 1000, reason).catch(() => null);
await interaction.reply({ content: `${user.tag} wurde für ${minutes} Minuten in Timeout gesetzt.` }); await interaction.reply({ content: `${user.tag} wurde für ${minutes} Minuten in Timeout gesetzt.` });
context.logging.logAction(user, 'Timeout', reason); context.logging.logAction(user, 'Timeout', reason, interaction.guild);
} }
}; };

View File

@@ -19,7 +19,7 @@ const command: SlashCommand = {
} }
await member.timeout(null).catch(() => null); await member.timeout(null).catch(() => null);
await interaction.reply({ content: `${user.tag} ist nun entmuted.` }); await interaction.reply({ content: `${user.tag} ist nun entmuted.` });
context.logging.logAction(user, 'Unmute'); context.logging.logAction(user, 'Unmute', undefined, interaction.guild);
} }
}; };

View File

@@ -17,11 +17,13 @@ import { KnowledgeBaseService } from '../services/knowledgeBaseService';
import { RegisterService } from '../services/registerService'; import { RegisterService } from '../services/registerService';
import { StatsService } from '../services/statsService'; import { StatsService } from '../services/statsService';
const logging = new LoggingService();
export const context = { export const context = {
client: null as Client | null, client: null as Client | null,
commandHandler: null as CommandHandler | null, commandHandler: null as CommandHandler | null,
automod: new AutoModService(true, true), logging,
logging: new LoggingService(), automod: new AutoModService(logging, true, true),
music: new MusicService(), music: new MusicService(),
tickets: new TicketService(), tickets: new TicketService(),
leveling: new LevelService(), leveling: new LevelService(),

View File

@@ -5,7 +5,7 @@ import { context } from '../config/context';
const event: EventHandler = { const event: EventHandler = {
name: 'guildBanAdd', name: 'guildBanAdd',
execute(ban: GuildBan) { execute(ban: GuildBan) {
context.logging.logAction(ban.user, 'Ban'); context.logging.logAction(ban.user, 'Ban', undefined, ban.guild);
} }
}; };

View File

@@ -8,7 +8,7 @@ const event: EventHandler = {
async execute(message: Message) { async execute(message: Message) {
const cfg = message.guildId ? settingsStore.get(message.guildId) : undefined; const cfg = message.guildId ? settingsStore.get(message.guildId) : undefined;
if (message.guildId) context.admin.trackEvent('message', message.guildId); if (message.guildId) context.admin.trackEvent('message', message.guildId);
if (cfg?.automodEnabled === true) context.automod.checkMessage(message, cfg.automodConfig); if (cfg?.automodEnabled === true) context.automod.checkMessage(message, cfg);
if (cfg?.levelingEnabled === true) context.leveling.handleMessage(message); if (cfg?.levelingEnabled === true) context.leveling.handleMessage(message);
// Ticket SLA + KB // Ticket SLA + KB
await context.tickets.trackFirstResponse(message); await context.tickets.trackFirstResponse(message);

View File

@@ -1,5 +1,7 @@
import { Collection, Message, PermissionFlagsBits } from 'discord.js'; import { Collection, Message, PermissionFlagsBits } from 'discord.js';
import { logger } from '../utils/logger'; import { logger } from '../utils/logger';
import { GuildSettings } from '../config/state';
import { LoggingService } from './loggingService';
export interface AutomodConfig { export interface AutomodConfig {
spamThreshold?: number; spamThreshold?: number;
@@ -37,11 +39,13 @@ export class AutoModService {
}; };
private defaultBadwords = ['badword', 'spamword']; private defaultBadwords = ['badword', 'spamword'];
constructor(private linkFilterEnabled = true, private antiSpamEnabled = true) {} constructor(private logging?: LoggingService, private linkFilterEnabled = true, private antiSpamEnabled = true) {}
public async checkMessage(message: Message, cfg?: AutomodConfig) { public async checkMessage(message: Message, cfg?: AutomodConfig | GuildSettings) {
if (message.author.bot) return; if (message.author.bot || message.webhookId) return;
const config = { ...this.defaults, ...(cfg ?? {}) }; if (!message.inGuild()) return;
const guildConfig = (cfg as GuildSettings)?.automodConfig ? (cfg as GuildSettings).automodConfig : cfg;
const config = { ...this.defaults, ...(guildConfig ?? {}) };
const member = message.member; const member = message.member;
if (member?.roles.cache.size && Array.isArray(config.whitelistRoles) && config.whitelistRoles.length) { if (member?.roles.cache.size && Array.isArray(config.whitelistRoles) && config.whitelistRoles.length) {
@@ -51,22 +55,17 @@ export class AutoModService {
if (this.linkFilterEnabled && config.deleteLinks !== false && this.containsLink(message.content, config.linkWhitelist)) { if (this.linkFilterEnabled && config.deleteLinks !== false && this.containsLink(message.content, config.linkWhitelist)) {
if (message.member?.permissions.has(PermissionFlagsBits.ManageGuild)) return false; if (message.member?.permissions.has(PermissionFlagsBits.ManageGuild)) return false;
message.delete().catch(() => undefined); await this.deleteMessageWithReason(message, `${message.author}, Links sind hier nicht erlaubt.`);
message.channel const reason = `Link gefunden (nicht freigegeben)${config.linkWhitelist?.length ? ` | Whitelist: ${config.linkWhitelist.join(', ')}` : ''}`;
.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}`); logger.info(`Deleted link from ${message.author.tag}`);
await this.logAutomodAction(message, config, 'link_filter'); await this.logAutomodAction(message, config, 'link_filter', reason);
return true; return true;
} }
if (config.badWordFilter !== false && this.containsBadword(message.content, config.customBadwords)) { if (config.badWordFilter !== false && this.containsBadword(message.content, config.customBadwords)) {
if (message.member?.permissions.has(PermissionFlagsBits.ManageGuild)) return false; if (message.member?.permissions.has(PermissionFlagsBits.ManageGuild)) return false;
message.delete().catch(() => undefined); await this.deleteMessageWithReason(message, `${message.author}, bitte auf deine Wortwahl achten.`);
message.channel await this.logAutomodAction(message, config, 'badword', 'Badword erkannt', message.content);
.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; return true;
} }
@@ -74,11 +73,9 @@ export class AutoModService {
const letters = message.content.replace(/[^a-zA-Z]/g, ''); const letters = message.content.replace(/[^a-zA-Z]/g, '');
const upper = letters.replace(/[^A-Z]/g, ''); const upper = letters.replace(/[^A-Z]/g, '');
if (letters.length >= 10 && upper.length / letters.length > 0.7) { if (letters.length >= 10 && upper.length / letters.length > 0.7) {
message.delete().catch(() => undefined); await this.deleteMessageWithReason(message, `${message.author}, bitte weniger Capslock nutzen.`);
message.channel const ratio = Math.round((upper.length / letters.length) * 100);
.send({ content: `${message.author}, bitte weniger Capslock nutzen.` }) await this.logAutomodAction(message, config, 'capslock', `Caps Anteil ${ratio}%`, message.content);
.then((m) => setTimeout(() => m.delete().catch(() => undefined), 5000));
await this.logAutomodAction(message, config, 'capslock', message.content);
return true; return true;
} }
} }
@@ -98,12 +95,11 @@ export class AutoModService {
if (tracker.count >= threshold) { if (tracker.count >= threshold) {
const timeoutMs = (config.spamTimeoutMinutes ?? this.defaults.spamTimeoutMinutes!) * 60 * 1000; const timeoutMs = (config.spamTimeoutMinutes ?? this.defaults.spamTimeoutMinutes!) * 60 * 1000;
message.member?.timeout(timeoutMs, 'Automod: Spam').catch(() => undefined); message.member?.timeout(timeoutMs, 'Automod: Spam').catch(() => undefined);
message.channel await this.deleteMessageWithReason(message, `${message.author}, bitte langsamer schreiben (Spam-Schutz).`);
.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`); logger.warn(`Timed out ${message.author.tag} for spam`);
this.spamTracker.delete(message.author.id); this.spamTracker.delete(message.author.id);
await this.logAutomodAction(message, config, 'spam', `Count ${tracker.count}`); const reason = `Spam erkannt (${tracker.count}/${threshold} Nachrichten innerhalb ${config.windowMs ?? this.windowMs}ms)`;
await this.logAutomodAction(message, config, 'spam', reason);
return true; return true;
} }
} }
@@ -125,10 +121,30 @@ export class AutoModService {
return !normalized.some((w) => url.includes(w)); return !normalized.some((w) => url.includes(w));
} }
private async logAutomodAction(message: Message, config: AutomodConfig, action: string, details?: string) { private async deleteMessageWithReason(message: Message, response: string) {
await message.delete().catch(() => undefined);
await message.channel
.send({ content: response })
.then((m) => setTimeout(() => m.delete().catch(() => undefined), 5000))
.catch(() => undefined);
}
private async logAutomodAction(message: Message, config: AutomodConfig, action: string, reason: string, content?: string) {
try { try {
const guild = message.guild; const guild = message.guild;
if (!guild) return; if (!guild) return;
if (this.logging) {
this.logging.logAutomodAction(guild, {
userTag: message.author.tag,
userId: message.author.id,
action,
reason,
content,
channel: guild.channels.cache.get(message.channelId) ?? null,
messageUrl: message.url
});
return;
}
const loggingCfg = config.loggingConfig || {}; const loggingCfg = config.loggingConfig || {};
const flags = loggingCfg.categories || {}; const flags = loggingCfg.categories || {};
if (flags.automodActions === false) return; if (flags.automodActions === false) return;
@@ -136,8 +152,8 @@ export class AutoModService {
if (!channelId) return; if (!channelId) return;
const channel = await guild.channels.fetch(channelId).catch(() => null); const channel = await guild.channels.fetch(channelId).catch(() => null);
if (!channel || !channel.isTextBased()) return; if (!channel || !channel.isTextBased()) return;
const content = `[Automod] ${action} by ${message.author.tag}${details ? ` | ${details}` : ''}`; const body = `[Automod] ${action} by ${message.author.tag} | ${reason}${content ? ` | ${content.slice(0, 1800)}` : ''}`;
await channel.send({ content }); await channel.send({ content: body });
} catch (err) { } catch (err) {
logger.error('Automod log failed', err); logger.error('Automod log failed', err);
} }

View File

@@ -45,7 +45,7 @@ export class LoggingService {
private resolve(guild: Guild) { private resolve(guild: Guild) {
const cfg = settingsStore.get(guild.id); const cfg = settingsStore.get(guild.id);
const loggingCfg = cfg?.loggingConfig || cfg?.automodConfig?.loggingConfig || {}; const loggingCfg = cfg?.loggingConfig || cfg?.automodConfig?.loggingConfig || {};
const logChannelId = loggingCfg.logChannelId || cfg?.logChannelId || this.fallbackLogChannelId; const logChannelId = loggingCfg.logChannelId || cfg?.automodConfig?.logChannelId || cfg?.logChannelId || this.fallbackLogChannelId;
const flags = loggingCfg.categories || {}; const flags = loggingCfg.categories || {};
const channel = logChannelId ? guild.channels.cache.get(logChannelId) : null; const channel = logChannelId ? guild.channels.cache.get(logChannelId) : null;
return { channel: channel && channel.type === 0 ? (channel as TextChannel) : null, flags }; return { channel: channel && channel.type === 0 ? (channel as TextChannel) : null, flags };
@@ -128,11 +128,11 @@ export class LoggingService {
}); });
} }
logAction(user: User, action: string, reason?: string) { logAction(user: User | GuildMember, action: string, reason?: string, guild?: Guild) {
const guild = user instanceof GuildMember ? user.guild : null; const resolvedGuild = guild ?? (user instanceof GuildMember ? user.guild : null);
if (!guild) return; if (!resolvedGuild) return;
if (!this.shouldLog(guild, 'automodActions')) return; if (!this.shouldLog(resolvedGuild, 'automodActions')) return;
const { channel } = this.resolve(guild); const { channel } = this.resolve(resolvedGuild);
if (!channel) return; if (!channel) return;
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle('Moderation') .setTitle('Moderation')
@@ -141,7 +141,7 @@ export class LoggingService {
.setColor(0x7289da) .setColor(0x7289da)
.setTimestamp(); .setTimestamp();
channel.send({ embeds: [embed] }).catch((err) => logger.error('Failed to log action', err)); channel.send({ embeds: [embed] }).catch((err) => logger.error('Failed to log action', err));
const guildId = (user as GuildMember)?.guild?.id; const guildId = resolvedGuild.id;
if (guildId) { if (guildId) {
adminSink?.pushGuildLog({ adminSink?.pushGuildLog({
guildId, guildId,
@@ -154,6 +154,36 @@ export class LoggingService {
} }
} }
logAutomodAction(guild: Guild, options: { userTag: string; userId: string; action: string; reason: string; content?: string; channel?: GuildChannel | null; messageUrl?: string }) {
if (!this.shouldLog(guild, 'automodActions')) return;
const { channel } = this.resolve(guild);
if (!channel) return;
const embed = new EmbedBuilder()
.setTitle('Automod')
.setDescription(`${options.userTag} (${options.userId}) -> ${options.action}`)
.addFields(
{ name: 'Grund', value: this.safeField(options.reason) },
{ name: 'Kanal', value: options.channel ? `<#${options.channel.id}>` : 'Unbekannt' }
)
.setColor(0xff006e)
.setTimestamp();
if (options.content) {
embed.addFields({ name: 'Nachricht', value: this.safeField(options.content) });
}
if (options.messageUrl) {
embed.addFields({ name: 'Link', value: options.messageUrl });
}
channel.send({ embeds: [embed] }).catch((err) => logger.error('Failed to log automod action', err));
adminSink?.pushGuildLog({
guildId: guild.id,
level: 'INFO',
message: `Automod: ${options.action} (${options.userTag})`,
timestamp: Date.now(),
category: 'automodActions'
});
adminSink?.trackGuildEvent(guild.id, 'automod');
}
logRoleUpdate(member: GuildMember, added: string[], removed: string[]) { logRoleUpdate(member: GuildMember, added: string[], removed: string[]) {
const guildId = member.guild.id; const guildId = member.guild.id;
adminSink?.pushGuildLog({ adminSink?.pushGuildLog({