Add role selection system with select menus
All checks were successful
Auto Build and Push Docker Image / build (push) Successful in 7s
All checks were successful
Auto Build and Push Docker Image / build (push) Successful in 7s
This commit is contained in:
@@ -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 |
|
||||||
|
|||||||
414
src/commands/utility/rolesetup.ts
Normal file
414
src/commands/utility/rolesetup.ts
Normal 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;
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user