From ad988c93337b9d9e0a5822d4ff83fc4a99cbed23 Mon Sep 17 00:00:00 2001 From: coding Date: Thu, 2 Apr 2026 10:55:43 +0000 Subject: [PATCH] Add role selection system with select menus --- AGENTS-BOT.md | 65 ++++- src/commands/utility/rolesetup.ts | 414 ++++++++++++++++++++++++++++++ src/events/interactionCreate.ts | 80 +++++- src/events/ready.ts | 40 ++- src/structures/Database.ts | 28 ++ 5 files changed, 624 insertions(+), 3 deletions(-) create mode 100644 src/commands/utility/rolesetup.ts diff --git a/AGENTS-BOT.md b/AGENTS-BOT.md index 696cc65..cc00bd0 100644 --- a/AGENTS-BOT.md +++ b/AGENTS-BOT.md @@ -16,7 +16,8 @@ The bot uses a **Modular Command & Event Loading** pattern with **ESM (ECMAScrip 7. **Auto-Response System:** Trigger word detection for automatic replies. 8. **Welcome/Goodbye System:** Guild member add/remove events with customizable messages. 9. **Logging System:** Configurable event logging (messages, roles, moderation, etc.). -10. **Grouped Commands:** Admin/Owner/Trigger/Timer commands grouped under subcommands. +10. **Role Selection System:** Self-service role assignment via select menus. +11. **Grouped Commands:** Admin/Owner/Trigger/Timer commands grouped under subcommands. ## 📁 Project Structure @@ -30,6 +31,7 @@ pixelpoebel/ │ │ ├── ping.ts │ │ ├── help.ts │ │ ├── twitch.ts +│ │ ├── rolesetup.ts # Self-service role selection │ │ ├── trigger.ts # Auto-responses │ │ ├── timer.ts # Reminders │ │ ├── welcome.ts # Welcome/Goodbye messages @@ -165,6 +167,30 @@ CREATE TABLE reminders ( target_time DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); + +-- role_categories +CREATE TABLE role_categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + guild_id TEXT NOT NULL, + channel_id TEXT NOT NULL, + message_id TEXT NOT NULL, + category_name TEXT NOT NULL, + max_roles INTEGER DEFAULT 0, + exclusive BOOLEAN DEFAULT 0, + remove_on_delete BOOLEAN DEFAULT 0, + UNIQUE(guild_id, category_name) +); + +-- role_options +CREATE TABLE role_options ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + category_id INTEGER NOT NULL, + label TEXT NOT NULL, + emoji TEXT, + role_id TEXT NOT NULL, + UNIQUE(category_id, role_id), + FOREIGN KEY (category_id) REFERENCES role_categories(id) ON DELETE CASCADE +); ``` ### 3. Key Components @@ -392,6 +418,42 @@ await channel.bulkDelete(filtered, true); **Logging:** Purge actions are logged to the moderation log channel with event type `messages`. +### 12. Role Selection System + +**Overview:** Self-service role assignment via Discord Select Menus. Users can choose roles from a dropdown message without needing admin intervention. + +**Admin Commands:** +```typescript +/rolesetup create <#channel> // Create new role selection message +/rolesetup add <@rolle> [emoji] // Add role option +/rolesetup edit <@rolle> [new_emoji] // Change emoji +/rolesetup remove <@rolle> // Remove role option +/rolesetup delete // Delete entire category +/rolesetup config max:1 exclusive:true // Configure settings +/rolesetup list // Show all configurations +``` + +**Config Options:** +| Option | Default | Description | +|--------|---------|-------------| +| `max_roles` | 0 (unlimited) | Maximum roles a user can select from this category | +| `exclusive` | false | If true, selecting a role removes other roles in this category | +| `remove_on_delete` | false | Remove role from users when option is deleted | + +**User Interaction:** +- User clicks on Select Menu dropdown +- Role is added/removed based on config +- Ephemeral confirmation message appears (auto-hides after 60s) + +**Database Schema:** +- `role_categories`: Stores category settings (channel, message_id, config) +- `role_options`: Stores individual role options with labels and emojis +- CASCADE DELETE removes options when category is deleted + +**Role Validation:** +- On bot startup, validates that all stored role IDs still exist in Discord +- Missing roles are removed from database (and optionally from users) + ## 🚀 Build & Deploy ```bash @@ -425,6 +487,7 @@ docker-compose up -d --build | `/ping` | – | Public | | `/help` | – | Public | | `/twitch` | online, add, remove, list, listall, help | Public/Admin | +| `/rolesetup` | create, add, edit, remove, delete, config, list | Admin | | `/trigger` | add, remove, list, help | Public/Admin | | `/timer` | add, remove, list, listall, help | Public/Admin | | `/welcome` | setchannel, add, remove, list, help | Admin | diff --git a/src/commands/utility/rolesetup.ts b/src/commands/utility/rolesetup.ts new file mode 100644 index 0000000..312b4c1 --- /dev/null +++ b/src/commands/utility/rolesetup.ts @@ -0,0 +1,414 @@ +import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags, ChannelType, ComponentType, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, GuildMember } from 'discord.js'; +import { Command } from '../../structures/Command.js'; + +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; + + const message = await channel.messages.fetch(category.message_id); + if (!message) return; + + const options: any[] = client.DB.all('SELECT * FROM role_options WHERE category_id = ?', category.id); + + if (options.length === 0) { + await message.edit({ + content: `**${category.category_name}**\nWähle unten eine Rolle aus:\n\n*Noch keine Rollen konfiguriert.*`, + components: [] + }); + return; + } + + const selectMenu = new StringSelectMenuBuilder() + .setCustomId(`role_select_${category.category_name}`) + .setPlaceholder('Wähle eine Rolle...') + .addOptions(options.map((opt: any) => + new StringSelectMenuOptionBuilder() + .setLabel(opt.label.replace(/^[^\s]+\s/, '')) + .setValue(opt.role_id) + .setEmoji(opt.emoji || '') + )); + + await message.edit({ + content: `**${category.category_name}**\nWähle unten eine Rolle aus:`, + components: [{ type: ComponentType.ActionRow, components: [selectMenu] }] + }); + } catch (error) { + console.error('[ROLESETUP] Error updating message:', error); + } +} + +const command: Command = { + data: new SlashCommandBuilder() + .setName('rolesetup') + .setDescription('Rollen-Auswahl konfigurieren') + .addSubcommand(subcommand => + subcommand + .setName('create') + .setDescription('Erstellt eine neue Rollen-Auswahl Message') + .addChannelOption(option => + option.setName('channel') + .setDescription('Der Kanal für die Rollen-Message') + .addChannelTypes(ChannelType.GuildText) + .setRequired(true)) + .addStringOption(option => + option.setName('name') + .setDescription('Name der Kategorie (z.B. "Farben", "Spiele")') + .setRequired(true))) + .addSubcommand(subcommand => + subcommand + .setName('add') + .setDescription('Fügt eine neue Rolle zur Auswahl hinzu') + .addStringOption(option => + option.setName('kategorie') + .setDescription('Der Name der Kategorie') + .setRequired(true)) + .addRoleOption(option => + option.setName('rolle') + .setDescription('Die Discord-Rolle') + .setRequired(true)) + .addStringOption(option => + option.setName('emoji') + .setDescription('Optional: Emoji für die Option') + .setRequired(false))) + .addSubcommand(subcommand => + subcommand + .setName('edit') + .setDescription('Ändert eine bestehende Option') + .addStringOption(option => + option.setName('kategorie') + .setDescription('Der Name der Kategorie') + .setRequired(true)) + .addRoleOption(option => + option.setName('rolle') + .setDescription('Die Discord-Rolle zum Ändern') + .setRequired(true)) + .addStringOption(option => + option.setName('neue_rolle') + .setDescription('Die neue Discord-Rolle') + .setRequired(false)) + .addStringOption(option => + option.setName('neues_emoji') + .setDescription('Das neue Emoji') + .setRequired(false))) + .addSubcommand(subcommand => + subcommand + .setName('remove') + .setDescription('Entfernt eine Rolle von der Auswahl') + .addStringOption(option => + option.setName('kategorie') + .setDescription('Der Name der Kategorie') + .setRequired(true)) + .addRoleOption(option => + option.setName('rolle') + .setDescription('Die Discord-Rolle zum Entfernen') + .setRequired(true))) + .addSubcommand(subcommand => + subcommand + .setName('delete') + .setDescription('Löscht eine Kategorie und deren Message') + .addStringOption(option => + option.setName('kategorie') + .setDescription('Der Name der Kategorie') + .setRequired(true))) + .addSubcommand(subcommand => + subcommand + .setName('config') + .setDescription('Konfiguriert die Kategorie-Einstellungen') + .addStringOption(option => + option.setName('kategorie') + .setDescription('Der Name der Kategorie') + .setRequired(true)) + .addIntegerOption(option => + option.setName('max_rollen') + .setDescription('Maximale Rollen pro User (0 = unbegrenzt)') + .setRequired(false) + .setMinValue(0)) + .addBooleanOption(option => + option.setName('exklusiv') + .setDescription('Nur eine Rolle pro User erlaubt') + .setRequired(false)) + .addBooleanOption(option => + option.setName('entfernen_bei_loeschung') + .setDescription('Rolle von Usern entfernen wenn gelöscht') + .setRequired(false))) + .addSubcommand(subcommand => + subcommand + .setName('list') + .setDescription('Listet alle Rollen-Kategorien auf')), + category: 'Admin', + async execute(interaction: any) { + const subcommand = interaction.options.getSubcommand(); + const guildId = interaction.guildId; + const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers); + const DB = interaction.client.DB; + + if (subcommand === 'list') { + const categories: any[] = DB.all('SELECT * FROM role_categories WHERE guild_id = ? ORDER BY category_name ASC', guildId); + + if (categories.length === 0) { + await interaction.reply({ content: 'Keine Rollen-Kategorien konfiguriert.', flags: [MessageFlags.Ephemeral] }); + return; + } + + const embed = new EmbedBuilder() + .setTitle('📋 Rollen-Kategorien') + .setColor(0x3498db) + .setTimestamp(); + + for (const cat of categories) { + const options: any[] = DB.all('SELECT * FROM role_options WHERE category_id = ?', cat.id); + const channel = interaction.guild?.channels.cache.get(cat.channel_id); + const configStr = []; + if (cat.max_roles > 0) configStr.push(`Max: ${cat.max_roles}`); + if (cat.exclusive) configStr.push('Exklusiv'); + if (cat.remove_on_delete) configStr.push('Auto-Entfernen'); + + embed.addFields({ + name: `${cat.category_name} ${configStr.length > 0 ? `(${configStr.join(', ')})` : ''}`, + value: `Kanal: ${channel?.toString() || '#' + cat.channel_id}\nOptionen: ${options.length}\nNachricht: ${cat.message_id !== 'pending' ? '[✅]' : '[⏳]'}`, + inline: false + }); + } + + await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] }); + return; + } + + if (subcommand === 'create') { + if (!isAdmin) { + await interaction.reply({ content: '❌ Keine Berechtigung. Moderatoren erforderlich.', flags: [MessageFlags.Ephemeral] }); + return; + } + + const channel = interaction.options.getChannel('channel'); + const categoryName = interaction.options.getString('name'); + + const existing = DB.get('SELECT * FROM role_categories WHERE guild_id = ? AND category_name = ?', guildId, categoryName); + if (existing) { + await interaction.reply({ content: `❌ Kategorie "${categoryName}" existiert bereits.`, flags: [MessageFlags.Ephemeral] }); + return; + } + + const selectMenu = new StringSelectMenuBuilder() + .setCustomId(`role_select_${categoryName}`) + .setPlaceholder('Wähle eine Rolle...'); + + const row = { type: ComponentType.ActionRow, components: [selectMenu] }; + + const sentMessage = await channel.send({ + content: `**${categoryName}**\nWähle unten eine Rolle aus:`, + components: [row] + }); + + DB.run( + 'INSERT INTO role_categories (guild_id, channel_id, message_id, category_name, max_roles, exclusive, remove_on_delete) VALUES (?, ?, ?, ?, 0, 0, 0)', + guildId, + channel.id, + sentMessage.id, + categoryName + ); + + await interaction.reply({ content: `✅ Kategorie "${categoryName}" erstellt in ${channel}.`, flags: [MessageFlags.Ephemeral] }); + return; + } + + if (subcommand === 'add') { + if (!isAdmin) { + await interaction.reply({ content: '❌ Keine Berechtigung. Moderatoren erforderlich.', flags: [MessageFlags.Ephemeral] }); + return; + } + + const kategorie = interaction.options.getString('kategorie'); + const role = interaction.options.getRole('rolle'); + const emoji = interaction.options.getString('emoji'); + + const category: any = DB.get('SELECT * FROM role_categories WHERE guild_id = ? AND category_name = ?', guildId, kategorie); + if (!category) { + await interaction.reply({ content: `❌ Kategorie "${kategorie}" nicht gefunden.`, flags: [MessageFlags.Ephemeral] }); + return; + } + + const existing = DB.get('SELECT * FROM role_options WHERE category_id = ? AND role_id = ?', category.id, role.id); + if (existing) { + await interaction.reply({ content: `❌ Rolle ${role.name} ist bereits in "${kategorie}".`, flags: [MessageFlags.Ephemeral] }); + return; + } + + const label = emoji ? `${emoji} ${role.name}` : role.name; + + DB.run( + 'INSERT INTO role_options (category_id, label, emoji, role_id) VALUES (?, ?, ?, ?)', + category.id, + label, + emoji || null, + role.id + ); + + await updateRoleMessage(interaction.client, category); + await interaction.reply({ content: `✅ ${role.name} zu "${kategorie}" hinzugefügt.`, flags: [MessageFlags.Ephemeral] }); + return; + } + + if (subcommand === 'edit') { + if (!isAdmin) { + await interaction.reply({ content: '❌ Keine Berechtigung. Moderatoren erforderlich.', flags: [MessageFlags.Ephemeral] }); + return; + } + + const kategorie = interaction.options.getString('kategorie'); + const oldRole = interaction.options.getRole('rolle'); + const newRole = interaction.options.getRole('neue_rolle'); + const newEmoji = interaction.options.getString('neues_emoji'); + + const category: any = DB.get('SELECT * FROM role_categories WHERE guild_id = ? AND category_name = ?', guildId, kategorie); + if (!category) { + await interaction.reply({ content: `❌ Kategorie "${kategorie}" nicht gefunden.`, flags: [MessageFlags.Ephemeral] }); + return; + } + + const option: any = DB.get('SELECT * FROM role_options WHERE category_id = ? AND role_id = ?', category.id, oldRole.id); + if (!option) { + await interaction.reply({ content: `❌ Rolle ${oldRole.name} ist nicht in "${kategorie}".`, flags: [MessageFlags.Ephemeral] }); + return; + } + + const finalEmoji = newEmoji !== null ? newEmoji : option.emoji; + const finalRoleId = newRole ? newRole.id : oldRole.id; + const finalLabel = finalEmoji ? `${finalEmoji} ${newRole ? newRole.name : oldRole.name}` : (newRole ? newRole.name : oldRole.name); + + DB.run( + 'UPDATE role_options SET label = ?, emoji = ?, role_id = ? WHERE id = ?', + finalLabel, + finalEmoji, + finalRoleId, + option.id + ); + + await updateRoleMessage(interaction.client, category); + await interaction.reply({ content: `✅ Option in "${kategorie}" aktualisiert.`, flags: [MessageFlags.Ephemeral] }); + return; + } + + if (subcommand === 'remove') { + if (!isAdmin) { + await interaction.reply({ content: '❌ Keine Berechtigung. Moderatoren erforderlich.', flags: [MessageFlags.Ephemeral] }); + return; + } + + const kategorie = interaction.options.getString('kategorie'); + const role = interaction.options.getRole('rolle'); + + const category: any = DB.get('SELECT * FROM role_categories WHERE guild_id = ? AND category_name = ?', guildId, kategorie); + if (!category) { + await interaction.reply({ content: `❌ Kategorie "${kategorie}" nicht gefunden.`, flags: [MessageFlags.Ephemeral] }); + return; + } + + const option: any = DB.get('SELECT * FROM role_options WHERE category_id = ? AND role_id = ?', category.id, role.id); + if (!option) { + await interaction.reply({ content: `❌ Rolle ${role.name} ist nicht in "${kategorie}".`, flags: [MessageFlags.Ephemeral] }); + return; + } + + if (category.remove_on_delete) { + const guild = interaction.guild; + const discordRole = guild.roles.cache.get(role.id); + if (discordRole) { + const members = guild.members.cache.filter((m: any) => m.roles.cache.has(role.id)); + for (const member of members.values()) { + await member.roles.remove(role.id); + } + } + } + + DB.run('DELETE FROM role_options WHERE id = ?', option.id); + await updateRoleMessage(interaction.client, category); + await interaction.reply({ content: `✅ ${role.name} aus "${kategorie}" entfernt.`, flags: [MessageFlags.Ephemeral] }); + return; + } + + if (subcommand === 'delete') { + if (!isAdmin) { + await interaction.reply({ content: '❌ Keine Berechtigung. Moderatoren erforderlich.', flags: [MessageFlags.Ephemeral] }); + return; + } + + const kategorie = interaction.options.getString('kategorie'); + + const category: any = DB.get('SELECT * FROM role_categories WHERE guild_id = ? AND category_name = ?', guildId, kategorie); + if (!category) { + await interaction.reply({ content: `❌ Kategorie "${kategorie}" nicht gefunden.`, flags: [MessageFlags.Ephemeral] }); + return; + } + + if (category.remove_on_delete) { + const options: any[] = DB.all('SELECT * FROM role_options WHERE category_id = ?', category.id); + const guild = interaction.guild; + for (const opt of options) { + const discordRole = guild.roles.cache.get(opt.role_id); + if (discordRole) { + const members = guild.members.cache.filter((m: any) => m.roles.cache.has(opt.role_id)); + for (const member of members.values()) { + await member.roles.remove(opt.role_id); + } + } + } + } + + try { + const channel = interaction.guild?.channels.cache.get(category.channel_id); + if (channel) { + const message = await channel.messages.fetch(category.message_id); + if (message) await message.delete(); + } + } catch { } + + DB.run('DELETE FROM role_categories WHERE id = ?', category.id); + + await interaction.reply({ content: `✅ Kategorie "${kategorie}" gelöscht.`, flags: [MessageFlags.Ephemeral] }); + return; + } + + if (subcommand === 'config') { + if (!isAdmin) { + await interaction.reply({ content: '❌ Keine Berechtigung. Moderatoren erforderlich.', flags: [MessageFlags.Ephemeral] }); + return; + } + + const kategorie = interaction.options.getString('kategorie'); + const maxRollen = interaction.options.getInteger('max_rollen'); + const exklusiv = interaction.options.getBoolean('exklusiv'); + const entfernenBeiLoeschung = interaction.options.getBoolean('entfernen_bei_loeschung'); + + const category: any = DB.get('SELECT * FROM role_categories WHERE guild_id = ? AND category_name = ?', guildId, kategorie); + if (!category) { + await interaction.reply({ content: `❌ Kategorie "${kategorie}" nicht gefunden.`, flags: [MessageFlags.Ephemeral] }); + return; + } + + const updates: string[] = []; + if (maxRollen !== null) { + DB.run('UPDATE role_categories SET max_roles = ? WHERE id = ?', maxRollen, category.id); + updates.push(`Max-Rollen: ${maxRollen === 0 ? 'unbegrenzt' : maxRollen}`); + } + if (exklusiv !== null) { + DB.run('UPDATE role_categories SET exclusive = ? WHERE id = ?', exklusiv ? 1 : 0, category.id); + updates.push(`Exklusiv: ${exklusiv ? 'ja' : 'nein'}`); + } + if (entfernenBeiLoeschung !== null) { + DB.run('UPDATE role_categories SET remove_on_delete = ? WHERE id = ?', entfernenBeiLoeschung ? 1 : 0, category.id); + updates.push(`Auto-Entfernen: ${entfernenBeiLoeschung ? 'ja' : 'nein'}`); + } + + if (updates.length === 0) { + await interaction.reply({ content: 'Keine Änderungen angegeben.', flags: [MessageFlags.Ephemeral] }); + return; + } + + await interaction.reply({ content: `✅ Konfiguration aktualisiert: ${updates.join(', ')}`, flags: [MessageFlags.Ephemeral] }); + } + } +}; + +export default command; \ No newline at end of file diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index a2d0de6..c7c338e 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -1,8 +1,86 @@ -import { Events, MessageFlags } from 'discord.js'; +import { Events, MessageFlags, ComponentType } from 'discord.js'; export default { name: Events.InteractionCreate, async execute(interaction: any, client: any) { + if (interaction.isStringSelectMenu()) { + const customId = interaction.customId; + + if (customId.startsWith('role_select_')) { + const categoryName = customId.replace('role_select_', ''); + const selectedRoleId = interaction.values[0]; + const member = interaction.member; + const guild = interaction.guild; + + const category: any = client.DB.get( + 'SELECT * FROM role_categories WHERE guild_id = ? AND category_name = ?', + guild.id, + categoryName + ); + + if (!category) { + await interaction.reply({ content: 'Kategorie nicht gefunden.', flags: [MessageFlags.Ephemeral] }); + return; + } + + const option: any = client.DB.get( + 'SELECT * FROM role_options WHERE category_id = ? AND role_id = ?', + category.id, + selectedRoleId + ); + + if (!option) { + await interaction.reply({ content: 'Rolle nicht gefunden.', flags: [MessageFlags.Ephemeral] }); + return; + } + + const discordRole = guild.roles.cache.get(selectedRoleId); + if (!discordRole) { + await interaction.reply({ content: 'Diese Rolle existiert nicht mehr.', flags: [MessageFlags.Ephemeral] }); + return; + } + + const hasRole = member.roles.cache.has(selectedRoleId); + + if (category.exclusive) { + const options: any[] = client.DB.all('SELECT * FROM role_options WHERE category_id = ?', category.id); + for (const opt of options) { + if (opt.role_id !== selectedRoleId && member.roles.cache.has(opt.role_id)) { + await member.roles.remove(opt.role_id); + } + } + await member.roles.add(selectedRoleId); + await interaction.reply({ + content: `Du hast jetzt die Rolle ${option.emoji ? option.emoji + ' ' : ''}${discordRole.name}!`, + flags: [MessageFlags.Ephemeral] + }); + } else { + if (hasRole) { + await member.roles.remove(selectedRoleId); + await interaction.reply({ + content: `Die Rolle ${option.emoji ? option.emoji + ' ' : ''}${discordRole.name} wurde entfernt.`, + flags: [MessageFlags.Ephemeral] + }); + } 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) { + await interaction.reply({ + content: `Du kannst maximal ${category.max_roles} Rollen aus dieser Kategorie haben.`, + flags: [MessageFlags.Ephemeral] + }); + return; + } + await member.roles.add(selectedRoleId); + await interaction.reply({ + content: `Du hast jetzt die Rolle ${option.emoji ? option.emoji + ' ' : ''}${discordRole.name}!`, + flags: [MessageFlags.Ephemeral] + }); + } + } + return; + } + } + if (!interaction.isChatInputCommand()) return; const command = client.commands.get(interaction.commandName); diff --git a/src/events/ready.ts b/src/events/ready.ts index 42846a0..fb6c2a3 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -1,10 +1,48 @@ import { Events } from 'discord.js'; +async function validateRoles(client: any) { + const categories: any[] = client.DB.all('SELECT * FROM role_categories'); + let removedCount = 0; + + for (const category of categories) { + const guild = client.guilds.cache.get(category.guild_id); + if (!guild) continue; + + const options: any[] = client.DB.all('SELECT * FROM role_options WHERE category_id = ?', category.id); + const validOptions: any[] = []; + + for (const option of options) { + const roleExists = guild.roles.cache.has(option.role_id); + if (roleExists) { + validOptions.push(option); + } else { + if (category.remove_on_delete) { + const members = guild.members.cache.filter((m: any) => m.roles.cache.has(option.role_id)); + for (const member of members.values()) { + await member.roles.remove(option.role_id); + } + } + removedCount++; + } + } + + if (validOptions.length === 0 && options.length > 0) { + console.log(`[ROLESETUP] Category "${category.category_name}" has no valid roles left. Consider deleting it.`); + } + } + + if (removedCount > 0) { + console.log(`[ROLESETUP] Removed ${removedCount} invalid role options from database.`); + } +} + export default { name: Events.ClientReady, once: true, - execute(client: any) { + async execute(client: any) { console.log(`[READY] Logged in as ${client.user.tag}`); console.log(`[READY] Serving ${client.guilds.cache.size} servers`); + + await validateRoles(client); }, }; \ No newline at end of file diff --git a/src/structures/Database.ts b/src/structures/Database.ts index 62c0e3e..32ee747 100644 --- a/src/structures/Database.ts +++ b/src/structures/Database.ts @@ -103,6 +103,34 @@ export class DB { ) `).run(); + // Role Categories + db.prepare(` + CREATE TABLE IF NOT EXISTS role_categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + guild_id TEXT NOT NULL, + channel_id TEXT NOT NULL, + message_id TEXT NOT NULL, + category_name TEXT NOT NULL, + max_roles INTEGER DEFAULT 0, + exclusive BOOLEAN DEFAULT 0, + remove_on_delete BOOLEAN DEFAULT 0, + UNIQUE(guild_id, category_name) + ) + `).run(); + + // Role Options + db.prepare(` + CREATE TABLE IF NOT EXISTS role_options ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + category_id INTEGER NOT NULL, + label TEXT NOT NULL, + emoji TEXT, + role_id TEXT NOT NULL, + UNIQUE(category_id, role_id), + FOREIGN KEY (category_id) REFERENCES role_categories(id) ON DELETE CASCADE + ) + `).run(); + // Migration: Add missing columns to existing guild_settings const columns = db.prepare("PRAGMA table_info(guild_settings)").all() as any[]; const columnNames = columns.map((c: any) => c.name);