diff --git a/commands/vote.js b/commands/vote.js index 3541f43d..848c7c41 100644 --- a/commands/vote.js +++ b/commands/vote.js @@ -1,240 +1,200 @@ const Discord = require('discord.js'); -const getFeedback = require('./getFeedback'); -const ErrorLogger = require('../lib/logError'); +const { getFeedback } = require('./getFeedback'); const voteConfigurationTemplates = require('../data/voteConfiguration.json'); +const SlashArgType = require('discord-api-types/v10').ApplicationCommandOptionType; +const { slashArg, slashCommandJSON } = require('../utils.js'); +const { createReactionRow } = require('../redis.js'); module.exports = { name: 'vote', role: 'headrl', - args: ' [ (user2) (user3)...]', requiredArgs: 2, description: 'Puts up a vote for the person based on your role input', - async execute(message, args, bot, db) { - const voteModule = new Vote(message, args, bot, db); - await voteModule.startProcess(); - } -}; - -class Vote { - /** - * @param {Discord.Message} message - * @param {Array} args - * @param {Discord.Client} bot - * @param {import('mysql').Connection} db - */ - - constructor(message, args, bot, db) { - // Basic assignments from parameters - this.message = message; - this.args = args; - this.bot = bot; - this.db = db; - - // Guild and member-related assignments - this.guild = message.guild; - this.member = message.member || {}; - this.channel = message.channel || {}; - this.settings = bot.settings[this.guild.id] || {}; - this.emojiDatabase = bot.storedEmojis || {}; - - // Role and template initializations - this.roleType = args.shift() || ''; - this.guildVoteTemplates = null; - this.template = null; - this.embedStyling = { color: null, image: null }; - this.voteConfigurationTemplates = voteConfigurationTemplates; - - this.emojis = ['✅', '❌', '👀']; - this.voteConfiguration = { - channel: this.channel, + args: [ + slashArg(SlashArgType.Role, 'role', { + description: 'The role that the vote is for' + }), + slashArg(SlashArgType.String, 'users', { + description: 'The users (space seperated) that will be up for a vote' + }), + ], + varargs: true, + getSlashCommandData(guild) { return slashCommandJSON(this, guild); }, + async execute(message, args, bot) { + const { guild, member, channel } = message; + + const voteConfiguration = new VoteConfiguration({ + channel, + role: message.options.getRole('role'), maximumFeedbacks: 5, - members: this.args.map(member => this.guild.findMember(member)).filter(member => member != undefined), - role: this.guild.findRole(this.roleType) - }; - } - - async startProcess() { - if (!this.voteConfiguration.role) { return await this.channel.send(`Could not find role \`${this.roleType}\``); } - if (this.voteConfiguration.members.length == 0) { return await this.channel.send('No members found.'); } - - this.getVoteConfigurationButtons(); - await this.message.delete(); - await this.sendConfirmationMessage(); - await this.updateConfirmationMessage(); - } - - async sendConfirmationMessage() { - this.embed = new Discord.EmbedBuilder() - .setColor(this.member.roles.highest.hexColor) - .setAuthor({ name: 'Vote Configuration', iconURL: this.member.user.displayAvatarURL({ dynamic: true }) }) - .setDescription('Loading...'); - this.voteConfigurationMessage = await this.channel.send({ embeds: [this.embed], components: [this.getVoteConfigurationButtons()] }); - - this.voteConfigurationMessageInteractionCollector = new Discord.InteractionCollector(this.bot, { message: this.voteConfigurationMessage, interactionType: Discord.InteractionType.MessageComponent, componentType: Discord.ComponentType.Button }); - this.voteConfigurationMessageInteractionCollector.on('collect', async (interaction) => await this.interactionHandler(interaction)); - } - - async updateConfirmationMessage() { - const voteConfigurationDescription = this.getVoteConfigurationDescription(); - this.embed = new Discord.EmbedBuilder() - .setColor(this.member.roles.highest.hexColor) - .setAuthor({ name: 'Vote Configuration', iconURL: this.member.user.displayAvatarURL({ dynamic: true }) }) - .setDescription(voteConfigurationDescription); - await this.voteConfigurationMessage.edit({ embeds: [this.embed], components: [this.getVoteConfigurationButtons()] }); - } - - getVoteConfigurationDescription() { - return ` - This vote will be for ${this.voteConfiguration.role}, inside of ${this.voteConfiguration.channel} - ${this.emojiDatabase.feedback.text} \`${this.voteConfiguration.maximumFeedbacks}\` - - ## Leaders - ${this.voteConfiguration.members.join(', ')} - `; - } - - getVoteConfigurationButtons() { - return new Discord.ActionRowBuilder() - .addComponents([ - new Discord.ButtonBuilder() - .setLabel('✅ Confirm') - .setStyle(3) - .setCustomId('voteConfirm'), - new Discord.ButtonBuilder() - .setLabel('Feedbacks') - .setStyle(2) - .setEmoji(this.emojiDatabase.feedback.id) - .setCustomId('voteFeedbackConfigure'), - new Discord.ButtonBuilder() - .setLabel('# Channel') - .setStyle(2) - .setCustomId('voteChannelConfigure'), - new Discord.ButtonBuilder() - .setLabel('@ Role') - .setStyle(2) - .setCustomId('voteRoleConfigure'), - new Discord.ButtonBuilder() - .setLabel('❌ Cancel') - .setStyle(4) - .setCustomId('voteCancel') - ]); - } - - async interactionHandler(interaction) { - if (interaction.member.id != this.member.id) { - return await interaction.reply({ content: 'You are not permitted to configure this', ephemeral: true }); - } + members: message.options.getString('users').split(' ').concat(message.options.getVarargs() || []).map(member => guild.findMember(member)).filter(member => member != undefined) + }); - switch (interaction.customId) { + if (voteConfiguration.members.length == 0) { return await message.reply('No members found.'); } + + const embed = voteConfiguration.confirmationMessage(member, bot.storedEmojis); + const voteConfigurationButtons = generateVoteConfigurationButtons(bot); + const voteConfigurationMessage = await message.reply({ embeds: [embed], components: [voteConfigurationButtons] }); + createReactionRow(voteConfigurationMessage, module.exports.name, 'interactionHandler', voteConfigurationButtons, message.author, voteConfiguration.toJSON()); + }, + async interactionHandler(bot, message, db, choice, voteConfigurationJSON, updateState) { + const emojiDatabase = bot.storedEmojis || {}; + const interaction = message.interaction; // eslint-disable-line prefer-destructuring + const member = interaction.member; // eslint-disable-line prefer-destructuring + const voteConfiguration = VoteConfiguration.fromJSON(interaction.guild, voteConfigurationJSON); + switch (choice) { case 'voteConfirm': - await this.buttonVoteConfirm(interaction); + await message.delete(); + Promise.all(voteConfiguration.members.map(async memberId => { + const member = await interaction.guild.members.fetch(memberId); + const feedbacks = await getFeedback(member, interaction.guild, bot); + await startVote(bot.settings[interaction.guild.id], voteConfiguration, member, feedbacks); + })); break; case 'voteCancel': - await this.buttonVoteCancel(interaction); + await message.delete(); break; - case 'voteFeedbackConfigure': - await this.buttonVoteFeedbackConfigure(interaction); + case 'voteFeedbackConfigure': { + const confirmationMessage = await interaction.reply({ embeds: [voteConfiguration.getEmbed(member, 'Choose how many feedbacks you want ViBot to look through')], fetchReply: true }); + const choice = await confirmationMessage.confirmNumber(10, interaction.member.id); + await confirmationMessage.delete(); + if (!choice || isNaN(choice) || choice == 'Cancelled') return; + voteConfiguration.maximumFeedbacks = choice; + await updateState('maximumFeedbacks', choice); + await interaction.message.edit({ embeds: [voteConfiguration.confirmationMessage(member, emojiDatabase)], components: [generateVoteConfigurationButtons(bot)] }); break; - case 'voteChannelConfigure': - await this.buttonVoteChannelConfigure(interaction); + } + case 'voteChannelConfigure': { + await interaction.update({ embeds: [voteConfiguration.getEmbed(member, 'Type a different channel for the vote to be put up in')] }); + const channelMessage = await interaction.channel.next(null, null, member.id); + const channel = await interaction.guild.findChannel(channelMessage.content); + if (channel) { + voteConfiguration.channel = channel; + await updateState('channel', channel.id); + } else { + await interaction.channel.send('Invalid channel. Please type the name of a channel.'); + } + await interaction.message.edit({ embeds: [voteConfiguration.confirmationMessage(member, emojiDatabase)], components: [generateVoteConfigurationButtons(bot)] }); break; - case 'voteRoleConfigure': - await this.buttonVoteRoleConfigure(interaction); + } + case 'voteRoleConfigure': { + await interaction.update({ embeds: [voteConfiguration.getEmbed(member, 'Type a different role for the vote')] }); + const roleMessage = await interaction.channel.next(null, null, member.id); + const role = await interaction.guild.findRole(roleMessage.content); + if (role) { + voteConfiguration.role = role; + await updateState('role', role.id); + } else { + await interaction.channel.send('Invalid role. Please type the name of a role.'); + } + await interaction.message.edit({ embeds: [voteConfiguration.confirmationMessage(member, emojiDatabase)], components: [generateVoteConfigurationButtons(bot)] }); break; + } default: - this.channel.send('How?'); + console.log('Invalid choice', choice); break; } } +}; - async endVoteConfigurationPhase(interaction) { - await interaction.message.delete(); - this.voteConfigurationMessageInteractionCollector.stop(); - } - - async buttonVoteConfirm(interaction) { - try { - await interaction.reply({ content: 'The votes will be put up', ephemeral: true }); - await this.endVoteConfigurationPhase(interaction); - this.getEmbedStyling(); - Promise.all(this.voteConfiguration.members.map(async member => { - const feedbacks = await getFeedback.getFeedback(member, this.guild, this.bot); - await this.startVote(member, feedbacks); - })); - } catch (error) { - ErrorLogger.log(error, this.bot, this.guild); - } +class VoteConfiguration { + constructor({ channel, maximumFeedbacks, members, role }) { + this.channel = channel; + this.maximumFeedbacks = maximumFeedbacks; + this.members = members; + this.role = role; } - async startVote(member, feedbacks) { - let embedColor = this.voteConfiguration.role.hexColor; - if (this.embedStyling != undefined && this.embedStyling.embedColor) { embedColor = this.embedStyling.embedColor; } - this.embed = new Discord.EmbedBuilder() - .setColor(embedColor) - .setAuthor({ name: `${member.displayName} to ${this.voteConfiguration.role.name}`, iconURL: member.user.displayAvatarURL({ dynamic: true }) }) - .setDescription(`${member} \`${member.displayName}\``); - if (this.embedStyling != undefined && this.embedStyling.image) { this.embed.setThumbnail(this.embedStyling.image); } - this.embed.addFields({ - name: 'Recent Feedback:', - value: `${feedbacks.length != 0 ? `${feedbacks.slice( - 0, this.voteConfiguration.maximumFeedbacks).map( - (feedback, index) => `\`${(index + 1).toString().padStart(2, ' ')}\` ${feedback}`).join('\n')}` : 'None'}`, - inline: false + static fromJSON(guild, json) { + return new this({ + ...json, + channel: guild.channels.cache.get(json.channel), + members: json.members.map(memberId => guild.members.cache.get(memberId)), + role: guild.roles.cache.get(json.role) }); - const voteMessage = await this.voteConfiguration.channel.send({ embeds: [this.embed] }); - for (const emoji of this.emojis) { voteMessage.react(emoji); } } - getEmbedStyling() { - const settingRoleName = Object.keys(this.settings.roles) - .find(roleName => this.settings.roles[roleName] === this.voteConfiguration.role.id); - if (!settingRoleName) { return; } - this.embedStyling = this.voteConfigurationTemplates - .find(template => template.settingRole === settingRoleName); + toJSON() { + return { + channel: this.channel.id, + maximumFeedbacks: this.maximumFeedbacks, + members: this.members.map(m => m.id), + role: this.role.id + }; } - async buttonVoteCancel(interaction) { - await interaction.reply({ content: 'You have decided to cancel the votes', ephemeral: true }); - await this.endVoteConfigurationPhase(interaction); + getEmbed(member, description) { + return new Discord.EmbedBuilder() + .setColor(member.roles.highest.hexColor) + .setAuthor({ + name: 'Vote Configuration', + iconURL: member.user.displayAvatarURL({ dynamic: true }) + }) + .setDescription(description); } - async buttonVoteFeedbackConfigure(interaction) { - const embedFeedbackConfigure = this.getBaseEmbed(); - embedFeedbackConfigure.setDescription('Choose how many feedbacks you want ViBot to look through'); - const confirmationMessage = await interaction.reply({ embeds: [embedFeedbackConfigure], fetchReply: true }); - const choice = await confirmationMessage.confirmNumber(10, interaction.member.id); - if (!choice || isNaN(choice) || choice == 'Cancelled') return await confirmationMessage.delete(); - await confirmationMessage.delete(); - this.voteConfiguration.maximumFeedbacks = choice; - await this.updateConfirmationMessage(); - } + description(emojiDatabase) { + return ` + This vote will be for ${this.role}, inside of ${this.channel} + ${emojiDatabase.feedback.text} \`${this.maximumFeedbacks}\` - async buttonVoteChannelConfigure(interaction) { - const embedFeedbackConfigure = this.getBaseEmbed(); - embedFeedbackConfigure.setDescription('Type a different channel for the vote to be put up in'); - await interaction.update({ embeds: [embedFeedbackConfigure] }); - const configurationRepliedMessage = await interaction.channel.next(null, null, interaction.member.id); - const channel = await this.guild.findChannel(configurationRepliedMessage.content); - if (channel) { this.voteConfiguration.channel = channel; } - await this.updateConfirmationMessage(); + ## Leaders + ${this.members.join(', ')} + `; } - async buttonVoteRoleConfigure(interaction) { - const embedFeedbackConfigure = this.getBaseEmbed(); - embedFeedbackConfigure.setDescription('Type a different role for the vote'); - await interaction.update({ embeds: [embedFeedbackConfigure] }); - const configurationRepliedMessage = await interaction.channel.next(null, null, interaction.member.id); - const role = await this.guild.findRole(configurationRepliedMessage.content); - if (role) { this.voteConfiguration.role = role; } - await this.updateConfirmationMessage(); + confirmationMessage(member, emojiDatabase) { + return this.getEmbed(member, this.description(emojiDatabase)); } +} - getBaseEmbed() { - return new Discord.EmbedBuilder() - .setColor(this.member.roles.highest.hexColor) - .setAuthor({ - name: 'Vote Configuration', - iconURL: this.member.user.displayAvatarURL({ dynamic: true }) - }); - } +async function startVote(settings, voteConfiguration, member, feedbacks) { + const settingRoleName = Object.keys(settings.roles) + .find(roleName => settings.roles[roleName] === voteConfiguration.role.id); + if (!settingRoleName) { return; } + const embedStyling = voteConfigurationTemplates + .find(template => template.settingRole === settingRoleName); + let embedColor = voteConfiguration.role.hexColor; + if (embedStyling != undefined && embedStyling.embedColor) { embedColor = embedStyling.embedColor; } + const embed = new Discord.EmbedBuilder() + .setColor(embedColor) + .setAuthor({ name: `${member.displayName} to ${voteConfiguration.role.name}`, iconURL: member.user.displayAvatarURL({ dynamic: true }) }) + .setDescription(`${member} \`${member.displayName}\``); + if (embedStyling != undefined && embedStyling.image) { embed.setThumbnail(embedStyling.image); } + embed.addFields({ + name: 'Recent Feedback:', + value: `${feedbacks.length != 0 ? `${feedbacks.slice( + 0, voteConfiguration.maximumFeedbacks).map( + (feedback, index) => `\`${(index + 1).toString().padStart(2, ' ')}\` ${feedback}`).join('\n')}` : 'None'}`, + inline: false + }); + const voteMessage = await voteConfiguration.channel.send({ embeds: [embed] }); + for (const emoji of ['✅', '❌', '👀']) { voteMessage.react(emoji); } +} + +function generateVoteConfigurationButtons(bot) { + return new Discord.ActionRowBuilder() + .addComponents([ + new Discord.ButtonBuilder() + .setLabel('✅ Confirm') + .setStyle(3) + .setCustomId('voteConfirm'), + new Discord.ButtonBuilder() + .setLabel('Feedbacks') + .setStyle(2) + .setEmoji(bot.storedEmojis.feedback.id) + .setCustomId('voteFeedbackConfigure'), + new Discord.ButtonBuilder() + .setLabel('# Channel') + .setStyle(2) + .setCustomId('voteChannelConfigure'), + new Discord.ButtonBuilder() + .setLabel('@ Role') + .setStyle(2) + .setCustomId('voteRoleConfigure'), + new Discord.ButtonBuilder() + .setLabel('❌ Cancel') + .setStyle(4) + .setCustomId('voteCancel') + ]); } diff --git a/package.json b/package.json index 27decabb..14cfb73f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vibot", - "version": "8.10.5", + "version": "8.11.0", "description": "ViBot", "main": "index.js", "dependencies": { diff --git a/redis.js b/redis.js index 0b98c9d3..e873480b 100644 --- a/redis.js +++ b/redis.js @@ -42,20 +42,32 @@ module.exports = { }, async handleReactionRow(bot, interaction) { if (!(interaction instanceof Discord.ButtonInteraction)) return false; + const messageKey = `messagebuttons:${interaction.message.id}`; // eslint-disable-next-line new-cap - const data = await client.HGETALL('messagebuttons:' + interaction.message.id); + const data = await client.HGETALL(messageKey); if (!data.command || !data.callback) return false; if (data.allowedUser && data.allowedUser != interaction.user.id) return false; const command = bot.commands.get(data.command) || bot.commands.find(cmd => cmd.alias && cmd.alias.includes(data.command)); const callback = command[data.callback]; const db = getDB(interaction.guild.id); + const updateStateFunc = async (k, v) => { + // eslint-disable-next-line new-cap + await client.EVAL(` + local state = cjson.decode(redis.call('HGET', KEYS[1], 'state')) + state[ARGV[1]] = ARGV[2] + redis.call('HSET', KEYS[1], 'state', cjson.encode(state)) + `, { + keys: [messageKey], + arguments: [k, v] + }); + }; if (data.token) { const resp = new MockMessage(new Discord.InteractionWebhook(bot, data.whid, data.token), interaction); - await callback(bot, resp, db, interaction.customId, JSON.parse(data.state)); + await callback(bot, resp, db, interaction.customId, JSON.parse(data.state), updateStateFunc); } else { const resp = interaction.message; resp.interaction = interaction; - await callback(bot, resp, db, interaction.customId, JSON.parse(data.state)); + await callback(bot, resp, db, interaction.customId, JSON.parse(data.state), updateStateFunc); } return true; } diff --git a/utils.js b/utils.js index 9bd59407..466418ae 100644 --- a/utils.js +++ b/utils.js @@ -88,6 +88,10 @@ class LegacyCommandOptions { if (this.#optTypeMatch(SlashArgType.User, k)) return this.args[k]; } + getRole(k) { + if (this.#optTypeMatch(SlashArgType.Role, k)) return this.args[k]; + } + getInteger(k) { if (this.#optTypeMatch(SlashArgType.Integer, k)) return this.args[k]; } @@ -113,6 +117,11 @@ class LegacyCommandOptions { this.#users.push(member); return member; } + case SlashArgType.Role: { + const role = this.#message.guild.findRole(value); + if (!role) throw new LegacyParserError(`Role \`${value}\` not found`); + return role; + } case SlashArgType.String: return value; case SlashArgType.Integer: return parseInt(value); case SlashArgType.Attachment: return this.#attachments.shift();