fix & refactor: centralize event logging, add DB indexes, fix mute timeout, role menu & owner permission bugs
All checks were successful
Auto Build and Push Docker Image / build (push) Successful in 20s

This commit is contained in:
sarah
2026-08-02 22:53:32 +02:00
parent 0e1630f80b
commit 5c40faa5c9
20 changed files with 245 additions and 261 deletions

View File

@@ -17,8 +17,9 @@ The bot uses a **Modular Command & Event Loading** pattern with **ESM (ECMAScrip
8. **Reminder System:** Periodic reminder checking with `target_time` tracking. 8. **Reminder System:** Periodic reminder checking with `target_time` tracking.
9. **Auto-Response System:** Trigger word detection for automatic replies (cached). 9. **Auto-Response System:** Trigger word detection for automatic replies (cached).
10. **Welcome/Goodbye System:** Guild member add/remove events with customizable messages. 10. **Welcome/Goodbye System:** Guild member add/remove events with customizable messages.
11. **Logging System:** Configurable event logging (messages, roles, moderation, etc.). 11. **Logging System:** Configurable event logging (messages, roles, moderation, etc.) via centralized `EventLogger`.
12. **Role Selection System:** Self-service role assignment via select menus with support for exclusive categories and max-role limits. 12. **Role Selection System:** Self-service role assignment via select menus with support for exclusive categories and max-role limits.
13. **Centralized Event Logger:** Kapsel event logs into `EventLogger.sendLog()` to eliminate duplicated logging logic across event files.
## 📁 Project Structure ## 📁 Project Structure
@@ -34,7 +35,8 @@ pixelpoebel/
│ └── structures/ │ └── structures/
│ ├── ExtendedClient.ts # Typed client with DB property │ ├── ExtendedClient.ts # Typed client with DB property
│ ├── Command.ts │ ├── Command.ts
│ ├── Database.ts # Database with Transaction & Surgical Caching │ ├── Database.ts # Database with Transaction, Indexes & Surgical Caching
│ ├── EventLogger.ts # Centralized event logging helper
│ ├── TwitchManager.ts # Batch Polling & Transaction logic │ ├── TwitchManager.ts # Batch Polling & Transaction logic
│ ├── TwitchMonitor.ts # IRC Monitoring & Webhook reuse │ ├── TwitchMonitor.ts # IRC Monitoring & Webhook reuse
│ ├── TwitchCache.ts # IRC Message FIFO Cache │ ├── TwitchCache.ts # IRC Message FIFO Cache
@@ -53,7 +55,7 @@ pixelpoebel/
### 1. Database with Surgical Caching ### 1. Database with Surgical Caching
The database uses WAL mode and foreign keys. Caching is surgical (clears specific guild entries when possible). The database uses WAL mode, foreign keys, and performance indexes for frequent lookups. Caching is surgical (clears specific guild entries when possible).
```typescript ```typescript
// src/structures/Database.ts // src/structures/Database.ts
@@ -63,7 +65,7 @@ export class DB {
static init() { static init() {
db.pragma('journal_mode = WAL'); db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON'); db.pragma('foreign_keys = ON');
// ... Table creation ... // ... Table creation & indexes ...
} }
static run(query: string, ...params: any[]) { static run(query: string, ...params: any[]) {
@@ -144,6 +146,11 @@ if (process.env.AUTO_DEPLOY !== 'false') {
2. **Foreign Keys:** Always enable `foreign_keys = ON` to maintain data integrity. 2. **Foreign Keys:** Always enable `foreign_keys = ON` to maintain data integrity.
3. **Type Safety:** Use `ExtendedClient` instead of `any` for the client instance. 3. **Type Safety:** Use `ExtendedClient` instead of `any` for the client instance.
4. **Webhook Reuse:** Map `WebhookClient` instances to their monitor keys to prevent leaks. 4. **Webhook Reuse:** Map `WebhookClient` instances to their monitor keys to prevent leaks.
5. **Partial Message Handling:** In `messageDelete` and `messageUpdate` events, always check for null `author` property to avoid runtime crashes on uncached partial messages. 5. **Partial Message Handling:** In `messageDelete` and `messageUpdate` events, always handle partial messages safely (e.g. `message.author` or `message.content` may be missing for uncached messages).
6. **Centralized Settings Cache:** Always use `client.DB.getSettings(guildId)` instead of raw SQLite select queries for `guild_settings` to leverage central caching and automatic default generation. 6. **Centralized Settings Cache:** Always use `client.DB.getSettings(guildId)` instead of raw SQLite select queries for `guild_settings` to leverage central caching and automatic default generation.
7. **Category Role Selection Limits:** Do not count general guild roles when checking role selection limits; filter the user's role list using only the specific role IDs registered under that category. 7. **Category Role Selection Limits:** Do not count general guild roles when checking role selection limits; filter the user's role list using only the specific role IDs registered under that category.
8. **Event Logger Centralization:** Use `EventLogger.sendLog(client, guildId, event, embed)` for logging server events instead of inline/duplicated helper functions.
9. **Discord Timeout Limit:** Discord API limits timeouts to maximum 28 days (`2419200000 ms`). Never pass `null` to `member.timeout(durationMs)` when muting.
10. **Role Menu Error Handling:** Always wrap `member.roles.add` and `member.roles.remove` in `try/catch` blocks in interaction listeners to handle permission/hierarchy errors gracefully.
11. **Select Menu Emoji Handling:** Never pass an empty string `''` to `setEmoji()`. Only call `setEmoji()` when a valid, non-empty emoji string is provided.
12. **Team Application Owner Support:** When checking bot owner status, check `process.env.BOT_OWNER_ID` or test if `client.application.owner` is a `Team` object with `owner.members.has(userId)`.

View File

@@ -140,11 +140,13 @@ npm run deploy
## 📡 Features & Optimierungen ## 📡 Features & Optimierungen
-**Modular Architecture:** TypeScript-basiert, ESM-Unterstützung, strikte Typisierung. -**Modular Architecture:** TypeScript-basiert, ESM-Unterstützung, strikte Typisierung.
-**High Performance Database:** SQLite mit **WAL-Modus**, **Fremdschlüssel-Unterstützung** und **chirurgischem In-Memory Caching** für Guild-Settings und Trigger. -**High Performance Database:** SQLite mit **WAL-Modus**, **Fremdschlüssel-Unterstützung**, **Datenbank-Indizes** (für schnelle Abfragen von Mod-Logs, Timern und Twitch-Monitoren) und **chirurgischem In-Memory Caching** für Guild-Settings und Trigger.
-**Centralized Event Logging:** Ein zentrales `EventLogger`-System zur strukturierten und verlässlichen Erfassung von Server-Ereignissen (Nachrichten, Rollen, Nicknames, Kanäle, Kicks, Bans, Warnungen).
-**Optimized Twitch Polling:** Nutzt **Batch-Requests** (100 Kanäle pro Request), Datenbank-Transaktionen und ein 2-Minuten-Intervall für zeitnahe Benachrichtigungen. -**Optimized Twitch Polling:** Nutzt **Batch-Requests** (100 Kanäle pro Request), Datenbank-Transaktionen und ein 2-Minuten-Intervall für zeitnahe Benachrichtigungen.
-**TwitchMonitor IRC:** Echtzeit IRC Chat-Logging (Mod-Events) via **wiederverwendbaren Webhooks** und FIFO-Cache. -**TwitchMonitor IRC:** Echtzeit IRC Chat-Logging (Mod-Events) via **wiederverwendbaren Webhooks** und FIFO-Cache.
-**Advanced Moderation:** Warn-System mit Auto-Actions, umfangreiches Purge-System. -**Advanced Moderation & Safety:** Warn-System mit Auto-Actions, umfangreiches Purge-System, abgesicherte Timeout-Limits (max. 28 Tage gemäß Discord-API) und vollständiges Exception handling.
-**Role Selection:** Self-Service Rollen-System über Discord Select Menus. -**Role Selection:** Self-Service Rollen-System über Discord Select Menus mit automatischer Berechtigungsprüfung und Fehlerabfangung.
-**Smart Auto-Response System:** Präzises Trigger-Wort-Matching mit Wortgrenzen-Regex (`\b`), um Falsch-Positive zu vermeiden.
-**Reminders:** Intelligentes Zeit-Parsing (z.B. "morgen 15:00", "30min"). -**Reminders:** Intelligentes Zeit-Parsing (z.B. "morgen 15:00", "30min").
-**Event Logging:** Detaillierte Erfassung von Server-Ereignissen. -**Developer & Team Support:** Unterstützung von Discord Developer Teams und `BOT_OWNER_ID` Fallback für Owner-Befehle.
-**Docker Support:** Containerisiertes Deployment inklusive Volume-Mounts für Daten. -**Docker Support:** Containerisiertes Deployment inklusive Volume-Mounts für Daten.

View File

@@ -1,27 +1,7 @@
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, GuildMember } from 'discord.js'; import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, GuildMember } from 'discord.js';
import { Command } from '../../structures/Command.js'; import { Command } from '../../structures/Command.js';
import { ExtendedClient } from '../../structures/ExtendedClient.js'; import { ExtendedClient } from '../../structures/ExtendedClient.js';
import { EventLogger } from '../../structures/EventLogger.js';
async function sendModerationLog(client: ExtendedClient, guildId: string, event: string, embed: EmbedBuilder) {
const DB = client.DB;
const settings = DB.getSettings(guildId);
if (!settings) return;
if (!settings.log_events) return;
const activeEvents = settings.log_events.split(',').filter(Boolean);
if (!activeEvents.includes(event)) return;
if (!settings.log_channel) return;
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
if (!channel || !channel.isTextBased()) return;
try {
await (channel as any).send({ embeds: [embed] });
} catch (error) {
console.error(`[LOG] Error sending moderation log (${event}):`, error);
}
}
const command: Command = { const command: Command = {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
@@ -256,7 +236,7 @@ const command: Command = {
{ name: 'Grund', value: reason, inline: false } { name: 'Grund', value: reason, inline: false }
) )
.setTimestamp(); .setTimestamp();
await sendModerationLog(interaction.client, guildId, 'kicks', embed); await EventLogger.sendLog(interaction.client, guildId, 'kicks', embed);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
await interaction.reply({ content: '❌ Fehler beim Kicken.', ephemeral: true }); await interaction.reply({ content: '❌ Fehler beim Kicken.', ephemeral: true });
@@ -330,7 +310,7 @@ const command: Command = {
{ name: 'Warnungen', value: `${warnCount}/${warnThreshold}`, inline: true } { name: 'Warnungen', value: `${warnCount}/${warnThreshold}`, inline: true }
) )
.setTimestamp(); .setTimestamp();
await sendModerationLog(interaction.client, guildId, 'warns', embed); await EventLogger.sendLog(interaction.client, guildId, 'warns', embed);
return; return;
} }
@@ -373,10 +353,11 @@ const command: Command = {
// Parse duration // Parse duration
let durationMs: number | null = null; let durationMs: number | null = null;
let durationText = ''; let durationText = '';
const MAX_TIMEOUT_MS = 28 * 24 * 60 * 60 * 1000; // 28 days max Discord timeout
if (unit === 'perm') { if (unit === 'perm') {
durationMs = null; // Permanent = null durationMs = MAX_TIMEOUT_MS;
durationText = 'Permanent'; durationText = '28 Tage (Maximaler Discord Timeout)';
} else if (duration) { } else if (duration) {
const unitSeconds: Record<string, number> = { const unitSeconds: Record<string, number> = {
sec: 1, sec: 1,
@@ -395,8 +376,10 @@ const command: Command = {
} }
durationMs = duration * multiplier * 1000; durationMs = duration * multiplier * 1000;
if (durationMs > MAX_TIMEOUT_MS) {
// Human-readable duration durationMs = MAX_TIMEOUT_MS;
durationText = `${duration} ${unit} (auf 28 Tage beschränkt)`;
} else {
const unitNames: Record<string, string> = { const unitNames: Record<string, string> = {
sec: 'Sekunden', sec: 'Sekunden',
min: 'Minuten', min: 'Minuten',
@@ -406,7 +389,8 @@ const command: Command = {
month: 'Monate', month: 'Monate',
year: 'Jahre' year: 'Jahre'
}; };
durationText = `${duration} ${unitNames[unit!]}`; durationText = `${duration} ${unitNames[unit!] || unit}`;
}
} else { } else {
await interaction.reply({ content: '❌ Dauer oder Zeiteinheit angeben.', ephemeral: true }); await interaction.reply({ content: '❌ Dauer oder Zeiteinheit angeben.', ephemeral: true });
return; return;
@@ -426,7 +410,7 @@ const command: Command = {
{ name: 'Grund', value: reason, inline: false } { name: 'Grund', value: reason, inline: false }
) )
.setTimestamp(); .setTimestamp();
await sendModerationLog(interaction.client, guildId, 'mutes', embed); await EventLogger.sendLog(interaction.client, guildId, 'mutes', embed);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
await interaction.reply({ content: '❌ Fehler beim Muten.', ephemeral: true }); await interaction.reply({ content: '❌ Fehler beim Muten.', ephemeral: true });
@@ -481,7 +465,7 @@ const command: Command = {
{ name: 'Nachrichten gelöscht', value: `${deleteDays} Tage`, inline: true } { name: 'Nachrichten gelöscht', value: `${deleteDays} Tage`, inline: true }
) )
.setTimestamp(); .setTimestamp();
await sendModerationLog(interaction.client, guildId, 'bans', embed); await EventLogger.sendLog(interaction.client, guildId, 'bans', embed);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
await interaction.reply({ content: '❌ Fehler beim Bannen.', ephemeral: true }); await interaction.reply({ content: '❌ Fehler beim Bannen.', ephemeral: true });
@@ -659,7 +643,7 @@ const command: Command = {
purgeSubcommand === 'user' ? 'Nach Nutzer' : 'Alle', inline: true } purgeSubcommand === 'user' ? 'Nach Nutzer' : 'Alle', inline: true }
) )
.setTimestamp(); .setTimestamp();
await sendModerationLog(interaction.client, guildId, 'messages', embed); await EventLogger.sendLog(interaction.client, guildId, 'messages', embed);
} }
} catch (error) { } catch (error) {
console.error('[ADMIN] Purge error:', error); console.error('[ADMIN] Purge error:', error);

View File

@@ -10,8 +10,23 @@ const command: Command = {
async execute(interaction: any, client: any) { async execute(interaction: any, client: any) {
if (!client.application?.owner) await client.application?.fetch(); if (!client.application?.owner) await client.application?.fetch();
const isOwner = interaction.user.id === client.application?.owner?.id; const owner = client.application?.owner;
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.Administrator); const envOwnerId = process.env.BOT_OWNER_ID;
let isOwner = false;
if (envOwnerId && interaction.user.id === envOwnerId) {
isOwner = true;
} else if (owner) {
if ('members' in owner && owner.members) {
isOwner = owner.members.has(interaction.user.id);
} else {
isOwner = interaction.user.id === owner.id;
}
}
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers) ||
interaction.memberPermissions?.has(PermissionFlagsBits.Administrator) ||
interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild);
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle('📖 Hilfe-Menü') .setTitle('📖 Hilfe-Menü')

View File

@@ -50,7 +50,8 @@ const command: Command = {
.addSubcommand(subcommand => .addSubcommand(subcommand =>
subcommand subcommand
.setName('help') .setName('help')
.setDescription('Hilfe zu Log-Befehlen (Mod).')), .setDescription('Hilfe zu Log-Befehlen (Mod).'))
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
category: 'Admin', category: 'Admin',
async execute(interaction: any) { async execute(interaction: any) {
const subcommand = interaction.options.getSubcommand(); const subcommand = interaction.options.getSubcommand();

View File

@@ -27,8 +27,22 @@ const command: Command = {
async execute(interaction: any, client: any) { async execute(interaction: any, client: any) {
if (!client.application?.owner) await client.application?.fetch(); if (!client.application?.owner) await client.application?.fetch();
if (interaction.user.id !== client.application?.owner?.id) { const owner = client.application?.owner;
await interaction.reply({ content: 'Keine Berechtigung.', ephemeral: true }); const envOwnerId = process.env.BOT_OWNER_ID;
let isOwner = false;
if (envOwnerId && interaction.user.id === envOwnerId) {
isOwner = true;
} else if (owner) {
if ('members' in owner && owner.members) {
isOwner = owner.members.has(interaction.user.id);
} else {
isOwner = interaction.user.id === owner.id;
}
}
if (!isOwner) {
await interaction.reply({ content: 'Keine Berechtigung. Nur für den Bot-Owner.', ephemeral: true });
return; return;
} }

View File

@@ -19,12 +19,15 @@ export async function updateRoleMessage(client: any, category: any) {
return; return;
} }
const roleOptions = options.map((opt: any) => const roleOptions = options.map((opt: any) => {
new StringSelectMenuOptionBuilder() const menuOption = new StringSelectMenuOptionBuilder()
.setLabel(opt.label.replace(/^[^\s]+\s/, '')) .setLabel(opt.label.replace(/^[^\s]+\s/, ''))
.setValue(opt.role_id) .setValue(opt.role_id);
.setEmoji(opt.emoji || '') if (opt.emoji && opt.emoji.trim().length > 0) {
); menuOption.setEmoji(opt.emoji.trim());
}
return menuOption;
});
roleOptions.push( roleOptions.push(
new StringSelectMenuOptionBuilder() new StringSelectMenuOptionBuilder()
@@ -144,7 +147,8 @@ const command: Command = {
.addSubcommand(subcommand => .addSubcommand(subcommand =>
subcommand subcommand
.setName('list') .setName('list')
.setDescription('Listet alle Rollen-Kategorien auf')), .setDescription('Listet alle Rollen-Kategorien auf'))
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
category: 'Admin', category: 'Admin',
async execute(interaction: any) { async execute(interaction: any) {
const subcommand = interaction.options.getSubcommand(); const subcommand = interaction.options.getSubcommand();

View File

@@ -32,7 +32,8 @@ const command: Command = {
.addSubcommand(subcommand => .addSubcommand(subcommand =>
subcommand subcommand
.setName('help') .setName('help')
.setDescription('Zeigt Hilfe zu den Trigger-Befehlen an.')), .setDescription('Zeigt Hilfe zu den Trigger-Befehlen an.'))
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
category: 'Admin', category: 'Admin',
async execute(interaction: any) { async execute(interaction: any) {
const subcommand = interaction.options.getSubcommand(); const subcommand = interaction.options.getSubcommand();

View File

@@ -34,7 +34,8 @@ const command: Command = {
.addSubcommand(subcommand => .addSubcommand(subcommand =>
subcommand subcommand
.setName('help') .setName('help')
.setDescription('Zeigt Hilfe zu den TwitchMonitor-Befehlen')), .setDescription('Zeigt Hilfe zu den TwitchMonitor-Befehlen'))
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
category: 'Admin', category: 'Admin',
async execute(interaction: any) { async execute(interaction: any) {
const subcommand = interaction.options.getSubcommand(); const subcommand = interaction.options.getSubcommand();

View File

@@ -48,7 +48,8 @@ const command: Command = {
.addSubcommand(subcommand => .addSubcommand(subcommand =>
subcommand subcommand
.setName('help') .setName('help')
.setDescription('Hilfe zu Welcome-Befehlen.')), .setDescription('Hilfe zu Welcome-Befehlen.'))
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
category: 'Admin', category: 'Admin',
async execute(interaction: any) { async execute(interaction: any) {
const subcommand = interaction.options.getSubcommand(); const subcommand = interaction.options.getSubcommand();

View File

@@ -1,21 +1,5 @@
import { Events, GuildChannel, EmbedBuilder } from 'discord.js'; import { Events, GuildChannel, EmbedBuilder } from 'discord.js';
import { EventLogger } from '../structures/EventLogger.js';
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
const settings = client.DB.getSettings(guildId);
if (!settings?.log_events || !settings?.log_channel) return;
const activeEvents = settings.log_events.split(',').filter(Boolean);
if (!activeEvents.includes(event)) return;
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
if (!channel || !channel.isTextBased()) return;
try {
await (channel as any).send({ embeds: [embed] });
} catch (error) {
console.error(`[LOG] Error sending log (${event}):`, error);
}
}
export default { export default {
name: Events.ChannelCreate, name: Events.ChannelCreate,
@@ -31,6 +15,6 @@ export default {
) )
.setTimestamp(); .setTimestamp();
await sendLog(channel.client, channel.guild.id, 'channels', embed); await EventLogger.sendLog(channel.client as any, channel.guild.id, 'channels', embed);
}, },
}; };

View File

@@ -1,21 +1,5 @@
import { Events, GuildChannel, EmbedBuilder } from 'discord.js'; import { Events, GuildChannel, EmbedBuilder } from 'discord.js';
import { EventLogger } from '../structures/EventLogger.js';
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
const settings = client.DB.getSettings(guildId);
if (!settings?.log_events || !settings?.log_channel) return;
const activeEvents = settings.log_events.split(',').filter(Boolean);
if (!activeEvents.includes(event)) return;
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
if (!channel || !channel.isTextBased()) return;
try {
await (channel as any).send({ embeds: [embed] });
} catch (error) {
console.error(`[LOG] Error sending log (${event}):`, error);
}
}
export default { export default {
name: Events.ChannelDelete, name: Events.ChannelDelete,
@@ -31,6 +15,6 @@ export default {
) )
.setTimestamp(); .setTimestamp();
await sendLog(channel.client, channel.guild.id, 'channels', embed); await EventLogger.sendLog(channel.client as any, channel.guild.id, 'channels', embed);
}, },
}; };

View File

@@ -1,24 +1,5 @@
import { Events, GuildMember, TextChannel, EmbedBuilder } from 'discord.js'; import { Events, GuildMember, TextChannel, EmbedBuilder } from 'discord.js';
import { EventLogger } from '../structures/EventLogger.js';
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
const DB = client.DB;
const settings = DB.getSettings(guildId);
if (!settings?.log_events) return;
const activeEvents = settings.log_events.split(',').filter(Boolean);
if (!activeEvents.includes(event)) return;
if (!settings?.log_channel) return;
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
if (!channel || !channel.isTextBased()) return;
try {
await (channel as any).send({ embeds: [embed] });
} catch (error) {
console.error(`[LOG] Error sending log (${event}):`, error);
}
}
export default { export default {
name: Events.GuildMemberRemove, name: Events.GuildMemberRemove,
@@ -36,7 +17,7 @@ export default {
) )
.setTimestamp(); .setTimestamp();
await sendLog(member.client, guildId, 'leaves', embed); await EventLogger.sendLog(member.client as any, guildId, 'leaves', embed);
// Send goodbye message if configured // Send goodbye message if configured
const settings = DB.getSettings(guildId); const settings = DB.getSettings(guildId);

View File

@@ -1,21 +1,5 @@
import { Events, GuildMember, EmbedBuilder } from 'discord.js'; import { Events, GuildMember, EmbedBuilder } from 'discord.js';
import { EventLogger } from '../structures/EventLogger.js';
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
const settings = client.DB.getSettings(guildId);
if (!settings?.log_events || !settings?.log_channel) return;
const activeEvents = settings.log_events.split(',').filter(Boolean);
if (!activeEvents.includes(event)) return;
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
if (!channel || !channel.isTextBased()) return;
try {
await (channel as any).send({ embeds: [embed] });
} catch (error) {
console.error(`[LOG] Error sending log (${event}):`, error);
}
}
export default { export default {
name: Events.GuildMemberUpdate, name: Events.GuildMemberUpdate,
@@ -36,7 +20,7 @@ export default {
) )
.setTimestamp(); .setTimestamp();
await sendLog(newMember.client, guildId, 'nicks', embed); await EventLogger.sendLog(newMember.client as any, guildId, 'nicks', embed);
} }
// Role changes // Role changes
@@ -46,30 +30,30 @@ export default {
const addedRoles = newRoles.filter(r => !oldRoles.has(r.id)); const addedRoles = newRoles.filter(r => !oldRoles.has(r.id));
const removedRoles = oldRoles.filter(r => !newRoles.has(r.id)); const removedRoles = oldRoles.filter(r => !newRoles.has(r.id));
for (const role of addedRoles) { for (const role of addedRoles.values()) {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle('🎭 Rolle hinzugefügt') .setTitle('🎭 Rolle hinzugefügt')
.setColor(0x27ae60) .setColor(0x27ae60)
.addFields( .addFields(
{ name: 'Nutzer', value: `${newMember.user.tag} (<@${newMember.id}>)`, inline: true }, { name: 'Nutzer', value: `${newMember.user.tag} (<@${newMember.id}>)`, inline: true },
{ name: 'Rolle', value: `${role[1].name} (<@&${role[1].id}>)`, inline: true } { name: 'Rolle', value: `${role.name} (<@&${role.id}>)`, inline: true }
) )
.setTimestamp(); .setTimestamp();
await sendLog(newMember.client, guildId, 'roles', embed); await EventLogger.sendLog(newMember.client as any, guildId, 'roles', embed);
} }
for (const role of removedRoles) { for (const role of removedRoles.values()) {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle('🎭 Rolle entfernt') .setTitle('🎭 Rolle entfernt')
.setColor(0xe74c3c) .setColor(0xe74c3c)
.addFields( .addFields(
{ name: 'Nutzer', value: `${newMember.user.tag} (<@${newMember.id}>)`, inline: true }, { name: 'Nutzer', value: `${newMember.user.tag} (<@${newMember.id}>)`, inline: true },
{ name: 'Rolle', value: `${role[1].name} (<@&${role[1].id}>)`, inline: true } { name: 'Rolle', value: `${role.name} (<@&${role.id}>)`, inline: true }
) )
.setTimestamp(); .setTimestamp();
await sendLog(newMember.client, guildId, 'roles', embed); await EventLogger.sendLog(newMember.client as any, guildId, 'roles', embed);
} }
}, },
}; };

View File

@@ -29,6 +29,7 @@ export default {
return; return;
} }
try {
if (selectedValue.startsWith('remove_')) { if (selectedValue.startsWith('remove_')) {
const options: any[] = client.DB.all('SELECT * FROM role_options WHERE category_id = ?', category.id); const options: any[] = client.DB.all('SELECT * FROM role_options WHERE category_id = ?', category.id);
let removedCount = 0; let removedCount = 0;
@@ -63,7 +64,7 @@ export default {
return; return;
} }
const discordRole = guild.roles.cache.get(selectedValue); const discordRole = guild.roles.cache.get(selectedValue) || await guild.roles.fetch(selectedValue).catch(() => null);
if (!discordRole) { if (!discordRole) {
await interaction.reply({ content: 'Diese Rolle existiert nicht mehr.', ephemeral: true }); await interaction.reply({ content: 'Diese Rolle existiert nicht mehr.', ephemeral: true });
return; return;
@@ -115,6 +116,16 @@ export default {
}); });
} }
} }
} catch (error) {
console.error('[ROLESETUP] Role assignment error:', error);
const errorMsg = '❌ Fehler beim Ändern der Rolle. Mir fehlen eventuell die Rechte dazu.';
if (interaction.replied || interaction.deferred) {
await interaction.followUp({ content: errorMsg, ephemeral: true });
} else {
await interaction.reply({ content: errorMsg, ephemeral: true });
}
}
return;
return; return;
} }
} }

