diff --git a/AGENTS-BOT.md b/AGENTS-BOT.md index e6c2145..5097e63 100644 --- a/AGENTS-BOT.md +++ b/AGENTS-BOT.md @@ -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. 9. **Auto-Response System:** Trigger word detection for automatic replies (cached). 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. +13. **Centralized Event Logger:** Kapsel event logs into `EventLogger.sendLog()` to eliminate duplicated logging logic across event files. ## 📁 Project Structure @@ -34,7 +35,8 @@ pixelpoebel/ │ └── structures/ │ ├── ExtendedClient.ts # Typed client with DB property │ ├── 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 │ ├── TwitchMonitor.ts # IRC Monitoring & Webhook reuse │ ├── TwitchCache.ts # IRC Message FIFO Cache @@ -53,7 +55,7 @@ pixelpoebel/ ### 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 // src/structures/Database.ts @@ -63,7 +65,7 @@ export class DB { static init() { db.pragma('journal_mode = WAL'); db.pragma('foreign_keys = ON'); - // ... Table creation ... + // ... Table creation & indexes ... } 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. 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. -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. 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)`. diff --git a/README.md b/README.md index 5683354..8ec7d7d 100644 --- a/README.md +++ b/README.md @@ -140,11 +140,13 @@ npm run deploy ## 📡 Features & Optimierungen - ✅ **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. - ✅ **TwitchMonitor IRC:** Echtzeit IRC Chat-Logging (Mod-Events) via **wiederverwendbaren Webhooks** und FIFO-Cache. -- ✅ **Advanced Moderation:** Warn-System mit Auto-Actions, umfangreiches Purge-System. -- ✅ **Role Selection:** Self-Service Rollen-System ĂŒber Discord Select Menus. +- ✅ **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 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"). -- ✅ **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. diff --git a/src/commands/utility/admin.ts b/src/commands/utility/admin.ts index 39b1f5b..74ac9e6 100644 --- a/src/commands/utility/admin.ts +++ b/src/commands/utility/admin.ts @@ -1,27 +1,7 @@ import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, GuildMember } from 'discord.js'; import { Command } from '../../structures/Command.js'; import { ExtendedClient } from '../../structures/ExtendedClient.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); - } -} +import { EventLogger } from '../../structures/EventLogger.js'; const command: Command = { data: new SlashCommandBuilder() @@ -256,7 +236,7 @@ const command: Command = { { name: 'Grund', value: reason, inline: false } ) .setTimestamp(); - await sendModerationLog(interaction.client, guildId, 'kicks', embed); + await EventLogger.sendLog(interaction.client, guildId, 'kicks', embed); } catch (error) { console.error(error); await interaction.reply({ content: '❌ Fehler beim Kicken.', ephemeral: true }); @@ -330,7 +310,7 @@ const command: Command = { { name: 'Warnungen', value: `${warnCount}/${warnThreshold}`, inline: true } ) .setTimestamp(); - await sendModerationLog(interaction.client, guildId, 'warns', embed); + await EventLogger.sendLog(interaction.client, guildId, 'warns', embed); return; } @@ -373,10 +353,11 @@ const command: Command = { // Parse duration let durationMs: number | null = null; let durationText = ''; + const MAX_TIMEOUT_MS = 28 * 24 * 60 * 60 * 1000; // 28 days max Discord timeout if (unit === 'perm') { - durationMs = null; // Permanent = null - durationText = 'Permanent'; + durationMs = MAX_TIMEOUT_MS; + durationText = '28 Tage (Maximaler Discord Timeout)'; } else if (duration) { const unitSeconds: Record = { sec: 1, @@ -395,18 +376,21 @@ const command: Command = { } durationMs = duration * multiplier * 1000; - - // Human-readable duration - const unitNames: Record = { - sec: 'Sekunden', - min: 'Minuten', - hour: 'Stunden', - day: 'Tage', - week: 'Wochen', - month: 'Monate', - year: 'Jahre' - }; - durationText = `${duration} ${unitNames[unit!]}`; + if (durationMs > MAX_TIMEOUT_MS) { + durationMs = MAX_TIMEOUT_MS; + durationText = `${duration} ${unit} (auf 28 Tage beschrĂ€nkt)`; + } else { + const unitNames: Record = { + sec: 'Sekunden', + min: 'Minuten', + hour: 'Stunden', + day: 'Tage', + week: 'Wochen', + month: 'Monate', + year: 'Jahre' + }; + durationText = `${duration} ${unitNames[unit!] || unit}`; + } } else { await interaction.reply({ content: '❌ Dauer oder Zeiteinheit angeben.', ephemeral: true }); return; @@ -426,7 +410,7 @@ const command: Command = { { name: 'Grund', value: reason, inline: false } ) .setTimestamp(); - await sendModerationLog(interaction.client, guildId, 'mutes', embed); + await EventLogger.sendLog(interaction.client, guildId, 'mutes', embed); } catch (error) { console.error(error); 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 } ) .setTimestamp(); - await sendModerationLog(interaction.client, guildId, 'bans', embed); + await EventLogger.sendLog(interaction.client, guildId, 'bans', embed); } catch (error) { console.error(error); await interaction.reply({ content: '❌ Fehler beim Bannen.', ephemeral: true }); @@ -659,7 +643,7 @@ const command: Command = { purgeSubcommand === 'user' ? 'Nach Nutzer' : 'Alle', inline: true } ) .setTimestamp(); - await sendModerationLog(interaction.client, guildId, 'messages', embed); + await EventLogger.sendLog(interaction.client, guildId, 'messages', embed); } } catch (error) { console.error('[ADMIN] Purge error:', error); diff --git a/src/commands/utility/help.ts b/src/commands/utility/help.ts index 876d579..91061b1 100644 --- a/src/commands/utility/help.ts +++ b/src/commands/utility/help.ts @@ -10,8 +10,23 @@ const command: Command = { async execute(interaction: any, client: any) { if (!client.application?.owner) await client.application?.fetch(); - const isOwner = interaction.user.id === client.application?.owner?.id; - const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.Administrator); + const owner = client.application?.owner; + 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() .setTitle('📖 Hilfe-MenĂŒ') diff --git a/src/commands/utility/log.ts b/src/commands/utility/log.ts index 29f59b1..188c52e 100644 --- a/src/commands/utility/log.ts +++ b/src/commands/utility/log.ts @@ -50,7 +50,8 @@ const command: Command = { .addSubcommand(subcommand => subcommand .setName('help') - .setDescription('Hilfe zu Log-Befehlen (Mod).')), + .setDescription('Hilfe zu Log-Befehlen (Mod).')) + .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers), category: 'Admin', async execute(interaction: any) { const subcommand = interaction.options.getSubcommand(); diff --git a/src/commands/utility/owner.ts b/src/commands/utility/owner.ts index db54c1e..1ee00d2 100644 --- a/src/commands/utility/owner.ts +++ b/src/commands/utility/owner.ts @@ -27,8 +27,22 @@ const command: Command = { async execute(interaction: any, client: any) { if (!client.application?.owner) await client.application?.fetch(); - if (interaction.user.id !== client.application?.owner?.id) { - await interaction.reply({ content: 'Keine Berechtigung.', ephemeral: true }); + const owner = client.application?.owner; + 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; } diff --git a/src/commands/utility/rolesetup.ts b/src/commands/utility/rolesetup.ts index 2f3276c..325f5ac 100644 --- a/src/commands/utility/rolesetup.ts +++ b/src/commands/utility/rolesetup.ts @@ -19,12 +19,15 @@ export async function updateRoleMessage(client: any, category: any) { return; } - const roleOptions = options.map((opt: any) => - new StringSelectMenuOptionBuilder() + const roleOptions = options.map((opt: any) => { + const menuOption = new StringSelectMenuOptionBuilder() .setLabel(opt.label.replace(/^[^\s]+\s/, '')) - .setValue(opt.role_id) - .setEmoji(opt.emoji || '') - ); + .setValue(opt.role_id); + if (opt.emoji && opt.emoji.trim().length > 0) { + menuOption.setEmoji(opt.emoji.trim()); + } + return menuOption; + }); roleOptions.push( new StringSelectMenuOptionBuilder() @@ -144,7 +147,8 @@ const command: Command = { .addSubcommand(subcommand => subcommand .setName('list') - .setDescription('Listet alle Rollen-Kategorien auf')), + .setDescription('Listet alle Rollen-Kategorien auf')) + .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers), category: 'Admin', async execute(interaction: any) { const subcommand = interaction.options.getSubcommand(); diff --git a/src/commands/utility/trigger.ts b/src/commands/utility/trigger.ts index 0889cf3..5b7d69e 100644 --- a/src/commands/utility/trigger.ts +++ b/src/commands/utility/trigger.ts @@ -32,7 +32,8 @@ const command: Command = { .addSubcommand(subcommand => subcommand .setName('help') - .setDescription('Zeigt Hilfe zu den Trigger-Befehlen an.')), + .setDescription('Zeigt Hilfe zu den Trigger-Befehlen an.')) + .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers), category: 'Admin', async execute(interaction: any) { const subcommand = interaction.options.getSubcommand(); diff --git a/src/commands/utility/twitchmonitor.ts b/src/commands/utility/twitchmonitor.ts index d9e8930..2a39f00 100644 --- a/src/commands/utility/twitchmonitor.ts +++ b/src/commands/utility/twitchmonitor.ts @@ -34,7 +34,8 @@ const command: Command = { .addSubcommand(subcommand => subcommand .setName('help') - .setDescription('Zeigt Hilfe zu den TwitchMonitor-Befehlen')), + .setDescription('Zeigt Hilfe zu den TwitchMonitor-Befehlen')) + .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers), category: 'Admin', async execute(interaction: any) { const subcommand = interaction.options.getSubcommand(); diff --git a/src/commands/utility/welcome.ts b/src/commands/utility/welcome.ts index 3ce14c7..ebac5a8 100644 --- a/src/commands/utility/welcome.ts +++ b/src/commands/utility/welcome.ts @@ -48,7 +48,8 @@ const command: Command = { .addSubcommand(subcommand => subcommand .setName('help') - .setDescription('Hilfe zu Welcome-Befehlen.')), + .setDescription('Hilfe zu Welcome-Befehlen.')) + .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers), category: 'Admin', async execute(interaction: any) { const subcommand = interaction.options.getSubcommand(); diff --git a/src/events/channelCreate.ts b/src/events/channelCreate.ts index 88021c1..ba3d1ec 100644 --- a/src/events/channelCreate.ts +++ b/src/events/channelCreate.ts @@ -1,21 +1,5 @@ import { Events, GuildChannel, EmbedBuilder } from 'discord.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); - } -} +import { EventLogger } from '../structures/EventLogger.js'; export default { name: Events.ChannelCreate, @@ -31,6 +15,6 @@ export default { ) .setTimestamp(); - await sendLog(channel.client, channel.guild.id, 'channels', embed); + await EventLogger.sendLog(channel.client as any, channel.guild.id, 'channels', embed); }, }; \ No newline at end of file diff --git a/src/events/channelDelete.ts b/src/events/channelDelete.ts index 5ffa5b0..a3352db 100644 --- a/src/events/channelDelete.ts +++ b/src/events/channelDelete.ts @@ -1,21 +1,5 @@ import { Events, GuildChannel, EmbedBuilder } from 'discord.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); - } -} +import { EventLogger } from '../structures/EventLogger.js'; export default { name: Events.ChannelDelete, @@ -31,6 +15,6 @@ export default { ) .setTimestamp(); - await sendLog(channel.client, channel.guild.id, 'channels', embed); + await EventLogger.sendLog(channel.client as any, channel.guild.id, 'channels', embed); }, }; \ No newline at end of file diff --git a/src/events/guildMemberRemove.ts b/src/events/guildMemberRemove.ts index d96614e..1e92e3d 100644 --- a/src/events/guildMemberRemove.ts +++ b/src/events/guildMemberRemove.ts @@ -1,24 +1,5 @@ import { Events, GuildMember, TextChannel, EmbedBuilder } from 'discord.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); - } -} +import { EventLogger } from '../structures/EventLogger.js'; export default { name: Events.GuildMemberRemove, @@ -36,7 +17,7 @@ export default { ) .setTimestamp(); - await sendLog(member.client, guildId, 'leaves', embed); + await EventLogger.sendLog(member.client as any, guildId, 'leaves', embed); // Send goodbye message if configured const settings = DB.getSettings(guildId); diff --git a/src/events/guildMemberUpdate.ts b/src/events/guildMemberUpdate.ts index 52af7b7..3eb2f6a 100644 --- a/src/events/guildMemberUpdate.ts +++ b/src/events/guildMemberUpdate.ts @@ -1,21 +1,5 @@ import { Events, GuildMember, EmbedBuilder } from 'discord.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); - } -} +import { EventLogger } from '../structures/EventLogger.js'; export default { name: Events.GuildMemberUpdate, @@ -36,7 +20,7 @@ export default { ) .setTimestamp(); - await sendLog(newMember.client, guildId, 'nicks', embed); + await EventLogger.sendLog(newMember.client as any, guildId, 'nicks', embed); } // Role changes @@ -46,30 +30,30 @@ export default { const addedRoles = newRoles.filter(r => !oldRoles.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() .setTitle('🎭 Rolle hinzugefĂŒgt') .setColor(0x27ae60) .addFields( { 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(); - 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() .setTitle('🎭 Rolle entfernt') .setColor(0xe74c3c) .addFields( { 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(); - await sendLog(newMember.client, guildId, 'roles', embed); + await EventLogger.sendLog(newMember.client as any, guildId, 'roles', embed); } }, }; \ No newline at end of file diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index dc7e07d..871d7e3 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -29,93 +29,104 @@ export default { return; } - if (selectedValue.startsWith('remove_')) { - const options: any[] = client.DB.all('SELECT * FROM role_options WHERE category_id = ?', category.id); - let removedCount = 0; - for (const opt of options) { - if (member.roles.cache.has(opt.role_id)) { - await member.roles.remove(opt.role_id); - removedCount++; - } - } - if (removedCount > 0) { - await interaction.reply({ - content: `Alle Rollen aus "${categoryName}" wurden entfernt.`, - ephemeral: true - }); - } else { - await interaction.reply({ - content: `Du hast keine Rollen aus "${categoryName}".`, - ephemeral: true - }); - } - return; - } - - const option: any = client.DB.get( - 'SELECT * FROM role_options WHERE category_id = ? AND role_id = ?', - category.id, - selectedValue - ); - - if (!option) { - await interaction.reply({ content: 'Rolle nicht gefunden.', ephemeral: true }); - return; - } - - const discordRole = guild.roles.cache.get(selectedValue); - if (!discordRole) { - await interaction.reply({ content: 'Diese Rolle existiert nicht mehr.', ephemeral: true }); - return; - } - - const hasRole = member.roles.cache.has(selectedValue); - - if (category.exclusive) { - if (hasRole) { - await member.roles.remove(selectedValue); - await interaction.reply({ - content: `Die Rolle ${option.emoji ? option.emoji + ' ' : ''}${discordRole.name} wurde entfernt.`, - ephemeral: true - }); - } else { + try { + if (selectedValue.startsWith('remove_')) { const options: any[] = client.DB.all('SELECT * FROM role_options WHERE category_id = ?', category.id); + let removedCount = 0; for (const opt of options) { - if (opt.role_id !== selectedValue && member.roles.cache.has(opt.role_id)) { + if (member.roles.cache.has(opt.role_id)) { await member.roles.remove(opt.role_id); + removedCount++; } } - await member.roles.add(selectedValue); - await interaction.reply({ - content: `Du hast jetzt die Rolle ${option.emoji ? option.emoji + ' ' : ''}${discordRole.name}!`, - ephemeral: true - }); - } - } else { - if (hasRole) { - await member.roles.remove(selectedValue); - await interaction.reply({ - content: `Die Rolle ${option.emoji ? option.emoji + ' ' : ''}${discordRole.name} wurde entfernt.`, - ephemeral: true - }); - } else { - const options: any[] = client.DB.all('SELECT * FROM role_options WHERE category_id = ?', category.id); - const userCategoryRoles = options.filter((opt: any) => member.roles.cache.has(opt.role_id)).length; - if (category.max_roles > 0 && userCategoryRoles >= category.max_roles) { + if (removedCount > 0) { await interaction.reply({ - content: `Du kannst maximal ${category.max_roles} Rollen aus dieser Kategorie haben.`, + content: `Alle Rollen aus "${categoryName}" wurden entfernt.`, + ephemeral: true + }); + } else { + await interaction.reply({ + content: `Du hast keine Rollen aus "${categoryName}".`, ephemeral: true }); - return; } - await member.roles.add(selectedValue); - await interaction.reply({ - content: `Du hast jetzt die Rolle ${option.emoji ? option.emoji + ' ' : ''}${discordRole.name}!`, - ephemeral: true - }); + return; + } + + const option: any = client.DB.get( + 'SELECT * FROM role_options WHERE category_id = ? AND role_id = ?', + category.id, + selectedValue + ); + + if (!option) { + await interaction.reply({ content: 'Rolle nicht gefunden.', ephemeral: true }); + return; + } + + const discordRole = guild.roles.cache.get(selectedValue) || await guild.roles.fetch(selectedValue).catch(() => null); + if (!discordRole) { + await interaction.reply({ content: 'Diese Rolle existiert nicht mehr.', ephemeral: true }); + return; + } + + const hasRole = member.roles.cache.has(selectedValue); + + if (category.exclusive) { + if (hasRole) { + await member.roles.remove(selectedValue); + await interaction.reply({ + content: `Die Rolle ${option.emoji ? option.emoji + ' ' : ''}${discordRole.name} wurde entfernt.`, + ephemeral: true + }); + } else { + const options: any[] = client.DB.all('SELECT * FROM role_options WHERE category_id = ?', category.id); + for (const opt of options) { + if (opt.role_id !== selectedValue && member.roles.cache.has(opt.role_id)) { + await member.roles.remove(opt.role_id); + } + } + await member.roles.add(selectedValue); + await interaction.reply({ + content: `Du hast jetzt die Rolle ${option.emoji ? option.emoji + ' ' : ''}${discordRole.name}!`, + ephemeral: true + }); + } + } else { + if (hasRole) { + await member.roles.remove(selectedValue); + await interaction.reply({ + content: `Die Rolle ${option.emoji ? option.emoji + ' ' : ''}${discordRole.name} wurde entfernt.`, + ephemeral: true + }); + } else { + const options: any[] = client.DB.all('SELECT * FROM role_options WHERE category_id = ?', category.id); + const userCategoryRoles = options.filter((opt: any) => member.roles.cache.has(opt.role_id)).length; + if (category.max_roles > 0 && userCategoryRoles >= category.max_roles) { + await interaction.reply({ + content: `Du kannst maximal ${category.max_roles} Rollen aus dieser Kategorie haben.`, + ephemeral: true + }); + return; + } + await member.roles.add(selectedValue); + await interaction.reply({ + content: `Du hast jetzt die Rolle ${option.emoji ? option.emoji + ' ' : ''}${discordRole.name}!`, + ephemeral: true + }); + } + } + } 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; } } diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index a3e6d27..0838d8f 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -10,10 +10,16 @@ export default { if (triggers.length === 0) return; - const contentLower = message.content.toLowerCase(); + function escapeRegex(str: string) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } 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); return; } diff --git a/src/events/messageDelete.ts b/src/events/messageDelete.ts index 27b98bd..54b080c 100644 --- a/src/events/messageDelete.ts +++ b/src/events/messageDelete.ts @@ -1,44 +1,33 @@ -import { Events, Message, TextChannel, EmbedBuilder } from 'discord.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); - } -} +import { Events, Message, EmbedBuilder } from 'discord.js'; +import { EventLogger } from '../structures/EventLogger.js'; export default { name: Events.MessageDelete, 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() .setTitle('đŸ—‘ïž Nachricht gelöscht') .setColor(0xe74c3c) .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 } ) .setTimestamp(); if (message.content) { 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 }); } - await sendLog(message.client, message.guildId!, 'messages', embed); + await EventLogger.sendLog(message.client as any, message.guildId!, 'messages', embed); }, }; \ No newline at end of file diff --git a/src/events/messageUpdate.ts b/src/events/messageUpdate.ts index 4731370..fd435ef 100644 --- a/src/events/messageUpdate.ts +++ b/src/events/messageUpdate.ts @@ -1,21 +1,5 @@ import { Events, Message, EmbedBuilder } from 'discord.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); - } -} +import { EventLogger } from '../structures/EventLogger.js'; export default { name: Events.MessageUpdate, @@ -37,6 +21,6 @@ export default { ) .setTimestamp(); - await sendLog(newMessage.client, newMessage.guildId!, 'messages', embed); + await EventLogger.sendLog(newMessage.client as any, newMessage.guildId!, 'messages', embed); }, }; \ No newline at end of file diff --git a/src/structures/Database.ts b/src/structures/Database.ts index 9ab3e87..1215567 100644 --- a/src/structures/Database.ts +++ b/src/structures/Database.ts @@ -151,6 +151,11 @@ export class DB { ) `).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 const columns = db.prepare("PRAGMA table_info(guild_settings)").all() as any[]; const columnNames = columns.map((c: any) => c.name); diff --git a/src/structures/EventLogger.ts b/src/structures/EventLogger.ts new file mode 100644 index 0000000..637a387 --- /dev/null +++ b/src/structures/EventLogger.ts @@ -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); + } + } +}