From e9b3b630b6cfa049192f9b8ecac471ea01ca589d Mon Sep 17 00:00:00 2001 From: Chris Ham <431647+greenham@users.noreply.github.com> Date: Sat, 16 Aug 2025 21:05:46 -0700 Subject: [PATCH] Add smart contextual autocomplete for admin role management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add autocomplete to /config roles add/remove commands - /config roles add only shows roles not currently self-assignable - /config roles remove only shows roles currently self-assignable - Filter out bot roles, @everyone, and unmanageable roles - Convert role parameter to string with autocomplete for better UX - Add role name validation and lookup in config command Admin Experience Improvements: - Smart role suggestions prevent duplicate/invalid selections - No more seeing roles already in the self-assignable list - Only see roles the bot can actually manage - Clean, focused autocomplete interface 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/commands/slash/config.js | 77 ++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/src/commands/slash/config.js b/src/commands/slash/config.js index cfd5c76..1c57b28 100644 --- a/src/commands/slash/config.js +++ b/src/commands/slash/config.js @@ -112,10 +112,11 @@ module.exports = { { name: 'Show current roles', value: 'list' } ) ) - .addRoleOption(option => + .addStringOption(option => option.setName('role') .setDescription('The role to add or remove (not needed for list/clear)') .setRequired(false) + .setAutocomplete(true) ) ), @@ -193,7 +194,7 @@ module.exports = { case 'roles': const action = interaction.options.getString('action'); - const role = interaction.options.getRole('role'); + const roleName = interaction.options.getString('role'); if (action === 'list') { const allowedRoleIds = databaseService.getAllowedRoleIds(interaction.guild.id); @@ -240,13 +241,25 @@ module.exports = { break; } - if (!role) { + if (!roleName) { return interaction.reply({ content: '❌ You must specify a role for add/remove actions.', flags: [MessageFlags.Ephemeral] }); } + // Find the role by name + const role = interaction.guild.roles.cache.find(r => + r.name.toLowerCase() === roleName.toLowerCase() + ); + + if (!role) { + return interaction.reply({ + content: `❌ Role **${roleName}** not found on this server.`, + 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) { @@ -290,5 +303,63 @@ module.exports = { await interaction.reply({ embeds: [embed] }); console.log(`Configuration updated for ${interaction.guild.name}: ${subcommand} by @${interaction.user.username}`); + }, + + async autocomplete(interaction, guildConfig) { + const subcommand = interaction.options.getSubcommand(); + + // Only handle autocomplete for roles subcommand + if (subcommand !== 'roles') { + return interaction.respond([]); + } + + const action = interaction.options.getString('action'); + const focusedValue = interaction.options.getFocused().toLowerCase(); + const databaseService = configManager.databaseService; + + if (!databaseService) { + return interaction.respond([]); + } + + // Get currently allowed role IDs + const allowedRoleIds = databaseService.getAllowedRoleIds(interaction.guild.id); + let availableRoles = []; + + if (action === 'add') { + // For add: show roles NOT currently in the allowed list + const allRoles = interaction.guild.roles.cache + .filter(role => + !role.managed && // Exclude bot/integration roles + role.id !== interaction.guild.id && // Exclude @everyone + !allowedRoleIds.includes(role.id) && // Exclude already allowed roles + role.position < interaction.guild.members.me.roles.highest.position // Only manageable roles + ); + + availableRoles = Array.from(allRoles.values()); + + } else if (action === 'remove') { + // For remove: show roles currently in the allowed list + for (const roleId of allowedRoleIds) { + try { + const role = await interaction.guild.roles.fetch(roleId); + if (role) { + availableRoles.push(role); + } + } catch (error) { + // Role doesn't exist anymore, skip + } + } + } + + // Filter based on what user has typed and limit to 25 + const filtered = availableRoles + .filter(role => role.name.toLowerCase().includes(focusedValue)) + .slice(0, 25) + .map(role => ({ + name: role.name, + value: role.name + })); + + await interaction.respond(filtered); } }; \ No newline at end of file