Files
ghbot/src/commands/slash/config.js
Chris Ham 61a376cfbb Modernize role management system with slash commands and role IDs
- 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>
2025-08-16 18:07:07 -07:00

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}`);
}
};