View File

@@ -10,10 +10,16 @@ export default {
if (triggers.length === 0) return; if (triggers.length === 0) return;
const contentLower = message.content.toLowerCase(); function escapeRegex(str: string) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
for (const trigger of triggers) { for (const trigger of triggers) {
if (contentLower.includes(trigger.trigger_word.toLowerCase())) { const word = trigger.trigger_word.trim().toLowerCase();
if (!word) continue;
const pattern = new RegExp(`(?:^|\\s|\\b)${escapeRegex(word)}(?:$|\\s|\\b)`, 'i');
if (pattern.test(message.content)) {
await message.channel.send(trigger.response_text); await message.channel.send(trigger.response_text);
return; return;
} }

View File

@@ -1,44 +1,33 @@
import { Events, Message, TextChannel, EmbedBuilder } from 'discord.js'; import { Events, Message, EmbedBuilder } from 'discord.js';
import { EventLogger } from '../structures/EventLogger.js';
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
const settings = client.DB.getSettings(guildId);
if (!settings?.log_events || !settings?.log_channel) return;
const activeEvents = settings.log_events.split(',').filter(Boolean);
if (!activeEvents.includes(event)) return;
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
if (!channel || !channel.isTextBased()) return;
try {
await (channel as any).send({ embeds: [embed] });
} catch (error) {
console.error(`[LOG] Error sending log (${event}):`, error);
}
}
export default { export default {
name: Events.MessageDelete, name: Events.MessageDelete,
async execute(message: Message) { async execute(message: Message) {
if (!message.guild || !message.author || message.author.bot) return; if (!message.guild) return;
if (message.author?.bot) return;
const authorTag = message.author ? `${message.author.tag} (<@${message.author.id}>)` : 'Unbekannter Nutzer (Nicht im Cache)';
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle('🗑️ Nachricht gelöscht') .setTitle('🗑️ Nachricht gelöscht')
.setColor(0xe74c3c) .setColor(0xe74c3c)
.addFields( .addFields(
{ name: 'Autor', value: `${message.author.tag} (<@${message.author.id}>)`, inline: true }, { name: 'Autor', value: authorTag, inline: true },
{ name: 'Kanal', value: `<#${message.channelId}>`, inline: true } { name: 'Kanal', value: `<#${message.channelId}>`, inline: true }
) )
.setTimestamp(); .setTimestamp();
if (message.content) { if (message.content) {
embed.addFields({ name: 'Inhalt', value: message.content.substring(0, 1024), inline: false }); embed.addFields({ name: 'Inhalt', value: message.content.substring(0, 1024), inline: false });
} else {
embed.addFields({ name: 'Inhalt', value: '*(Nachricht nicht im Cache oder enthielt nur Medien)*', inline: false });
} }
if (message.attachments.size > 0) { if (message.attachments && message.attachments.size > 0) {
embed.addFields({ name: 'Anhänge', value: `${message.attachments.size} Datei(en)`, inline: true }); embed.addFields({ name: 'Anhänge', value: `${message.attachments.size} Datei(en)`, inline: true });
} }
await sendLog(message.client, message.guildId!, 'messages', embed); await EventLogger.sendLog(message.client as any, message.guildId!, 'messages', embed);
}, },
}; };

View File

@@ -1,21 +1,5 @@
import { Events, Message, EmbedBuilder } from 'discord.js'; import { Events, Message, EmbedBuilder } from 'discord.js';
import { EventLogger } from '../structures/EventLogger.js';
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
const settings = client.DB.getSettings(guildId);
if (!settings?.log_events || !settings?.log_channel) return;
const activeEvents = settings.log_events.split(',').filter(Boolean);
if (!activeEvents.includes(event)) return;
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
if (!channel || !channel.isTextBased()) return;
try {
await (channel as any).send({ embeds: [embed] });
} catch (error) {
console.error(`[LOG] Error sending log (${event}):`, error);
}
}
export default { export default {
name: Events.MessageUpdate, name: Events.MessageUpdate,
@@ -37,6 +21,6 @@ export default {
) )
.setTimestamp(); .setTimestamp();
await sendLog(newMessage.client, newMessage.guildId!, 'messages', embed); await EventLogger.sendLog(newMessage.client as any, newMessage.guildId!, 'messages', embed);
}, },
}; };

