diff --git a/AGENTS-BOT.md b/AGENTS-BOT.md index 04e9412..e6c2145 100644 --- a/AGENTS-BOT.md +++ b/AGENTS-BOT.md @@ -144,3 +144,6 @@ 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. +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. diff --git a/src/commands/utility/admin.ts b/src/commands/utility/admin.ts index 215fc47..39b1f5b 100644 --- a/src/commands/utility/admin.ts +++ b/src/commands/utility/admin.ts @@ -4,16 +4,16 @@ import { ExtendedClient } from '../../structures/ExtendedClient.js'; async function sendModerationLog(client: ExtendedClient, guildId: string, event: string, embed: EmbedBuilder) { const DB = client.DB; - const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId); + const settings = DB.getSettings(guildId); + if (!settings) return; - if (!settings?.log_events) return; + if (!settings.log_events) return; const activeEvents = settings.log_events.split(',').filter(Boolean); if (!activeEvents.includes(event)) return; - const logSettings: any = DB.get('SELECT log_channel FROM guild_settings WHERE guild_id = ?', guildId); - if (!logSettings?.log_channel) return; + if (!settings.log_channel) return; - const channel = client.guilds.cache.get(guildId)?.channels.cache.get(logSettings.log_channel); + const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel); if (!channel || !channel.isTextBased()) return; try { @@ -227,11 +227,7 @@ const command: Command = { } // Get or create guild settings - let settings: any = DB.get('SELECT * FROM guild_settings WHERE guild_id = ?', guildId); - if (!settings) { - DB.run('INSERT INTO guild_settings (guild_id) VALUES (?)', guildId); - settings = DB.get('SELECT * FROM guild_settings WHERE guild_id = ?', guildId); - } + const settings = DB.getSettings(guildId); if (subcommand === 'kick') { const target = interaction.options.getMember('target') as GuildMember; diff --git a/src/commands/utility/log.ts b/src/commands/utility/log.ts index 0a9af7c..29f59b1 100644 --- a/src/commands/utility/log.ts +++ b/src/commands/utility/log.ts @@ -58,8 +58,6 @@ const command: Command = { const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers); const DB = interaction.client.DB; - // Ensure guild settings exist - DB.run('INSERT OR IGNORE INTO guild_settings (guild_id) VALUES (?)', guildId); if (subcommand === 'help') { const embed = new EmbedBuilder() @@ -99,7 +97,7 @@ const command: Command = { } if (subcommand === 'list') { - const settings: any = DB.get('SELECT * FROM guild_settings WHERE guild_id = ?', guildId); + const settings = DB.getSettings(guildId); const channel = settings?.log_channel ? `<#${settings.log_channel}>` @@ -128,7 +126,7 @@ const command: Command = { if (subcommand === 'enable') { const event = interaction.options.getString('event')!; - const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId); + const settings = DB.getSettings(guildId); const currentEvents = (settings?.log_events || '').split(',').filter(Boolean); @@ -151,7 +149,7 @@ const command: Command = { if (subcommand === 'disable') { const event = interaction.options.getString('event')!; - const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId); + const settings = DB.getSettings(guildId); const currentEvents = (settings?.log_events || '').split(',').filter(Boolean); diff --git a/src/commands/utility/rolesetup.ts b/src/commands/utility/rolesetup.ts index 7336629..2f3276c 100644 --- a/src/commands/utility/rolesetup.ts +++ b/src/commands/utility/rolesetup.ts @@ -1,7 +1,7 @@ import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, ChannelType, ComponentType, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from 'discord.js'; import { Command } from '../../structures/Command.js'; -async function updateRoleMessage(client: any, category: any) { +export async function updateRoleMessage(client: any, category: any) { try { const channel = client.guilds.cache.get(category.guild_id)?.channels.cache.get(category.channel_id); if (!channel) return; diff --git a/src/commands/utility/welcome.ts b/src/commands/utility/welcome.ts index de19e46..3ce14c7 100644 --- a/src/commands/utility/welcome.ts +++ b/src/commands/utility/welcome.ts @@ -56,8 +56,6 @@ const command: Command = { const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers); const DB = interaction.client.DB; - // Ensure guild settings exist - DB.run('INSERT OR IGNORE INTO guild_settings (guild_id) VALUES (?)', guildId); if (subcommand === 'help') { const embed = new EmbedBuilder() @@ -80,7 +78,7 @@ const command: Command = { } if (subcommand === 'list') { - const settings: any = DB.get('SELECT * FROM guild_settings WHERE guild_id = ?', guildId); + const settings = DB.getSettings(guildId); const channel = settings?.welcome_channel ? `<#${settings.welcome_channel}>` diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index f3bf613..dc7e07d 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -99,8 +99,9 @@ export default { ephemeral: true }); } else { - const userRoles = member.roles.cache.filter((r: any) => !r.managed && r.id !== guild.id).size; - if (category.max_roles > 0 && userRoles >= category.max_roles) { + 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 diff --git a/src/events/messageDelete.ts b/src/events/messageDelete.ts index c301ef4..27b98bd 100644 --- a/src/events/messageDelete.ts +++ b/src/events/messageDelete.ts @@ -20,7 +20,7 @@ async function sendLog(client: any, guildId: string, event: string, embed: Embed export default { name: Events.MessageDelete, async execute(message: Message) { - if (!message.guild || message.author.bot) return; + if (!message.guild || !message.author || message.author.bot) return; const embed = new EmbedBuilder() .setTitle('🗑️ Nachricht gelöscht') diff --git a/src/events/messageUpdate.ts b/src/events/messageUpdate.ts index db1bd0e..4731370 100644 --- a/src/events/messageUpdate.ts +++ b/src/events/messageUpdate.ts @@ -20,7 +20,7 @@ async function sendLog(client: any, guildId: string, event: string, embed: Embed export default { name: Events.MessageUpdate, async execute(oldMessage: Message, newMessage: Message) { - if (!newMessage.guild || newMessage.author?.bot) return; + if (!newMessage.guild || !newMessage.author || newMessage.author.bot) return; if (!oldMessage.content || !newMessage.content) return; if (oldMessage.content === newMessage.content) return; diff --git a/src/events/ready.ts b/src/events/ready.ts index 8fbf465..373df10 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -1,6 +1,7 @@ import { Events } from 'discord.js'; import { TwitchMonitor } from '../structures/TwitchMonitor.js'; import { ExtendedClient } from '../structures/ExtendedClient.js'; +import { updateRoleMessage } from '../commands/utility/rolesetup.js'; async function validateRoles(client: ExtendedClient) { const categories: any[] = client.DB.all('SELECT * FROM role_categories'); @@ -12,6 +13,7 @@ async function validateRoles(client: ExtendedClient) { const options: any[] = client.DB.all('SELECT * FROM role_options WHERE category_id = ?', category.id); const validOptions: any[] = []; + let categoryUpdated = false; for (const option of options) { const roleExists = guild.roles.cache.has(option.role_id); @@ -24,10 +26,16 @@ async function validateRoles(client: ExtendedClient) { await member.roles.remove(option.role_id).catch(() => null); } } + client.DB.run('DELETE FROM role_options WHERE id = ?', option.id); + categoryUpdated = true; removedCount++; } } + if (categoryUpdated) { + await updateRoleMessage(client, category); + } + if (validOptions.length === 0 && options.length > 0) { console.log(`[ROLESETUP] Category "${category.category_name}" has no valid roles left. Consider deleting it.`); } diff --git a/src/structures/Database.ts b/src/structures/Database.ts index 7b15978..9ab3e87 100644 --- a/src/structures/Database.ts +++ b/src/structures/Database.ts @@ -172,14 +172,18 @@ export class DB { } /** - * Optimized getter for guild settings with caching + * Optimized getter for guild settings with caching (automatically creates settings if missing) */ static getSettings(guildId: string): any { if (this.settingsCache.has(guildId)) { return this.settingsCache.get(guildId); } - const settings = db.prepare('SELECT * FROM guild_settings WHERE guild_id = ?').get(guildId); + let settings = db.prepare('SELECT * FROM guild_settings WHERE guild_id = ?').get(guildId); + if (!settings) { + db.prepare('INSERT OR IGNORE INTO guild_settings (guild_id) VALUES (?)').run(guildId); + settings = db.prepare('SELECT * FROM guild_settings WHERE guild_id = ?').get(guildId); + } if (settings) { this.settingsCache.set(guildId, settings); }