- Convert role management from prefix to slash commands (/role add/remove/list) - Update database schema to store role IDs as JSON arrays instead of regex patterns - Add /config roles command for administrators to manage allowed roles - Simplify database schema by reusing allowed_roles_for_request field as JSON - Add database reset script (pnpm reset-db) for easy testing and migration - Update config format to only support array format (no backward compatibility) Role Management Features: - /role add <role> - Self-assign roles with dropdown selection - /role remove <role> - Remove roles with dropdown selection - /role list - Show available self-assignable roles - /config roles add/remove/list/clear - Administrator role management Technical Improvements: - Role ID based matching (more reliable than name-based regex) - Type-safe role selection with Discord's native role picker - Permission hierarchy validation - Rich embed responses with proper error handling - Ephemeral responses for clean chat experience 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
284 lines
10 KiB
JavaScript
284 lines
10 KiB
JavaScript
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, MessageFlags } = require('discord.js');
|
|
const configManager = require('../../config/config');
|
|
|
|
module.exports = {
|
|
async formatAllowedRoles(guild, guildConfig) {
|
|
const databaseService = configManager.databaseService;
|
|
if (!databaseService) return 'Database unavailable';
|
|
|
|
const allowedRoleIds = databaseService.getAllowedRoleIds(guild.id);
|
|
|
|
if (allowedRoleIds.length === 0) {
|
|
return 'None configured';
|
|
}
|
|
|
|
const roles = [];
|
|
for (const roleId of allowedRoleIds) {
|
|
try {
|
|
const role = await guild.roles.fetch(roleId);
|
|
if (role) roles.push(role.name);
|
|
} catch (error) {
|
|
roles.push(`<deleted role: ${roleId}>`);
|
|
}
|
|
}
|
|
|
|
return roles.length > 0 ? roles.join(', ') : 'None configured';
|
|
},
|
|
|
|
data: new SlashCommandBuilder()
|
|
.setName('config')
|
|
.setDescription('Manage server configuration')
|
|
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
|
|
.addSubcommand(subcommand =>
|
|
subcommand
|
|
.setName('show')
|
|
.setDescription('Show current server configuration')
|
|
)
|
|
.addSubcommand(subcommand =>
|
|
subcommand
|
|
.setName('prefix')
|
|
.setDescription('Set the command prefix')
|
|
.addStringOption(option =>
|
|
option.setName('new_prefix')
|
|
.setDescription('The new command prefix')
|
|
.setRequired(true)
|
|
.setMaxLength(5)
|
|
)
|
|
)
|
|
.addSubcommand(subcommand =>
|
|
subcommand
|
|
.setName('sfx')
|
|
.setDescription('Enable or disable sound effects')
|
|
.addBooleanOption(option =>
|
|
option.setName('enabled')
|
|
.setDescription('Enable sound effects')
|
|
.setRequired(true)
|
|
)
|
|
)
|
|
.addSubcommand(subcommand =>
|
|
subcommand
|
|
.setName('volume')
|
|
.setDescription('Set sound effects volume')
|
|
.addNumberOption(option =>
|
|
option.setName('level')
|
|
.setDescription('Volume level (0.1 to 1.0)')
|
|
.setRequired(true)
|
|
.setMinValue(0.1)
|
|
.setMaxValue(1.0)
|
|
)
|
|
)
|
|
.addSubcommand(subcommand =>
|
|
subcommand
|
|
.setName('funfacts')
|
|
.setDescription('Enable or disable fun facts')
|
|
.addBooleanOption(option =>
|
|
option.setName('enabled')
|
|
.setDescription('Enable fun facts')
|
|
.setRequired(true)
|
|
)
|
|
)
|
|
.addSubcommand(subcommand =>
|
|
subcommand
|
|
.setName('hamfacts')
|
|
.setDescription('Enable or disable ham facts')
|
|
.addBooleanOption(option =>
|
|
option.setName('enabled')
|
|
.setDescription('Enable ham facts')
|
|
.setRequired(true)
|
|
)
|
|
)
|
|
.addSubcommand(subcommand =>
|
|
subcommand
|
|
.setName('sfxchannels')
|
|
.setDescription('Set allowed channels for sound effects (regex pattern)')
|
|
.addStringOption(option =>
|
|
option.setName('pattern')
|
|
.setDescription('Channel name pattern (leave empty to allow all channels)')
|
|
.setRequired(false)
|
|
)
|
|
)
|
|
.addSubcommand(subcommand =>
|
|
subcommand
|
|
.setName('roles')
|
|
.setDescription('Manage self-assignable roles')
|
|
.addStringOption(option =>
|
|
option.setName('action')
|
|
.setDescription('Action to perform')
|
|
.setRequired(true)
|
|
.addChoices(
|
|
{ name: 'Add role to list', value: 'add' },
|
|
{ name: 'Remove role from list', value: 'remove' },
|
|
{ name: 'Clear all roles', value: 'clear' },
|
|
{ name: 'Show current roles', value: 'list' }
|
|
)
|
|
)
|
|
.addRoleOption(option =>
|
|
option.setName('role')
|
|
.setDescription('The role to add or remove (not needed for list/clear)')
|
|
.setRequired(false)
|
|
)
|
|
),
|
|
|
|
async execute(interaction, guildConfig) {
|
|
const subcommand = interaction.options.getSubcommand();
|
|
const databaseService = configManager.databaseService;
|
|
|
|
if (!databaseService) {
|
|
return interaction.reply({
|
|
content: '❌ Database service not available.',
|
|
flags: [MessageFlags.Ephemeral]
|
|
});
|
|
}
|
|
|
|
if (subcommand === 'show') {
|
|
const embed = new EmbedBuilder()
|
|
.setTitle(`⚙️ Configuration for ${interaction.guild.name}`)
|
|
.setColor(0x21c629)
|
|
.addFields([
|
|
{ name: 'Prefix', value: `\`${guildConfig.prefix}\``, inline: true },
|
|
{ name: 'SFX Enabled', value: guildConfig.enableSfx ? '✅ Yes' : '❌ No', inline: true },
|
|
{ name: 'SFX Volume', value: `${Math.round(guildConfig.sfxVolume * 100)}%`, inline: true },
|
|
{ name: 'Fun Facts', value: guildConfig.enableFunFacts ? '✅ Enabled' : '❌ Disabled', inline: true },
|
|
{ name: 'Ham Facts', value: guildConfig.enableHamFacts ? '✅ Enabled' : '❌ Disabled', inline: true },
|
|
{ name: 'Allowed SFX Channels', value: guildConfig.allowedSfxChannels || 'All channels', inline: false },
|
|
{ name: 'Self-Assignable Roles', value: await this.formatAllowedRoles(interaction.guild, guildConfig), inline: false },
|
|
])
|
|
.setFooter({ text: 'Use /config commands to modify settings' });
|
|
|
|
return interaction.reply({ embeds: [embed] });
|
|
}
|
|
|
|
// Handle configuration updates
|
|
const newConfig = { ...guildConfig };
|
|
let updateMessage = '';
|
|
|
|
switch (subcommand) {
|
|
case 'prefix':
|
|
const newPrefix = interaction.options.getString('new_prefix');
|
|
newConfig.prefix = newPrefix;
|
|
updateMessage = `Command prefix updated to \`${newPrefix}\``;
|
|
break;
|
|
|
|
case 'sfx':
|
|
const sfxEnabled = interaction.options.getBoolean('enabled');
|
|
newConfig.enableSfx = sfxEnabled;
|
|
updateMessage = `Sound effects ${sfxEnabled ? 'enabled' : 'disabled'}`;
|
|
break;
|
|
|
|
case 'volume':
|
|
const volume = interaction.options.getNumber('level');
|
|
newConfig.sfxVolume = volume;
|
|
updateMessage = `SFX volume set to ${Math.round(volume * 100)}%`;
|
|
break;
|
|
|
|
case 'funfacts':
|
|
const funfactsEnabled = interaction.options.getBoolean('enabled');
|
|
newConfig.enableFunFacts = funfactsEnabled;
|
|
updateMessage = `Fun facts ${funfactsEnabled ? 'enabled' : 'disabled'}`;
|
|
break;
|
|
|
|
case 'hamfacts':
|
|
const hamfactsEnabled = interaction.options.getBoolean('enabled');
|
|
newConfig.enableHamFacts = hamfactsEnabled;
|
|
updateMessage = `Ham facts ${hamfactsEnabled ? 'enabled' : 'disabled'}`;
|
|
break;
|
|
|
|
case 'sfxchannels':
|
|
const channelPattern = interaction.options.getString('pattern');
|
|
newConfig.allowedSfxChannels = channelPattern || null;
|
|
updateMessage = channelPattern
|
|
? `SFX channels restricted to pattern: \`${channelPattern}\``
|
|
: 'SFX allowed in all channels';
|
|
break;
|
|
|
|
case 'roles':
|
|
const action = interaction.options.getString('action');
|
|
const role = interaction.options.getRole('role');
|
|
|
|
if (action === 'list') {
|
|
const allowedRoleIds = databaseService.getAllowedRoleIds(interaction.guild.id);
|
|
|
|
if (allowedRoleIds.length === 0) {
|
|
return interaction.reply({
|
|
content: '❌ No self-assignable roles are currently configured.',
|
|
flags: [MessageFlags.Ephemeral]
|
|
});
|
|
}
|
|
|
|
// Get role objects from IDs
|
|
const roles = [];
|
|
for (const roleId of allowedRoleIds) {
|
|
try {
|
|
const roleObj = await interaction.guild.roles.fetch(roleId);
|
|
if (roleObj) roles.push(roleObj);
|
|
} catch (error) {
|
|
console.warn(`Role ${roleId} not found in guild ${interaction.guild.id}`);
|
|
}
|
|
}
|
|
|
|
const embed = new EmbedBuilder()
|
|
.setTitle('📋 Self-Assignable Roles Configuration')
|
|
.setDescription(roles.length > 0 ? 'Currently configured roles:' : 'No valid roles found.')
|
|
.setColor(0x21c629)
|
|
.addFields(roles.length > 0 ? {
|
|
name: 'Allowed Roles',
|
|
value: roles.map(r => `• ${r}`).join('\n'),
|
|
inline: false
|
|
} : {
|
|
name: 'Status',
|
|
value: 'No roles configured or all configured roles have been deleted.',
|
|
inline: false
|
|
});
|
|
|
|
return interaction.reply({ embeds: [embed] });
|
|
}
|
|
|
|
if (action === 'clear') {
|
|
databaseService.updateAllowedRoleIds(interaction.guild.id, []);
|
|
updateMessage = 'Self-assignable roles list cleared';
|
|
updated = true;
|
|
break;
|
|
}
|
|
|
|
if (!role) {
|
|
return interaction.reply({
|
|
content: '❌ You must specify a role for add/remove actions.',
|
|
flags: [MessageFlags.Ephemeral]
|
|
});
|
|
}
|
|
|
|
// Check if bot can manage this role
|
|
if (!interaction.guild.members.me.permissions.has('ManageRoles') ||
|
|
role.position >= interaction.guild.members.me.roles.highest.position) {
|
|
return interaction.reply({
|
|
content: `❌ I cannot manage the **${role.name}** role due to permission hierarchy.`,
|
|
flags: [MessageFlags.Ephemeral]
|
|
});
|
|
}
|
|
|
|
if (action === 'add') {
|
|
databaseService.addAllowedRole(interaction.guild.id, role.id);
|
|
updateMessage = `Added **${role.name}** to self-assignable roles`;
|
|
} else if (action === 'remove') {
|
|
databaseService.removeAllowedRole(interaction.guild.id, role.id);
|
|
updateMessage = `Removed **${role.name}** from self-assignable roles`;
|
|
}
|
|
|
|
updated = true;
|
|
break;
|
|
}
|
|
|
|
// Update configuration in database
|
|
databaseService.upsertGuildConfig(newConfig);
|
|
|
|
const embed = new EmbedBuilder()
|
|
.setTitle('✅ Configuration Updated')
|
|
.setColor(0x00ff00)
|
|
.setDescription(updateMessage)
|
|
.setFooter({ text: 'Use /config show to see all settings' });
|
|
|
|
await interaction.reply({ embeds: [embed] });
|
|
|
|
console.log(`Configuration updated for ${interaction.guild.name}: ${subcommand} by @${interaction.user.username}`);
|
|
}
|
|
}; |