Add role selection system with select menus
All checks were successful
Auto Build and Push Docker Image / build (push) Successful in 7s

This commit is contained in:
2026-04-02 10:55:43 +00:00
parent f90f0cb7ab
commit ad988c9333
5 changed files with 624 additions and 3 deletions

View File

@@ -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. 7. **Auto-Response System:** Trigger word detection for automatic replies.
8. **Welcome/Goodbye System:** Guild member add/remove events with customizable messages. 8. **Welcome/Goodbye System:** Guild member add/remove events with customizable messages.
9. **Logging System:** Configurable event logging (messages, roles, moderation, etc.). 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 ## 📁 Project Structure
@@ -30,6 +31,7 @@ pixelpoebel/
│ │ ├── ping.ts │ │ ├── ping.ts
│ │ ├── help.ts │ │ ├── help.ts
│ │ ├── twitch.ts │ │ ├── twitch.ts
│ │ ├── rolesetup.ts # Self-service role selection
│ │ ├── trigger.ts # Auto-responses │ │ ├── trigger.ts # Auto-responses
│ │ ├── timer.ts # Reminders │ │ ├── timer.ts # Reminders
│ │ ├── welcome.ts # Welcome/Goodbye messages │ │ ├── welcome.ts # Welcome/Goodbye messages
@@ -165,6 +167,30 @@ CREATE TABLE reminders (
target_time DATETIME, target_time DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP 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 ### 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`. **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> <category_name> // Create new role selection message
/rolesetup add <kategorie> <@rolle> [emoji] // Add role option
/rolesetup edit <kategorie> <@rolle> [new_emoji] // Change emoji
/rolesetup remove <kategorie> <@rolle> // Remove role option
/rolesetup delete <kategorie> // Delete entire category
/rolesetup config <kategorie> 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 ## 🚀 Build & Deploy
```bash ```bash
@@ -425,6 +487,7 @@ docker-compose up -d --build
| `/ping` | | Public | | `/ping` | | Public |
| `/help` | | Public | | `/help` | | Public |
| `/twitch` | online, add, remove, list, listall, help | Public/Admin | | `/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 | | `/trigger` | add, remove, list, help | Public/Admin |
| `/timer` | add, remove, list, listall, help | Public/Admin | | `/timer` | add, remove, list, listall, help | Public/Admin |
| `/welcome` | setchannel, add, remove, list, help | Admin | | `/welcome` | setchannel, add, remove, list, help | Admin |

View File

@@ -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;

View File

@@ -1,8 +1,86 @@
import { Events, MessageFlags } from 'discord.js'; import { Events, MessageFlags, ComponentType } from 'discord.js';
export default { export default {
name: Events.InteractionCreate, name: Events.InteractionCreate,
async execute(interaction: any, client: any) { 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; if (!interaction.isChatInputCommand()) return;
const command = client.commands.get(interaction.commandName); const command = client.commands.get(interaction.commandName);

View File

@@ -1,10 +1,48 @@
import { Events } from 'discord.js'; 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 { export default {
name: Events.ClientReady, name: Events.ClientReady,
once: true, once: true,
execute(client: any) { async execute(client: any) {
console.log(`[READY] Logged in as ${client.user.tag}`); console.log(`[READY] Logged in as ${client.user.tag}`);
console.log(`[READY] Serving ${client.guilds.cache.size} servers`); console.log(`[READY] Serving ${client.guilds.cache.size} servers`);
await validateRoles(client);
}, },
}; };

View File

@@ -103,6 +103,34 @@ export class DB {
) )
`).run(); `).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 // Migration: Add missing columns to existing guild_settings
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);