mirror of
https://github.com/TheShadowEevee/Konpeki-Discord-Bot.git
synced 2025-01-11 14:38:49 -06:00
Add Reaction Roles sample command
This commit is contained in:
parent
8650468bd9
commit
c6379bee9a
1 changed files with 521 additions and 0 deletions
521
commands/reaction-roles.js
Normal file
521
commands/reaction-roles.js
Normal 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: [],
|
||||
});
|
||||
},
|
||||
};
|
Loading…
Reference in a new issue