View File

@@ -151,6 +151,11 @@ export class DB {
) )
`).run(); `).run();
// Performance Indexes
db.prepare('CREATE INDEX IF NOT EXISTS idx_mod_logs_guild_user ON mod_logs(guild_id, user_id, action)').run();
db.prepare('CREATE INDEX IF NOT EXISTS idx_reminders_target ON reminders(target_time)').run();
db.prepare('CREATE INDEX IF NOT EXISTS idx_twitch_monitors_guild ON twitch_monitors(guild_id)').run();
// Migration logic // Migration logic
const columns = db.prepare("PRAGMA table_info(guild_settings)").all() as any[]; const columns = db.prepare("PRAGMA table_info(guild_settings)").all() as any[];
const columnNames = columns.map((c: any) => c.name); const columnNames = columns.map((c: any) => c.name);

View File

@@ -0,0 +1,26 @@
import { EmbedBuilder, TextChannel } from 'discord.js';
import { ExtendedClient } from './ExtendedClient.js';
export class EventLogger {
static async sendLog(client: ExtendedClient, guildId: string, event: string, embed: EmbedBuilder) {
if (!guildId) return;
const settings = client.DB?.getSettings(guildId);
if (!settings?.log_events || !settings?.log_channel) return;
const activeEvents = settings.log_events.split(',').filter(Boolean);
if (!activeEvents.includes(event)) return;
const guild = client.guilds.cache.get(guildId);
if (!guild) return;
const channel = guild.channels.cache.get(settings.log_channel);
if (!channel || !channel.isTextBased()) return;
try {
await (channel as TextChannel).send({ embeds: [embed] });
} catch (error) {
console.error(`[LOG] Error sending log (${event}) in guild ${guildId}:`, error);
}
}
}