Add Reaction Roles sample command

This commit is contained in:
Michael 2023-09-13 08:42:23 -04:00
parent 8650468bd9
commit c6379bee9a

521
commands/reaction-roles.js Normal file
View file

@ -0,0 +1,521 @@
/*
* Konpeki Discord Bot - Slash Command Definition File
* reaction-roles.js - A utility for adding reaction roles via the Select Menu component
* See https://discord.com/developers/docs/interactions/message-components#select-menus
*
* PLEASE NOTE: This code is a certified mess. It is readable to someone with some knowledge, but be warned this is not a great template for new commands to be based on.
*/
const { ChannelSelectMenuBuilder, RoleSelectMenuBuilder, SlashCommandBuilder, ActionRowBuilder, ChannelType, ButtonBuilder, EmbedBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } = require('discord.js');
const wait = require('node:timers/promises').setTimeout;
let nullResponse, nullCheckResponse, channelID, roleArray, embedTitle, embedDescription, embedColor, embedFooter;
// The following is a list of all possible content embeds the bot will go through when setting up reaction roles.
// Despite the variable name, this isn't an array. It's easy to refer to it that way however.
// For the actual code, scroll down to "module.exports".
// For a list of component rows, scroll down to "rowArray"
// For a list of components, scroll down to "componentArray"
const embedArray = {
'interactionStart':
// This is the introduction embed. There will be commented out unused embed functions so that this may be referenced for other embeds.
new EmbedBuilder()
.setColor(0x00FFFF)
.setTitle('Reaction Role Setup Wizard')
// .setURL('https://discord.js.org/')
// .setAuthor({ name: 'Some name', iconURL: 'https://i.imgur.com/AfFp7pu.png', url: 'https://discord.js.org' })
.setDescription('Welcome to the Reaction Role Setup Wizard!')
// .setThumbnail('https://i.imgur.com/AfFp7pu.png')
.addFields(
{ name: 'Using this wizard', value: 'Throughout this setup wizard, you will be asked to fill out information. After completing a page, hit the Next button to continue.\nYou can also return to a previous page with the Back Button. Using this button will reset the page you were on, and the page you moved to, so only use it if you need to.' },
// { name: '\u200B', value: '\u200B' },
// { name: 'Inline field title', value: 'Some value here', inline: true },
{ name: 'Please Note', value: 'There is a theoretical maximum of 15 minutes to finish setup.\nAfter 15 minutes from use of the command, this interaction will stop working.', inline: true },
),
// .addFields({ name: 'Inline field title', value: 'Some value here', inline: true })
// .setImage('https://i.imgur.com/AfFp7pu.png')
// .setTimestamp()
// .setFooter({ text: 'Some footer text here', iconURL: 'https://i.imgur.com/AfFp7pu.png' }),
'channelSelect':
new EmbedBuilder()
.setColor(0x00FFFF)
.setTitle('Reaction Role Setup Wizard')
.setDescription('Please choose the channel to create the reaction role message in.'),
'roleSelect':
new EmbedBuilder()
.setColor(0x00FFFF)
.setTitle('Reaction Role Setup Wizard')
.setDescription('Please choose the roles for the reaction message.\nPlease note: There is a hard limit of 25 roles per message. This is a Discord limitation.'),
'preEmbedModal':
new EmbedBuilder()
.setColor(0x00FFFF)
.setTitle('Reaction Role Setup Wizard')
.setDescription('A popup will appear on the next page. Please fill out the information requested.\nIf you accidently close the popup, come back to this page then click Next again to reopen it.'),
'embedModal':
new EmbedBuilder()
.setColor(0x00FFFF)
.setTitle('Reaction Role Setup Wizard')
.setDescription('A popup should have appeared. Please fill out the information requested.\nIf you accidently closed the popup, go back a page then click Next again to reopen it.'),
'postEmbedModal':
new EmbedBuilder()
.setColor(0x00FFFF)
.setTitle('Reaction Role Setup Wizard')
.setDescription('If you accidently closed the popup, return now. Otherwise, continue.'),
'finalStatus':
new EmbedBuilder()
.setColor(0x00FFFF)
.setTitle('Reaction Role Setup Wizard')
.setDescription('Welcome to the Reaction Role Setup Wizard!')
.addFields(
{ name: 'Using this wizard', value: 'Throughout wizard, you will be asked to fill out information. After completing a page, hit the Next button to continue.\nYou can also return to a previous page with the Back Button. Using this button will reset the page you were on, and the page you moved to, so only use it if you need to.' },
{ name: 'Please Note', value: 'There is a theoretical maximum of 15 minutes to finish setup.\nAfter 15 minutes from use of the command, this interaction will stop working.', inline: true },
),
};
// The following is a list of all possible component rows the bot will go through when setting up reaction roles.
// Despite the variable name, this isn't an array. It's easy to refer to it that way however.
// For the actual code, scroll down to "module.exports".
// For a list of content embeds, scroll up to "embedArray"
// For a list of components, scroll down to "componentArray"
const rowArray = {
'introButtonRow':
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId('nextPage')
.setLabel('Begin')
.setStyle('Primary'),
),
'navigationButtonRow':
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId('previousPage')
.setLabel('Back')
.setStyle('Danger'),
new ButtonBuilder()
.setCustomId('nextPage')
.setLabel('Next')
.setStyle('Primary'),
),
'modalNextButtonRow':
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId('previousPage')
.setLabel('Back')
.setStyle('Danger'),
new ButtonBuilder()
.setCustomId('nextPageWithModal')
.setLabel('Next')
.setStyle('Primary'),
),
'modalPreviousToFinishRow':
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId('previousPageWithModal')
.setLabel('Back')
.setStyle('Danger'),
new ButtonBuilder()
.setCustomId('finalPage')
.setLabel('Next')
.setStyle('Primary'),
),
'finalButtonRow':
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId('previousPage')
.setLabel('Back')
.setStyle('Danger'),
new ButtonBuilder()
.setCustomId('finishButton')
.setLabel('Finish')
.setStyle('Success'),
),
'channelListSelectRow':
new ActionRowBuilder()
.addComponents(
new ChannelSelectMenuBuilder({
custom_id: 'channelSelect',
max_values: 1,
min_values: 1,
channel_types: [ChannelType.GuildText],
}),
),
'roleListSelectRow':
new ActionRowBuilder()
.addComponents(
new RoleSelectMenuBuilder({
custom_id: 'roleSelect',
max_values: 25,
min_values: 1,
}),
),
'embedModal':
new ModalBuilder()
.setCustomId('embedModal')
.setTitle('Write a message to go alongside the roles')
.addComponents(
new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('embedTitle')
.setLabel('Title')
.setValue('Pick your roles!')
.setStyle(TextInputStyle.Short)
.setMaxLength(40)
.setRequired(true),
),
new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('embedDescription')
.setLabel('Description / Information / Funny Quip')
.setValue('Click the buttons below to be assigned the listed role.')
.setStyle(TextInputStyle.Paragraph)
.setMaxLength(1000)
.setRequired(true),
),
new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('embedColor')
.setLabel('Accent Color (Optional) - Use hexcodes, no #.')
.setValue('00FFFF')
.setStyle(TextInputStyle.Short)
.setMinLength(6)
.setMaxLength(6)
.setRequired(false),
),
new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId('embedFooter')
.setLabel('Footer (Optional)')
.setStyle(TextInputStyle.Short)
.setMaxLength(40)
.setRequired(false),
),
),
};
// The following is a list of all possible pages the bot will go through when setting up reaction roles.
// Despite the variable name, this isn't an array. It's easy to refer to it that way however.
// For the actual code, scroll down to "module.exports".
// For a list of content embeds, scroll up to "embedArray"
// For a list of component rows, scroll up to "rowArray"
const componentArray = {
'interactionStart': {
'content': '',
'embeds': [embedArray.interactionStart],
'rows': [rowArray.introButtonRow],
'files': [],
},
'channelSelect': {
'content': '',
'embeds': [embedArray.channelSelect],
'rows': [rowArray.channelListSelectRow, rowArray.navigationButtonRow],
'files': [],
},
'roleSelect': {
'content': '',
'embeds': [embedArray.roleSelect],
'rows': [rowArray.roleListSelectRow, rowArray.navigationButtonRow],
'files': [],
},
'preEmbedModal': {
'content': '',
'embeds': [embedArray.preEmbedModal],
'rows': [rowArray.modalNextButtonRow],
'files': [],
},
'embedModal': {
'content': '',
'embeds': [embedArray.embedModal],
'rows': [rowArray.navigationButtonRow],
'files': [],
},
'postEmbedModal': {
'content': '',
'embeds': [embedArray.postEmbedModal],
'rows': [rowArray.modalPreviousToFinishRow],
'files': [],
},
'finalStatus': {
'content': '\nPlease Note: If *any* required items are incomplete, creating the reaction roles will silently fail. Ye be warned!',
'embeds': [],
'rows': [rowArray.finalButtonRow],
'files': [],
},
};
// This is a full list of all the pages to navigate through. Needs to match names in componentArray.
const pages = [componentArray.interactionStart, componentArray.channelSelect, componentArray.roleSelect, componentArray.preEmbedModal, componentArray.embedModal, componentArray.postEmbedModal, componentArray.finalStatus];
// This is a full list of all the modals to navigate through. Needs to match names in componentArray.
const modals = [null, null, null, null, rowArray.embedModal, null, null];
// This is the actual logic that runs the reaction role creation script.
module.exports = {
data: new SlashCommandBuilder()
.setName('reaction-roles')
.setDescription('Setup Reaction Roles in a Channel'),
async execute(interaction) {
let currPage = 0;
const component = pages[currPage];
const initiatingUser = interaction.user.id;
const response = await interaction.reply({
content: component.content,
embeds: component.embeds,
components: component.rows,
files: component.files,
ephemeral: true,
});
const filter = i => i.user.id === initiatingUser;
const collector = response.createMessageComponentCollector({ filter, time: 900_000 });
collector.on('collect', async i => {
if (i.customId === 'nextPage') {
nullResponse = await this.nullCheck(currPage);
currPage = nullResponse[0];
i.deferUpdate();
this.editReply(interaction, pages[currPage], nullResponse[1]);
}
if (i.customId === 'nextPageWithModal') {
currPage += 1;
i.showModal(modals[currPage]);
this.editReply(interaction, pages[currPage], '');
i.awaitModalSubmit({ time: 900_000, filter })
.then(async modalInteraction => {
embedTitle = modalInteraction.fields.getTextInputValue('embedTitle');
embedDescription = modalInteraction.fields.getTextInputValue('embedDescription');
embedColor = modalInteraction.fields.getTextInputValue('embedColor');
embedFooter = modalInteraction.fields.getTextInputValue('embedFooter');
modalInteraction.reply({ content: 'Submitted! This message should disappear shortly.', ephemeral: true });
await wait(2000);
modalInteraction.deleteReply();
})
// Cheat way of not logging errors when the modal expires
.catch(() => null);
}
if (i.customId === 'previousPage') {
currPage -= 1;
i.deferUpdate();
this.editReply(interaction, pages[currPage], '');
}
if (i.customId === 'previousPageWithModal') {
currPage -= 1;
i.showModal(modals[currPage]);
this.editReply(interaction, pages[currPage], '');
i.awaitModalSubmit({ time: 900_000, filter })
.then(async modalInteraction => {
embedTitle = modalInteraction.fields.getTextInputValue('embedTitle');
embedDescription = modalInteraction.fields.getTextInputValue('embedDescription');
embedColor = modalInteraction.fields.getTextInputValue('embedColor');
embedFooter = modalInteraction.fields.getTextInputValue('embedFooter');
modalInteraction.reply({ content: 'Submitted! This message should disappear shortly.', ephemeral: true });
await wait(2000);
modalInteraction.deleteReply();
})
// Cheat way of not logging errors when the modal expires
.catch(() => null);
}
if (i.customId === 'finalPage') {
currPage += 1;
i.deferUpdate();
nullCheckResponse = await this.finalNullCheck();
this.editReply(interaction, pages[currPage], 'You\'ve reached the last page!\nBelow is a status readout. Please ensure all non-optional items are marked Green. If a required item is marked Red, go back and start from that page again.\n\n' + nullCheckResponse);
}
if (i.customId === 'finishButton') {
i.deferUpdate();
interaction.deleteReply();
this.createReactRoleMessage(interaction);
}
// Second Page: Determine the channel for the role message
if (i.customId === 'channelSelect') {
channelID = i.values[0];
i.deferUpdate();
}
// Third Page: Determine the roles for the message
if (i.customId === 'roleSelect') {
roleArray = i.values;
i.deferUpdate(interaction);
}
});
},
async editReply(interaction, component, additionalContent) {
await interaction.editReply({
content: additionalContent + component.content,
embeds: component.embeds,
components: component.rows,
files: component.files,
});
},
async nullCheck(currentPage) {
switch (currentPage) {
case 1:
if (!channelID) {
return [currentPage, 'Please enter a response! If you have, please wait a second and try again.\n'];
}
break;
case 2:
if (!roleArray) {
return [currentPage, 'Please enter a response! If you have, please wait a second and try again.\n'];
}
break;
case 4:
if (!embedTitle) {
return [currentPage, 'Please enter a response to both required questions! If you have, please wait a second and try again.\n'];
}
if (!embedDescription) {
return [currentPage, 'Please enter a response to both required questions! If you have, please wait a second and try again.\n'];
}
break;
default:
return [currentPage + 1, ''];
}
return [currentPage + 1, ''];
},
async finalNullCheck() {
let content = ':green_circle: - No Problems\n:red_circle: - Missing Information\n\n';
if (!channelID) {
content += ':red_circle:';
}
else {
content += ':green_circle:';
}
content += ' Channel to Send Message In\n';
if (!roleArray) {
content += ':red_circle:';
}
else {
content += ':green_circle:';
}
content += ' Roles to Allow Self-Assignment\n';
if (!embedTitle) {
content += ':red_circle:';
}
else {
content += ':green_circle:';
}
content += ' Title for the Message\n';
if (!embedDescription) {
content += ':red_circle:';
}
else {
content += ':green_circle:';
}
content += ' Description for the Message\n';
if (!embedColor) {
content += ':red_circle:';
}
else {
content += ':green_circle:';
}
content += ' Color for the Message (Optional)\n';
if (!embedFooter) {
content += ':red_circle:';
}
else {
content += ':green_circle:';
}
content += ' Footer for the Message (Optional)\n';
return content;
},
async getButtonLayout(numberOfRoles) {
const rowCount = (numberOfRoles / 5) >> 0;
const lesserButtonCount = numberOfRoles % 5;
// -1 allows for the last row to be custom made to support the non full row count
return [rowCount - 1, lesserButtonCount];
},
async createReactRoleMessage(interaction) {
const client = interaction.client;
const guild = await client.guilds.fetch(interaction.guildId);
const embedChannel = client.channels.cache.get(channelID);
const buttonCount = await this.getButtonLayout(roleArray.length);
const reactionEmbed = new EmbedBuilder()
.setColor(embedColor)
.setTitle(embedTitle)
// .setAuthor({ name: 'Some name', iconURL: 'https://i.imgur.com/AfFp7pu.png', url: 'https://discord.js.org' })
.setDescription(embedDescription);
if (embedFooter) {
reactionEmbed.setFooter({ text: embedFooter });
}
const rowComponents = [];
let rows = 0;
let remainder = 0;
while (rows <= buttonCount[0]) {
rowComponents.push(
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId('role-' + roleArray[0 + (5 * rows)])
.setLabel((await guild.roles.fetch(roleArray[0 + (5 * rows)])).name)
.setStyle('Primary'),
new ButtonBuilder()
.setCustomId('role-' + roleArray[1 + (5 * rows)])
.setLabel((await guild.roles.fetch(roleArray[1 + (5 * rows)])).name)
.setStyle('Primary'),
new ButtonBuilder()
.setCustomId('role-' + roleArray[2 + (5 * rows)])
.setLabel((await guild.roles.fetch(roleArray[2 + (5 * rows)])).name)
.setStyle('Primary'),
new ButtonBuilder()
.setCustomId('role-' + roleArray[3 + (5 * rows)])
.setLabel((await guild.roles.fetch(roleArray[3 + (5 * rows)])).name)
.setStyle('Primary'),
new ButtonBuilder()
.setCustomId('role-' + roleArray[4 + (5 * rows)])
.setLabel((await guild.roles.fetch(roleArray[4 + (5 * rows)])).name)
.setStyle('Primary'),
),
);
rows += 1;
}
const reminaderRow = new ActionRowBuilder();
while (remainder < buttonCount[1]) {
reminaderRow.addComponents(
new ButtonBuilder()
.setCustomId('role-' + roleArray[remainder + (5 * rows)])
.setLabel((await guild.roles.fetch(roleArray[remainder + (5 * rows)])).name)
.setStyle('Primary'),
),
remainder += 1;
}
rowComponents.push(reminaderRow);
embedChannel.send({
content: '',
embeds: [reactionEmbed],
components: rowComponents,
files: [],
});
},
};