const { PermissionFlagsBits, ApplicationCommandOptionType, ChannelType, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, } = require('discord.js') const roleMenus = require('../../model/roleMenus') // Capped at 5 roles per menu — a single Discord action row holds at most 5 // buttons, and one row keeps this a single simple slash command instead of // needing a multi-step builder/modal flow. const MAX_ROLES = 5 // role1/label1 are declared inline in `data` (ahead of the optional // `description` option, per Discord's required-before-optional rule) — this // generates the rest, all optional. function roleOptions(from, to) { const opts = [] for (let i = from; i <= to; i++) { opts.push({ name: `role${i}`, description: `Role #${i}`, type: ApplicationCommandOptionType.Role, required: false }) opts.push({ name: `label${i}`, description: `Button label for role #${i} (default: role name)`, type: ApplicationCommandOptionType.String, required: false }) } return opts } module.exports = { data: { name: 'rolemenu', description: 'Post a button menu for self-assignable roles (up to 5).', default_member_permissions: PermissionFlagsBits.ManageGuild.toString(), // Discord requires all required options before any optional ones across // the whole array — role1 (required) must come before description // (optional), even though they read more naturally in the other order. options: [ { name: 'channel', description: 'Channel to post the menu in', type: ApplicationCommandOptionType.Channel, channel_types: [ChannelType.GuildText], required: true }, { name: 'title', description: 'Menu title', type: ApplicationCommandOptionType.String, required: true }, { name: 'role1', description: 'Role #1', type: ApplicationCommandOptionType.Role, required: true }, { name: 'description', description: 'Menu description', type: ApplicationCommandOptionType.String, required: false }, { name: 'label1', description: 'Button label for role #1 (default: role name)', type: ApplicationCommandOptionType.String, required: false }, ...roleOptions(2, MAX_ROLES), ], }, async execute(interaction) { const channel = interaction.options.getChannel('channel', true) const title = interaction.options.getString('title', true) const description = interaction.options.getString('description') || undefined const entries = [] for (let i = 1; i <= MAX_ROLES; i++) { const role = interaction.options.getRole(`role${i}`) if (!role) continue const label = interaction.options.getString(`label${i}`) || role.name entries.push({ roleId: role.id, label }) } if (entries.length === 0) { await interaction.reply({ content: 'Provide at least one role (role1).', ephemeral: true }) return } const embed = new EmbedBuilder().setTitle(title).setColor(0x6a8fc2) if (description) embed.setDescription(description) const row = new ActionRowBuilder().addComponents( entries.map((e) => new ButtonBuilder().setCustomId(`rolemenu:${e.roleId}`).setLabel(e.label).setStyle(ButtonStyle.Secondary), ), ) const message = await channel.send({ embeds: [embed], components: [row] }) await roleMenus.add({ guildId: interaction.guildId, channelId: channel.id, messageId: message.id, mapping: entries, createdBy: interaction.user.id, }) await interaction.reply({ content: `Role menu posted in ${channel}.`, ephemeral: true }) }, }