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>
This commit is contained in:
@@ -2,6 +2,29 @@ const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, MessageFlags } =
|
||||
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')
|
||||
@@ -77,10 +100,21 @@ module.exports = {
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('roles')
|
||||
.setDescription('Set roles that users can self-assign')
|
||||
.setDescription('Manage self-assignable roles')
|
||||
.addStringOption(option =>
|
||||
option.setName('pattern')
|
||||
.setDescription('Role pattern (pipe-separated, e.g., "streamer|vip|member")')
|
||||
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)
|
||||
)
|
||||
),
|
||||
@@ -107,7 +141,7 @@ module.exports = {
|
||||
{ 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: 'Allowed Roles', value: guildConfig.allowedRolesForRequest || 'None configured', inline: false },
|
||||
{ name: 'Self-Assignable Roles', value: await this.formatAllowedRoles(interaction.guild, guildConfig), inline: false },
|
||||
])
|
||||
.setFooter({ text: 'Use /config commands to modify settings' });
|
||||
|
||||
@@ -158,11 +192,79 @@ module.exports = {
|
||||
break;
|
||||
|
||||
case 'roles':
|
||||
const rolePattern = interaction.options.getString('pattern');
|
||||
newConfig.allowedRolesForRequest = rolePattern || null;
|
||||
updateMessage = rolePattern
|
||||
? `Self-assignable roles set to: \`${rolePattern}\``
|
||||
: 'Self-assignable roles cleared';
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
182
src/commands/slash/role.js
Normal file
182
src/commands/slash/role.js
Normal file
@@ -0,0 +1,182 @@
|
||||
const { SlashCommandBuilder, EmbedBuilder, MessageFlags } = require('discord.js');
|
||||
const configManager = require('../../config/config');
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('role')
|
||||
.setDescription('Manage your roles')
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('add')
|
||||
.setDescription('Add a role to yourself')
|
||||
.addRoleOption(option =>
|
||||
option.setName('role')
|
||||
.setDescription('The role to add')
|
||||
.setRequired(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('remove')
|
||||
.setDescription('Remove a role from yourself')
|
||||
.addRoleOption(option =>
|
||||
option.setName('role')
|
||||
.setDescription('The role to remove')
|
||||
.setRequired(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('list')
|
||||
.setDescription('Show available self-assignable roles')
|
||||
),
|
||||
|
||||
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]
|
||||
});
|
||||
}
|
||||
|
||||
// Get allowed role IDs for this guild
|
||||
const allowedRoleIds = databaseService.getAllowedRoleIds(interaction.guild.id);
|
||||
|
||||
if (subcommand === 'list') {
|
||||
if (allowedRoleIds.length === 0) {
|
||||
return interaction.reply({
|
||||
content: '❌ No roles are currently available for self-assignment on this server.',
|
||||
flags: [MessageFlags.Ephemeral]
|
||||
});
|
||||
}
|
||||
|
||||
// Get role objects from IDs
|
||||
const roles = [];
|
||||
for (const roleId of allowedRoleIds) {
|
||||
try {
|
||||
const role = await interaction.guild.roles.fetch(roleId);
|
||||
if (role) {
|
||||
roles.push(role);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Role ${roleId} not found in guild ${interaction.guild.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (roles.length === 0) {
|
||||
return interaction.reply({
|
||||
content: '❌ No valid self-assignable roles found. The configured roles may have been deleted.',
|
||||
flags: [MessageFlags.Ephemeral]
|
||||
});
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('📋 Available Self-Assignable Roles')
|
||||
.setDescription('You can add or remove these roles using `/role add` or `/role remove`:')
|
||||
.setColor(0x21c629)
|
||||
.addFields({
|
||||
name: 'Available Roles',
|
||||
value: roles.map(role => `• ${role}`).join('\n'),
|
||||
inline: false
|
||||
})
|
||||
.setFooter({ text: 'Use /role add or /role remove to manage your roles' });
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [embed],
|
||||
flags: [MessageFlags.Ephemeral]
|
||||
});
|
||||
}
|
||||
|
||||
// Handle add/remove subcommands
|
||||
const targetRole = interaction.options.getRole('role');
|
||||
|
||||
// Check if the role is in the allowed list
|
||||
if (!allowedRoleIds.includes(targetRole.id)) {
|
||||
return interaction.reply({
|
||||
content: `❌ **${targetRole.name}** is not available for self-assignment. Use \`/role list\` to see available roles.`,
|
||||
flags: [MessageFlags.Ephemeral]
|
||||
});
|
||||
}
|
||||
|
||||
// Check if bot can manage this role
|
||||
if (!interaction.guild.members.me.permissions.has('ManageRoles') ||
|
||||
targetRole.position >= interaction.guild.members.me.roles.highest.position) {
|
||||
return interaction.reply({
|
||||
content: `❌ I don't have permission to manage the **${targetRole.name}** role. Please contact an administrator.`,
|
||||
flags: [MessageFlags.Ephemeral]
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
if (subcommand === 'add') {
|
||||
// Check if user already has the role
|
||||
if (interaction.member.roles.cache.has(targetRole.id)) {
|
||||
return interaction.reply({
|
||||
content: `❌ You already have the **${targetRole.name}** role.`,
|
||||
flags: [MessageFlags.Ephemeral]
|
||||
});
|
||||
}
|
||||
|
||||
await interaction.member.roles.add(targetRole, 'User requested via slash command');
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('✅ Role Added')
|
||||
.setDescription(`Successfully added the **${targetRole.name}** role to your account.`)
|
||||
.setColor(0x00ff00)
|
||||
.setFooter({ text: 'Use /role remove to remove roles' });
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [embed],
|
||||
flags: [MessageFlags.Ephemeral]
|
||||
});
|
||||
|
||||
console.log(`Added role ${targetRole.name} to ${interaction.user.username} in ${interaction.guild.name}`);
|
||||
|
||||
} else if (subcommand === 'remove') {
|
||||
// Check if user has the role
|
||||
if (!interaction.member.roles.cache.has(targetRole.id)) {
|
||||
return interaction.reply({
|
||||
content: `❌ You don't have the **${targetRole.name}** role.`,
|
||||
flags: [MessageFlags.Ephemeral]
|
||||
});
|
||||
}
|
||||
|
||||
await interaction.member.roles.remove(targetRole, 'User requested via slash command');
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('✅ Role Removed')
|
||||
.setDescription(`Successfully removed the **${targetRole.name}** role from your account.`)
|
||||
.setColor(0x00ff00)
|
||||
.setFooter({ text: 'Use /role add to add roles' });
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [embed],
|
||||
flags: [MessageFlags.Ephemeral]
|
||||
});
|
||||
|
||||
console.log(`Removed role ${targetRole.name} from ${interaction.user.username} in ${interaction.guild.name}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error managing role ${targetRole.name}:`, error);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('❌ Role Management Error')
|
||||
.setDescription(`I encountered an error managing the **${targetRole.name}** role. Please contact an administrator.`)
|
||||
.setColor(0xff0000)
|
||||
.addFields({
|
||||
name: 'Possible Issues',
|
||||
value: '• Bot lacks Manage Roles permission\n• Role is higher than bot\'s highest role\n• Role is managed by an integration',
|
||||
inline: false
|
||||
});
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [embed],
|
||||
flags: [MessageFlags.Ephemeral]
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user