From 0dac5a5e0794937d9c6c36098df7abc86c3a0d01 Mon Sep 17 00:00:00 2001 From: Huntifer Date: Mon, 29 Jan 2024 20:51:11 -0600 Subject: [PATCH 1/8] Merging Huntifer PR 672 Switch vote to use restart safe interactions; redis upgrades; legacycommandparser upgrades; and add vote slash command --- commands/vote.js | 382 +++++++++++++++++++++-------------------------- redis.js | 18 ++- utils.js | 9 ++ 3 files changed, 195 insertions(+), 214 deletions(-) diff --git a/commands/vote.js b/commands/vote.js index bb1247a1..019326c8 100644 --- a/commands/vote.js +++ b/commands/vote.js @@ -1,16 +1,98 @@ const Discord = require('discord.js'); -const ErrorLogger = require('../lib/logError'); 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(); + 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: message.options.getString('users').split(' ').concat(message.options.getVarargs() || []).map(member => guild.findMember(member)).filter(member => member != undefined) + }); + + 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 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 message.delete(); + break; + 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 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 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: + console.log('Invalid choice', choice); + break; + } } }; @@ -34,226 +116,104 @@ async function getFeedback(member, guild, bot) { return mentions; } -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, - maximumFeedbacks: 5, - members: this.args.map(member => this.guild.findMember(member)).filter(member => member != undefined), - role: this.guild.findRole(this.roleType) - }; +class VoteConfiguration { + constructor({ channel, maximumFeedbacks, members, role }) { + this.channel = channel; + this.maximumFeedbacks = maximumFeedbacks; + this.members = members; + this.role = role; } - 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(); + 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) + }); } - 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)); + toJSON() { + return { + channel: this.channel.id, + maximumFeedbacks: this.maximumFeedbacks, + members: this.members.map(m => m.id), + role: this.role.id + }; } - 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()] }); + getEmbed(member, description) { + return new Discord.EmbedBuilder() + .setColor(member.roles.highest.hexColor) + .setAuthor({ + name: 'Vote Configuration', + iconURL: member.user.displayAvatarURL({ dynamic: true }) + }) + .setDescription(description); } - getVoteConfigurationDescription() { + description(emojiDatabase) { return ` - This vote will be for ${this.voteConfiguration.role}, inside of ${this.voteConfiguration.channel} - ${this.emojiDatabase.feedback.text} \`${this.voteConfiguration.maximumFeedbacks}\` - + This vote will be for ${this.role}, inside of ${this.channel} + ${emojiDatabase.feedback.text} \`${this.maximumFeedbacks}\` + ## Leaders - ${this.voteConfiguration.members.join(', ')} + ${this.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 }); - } - - switch (interaction.customId) { - case 'voteConfirm': - await this.buttonVoteConfirm(interaction); - break; - case 'voteCancel': - await this.buttonVoteCancel(interaction); - break; - case 'voteFeedbackConfigure': - await this.buttonVoteFeedbackConfigure(interaction); - break; - case 'voteChannelConfigure': - await this.buttonVoteChannelConfigure(interaction); - break; - case 'voteRoleConfigure': - await this.buttonVoteRoleConfigure(interaction); - break; - default: - this.channel.send('How?'); - 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(member, this.guild, this.bot); - await this.startVote(member, feedbacks); - })); - } catch (error) { - ErrorLogger.log(error, this.bot, this.guild); - } - } - - 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 - }); - 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); - } - - async buttonVoteCancel(interaction) { - await interaction.reply({ content: 'You have decided to cancel the votes', ephemeral: true }); - await this.endVoteConfigurationPhase(interaction); - } - - 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(); - } - - 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(); + confirmationMessage(member, emojiDatabase) { + return this.getEmbed(member, this.description(emojiDatabase)); } +} - 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(); - } +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); } +} - getBaseEmbed() { - return new Discord.EmbedBuilder() - .setColor(this.member.roles.highest.hexColor) - .setAuthor({ - name: 'Vote Configuration', - iconURL: this.member.user.displayAvatarURL({ dynamic: true }) - }); - } +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/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(); From 02c858a0df43b6572024a477eabc1f5397e0c86d Mon Sep 17 00:00:00 2001 From: tro2 <62850247+tro2@users.noreply.github.com> Date: Mon, 19 Feb 2024 14:50:00 -0800 Subject: [PATCH 2/8] In progress vote changes Renamed voteConfiguration to voteConfig Added documentation within vote.js Added feedback type identification and filtering IN PROGRESS Reworking vote setup interactions --- commands/vote.js | 329 ++++++++++++++++++++++-------------- data/voteConfig.json | 118 +++++++++++++ data/voteConfiguration.json | 57 ------- 3 files changed, 323 insertions(+), 181 deletions(-) create mode 100644 data/voteConfig.json delete mode 100644 data/voteConfiguration.json diff --git a/commands/vote.js b/commands/vote.js index 019326c8..b0ac28bc 100644 --- a/commands/vote.js +++ b/commands/vote.js @@ -1,8 +1,36 @@ const Discord = require('discord.js'); -const voteConfigurationTemplates = require('../data/voteConfiguration.json'); +const voteConfig = require('../data/voteConfig.json'); const SlashArgType = require('discord-api-types/v10').ApplicationCommandOptionType; const { slashArg, slashCommandJSON } = require('../utils.js'); const { createReactionRow } = require('../redis.js'); +/** +* @typedef FeedbackType +* @property {string[]} flags +* @property {string} emoji +* @property {string[]} roles +*/ +// TODO clean up unused properties +/** + * @typedef FeedbackData + * @property {string} feedbackContent + * @property {FeedbackType} tier + * @property {FeedbackType} dungeon + * @property {string} firstLine + * @property {string} mentionedID + * @property {string} feedbackURL + * @property {string} feedbackerID + * @property {number} timeStamp + * @property {FeedbackState} displayState + */ + +/** + * @enum {number} + */ +const FeedbackState = { + Included: 1, + Unidentified: 0, + Other: -1 +}; module.exports = { name: 'vote', @@ -20,73 +48,62 @@ module.exports = { varargs: true, getSlashCommandData(guild) { return slashCommandJSON(this, guild); }, async execute(message, args, bot) { - const { guild, member, channel } = message; + const { guild, channel } = message; - const voteConfiguration = new VoteConfiguration({ + const voteSetup = new VoteSetup({ channel, + feedbacks: [], role: message.options.getRole('role'), - maximumFeedbacks: 5, members: message.options.getString('users').split(' ').concat(message.options.getVarargs() || []).map(member => guild.findMember(member)).filter(member => member != undefined) }); - if (voteConfiguration.members.length == 0) { return await message.reply('No members found.'); } + if (voteSetup.members.length == 0) { return await message.reply('No members found.'); } + await getFeedbacks(voteSetup.members, guild, bot, voteSetup); - 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()); + const embed = getSetupEmbed(voteSetup); + const voteSetupButtons = generateVoteSetupButtons(bot); + const voteSetupMessage = await message.reply({ embeds: [embed], components: [voteSetupButtons] }); + createReactionRow(voteSetupMessage, module.exports.name, 'interactionHandler', voteSetupButtons, message.author, voteSetup.toJSON()); }, - async interactionHandler(bot, message, db, choice, voteConfigurationJSON, updateState) { - const emojiDatabase = bot.storedEmojis || {}; + async interactionHandler(bot, message, db, choice, voteSetupJSON, updateState) { 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); + const voteSetup = VoteSetup.fromJSON(interaction.guild, voteSetupJSON); switch (choice) { - case 'voteConfirm': - 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); - })); + case 'voteSend': + await startVote(bot.settings[interaction.guild.id], voteSetup, member); + // TODO finish the interactions + // move index to next member + // if last member in array, delete the message break; - case 'voteCancel': - await message.delete(); + case 'voteSkip': + // move index to next member + // if last member in array, delete the message break; - 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)] }); + case 'voteAbort': + await message.delete(); break; - } - 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)] }); + case 'voteAddFeedbacks': { + // const confirmationMessage = await interaction.reply({ embeds: [voteSetup.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; + // voteSetup.maximumFeedbacks = choice; + // await updateState('maximumFeedbacks', choice); + // await interaction.message.edit({ embeds: [voteSetup.confirmationMessage(member, emojiDatabase)], components: [generateVoteSetupButtons(bot)] }); break; } - 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)] }); + case 'voteRemoveFeedbacks': { + // await interaction.update({ embeds: [voteSetup.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) { + // voteSetup.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: [voteSetup.confirmationMessage(member, emojiDatabase)], components: [generateVoteSetupButtons(bot)] }); break; } default: @@ -96,30 +113,23 @@ module.exports = { } }; -async function getMessages(channel, limit) { - const sumMessages = []; - for (let i = 0; i <= limit; i += 100) { - const options = { limit: 100, before: i > 0 ? sumMessages[sumMessages.length - 1].id : null }; - // eslint-disable-next-line no-await-in-loop - const messages = await channel.messages.fetch(options); - sumMessages.push(...messages.map(m => m)); - if (messages.size != 100) break; - } - return sumMessages; -} - -async function getFeedback(member, guild, bot) { - const settings = bot.settings[member.guild.id]; - const feedbackChannel = guild.channels.cache.get(settings.channels.rlfeedback); - const messages = await getMessages(feedbackChannel, 500); - const mentions = messages.filter(m => m.mentions.users.get(member.id)).map(m => m.url); - return mentions; -} - -class VoteConfiguration { - constructor({ channel, maximumFeedbacks, members, role }) { +/** + * Represents vote data to be stored in case of restarts + * @class + */ +class VoteSetup { + /** + * Constructs a new VoteSetup object. + * @constructor + * @param {Object} options - The vote configuration options. + * @param {Channel} options.channel - The channel for the vote. + * @param {FeedbackData[]} options.feedbacks - the feedbacks for the vote. + * @param {Array} options.members - The members to be voted on. + * @param {Role} options.role - The role associated with the vote. + */ + constructor({ channel, feedbacks, members, role }) { this.channel = channel; - this.maximumFeedbacks = maximumFeedbacks; + this.feedbacks = feedbacks; this.members = members; this.role = role; } @@ -136,84 +146,155 @@ class VoteConfiguration { toJSON() { return { channel: this.channel.id, - maximumFeedbacks: this.maximumFeedbacks, + feedbacks: this.feedbacks, members: this.members.map(m => m.id), role: this.role.id }; } +} - getEmbed(member, description) { - return new Discord.EmbedBuilder() - .setColor(member.roles.highest.hexColor) - .setAuthor({ - name: 'Vote Configuration', - iconURL: member.user.displayAvatarURL({ dynamic: true }) - }) - .setDescription(description); - } - - description(emojiDatabase) { - return ` - This vote will be for ${this.role}, inside of ${this.channel} - ${emojiDatabase.feedback.text} \`${this.maximumFeedbacks}\` +/** + * Generates the confirmation message for the vote configuration. + * @param {Discord.Member} member - The member generating the confirmation message. + * @param {Object} emojiDatabase - The emoji database. + * @returns {string} The confirmation message for the vote configuration. + */ +function getSetupEmbed(voteSetup) { + const member = voteSetup.members[0]; + const embed = new Discord.EmbedBuilder() + .setColor(member.roles.highest.hexColor) + .setAuthor({ + name: 'Vote Configuration', + iconURL: member.user.displayAvatarURL({ dynamic: true }) + }) + .setDescription(` + This vote will be for to <@${member.id}> to ${voteSetup.role} + `); - ## Leaders - ${this.members.join(', ')} - `; - } + const memberFeedbacks = voteSetup.feedbacks.filter(feedback => feedback.mentionedID == member.id); + const includedFeedbacks = memberFeedbacks.filter(feedback => feedback.feedbackState == FeedbackState.Included); + const unidentifiedFeedbacks = memberFeedbacks.filter(feedback => feedback.feedbackState == FeedbackState.Unidentified); + const otherFeedbacks = memberFeedbacks.filter(feedback => feedback.feedbackState == FeedbackState.Other); + // TODO add length check for 1024 character limit, add feedback two as name and continue list + // also add indexes to the feedbacks if needed + embed.addFields([{ + name: 'Included Feedback:', + value: getFeedbackString(includedFeedbacks) || 'None' + }, + { + name: 'Unidentified Feedback:', + value: getFeedbackString(unidentifiedFeedbacks) || 'None' + }, + { + name: 'Other Feedback:', + value: getFeedbackString(otherFeedbacks) || 'None' + }]); + return embed; +} - confirmationMessage(member, emojiDatabase) { - return this.getEmbed(member, this.description(emojiDatabase)); - } +function getFeedbackString(feedbacks) { + return feedbacks.map(feedback => `\`${feedback.dungeon?.tag || ' ?? '} ${feedback.tier?.tag || ' ?? '}\` ${feedback.feedbackURL} `).join('\n'); } -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; +async function startVote(settings, voteSetup, member) { + const roleSettingsName = voteSetup.getRoleSettingsName(settings); + if (!roleSettingsName) { return; } + const embedStyling = voteConfig.templates + .find(template => template.settingRole === roleSettingsName); + let embedColor = voteSetup.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 }) }) + .setAuthor({ name: `${member.displayName} to ${voteSetup.role.name}`, iconURL: member.user.displayAvatarURL({ dynamic: true }) }) .setDescription(`${member} \`${member.displayName}\``); if (embedStyling != undefined && embedStyling.image) { embed.setThumbnail(embedStyling.image); } + const feedbacks = voteSetup.feedbacks.filter(feedback => feedback.mentionedID == member.id && feedback.feedbackState == FeedbackState.Included); 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 + name: 'Feedback:', + value: getFeedbackString(feedbacks) || 'None' }); - const voteMessage = await voteConfiguration.channel.send({ embeds: [embed] }); + const voteMessage = await voteSetup.channel.send({ embeds: [embed] }); for (const emoji of ['✅', '❌', '👀']) { voteMessage.react(emoji); } } -function generateVoteConfigurationButtons(bot) { +function generateVoteSetupButtons(bot) { return new Discord.ActionRowBuilder() .addComponents([ new Discord.ButtonBuilder() - .setLabel('✅ Confirm') + .setLabel('✅ Send') .setStyle(3) - .setCustomId('voteConfirm'), + .setCustomId('voteSend'), new Discord.ButtonBuilder() - .setLabel('Feedbacks') - .setStyle(2) - .setEmoji(bot.storedEmojis.feedback.id) - .setCustomId('voteFeedbackConfigure'), + .setLabel('❌ Skip') + .setStyle(1) + .setCustomId('voteSkip'), new Discord.ButtonBuilder() - .setLabel('# Channel') + .setLabel('Add') .setStyle(2) - .setCustomId('voteChannelConfigure'), + .setEmoji(bot.storedEmojis.feedback.id) + .setCustomId('voteAddFeedbacks'), new Discord.ButtonBuilder() - .setLabel('@ Role') + .setLabel('Remove') .setStyle(2) - .setCustomId('voteRoleConfigure'), + .setEmoji(bot.storedEmojis.feedback.id) + .setCustomId('voteRemoveFeedbacks'), new Discord.ButtonBuilder() - .setLabel('❌ Cancel') + .setLabel('❌ Abort') .setStyle(4) - .setCustomId('voteCancel') + .setCustomId('voteAbort') ]); } + +/** + * returns categorized feedbacks for the members + * @param {Discord.Member[]} members Members to find feedbacks for + * @param {Discord.Guild} guild Guild to find feedbacks in + * @param {*} bot + * @returns {Promise} The feedbacks for the members. + */ +async function getFeedbacks(members, guild, bot, voteSetup) { + const feedbackChannel = guild.channels.cache.get(bot.settings[guild.id].channels.rlfeedback); // TODO clean up settings usages, it's only one guild settings ever + // get the messages that mention the relevant members + const messages = await getMessages(feedbackChannel, 500); + // filter the messages to only ones that mention the members, message.mentions.members is Collection + const filteredMessages = messages.filter(message => members.some(member => message.mentions.members.has(member.id))); + const roleSettingsName = Object.keys(bot.settings[guild.id].roles).find(roleName => bot.settings[guild.id].roles[roleName] == voteSetup.role.id); + // map the messages to FeedbackData + voteSetup.feedbacks = filteredMessages.map(message => { + const mentionedID = message.mentions.users.first()?.id; + const feedbackerID = message.author.id; + const firstLine = message.content.split('\n')[0].toLowerCase(); // Convert to lowercase for searching + const dungeon = voteConfig.feedbackTypes.dungeon.find(dungeon => dungeon.flags.some(flag => firstLine.includes(flag))); + const tier = voteConfig.feedbackTypes.tier.find(tier => tier.flags.some(flag => firstLine.includes(flag))); + let displayState = FeedbackState.Other; + if (!dungeon || !tier) { + displayState = FeedbackState.Unidentified; + } else if (dungeon.roles.includes(roleSettingsName) && tier.roles.includes(roleSettingsName)) { + displayState = FeedbackState.Included; + } + console.log(displayState); + return { + tier, + dungeon, + firstLine: message.content.split('\n')[0], // Preserve the original case for the return + mentionedID, + feedbackURL: message.url, + display: FeedbackState.Other, + feedbackerID, + timeStamp: (message.createdTimestamp / 1000).toFixed(0), + displayState + }; + }) || []; +} + +async function getMessages(channel, limit) { + const sumMessages = []; + for (let i = 0; i <= limit; i += 100) { + const options = { limit: 100, before: i > 0 ? sumMessages[sumMessages.length - 1].id : null }; + // eslint-disable-next-line no-await-in-loop + const messages = await channel.messages.fetch(options); + sumMessages.push(...messages.map(m => m)); + if (messages.size != 100) break; + } + return sumMessages; +} diff --git a/data/voteConfig.json b/data/voteConfig.json new file mode 100644 index 00000000..b3808022 --- /dev/null +++ b/data/voteConfig.json @@ -0,0 +1,118 @@ +{ + "templates": [ + { + "settingRole": "almostHallsBanner", + "embedColor": "#b21111", + "image": "https://i.imgur.com/nPkovWR.png" + }, + { + "settingRole": "hallsBanner", + "embedColor": "#b5adb2", + "image": "https://i.imgur.com/G8FArqL.png" + }, + { + "settingRole": "vetHallsBanner", + "embedColor": "#1b006d", + "image": "https://i.imgur.com/7JGSvMq.png" + }, + { + "settingRole": "almostShattersBanner", + "embedColor": "#64442e", + "image": "https://i.imgur.com/vatlKfa.png" + }, + { + "settingRole": "shattersBanner", + "embedColor": "#5f6eec", + "image": "https://i.imgur.com/bnKFZjt.png" + }, + { + "settingRole": "vetShattersBanner", + "embedColor": "#35522b", + "image": "https://i.imgur.com/qL3BVpR.png" + }, + { + "settingRole": "almostOryxBanner", + "embedColor": "#7a122b", + "image": "https://i.imgur.com/3Biywi7.png" + }, + { + "settingRole": "oryxBanner", + "embedColor": "#7a122b", + "image": "https://i.imgur.com/DSVqdZo.png" + }, + { + "settingRole": "veteranOryxBanner", + "embedColor": "#ffee59", + "image": "https://media.discordapp.net/attachments/378994910927388683/1199528994597515294/celestrial.gif?ex=65c2df6e&is=65b06a6e&hm=77c6feb6bdca1f342ee7d3bcb83da4dbc457f235707e78b9b19937b70734c9a0&=" + }, + { + "settingRole": "fullskipBanner", + "embedColor": "#c1bca4", + "image": "https://i.imgur.com/TR12WfN.png" + }, + { + "settingRole": "vetFullskipBanner", + "embedColor": "#747c74", + "image": "https://i.imgur.com/s8tkTk4.png" + }], + "feedbackTypes": { + "dungeon": [ + { + "flags": ["void", "halls", "mbc"], + "tag": " Void", + "roles": ["almostHallsBanner", "hallsBanner", "vetHallsBanner"] + }, + { + "flags": ["cult"], + "tag": " Cult", + "roles": ["almostHallsBanner", "hallsBanner", "vetHallsBanner"] + }, + { + "flags": ["reading", "mapreading"], + "tag": " Map", + "roles": ["almostHallsBanner", "hallsBanner", "vetHallsBanner", "fullskipBanner", "vetFullskipBanner"] + }, + { + "flags": ["fullskip", "fs", "skip", "fskip"], + "tag": " Fskip", + "roles": ["fullskipBanner", "vetFullskipBanner"] + }, + { + "flags": ["oryx", "o3"], + "tag": " Oryx", + "roles": ["almostOryxBanner", "oryxBanner", "veteranOryxBanner"] + }, + { + "flags": ["shatters", "shatts"], + "tag": "Shatts", + "roles": ["almostShattersBanner", "shattersBanner", "vetShattersBanner"] + }], + "tier": [ + { + "flags": ["trl"], + "tag": "TRL ", + "roles": ["almostHallsBanner", "almostOryxBanner", "almostShattersBanner"] + }, + { + "flags": ["arl"], + "tag": "ARL ", + "roles": ["hallsBanner", "oryxBanner", "shattersBanner"] + }, + { + "flags": ["reading", "mapreading"], + "tag": "Read ", + "roles": ["almostHallsBanner", "hallsBanner", "vetHallsBanner", "fullskipBanner", "vetFullskipBanner"] + }, + { + "flags": ["ghost"], + "tag": "Ghost", + "roles": ["almostHallsBanner", "almostOryxBanner", "almostShattersBanner", "hallsBanner", "oryxBanner", "shattersBanner", "vetHallsBanner", "veteranOryxBanner", "vetShattersBanner", "fullskipBanner", "vetFullskipBanner"] + }, + { + "flags": ["vtrl", "vrl", "rl"], + "tag": "VTRL ", + "roles": ["vetHallsBanner", "veteranOryxBanner", "vetShattersBanner"] + } + ] + } +} \ No newline at end of file diff --git a/data/voteConfiguration.json b/data/voteConfiguration.json deleted file mode 100644 index e17aa22f..00000000 --- a/data/voteConfiguration.json +++ /dev/null @@ -1,57 +0,0 @@ -[ - { - "settingRole": "almostHallsBanner", - "embedColor": "#b21111", - "image": "https://i.imgur.com/nPkovWR.png" - }, - { - "settingRole": "hallsBanner", - "embedColor": "#b5adb2", - "image": "https://i.imgur.com/G8FArqL.png" - }, - { - "settingRole": "vetHallsBanner", - "embedColor": "#1b006d", - "image": "https://i.imgur.com/7JGSvMq.png" - }, - { - "settingRole": "almostShattersBanner", - "embedColor": "#64442e", - "image": "https://i.imgur.com/vatlKfa.png" - }, - { - "settingRole": "shattersBanner", - "embedColor": "#5f6eec", - "image": "https://i.imgur.com/bnKFZjt.png" - }, - { - "settingRole": "vetShattersBanner", - "embedColor": "#35522b", - "image": "https://i.imgur.com/qL3BVpR.png" - }, - { - "settingRole": "almostOryxBanner", - "embedColor": "#7a122b", - "image": "https://i.imgur.com/3Biywi7.png" - }, - { - "settingRole": "oryxBanner", - "embedColor": "#7a122b", - "image": "https://i.imgur.com/DSVqdZo.png" - }, - { - "settingRole": "veteranOryxBanner", - "embedColor": "#ffee59", - "image": "https://media.discordapp.net/attachments/378994910927388683/1199528994597515294/celestrial.gif?ex=65c2df6e&is=65b06a6e&hm=77c6feb6bdca1f342ee7d3bcb83da4dbc457f235707e78b9b19937b70734c9a0&=" - }, - { - "settingRole": "fullskipBanner", - "embedColor": "#c1bca4", - "image": "https://i.imgur.com/TR12WfN.png" - }, - { - "settingRole": "vetFullskipBanner", - "embedColor": "#747c74", - "image": "https://i.imgur.com/s8tkTk4.png" - } -] \ No newline at end of file From e9139280673112f373f70f42b4a09053fc62bb75 Mon Sep 17 00:00:00 2001 From: tro2 <62850247+tro2@users.noreply.github.com> Date: Mon, 19 Feb 2024 22:21:51 -0800 Subject: [PATCH 3/8] saving progress on vote.js work in progress --- commands/vote.js | 182 ++++++++++++++++++++++++------------------- data/voteConfig.json | 18 ++--- 2 files changed, 110 insertions(+), 90 deletions(-) diff --git a/commands/vote.js b/commands/vote.js index b0ac28bc..50828251 100644 --- a/commands/vote.js +++ b/commands/vote.js @@ -9,29 +9,20 @@ const { createReactionRow } = require('../redis.js'); * @property {string} emoji * @property {string[]} roles */ -// TODO clean up unused properties /** * @typedef FeedbackData - * @property {string} feedbackContent + * @property {string} messageID * @property {FeedbackType} tier * @property {FeedbackType} dungeon - * @property {string} firstLine - * @property {string} mentionedID + * @property {string[]} mentionedIDs * @property {string} feedbackURL - * @property {string} feedbackerID * @property {number} timeStamp - * @property {FeedbackState} displayState + * @property {FeedbackState} feedbackState */ -/** - * @enum {number} - */ -const FeedbackState = { - Included: 1, - Unidentified: 0, - Other: -1 -}; - +const FeedbackState = { Included: 0, Unidentified: 1, Other: 2 }; +// notes: +// the filtering goes based of the first line of the message, so if someone is giving feedback to multiple people in the same message (feedback on feedback for fullskips), it may get mistagged module.exports = { name: 'vote', role: 'headrl', @@ -58,7 +49,7 @@ module.exports = { }); if (voteSetup.members.length == 0) { return await message.reply('No members found.'); } - await getFeedbacks(voteSetup.members, guild, bot, voteSetup); + await getFeedbacks(guild, bot, voteSetup); const embed = getSetupEmbed(voteSetup); const voteSetupButtons = generateVoteSetupButtons(bot); @@ -72,7 +63,6 @@ module.exports = { switch (choice) { case 'voteSend': await startVote(bot.settings[interaction.guild.id], voteSetup, member); - // TODO finish the interactions // move index to next member // if last member in array, delete the message break; @@ -84,26 +74,43 @@ module.exports = { await message.delete(); break; case 'voteAddFeedbacks': { - // const confirmationMessage = await interaction.reply({ embeds: [voteSetup.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; + const { includedFeedbacks, unidentifiedFeedbacks, otherFeedbacks } = sortMemberFeedbacks(voteSetup.feedbacks, member); + const addableFeedbacks = unidentifiedFeedbacks.concat(otherFeedbacks).slice(0, 25); + let index = includedFeedbacks.length + 1; + const addSelectionMenu = new Discord.StringSelectMenuBuilder() + .setCustomId('voteAddFeedbacks') + .setPlaceholder('Feedbacks to add') + .setMinValues(1) + .setMaxValues(addableFeedbacks.length) + .addOptions(addableFeedbacks.map(feedback => ({ + label: `${index++}. ${feedback.dungeon?.tag || '??'} ${feedback.tier?.tag || '??'}`, + value: feedback.messageID + }))); + + const actionRow = new Discord.ActionRowBuilder() + .addComponents(addSelectionMenu); + + const reply = await interaction.reply({ content: 'Test string', components: [actionRow], ephemeral: true, fetchReply: true }); + const replyResponse = await reply.awaitMessageComponent({ componentType: Discord.ComponentType.StringSelect, time: 120000, filter: i => i.user.id == member.id }); // TODO add a timeout + await replyResponse.deferUpdate(); + // array of message IDs + console.log(replyResponse.values); + console.log(reply.id); + // delete the ephemeral message + await interaction.deleteReply(reply.id); + // TODO finish collecting feedbacks and altering the displayState on voteSetup + // TODO add custom value with modal response + // add function to grab custom feedback by message ID and process it + + // example from previous code // voteSetup.maximumFeedbacks = choice; // await updateState('maximumFeedbacks', choice); - // await interaction.message.edit({ embeds: [voteSetup.confirmationMessage(member, emojiDatabase)], components: [generateVoteSetupButtons(bot)] }); + await interaction.message.edit({ embeds: [getSetupEmbed(voteSetup)], components: [generateVoteSetupButtons(bot)] }); + console.log('updated Interaction'); break; } case 'voteRemoveFeedbacks': { - // await interaction.update({ embeds: [voteSetup.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) { - // voteSetup.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: [voteSetup.confirmationMessage(member, emojiDatabase)], components: [generateVoteSetupButtons(bot)] }); + // copy above code and change the filter to show only included feedbacks, no custom option on selectPanel break; } default: @@ -170,34 +177,52 @@ function getSetupEmbed(voteSetup) { .setDescription(` This vote will be for to <@${member.id}> to ${voteSetup.role} `); - - const memberFeedbacks = voteSetup.feedbacks.filter(feedback => feedback.mentionedID == member.id); - const includedFeedbacks = memberFeedbacks.filter(feedback => feedback.feedbackState == FeedbackState.Included); - const unidentifiedFeedbacks = memberFeedbacks.filter(feedback => feedback.feedbackState == FeedbackState.Unidentified); - const otherFeedbacks = memberFeedbacks.filter(feedback => feedback.feedbackState == FeedbackState.Other); + const { includedFeedbacks, unidentifiedFeedbacks, otherFeedbacks } = sortMemberFeedbacks(voteSetup.feedbacks, member); + let index = 1; // TODO add length check for 1024 character limit, add feedback two as name and continue list - // also add indexes to the feedbacks if needed embed.addFields([{ name: 'Included Feedback:', - value: getFeedbackString(includedFeedbacks) || 'None' + value: includedFeedbacks.map(feedback => getDisplayString(index++, feedback)).join('\n') || 'None' }, { - name: 'Unidentified Feedback:', - value: getFeedbackString(unidentifiedFeedbacks) || 'None' + name: 'Unknown Tags:', + value: unidentifiedFeedbacks.map(feedback => getDisplayString(index++, feedback)).join('\n') || 'None' }, { name: 'Other Feedback:', - value: getFeedbackString(otherFeedbacks) || 'None' + value: otherFeedbacks.map(feedback => getDisplayString(index++, feedback)).join('\n') || 'None' }]); return embed; } -function getFeedbackString(feedbacks) { - return feedbacks.map(feedback => `\`${feedback.dungeon?.tag || ' ?? '} ${feedback.tier?.tag || ' ?? '}\` ${feedback.feedbackURL} `).join('\n'); +function sortMemberFeedbacks(feedbacks, member) { + const includedFeedbacks = []; + const unidentifiedFeedbacks = []; + const otherFeedbacks = []; + // sort by timestamp desc (after push the newest feedback is at the highest index) + feedbacks.filter(feedback => feedback.mentionedIDs.includes(member.id)).sort((a, b) => b.timeStamp - a.timeStamp).forEach(feedback => { + if (feedback.feedbackState == FeedbackState.Included) { + includedFeedbacks.push(feedback); + } else if (feedback.feedbackState == FeedbackState.Unidentified) { + unidentifiedFeedbacks.push(feedback); + } else { + otherFeedbacks.push(feedback); + } + }); + return { includedFeedbacks, unidentifiedFeedbacks, otherFeedbacks }; +} + +function getDisplayString(index, feedback) { + const tags = (`${feedback.dungeon?.tag || '??'} ${feedback.tier?.tag || '??'}`).padStart(11); + return `\`${index}.\` \`${tags}\` ${feedback.feedbackURL} `; +} + +function getRoleSettingsName(settings, role) { + return Object.keys(settings.roles).find(roleName => settings.roles[roleName] == role.id); } async function startVote(settings, voteSetup, member) { - const roleSettingsName = voteSetup.getRoleSettingsName(settings); + const roleSettingsName = getRoleSettingsName(settings, voteSetup.role); if (!roleSettingsName) { return; } const embedStyling = voteConfig.templates .find(template => template.settingRole === roleSettingsName); @@ -208,10 +233,10 @@ async function startVote(settings, voteSetup, member) { .setAuthor({ name: `${member.displayName} to ${voteSetup.role.name}`, iconURL: member.user.displayAvatarURL({ dynamic: true }) }) .setDescription(`${member} \`${member.displayName}\``); if (embedStyling != undefined && embedStyling.image) { embed.setThumbnail(embedStyling.image); } - const feedbacks = voteSetup.feedbacks.filter(feedback => feedback.mentionedID == member.id && feedback.feedbackState == FeedbackState.Included); + const feedbacks = voteSetup.feedbacks.filter(feedback => feedback.mentionedIDs.includes[member.id] && feedback.feedbackState == FeedbackState.Included); embed.addFields({ name: 'Feedback:', - value: getFeedbackString(feedbacks) || 'None' + value: getDisplayString(feedbacks) || 'None' }); const voteMessage = await voteSetup.channel.send({ embeds: [embed] }); for (const emoji of ['✅', '❌', '👀']) { voteMessage.react(emoji); } @@ -252,49 +277,44 @@ function generateVoteSetupButtons(bot) { * @param {*} bot * @returns {Promise} The feedbacks for the members. */ -async function getFeedbacks(members, guild, bot, voteSetup) { - const feedbackChannel = guild.channels.cache.get(bot.settings[guild.id].channels.rlfeedback); // TODO clean up settings usages, it's only one guild settings ever - // get the messages that mention the relevant members - const messages = await getMessages(feedbackChannel, 500); - // filter the messages to only ones that mention the members, message.mentions.members is Collection - const filteredMessages = messages.filter(message => members.some(member => message.mentions.members.has(member.id))); - const roleSettingsName = Object.keys(bot.settings[guild.id].roles).find(roleName => bot.settings[guild.id].roles[roleName] == voteSetup.role.id); - // map the messages to FeedbackData +async function getFeedbacks(guild, bot, voteSetup) { + // TODO clean up settings usages, it's only one guild settings ever + async function fetchMessages(limit) { + const sumMessages = []; + const feedbackChannel = guild.channels.cache.get(bot.settings[guild.id].channels.rlfeedback); + for (let i = 0; i <= limit; i += 100) { + const options = { limit: 100, before: i > 0 ? sumMessages[sumMessages.length - 1].id : null }; + // eslint-disable-next-line no-await-in-loop + const fetchedMessages = await feedbackChannel.messages.fetch(options); + sumMessages.push(...fetchedMessages.map(m => m)); + if (fetchedMessages.size != 100) break; + } + return sumMessages; + } + const messages = await fetchMessages(500); + + const filteredMessages = messages.filter(message => voteSetup.members.some(member => message.mentions.users.has(member.id))); + //* @type {Feedback[]} */ voteSetup.feedbacks = filteredMessages.map(message => { - const mentionedID = message.mentions.users.first()?.id; - const feedbackerID = message.author.id; + const mentionedIDs = message.mentions.users.map(user => user.id); const firstLine = message.content.split('\n')[0].toLowerCase(); // Convert to lowercase for searching const dungeon = voteConfig.feedbackTypes.dungeon.find(dungeon => dungeon.flags.some(flag => firstLine.includes(flag))); const tier = voteConfig.feedbackTypes.tier.find(tier => tier.flags.some(flag => firstLine.includes(flag))); - let displayState = FeedbackState.Other; - if (!dungeon || !tier) { - displayState = FeedbackState.Unidentified; - } else if (dungeon.roles.includes(roleSettingsName) && tier.roles.includes(roleSettingsName)) { - displayState = FeedbackState.Included; + const roleSettingsName = getRoleSettingsName(bot.settings[guild.id], voteSetup.role); + let feedbackState = FeedbackState.Other; + if (!tier || !dungeon) { + feedbackState = FeedbackState.Unidentified; + } else if (tier.roles.includes(roleSettingsName) && dungeon.roles.includes(roleSettingsName)) { + feedbackState = FeedbackState.Included; } - console.log(displayState); return { + messageID: message.id, tier, dungeon, - firstLine: message.content.split('\n')[0], // Preserve the original case for the return - mentionedID, + mentionedIDs, feedbackURL: message.url, - display: FeedbackState.Other, - feedbackerID, timeStamp: (message.createdTimestamp / 1000).toFixed(0), - displayState + feedbackState }; - }) || []; -} - -async function getMessages(channel, limit) { - const sumMessages = []; - for (let i = 0; i <= limit; i += 100) { - const options = { limit: 100, before: i > 0 ? sumMessages[sumMessages.length - 1].id : null }; - // eslint-disable-next-line no-await-in-loop - const messages = await channel.messages.fetch(options); - sumMessages.push(...messages.map(m => m)); - if (messages.size != 100) break; - } - return sumMessages; + }); } diff --git a/data/voteConfig.json b/data/voteConfig.json index b3808022..d34b4713 100644 --- a/data/voteConfig.json +++ b/data/voteConfig.json @@ -59,27 +59,27 @@ "dungeon": [ { "flags": ["void", "halls", "mbc"], - "tag": " Void", + "tag": "Void", "roles": ["almostHallsBanner", "hallsBanner", "vetHallsBanner"] }, { "flags": ["cult"], - "tag": " Cult", + "tag": "Cult", "roles": ["almostHallsBanner", "hallsBanner", "vetHallsBanner"] }, { "flags": ["reading", "mapreading"], - "tag": " Map", + "tag": "Map", "roles": ["almostHallsBanner", "hallsBanner", "vetHallsBanner", "fullskipBanner", "vetFullskipBanner"] }, { "flags": ["fullskip", "fs", "skip", "fskip"], - "tag": " Fskip", + "tag": "Fskip", "roles": ["fullskipBanner", "vetFullskipBanner"] }, { "flags": ["oryx", "o3"], - "tag": " Oryx", + "tag": "Oryx", "roles": ["almostOryxBanner", "oryxBanner", "veteranOryxBanner"] }, { @@ -90,17 +90,17 @@ "tier": [ { "flags": ["trl"], - "tag": "TRL ", + "tag": "TRL", "roles": ["almostHallsBanner", "almostOryxBanner", "almostShattersBanner"] }, { "flags": ["arl"], - "tag": "ARL ", + "tag": "ARL", "roles": ["hallsBanner", "oryxBanner", "shattersBanner"] }, { "flags": ["reading", "mapreading"], - "tag": "Read ", + "tag": "Lesson", "roles": ["almostHallsBanner", "hallsBanner", "vetHallsBanner", "fullskipBanner", "vetFullskipBanner"] }, { @@ -110,7 +110,7 @@ }, { "flags": ["vtrl", "vrl", "rl"], - "tag": "VTRL ", + "tag": "VTRL", "roles": ["vetHallsBanner", "veteranOryxBanner", "vetShattersBanner"] } ] From ef1ab33dbc9d566f3f16b972b818c21ee53ea8fb Mon Sep 17 00:00:00 2001 From: tro2 <62850247+tro2@users.noreply.github.com> Date: Wed, 21 Feb 2024 12:51:29 -0800 Subject: [PATCH 4/8] update vote.js finalize changes before draft PR --- commands/vote.js | 82 ++++++++++++++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 34 deletions(-) diff --git a/commands/vote.js b/commands/vote.js index 50828251..d671d859 100644 --- a/commands/vote.js +++ b/commands/vote.js @@ -49,7 +49,7 @@ module.exports = { }); if (voteSetup.members.length == 0) { return await message.reply('No members found.'); } - await getFeedbacks(guild, bot, voteSetup); + await getFeedbacks(guild, bot.settings[guild.id], voteSetup); const embed = getSetupEmbed(voteSetup); const voteSetupButtons = generateVoteSetupButtons(bot); @@ -58,23 +58,35 @@ module.exports = { }, async interactionHandler(bot, message, db, choice, voteSetupJSON, updateState) { const interaction = message.interaction; // eslint-disable-line prefer-destructuring - const member = interaction.member; // eslint-disable-line prefer-destructuring + const member = message.member; // eslint-disable-line prefer-destructuring const voteSetup = VoteSetup.fromJSON(interaction.guild, voteSetupJSON); switch (choice) { case 'voteSend': - await startVote(bot.settings[interaction.guild.id], voteSetup, member); - // move index to next member - // if last member in array, delete the message + await sendVote(bot.settings[interaction.guild.id], voteSetup); + voteSetup.currentMemberIndex++; + updateState('currentMemberIndex', voteSetup.currentMemberIndex); + if (voteSetup.currentMemberIndex >= voteSetup.members.length) { + await message.delete(); + } else { + await interaction.message.edit({ embeds: [getSetupEmbed(voteSetup)], components: [generateVoteSetupButtons(bot)] }); + } break; case 'voteSkip': - // move index to next member - // if last member in array, delete the message + voteSetup.currentMemberIndex++; + updateState('currentMemberIndex', voteSetup.currentMemberIndex); + if (voteSetup.currentMemberIndex >= voteSetup.members.length) { + await message.delete(); + } else { + await interaction.message.edit({ embeds: [getSetupEmbed(voteSetup)], components: [generateVoteSetupButtons(bot)] }); + } break; case 'voteAbort': - await message.delete(); + await message.delete(); // TODO does this clean up this interaction? break; case 'voteAddFeedbacks': { - const { includedFeedbacks, unidentifiedFeedbacks, otherFeedbacks } = sortMemberFeedbacks(voteSetup.feedbacks, member); + // TODO add custom value with modal response + // add function to grab custom feedback by message ID and process it + const { includedFeedbacks, unidentifiedFeedbacks, otherFeedbacks } = sortMemberFeedbacks(voteSetup); const addableFeedbacks = unidentifiedFeedbacks.concat(otherFeedbacks).slice(0, 25); let index = includedFeedbacks.length + 1; const addSelectionMenu = new Discord.StringSelectMenuBuilder() @@ -91,22 +103,22 @@ module.exports = { .addComponents(addSelectionMenu); const reply = await interaction.reply({ content: 'Test string', components: [actionRow], ephemeral: true, fetchReply: true }); - const replyResponse = await reply.awaitMessageComponent({ componentType: Discord.ComponentType.StringSelect, time: 120000, filter: i => i.user.id == member.id }); // TODO add a timeout + let replyResponse; + try { // what is CollectorOptions.dispose? https://discord.js.org/docs/packages/discord.js/14.14.1/CollectorOptions:Interface#dispose + replyResponse = await reply.awaitMessageComponent({ componentType: Discord.ComponentType.StringSelect, time: 120000, filter: i => i.user.id == member.id }); + } catch (error) { + await reply.edit({ content: 'Timed out', components: [] }); + break; + } await replyResponse.deferUpdate(); // array of message IDs - console.log(replyResponse.values); - console.log(reply.id); - // delete the ephemeral message + replyResponse.values.forEach(messageID => { + const feedback = voteSetup.feedbacks.find(feedback => feedback.messageID == messageID); + feedback.feedbackState = FeedbackState.Included; + }); + updateState('feedbacks', voteSetup.feedbacks); await interaction.deleteReply(reply.id); - // TODO finish collecting feedbacks and altering the displayState on voteSetup - // TODO add custom value with modal response - // add function to grab custom feedback by message ID and process it - - // example from previous code - // voteSetup.maximumFeedbacks = choice; - // await updateState('maximumFeedbacks', choice); await interaction.message.edit({ embeds: [getSetupEmbed(voteSetup)], components: [generateVoteSetupButtons(bot)] }); - console.log('updated Interaction'); break; } case 'voteRemoveFeedbacks': { @@ -139,6 +151,7 @@ class VoteSetup { this.feedbacks = feedbacks; this.members = members; this.role = role; + this.currentMemberIndex = 0; } static fromJSON(guild, json) { @@ -155,7 +168,8 @@ class VoteSetup { channel: this.channel.id, feedbacks: this.feedbacks, members: this.members.map(m => m.id), - role: this.role.id + role: this.role.id, + currentMemberIndex: this.currentMemberIndex }; } } @@ -167,11 +181,11 @@ class VoteSetup { * @returns {string} The confirmation message for the vote configuration. */ function getSetupEmbed(voteSetup) { - const member = voteSetup.members[0]; + const member = voteSetup.members[voteSetup.currentMemberIndex]; const embed = new Discord.EmbedBuilder() .setColor(member.roles.highest.hexColor) .setAuthor({ - name: 'Vote Configuration', + name: `Vote Configuration ${voteSetup.currentMemberIndex + 1}/${voteSetup.members.length}`, iconURL: member.user.displayAvatarURL({ dynamic: true }) }) .setDescription(` @@ -195,12 +209,13 @@ function getSetupEmbed(voteSetup) { return embed; } -function sortMemberFeedbacks(feedbacks, member) { +function sortMemberFeedbacks(voteSetup) { + const member = voteSetup.members[voteSetup.currentMemberIndex]; const includedFeedbacks = []; const unidentifiedFeedbacks = []; const otherFeedbacks = []; // sort by timestamp desc (after push the newest feedback is at the highest index) - feedbacks.filter(feedback => feedback.mentionedIDs.includes(member.id)).sort((a, b) => b.timeStamp - a.timeStamp).forEach(feedback => { + voteSetup.feedbacks.filter(feedback => feedback.mentionedIDs.includes(member.id)).sort((a, b) => b.timeStamp - a.timeStamp).forEach(feedback => { if (feedback.feedbackState == FeedbackState.Included) { includedFeedbacks.push(feedback); } else if (feedback.feedbackState == FeedbackState.Unidentified) { @@ -221,13 +236,13 @@ function getRoleSettingsName(settings, role) { return Object.keys(settings.roles).find(roleName => settings.roles[roleName] == role.id); } -async function startVote(settings, voteSetup, member) { - const roleSettingsName = getRoleSettingsName(settings, voteSetup.role); - if (!roleSettingsName) { return; } +async function sendVote(voteSetup, roleSettingsName) { + if (!roleSettingsName) { return; } // TODO add error message for missing role settings name const embedStyling = voteConfig.templates .find(template => template.settingRole === roleSettingsName); let embedColor = voteSetup.role.hexColor; if (embedStyling != undefined && embedStyling.embedColor) { embedColor = embedStyling.embedColor; } + const member = voteSetup.members[voteSetup.currentMemberIndex]; const embed = new Discord.EmbedBuilder() .setColor(embedColor) .setAuthor({ name: `${member.displayName} to ${voteSetup.role.name}`, iconURL: member.user.displayAvatarURL({ dynamic: true }) }) @@ -242,7 +257,7 @@ async function startVote(settings, voteSetup, member) { for (const emoji of ['✅', '❌', '👀']) { voteMessage.react(emoji); } } -function generateVoteSetupButtons(bot) { +function generateVoteSetupButtons(bot) { // TODO disable remove button if no feedbacks are included return new Discord.ActionRowBuilder() .addComponents([ new Discord.ButtonBuilder() @@ -277,11 +292,10 @@ function generateVoteSetupButtons(bot) { * @param {*} bot * @returns {Promise} The feedbacks for the members. */ -async function getFeedbacks(guild, bot, voteSetup) { - // TODO clean up settings usages, it's only one guild settings ever +async function getFeedbacks(guild, settings, voteSetup) { async function fetchMessages(limit) { const sumMessages = []; - const feedbackChannel = guild.channels.cache.get(bot.settings[guild.id].channels.rlfeedback); + const feedbackChannel = guild.channels.cache.get(settings.channels.rlfeedback); for (let i = 0; i <= limit; i += 100) { const options = { limit: 100, before: i > 0 ? sumMessages[sumMessages.length - 1].id : null }; // eslint-disable-next-line no-await-in-loop @@ -300,7 +314,7 @@ async function getFeedbacks(guild, bot, voteSetup) { const firstLine = message.content.split('\n')[0].toLowerCase(); // Convert to lowercase for searching const dungeon = voteConfig.feedbackTypes.dungeon.find(dungeon => dungeon.flags.some(flag => firstLine.includes(flag))); const tier = voteConfig.feedbackTypes.tier.find(tier => tier.flags.some(flag => firstLine.includes(flag))); - const roleSettingsName = getRoleSettingsName(bot.settings[guild.id], voteSetup.role); + const roleSettingsName = getRoleSettingsName(settings, voteSetup.role); // TODO what happens if roleSettingsName is undefined? let feedbackState = FeedbackState.Other; if (!tier || !dungeon) { feedbackState = FeedbackState.Unidentified; From 60729d32243e644562d47bf52c08e9df3d1958d3 Mon Sep 17 00:00:00 2001 From: tro2 <62850247+tro2@users.noreply.github.com> Date: Thu, 22 Feb 2024 09:45:31 -0800 Subject: [PATCH 5/8] TEMP votejs debugging vote progression --- commands/vote.js | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/commands/vote.js b/commands/vote.js index d671d859..39c2aa8d 100644 --- a/commands/vote.js +++ b/commands/vote.js @@ -62,22 +62,25 @@ module.exports = { const voteSetup = VoteSetup.fromJSON(interaction.guild, voteSetupJSON); switch (choice) { case 'voteSend': - await sendVote(bot.settings[interaction.guild.id], voteSetup); + interaction.deferUpdate(); + await sendVote(voteSetup, getRoleSettingsName(bot.settings[interaction.guild.id], voteSetup.role)); + console.log(typeof voteSetup.currentMemberIndex, voteSetup.currentMemberIndex, voteSetup.members.length); voteSetup.currentMemberIndex++; - updateState('currentMemberIndex', voteSetup.currentMemberIndex); - if (voteSetup.currentMemberIndex >= voteSetup.members.length) { + updateState('currentMemberIndex', voteSetup.currentMemberIndex.toString()); // TODO debug why number is being reset to 0 + console.log('post', typeof voteSetup.currentMemberIndex, voteSetup.currentMemberIndex, voteSetup.members.length); + if (voteSetup.currentMemberIndex > voteSetup.members.length) { await message.delete(); } else { - await interaction.message.edit({ embeds: [getSetupEmbed(voteSetup)], components: [generateVoteSetupButtons(bot)] }); + await message.edit({ embeds: [getSetupEmbed(voteSetup)], components: [generateVoteSetupButtons(bot)] }); } break; case 'voteSkip': voteSetup.currentMemberIndex++; - updateState('currentMemberIndex', voteSetup.currentMemberIndex); + updateState('currentMemberIndex', voteSetup.currentMemberIndex.toString()); if (voteSetup.currentMemberIndex >= voteSetup.members.length) { await message.delete(); } else { - await interaction.message.edit({ embeds: [getSetupEmbed(voteSetup)], components: [generateVoteSetupButtons(bot)] }); + await message.edit({ embeds: [getSetupEmbed(voteSetup)], components: [generateVoteSetupButtons(bot)] }); } break; case 'voteAbort': @@ -86,6 +89,7 @@ module.exports = { case 'voteAddFeedbacks': { // TODO add custom value with modal response // add function to grab custom feedback by message ID and process it + // also make sure first 24 feedbacks show up in the select panel, since last is custom const { includedFeedbacks, unidentifiedFeedbacks, otherFeedbacks } = sortMemberFeedbacks(voteSetup); const addableFeedbacks = unidentifiedFeedbacks.concat(otherFeedbacks).slice(0, 25); let index = includedFeedbacks.length + 1; @@ -157,6 +161,7 @@ class VoteSetup { static fromJSON(guild, json) { return new this({ ...json, + currentMemberIndex: parseInt(json.currentMemberIndex), // TODO check if this is necessary, for some reason it's being reset to 0 but the setup panel is working fine channel: guild.channels.cache.get(json.channel), members: json.members.map(memberId => guild.members.cache.get(memberId)), role: guild.roles.cache.get(json.role) @@ -191,20 +196,20 @@ function getSetupEmbed(voteSetup) { .setDescription(` This vote will be for to <@${member.id}> to ${voteSetup.role} `); - const { includedFeedbacks, unidentifiedFeedbacks, otherFeedbacks } = sortMemberFeedbacks(voteSetup.feedbacks, member); + const { includedFeedbacks, unidentifiedFeedbacks, otherFeedbacks } = sortMemberFeedbacks(voteSetup); let index = 1; // TODO add length check for 1024 character limit, add feedback two as name and continue list embed.addFields([{ name: 'Included Feedback:', - value: includedFeedbacks.map(feedback => getDisplayString(index++, feedback)).join('\n') || 'None' + value: includedFeedbacks.map(feedback => getSetupDisplayString(index++, feedback)).join('\n') || 'None' }, { name: 'Unknown Tags:', - value: unidentifiedFeedbacks.map(feedback => getDisplayString(index++, feedback)).join('\n') || 'None' + value: unidentifiedFeedbacks.map(feedback => getSetupDisplayString(index++, feedback)).join('\n') || 'None' }, { name: 'Other Feedback:', - value: otherFeedbacks.map(feedback => getDisplayString(index++, feedback)).join('\n') || 'None' + value: otherFeedbacks.map(feedback => getSetupDisplayString(index++, feedback)).join('\n') || 'None' }]); return embed; } @@ -227,11 +232,16 @@ function sortMemberFeedbacks(voteSetup) { return { includedFeedbacks, unidentifiedFeedbacks, otherFeedbacks }; } -function getDisplayString(index, feedback) { +function getSetupDisplayString(index, feedback) { const tags = (`${feedback.dungeon?.tag || '??'} ${feedback.tier?.tag || '??'}`).padStart(11); return `\`${index}.\` \`${tags}\` ${feedback.feedbackURL} `; } +function getVoteDisplayString(index, feedback) { + const tags = (`${feedback.dungeon?.tag || '??'} ${feedback.tier?.tag || '??'}`).padStart(11); + return `\`${index}.\` \`${tags}\` ${feedback.feedbackURL} `; +} + function getRoleSettingsName(settings, role) { return Object.keys(settings.roles).find(roleName => settings.roles[roleName] == role.id); } @@ -248,10 +258,11 @@ async function sendVote(voteSetup, roleSettingsName) { .setAuthor({ name: `${member.displayName} to ${voteSetup.role.name}`, iconURL: member.user.displayAvatarURL({ dynamic: true }) }) .setDescription(`${member} \`${member.displayName}\``); if (embedStyling != undefined && embedStyling.image) { embed.setThumbnail(embedStyling.image); } - const feedbacks = voteSetup.feedbacks.filter(feedback => feedback.mentionedIDs.includes[member.id] && feedback.feedbackState == FeedbackState.Included); + const { includedFeedbacks } = sortMemberFeedbacks(voteSetup); + let index = 1; embed.addFields({ name: 'Feedback:', - value: getDisplayString(feedbacks) || 'None' + value: includedFeedbacks.map(feedback => getVoteDisplayString(index++, feedback)).join('\n') || 'None' }); const voteMessage = await voteSetup.channel.send({ embeds: [embed] }); for (const emoji of ['✅', '❌', '👀']) { voteMessage.react(emoji); } From 1a5a6fd562abfcf7274f2dee26b5dc20deedb8d6 Mon Sep 17 00:00:00 2001 From: tro2 <62850247+tro2@users.noreply.github.com> Date: Fri, 23 Feb 2024 01:26:22 -0800 Subject: [PATCH 6/8] Finalized vote changes Fixed up display of votes, added functionality to add and remove votes from included list --- commands/vote.js | 207 ++++++++++++++++++++++++------------------- data/voteConfig.json | 5 ++ 2 files changed, 123 insertions(+), 89 deletions(-) diff --git a/commands/vote.js b/commands/vote.js index 39c2aa8d..737148a8 100644 --- a/commands/vote.js +++ b/commands/vote.js @@ -2,7 +2,6 @@ const Discord = require('discord.js'); const voteConfig = require('../data/voteConfig.json'); const SlashArgType = require('discord-api-types/v10').ApplicationCommandOptionType; const { slashArg, slashCommandJSON } = require('../utils.js'); -const { createReactionRow } = require('../redis.js'); /** * @typedef FeedbackType * @property {string[]} flags @@ -23,6 +22,7 @@ const { createReactionRow } = require('../redis.js'); const FeedbackState = { Included: 0, Unidentified: 1, Other: 2 }; // notes: // the filtering goes based of the first line of the message, so if someone is giving feedback to multiple people in the same message (feedback on feedback for fullskips), it may get mistagged +// fullskip feedbacks are a little bit difficult to identify correctly, since they are not always tagged with the role module.exports = { name: 'vote', role: 'headrl', @@ -40,11 +40,16 @@ module.exports = { getSlashCommandData(guild) { return slashCommandJSON(this, guild); }, async execute(message, args, bot) { const { guild, channel } = message; + const role = message.options.getRole('role'); + const settingsRoleName = Object.keys(bot.settings[guild.id].roles).find(roleName => bot.settings[guild.id].roles[roleName] == role.id) || null; + const embedStyling = voteConfig.templates.find(template => template.settingRole === settingsRoleName) || null; const voteSetup = new VoteSetup({ channel, feedbacks: [], - role: message.options.getRole('role'), + role, + settingsRoleName, + embedStyling, members: message.options.getString('users').split(' ').concat(message.options.getVarargs() || []).map(member => guild.findMember(member)).filter(member => member != undefined) }); @@ -53,41 +58,45 @@ module.exports = { const embed = getSetupEmbed(voteSetup); const voteSetupButtons = generateVoteSetupButtons(bot); - const voteSetupMessage = await message.reply({ embeds: [embed], components: [voteSetupButtons] }); - createReactionRow(voteSetupMessage, module.exports.name, 'interactionHandler', voteSetupButtons, message.author, voteSetup.toJSON()); + const voteSetupMessage = await message.channel.send({ embeds: [embed], components: [voteSetupButtons] }); + if (!message.isInteraction) await message.delete(); + else { await message.deferReply(); await message.deleteReply(); } + + const interactionHandler = new Discord.InteractionCollector( + bot, + { + message: voteSetupMessage, + interactionType: Discord.InteractionType.MessageComponent, + componentType: Discord.ComponentType.Button, + filter: i => i.user.id == message.member.id + }); + interactionHandler.on('collect', interaction => this.interactionHandler(bot, interaction, voteSetup)); }, - async interactionHandler(bot, message, db, choice, voteSetupJSON, updateState) { - const interaction = message.interaction; // eslint-disable-line prefer-destructuring - const member = message.member; // eslint-disable-line prefer-destructuring - const voteSetup = VoteSetup.fromJSON(interaction.guild, voteSetupJSON); - switch (choice) { + async interactionHandler(bot, interaction, voteSetup) { + switch (interaction.customId) { case 'voteSend': interaction.deferUpdate(); - await sendVote(voteSetup, getRoleSettingsName(bot.settings[interaction.guild.id], voteSetup.role)); - console.log(typeof voteSetup.currentMemberIndex, voteSetup.currentMemberIndex, voteSetup.members.length); + await sendVote(voteSetup); voteSetup.currentMemberIndex++; - updateState('currentMemberIndex', voteSetup.currentMemberIndex.toString()); // TODO debug why number is being reset to 0 - console.log('post', typeof voteSetup.currentMemberIndex, voteSetup.currentMemberIndex, voteSetup.members.length); - if (voteSetup.currentMemberIndex > voteSetup.members.length) { - await message.delete(); + if (voteSetup.currentMemberIndex >= voteSetup.members.length) { + await interaction.message.delete(); } else { - await message.edit({ embeds: [getSetupEmbed(voteSetup)], components: [generateVoteSetupButtons(bot)] }); + await interaction.message.edit({ embeds: [getSetupEmbed(voteSetup)], components: [generateVoteSetupButtons(bot)] }); } break; case 'voteSkip': voteSetup.currentMemberIndex++; - updateState('currentMemberIndex', voteSetup.currentMemberIndex.toString()); if (voteSetup.currentMemberIndex >= voteSetup.members.length) { - await message.delete(); + await interaction.message.delete(); } else { - await message.edit({ embeds: [getSetupEmbed(voteSetup)], components: [generateVoteSetupButtons(bot)] }); + await interaction.message.edit({ embeds: [getSetupEmbed(voteSetup)], components: [generateVoteSetupButtons(bot)] }); } break; case 'voteAbort': - await message.delete(); // TODO does this clean up this interaction? + await interaction.message.delete(); break; case 'voteAddFeedbacks': { - // TODO add custom value with modal response + // idaa: add custom value with modal response // add function to grab custom feedback by message ID and process it // also make sure first 24 feedbacks show up in the select panel, since last is custom const { includedFeedbacks, unidentifiedFeedbacks, otherFeedbacks } = sortMemberFeedbacks(voteSetup); @@ -106,31 +115,62 @@ module.exports = { const actionRow = new Discord.ActionRowBuilder() .addComponents(addSelectionMenu); - const reply = await interaction.reply({ content: 'Test string', components: [actionRow], ephemeral: true, fetchReply: true }); - let replyResponse; + const reply = await interaction.reply({ content: 'Select the feedbacks to add, times out after 2 minutes.', components: [actionRow], ephemeral: true, fetchReply: true }); try { // what is CollectorOptions.dispose? https://discord.js.org/docs/packages/discord.js/14.14.1/CollectorOptions:Interface#dispose - replyResponse = await reply.awaitMessageComponent({ componentType: Discord.ComponentType.StringSelect, time: 120000, filter: i => i.user.id == member.id }); + const replyResponse = await reply.awaitMessageComponent({ componentType: Discord.ComponentType.StringSelect, time: 120000, filter: i => i.user.id == interaction.member.id }); + await replyResponse.deferUpdate(); + // array of message IDs + replyResponse.values.forEach(messageID => { + const feedback = voteSetup.feedbacks.find(feedback => feedback.messageID == messageID); + feedback.feedbackState = FeedbackState.Included; + }); + await interaction.deleteReply(reply.id); } catch (error) { - await reply.edit({ content: 'Timed out', components: [] }); + interaction.editReply({ content: 'Timed out', components: [] }); break; } - await replyResponse.deferUpdate(); - // array of message IDs - replyResponse.values.forEach(messageID => { - const feedback = voteSetup.feedbacks.find(feedback => feedback.messageID == messageID); - feedback.feedbackState = FeedbackState.Included; - }); - updateState('feedbacks', voteSetup.feedbacks); - await interaction.deleteReply(reply.id); await interaction.message.edit({ embeds: [getSetupEmbed(voteSetup)], components: [generateVoteSetupButtons(bot)] }); break; } case 'voteRemoveFeedbacks': { - // copy above code and change the filter to show only included feedbacks, no custom option on selectPanel + const { includedFeedbacks } = sortMemberFeedbacks(voteSetup); + if (includedFeedbacks.length == 0) { + interaction.deferUpdate(); + break; + } + let index = 1; + const addSelectionMenu = new Discord.StringSelectMenuBuilder() + .setCustomId('voteRemoveFeedbacks') + .setPlaceholder('Feedbacks to remove') + .setMinValues(1) + .setMaxValues(includedFeedbacks.length) + .addOptions(includedFeedbacks.map(feedback => ({ + label: `${index++}. ${feedback.dungeon?.tag || '??'} ${feedback.tier?.tag || '??'}`, + value: feedback.messageID + }))); + + const actionRow = new Discord.ActionRowBuilder() + .addComponents(addSelectionMenu); + + const reply = await interaction.reply({ content: 'Select the feedbacks to remove, times out after 2 minutes.', components: [actionRow], ephemeral: true, fetchReply: true }); + try { + const replyResponse = await reply.awaitMessageComponent({ componentType: Discord.ComponentType.StringSelect, time: 120000, filter: i => i.user.id == interaction.member.id }); + await replyResponse.deferUpdate(); + // array of message IDs + replyResponse.values.forEach(messageID => { + const feedback = voteSetup.feedbacks.find(feedback => feedback.messageID == messageID); + feedback.feedbackState = FeedbackState.Other; + }); + await interaction.deleteReply(reply.id); + } catch (error) { + interaction.editReply({ content: 'Timed out', components: [] }); + break; + } + await interaction.message.edit({ embeds: [getSetupEmbed(voteSetup)], components: [generateVoteSetupButtons(bot)] }); break; } default: - console.log('Invalid choice', choice); + console.log('Invalid choice'); break; } } @@ -149,34 +189,18 @@ class VoteSetup { * @param {FeedbackData[]} options.feedbacks - the feedbacks for the vote. * @param {Array} options.members - The members to be voted on. * @param {Role} options.role - The role associated with the vote. + * @param {string} options.settingsRoleName - The role name associated with the vote. + * @param {Object} options.embedStyling - The embed styling for the vote. */ - constructor({ channel, feedbacks, members, role }) { + constructor({ channel, feedbacks, members, role, settingsRoleName, embedStyling }) { this.channel = channel; this.feedbacks = feedbacks; this.members = members; this.role = role; + this.settingsRoleName = settingsRoleName; + this.embedStyling = embedStyling; this.currentMemberIndex = 0; } - - static fromJSON(guild, json) { - return new this({ - ...json, - currentMemberIndex: parseInt(json.currentMemberIndex), // TODO check if this is necessary, for some reason it's being reset to 0 but the setup panel is working fine - channel: guild.channels.cache.get(json.channel), - members: json.members.map(memberId => guild.members.cache.get(memberId)), - role: guild.roles.cache.get(json.role) - }); - } - - toJSON() { - return { - channel: this.channel.id, - feedbacks: this.feedbacks, - members: this.members.map(m => m.id), - role: this.role.id, - currentMemberIndex: this.currentMemberIndex - }; - } } /** @@ -188,29 +212,23 @@ class VoteSetup { function getSetupEmbed(voteSetup) { const member = voteSetup.members[voteSetup.currentMemberIndex]; const embed = new Discord.EmbedBuilder() - .setColor(member.roles.highest.hexColor) + .setColor(voteSetup.embedStyling ? voteSetup.embedStyling.embedColor : voteSetup.role.hexColor) .setAuthor({ name: `Vote Configuration ${voteSetup.currentMemberIndex + 1}/${voteSetup.members.length}`, iconURL: member.user.displayAvatarURL({ dynamic: true }) }) .setDescription(` + Remaining members: ${voteSetup.members.slice(voteSetup.currentMemberIndex + 1).map(member => `<@${member.id}>`).join(' ') || 'None'} + This vote will be for to <@${member.id}> to ${voteSetup.role} `); const { includedFeedbacks, unidentifiedFeedbacks, otherFeedbacks } = sortMemberFeedbacks(voteSetup); - let index = 1; - // TODO add length check for 1024 character limit, add feedback two as name and continue list - embed.addFields([{ - name: 'Included Feedback:', - value: includedFeedbacks.map(feedback => getSetupDisplayString(index++, feedback)).join('\n') || 'None' - }, - { - name: 'Unknown Tags:', - value: unidentifiedFeedbacks.map(feedback => getSetupDisplayString(index++, feedback)).join('\n') || 'None' - }, - { - name: 'Other Feedback:', - value: otherFeedbacks.map(feedback => getSetupDisplayString(index++, feedback)).join('\n') || 'None' - }]); + const feedbackFields = [ + ...generateDisplayFields(includedFeedbacks, 1, 'Included Feedback:', getSetupDisplayString), + ...generateDisplayFields(unidentifiedFeedbacks, 1 + includedFeedbacks.length, 'Unidentified Feedback:', getSetupDisplayString), + ...generateDisplayFields(otherFeedbacks, 1 + includedFeedbacks.length + unidentifiedFeedbacks.length, 'Other Feedback:', getSetupDisplayString) + ]; + embed.addFields(feedbackFields); return embed; } @@ -242,33 +260,45 @@ function getVoteDisplayString(index, feedback) { return `\`${index}.\` \`${tags}\` ${feedback.feedbackURL} `; } -function getRoleSettingsName(settings, role) { - return Object.keys(settings.roles).find(roleName => settings.roles[roleName] == role.id); +function generateDisplayFields(feedbacks, startIndex, name, getDisplayString) { + const feedbackFields = []; + let currentField = { + name, + value: '' + }; + + feedbacks.forEach(feedback => { + const feedbackString = getDisplayString(startIndex++, feedback); + if (currentField.value.length + feedbackString.length > 1024) { + feedbackFields.push(currentField); + currentField = { + name, + value: feedbackString + }; + } else { + currentField.value += feedbackString + '\n'; + } + }); + + if (currentField.value !== '') feedbackFields.push(currentField); + else feedbackFields.push({ name, value: 'None' }); + return feedbackFields; } -async function sendVote(voteSetup, roleSettingsName) { - if (!roleSettingsName) { return; } // TODO add error message for missing role settings name - const embedStyling = voteConfig.templates - .find(template => template.settingRole === roleSettingsName); - let embedColor = voteSetup.role.hexColor; - if (embedStyling != undefined && embedStyling.embedColor) { embedColor = embedStyling.embedColor; } +async function sendVote(voteSetup) { const member = voteSetup.members[voteSetup.currentMemberIndex]; const embed = new Discord.EmbedBuilder() - .setColor(embedColor) + .setColor(voteSetup.embedStyling ? voteSetup.embedStyling.embedColor : voteSetup.role.hexColor) .setAuthor({ name: `${member.displayName} to ${voteSetup.role.name}`, iconURL: member.user.displayAvatarURL({ dynamic: true }) }) .setDescription(`${member} \`${member.displayName}\``); - if (embedStyling != undefined && embedStyling.image) { embed.setThumbnail(embedStyling.image); } + if (voteSetup.embedStyling) { embed.setThumbnail(voteSetup.embedStyling.image); } const { includedFeedbacks } = sortMemberFeedbacks(voteSetup); - let index = 1; - embed.addFields({ - name: 'Feedback:', - value: includedFeedbacks.map(feedback => getVoteDisplayString(index++, feedback)).join('\n') || 'None' - }); + embed.addFields(generateDisplayFields(includedFeedbacks, 1, 'Feedback:', getVoteDisplayString)); const voteMessage = await voteSetup.channel.send({ embeds: [embed] }); for (const emoji of ['✅', '❌', '👀']) { voteMessage.react(emoji); } } -function generateVoteSetupButtons(bot) { // TODO disable remove button if no feedbacks are included +function generateVoteSetupButtons(bot) { return new Discord.ActionRowBuilder() .addComponents([ new Discord.ButtonBuilder() @@ -325,11 +355,10 @@ async function getFeedbacks(guild, settings, voteSetup) { const firstLine = message.content.split('\n')[0].toLowerCase(); // Convert to lowercase for searching const dungeon = voteConfig.feedbackTypes.dungeon.find(dungeon => dungeon.flags.some(flag => firstLine.includes(flag))); const tier = voteConfig.feedbackTypes.tier.find(tier => tier.flags.some(flag => firstLine.includes(flag))); - const roleSettingsName = getRoleSettingsName(settings, voteSetup.role); // TODO what happens if roleSettingsName is undefined? let feedbackState = FeedbackState.Other; if (!tier || !dungeon) { feedbackState = FeedbackState.Unidentified; - } else if (tier.roles.includes(roleSettingsName) && dungeon.roles.includes(roleSettingsName)) { + } else if (tier.roles.includes(voteSetup.settingsRoleName) && dungeon.roles.includes(voteSetup.settingsRoleName)) { feedbackState = FeedbackState.Included; } return { diff --git a/data/voteConfig.json b/data/voteConfig.json index d34b4713..56f54d83 100644 --- a/data/voteConfig.json +++ b/data/voteConfig.json @@ -108,6 +108,11 @@ "tag": "Ghost", "roles": ["almostHallsBanner", "almostOryxBanner", "almostShattersBanner", "hallsBanner", "oryxBanner", "shattersBanner", "vetHallsBanner", "veteranOryxBanner", "vetShattersBanner", "fullskipBanner", "vetFullskipBanner"] }, + { + "flags": ["fullskip", "fs", "skip", "fskip"], + "tag": " ", + "roles": ["fullskipBanner", "vetFullskipBanner"] + }, { "flags": ["vtrl", "vrl", "rl"], "tag": "VTRL", From 5eccc8662f5623db8b65bc9135e6368bb51e9ccd Mon Sep 17 00:00:00 2001 From: tro2 <62850247+tro2@users.noreply.github.com> Date: Fri, 23 Feb 2024 12:54:37 -0800 Subject: [PATCH 7/8] Updated vote Changed args to list of users Fixed a bug with failing to add feedbacks when list length is 0 Changed display of votes to use emojis --- commands/vote.js | 58 +++++++++++++++++++++++++++----------------- data/voteConfig.json | 21 ++++++++++++---- 2 files changed, 52 insertions(+), 27 deletions(-) diff --git a/commands/vote.js b/commands/vote.js index 737148a8..71916e60 100644 --- a/commands/vote.js +++ b/commands/vote.js @@ -2,6 +2,7 @@ const Discord = require('discord.js'); const voteConfig = require('../data/voteConfig.json'); const SlashArgType = require('discord-api-types/v10').ApplicationCommandOptionType; const { slashArg, slashCommandJSON } = require('../utils.js'); + /** * @typedef FeedbackType * @property {string[]} flags @@ -30,11 +31,17 @@ module.exports = { description: 'Puts up a vote for the person based on your role input', args: [ slashArg(SlashArgType.Role, 'role', { - description: 'The role that the vote is for' + description: 'The role that the vote is for', + required: true }), - slashArg(SlashArgType.String, 'users', { - description: 'The users (space seperated) that will be up for a vote' + slashArg(SlashArgType.User, 'user', { + description: 'User to put up for vote', + required: true }), + ...Array(14).fill(0).map((_, idx) => slashArg(SlashArgType.User, `user${idx + 2}`, { + description: 'User to put up for vote', + required: false + })), ], varargs: true, getSlashCommandData(guild) { return slashCommandJSON(this, guild); }, @@ -48,9 +55,11 @@ module.exports = { channel, feedbacks: [], role, + storedEmojis: bot.storedEmojis, settingsRoleName, embedStyling, - members: message.options.getString('users').split(' ').concat(message.options.getVarargs() || []).map(member => guild.findMember(member)).filter(member => member != undefined) + // members: message.options.getString('users').split(' ').concat(message.options.getVarargs() || []).map(member => guild.findMember(member)).filter(member => member != undefined) + members: [message.options.getMember('user'), ...Array(7).fill(0).map((_, idx) => message.options.getMember(`user${idx + 2}`))].filter(m => m) }); if (voteSetup.members.length == 0) { return await message.reply('No members found.'); } @@ -58,9 +67,9 @@ module.exports = { const embed = getSetupEmbed(voteSetup); const voteSetupButtons = generateVoteSetupButtons(bot); - const voteSetupMessage = await message.channel.send({ embeds: [embed], components: [voteSetupButtons] }); + const voteSetupMessage = await message.channel.send({ embeds: [embed], components: [voteSetupButtons], fetchReply: true, ephemeral: true }); if (!message.isInteraction) await message.delete(); - else { await message.deferReply(); await message.deleteReply(); } + else { message.deferReply(); message.deleteReply(); } const interactionHandler = new Discord.InteractionCollector( bot, @@ -101,6 +110,10 @@ module.exports = { // also make sure first 24 feedbacks show up in the select panel, since last is custom const { includedFeedbacks, unidentifiedFeedbacks, otherFeedbacks } = sortMemberFeedbacks(voteSetup); const addableFeedbacks = unidentifiedFeedbacks.concat(otherFeedbacks).slice(0, 25); + if (addableFeedbacks.length == 0) { + interaction.deferUpdate(); + break; + } let index = includedFeedbacks.length + 1; const addSelectionMenu = new Discord.StringSelectMenuBuilder() .setCustomId('voteAddFeedbacks') @@ -111,10 +124,8 @@ module.exports = { label: `${index++}. ${feedback.dungeon?.tag || '??'} ${feedback.tier?.tag || '??'}`, value: feedback.messageID }))); - const actionRow = new Discord.ActionRowBuilder() .addComponents(addSelectionMenu); - const reply = await interaction.reply({ content: 'Select the feedbacks to add, times out after 2 minutes.', components: [actionRow], ephemeral: true, fetchReply: true }); try { // what is CollectorOptions.dispose? https://discord.js.org/docs/packages/discord.js/14.14.1/CollectorOptions:Interface#dispose const replyResponse = await reply.awaitMessageComponent({ componentType: Discord.ComponentType.StringSelect, time: 120000, filter: i => i.user.id == interaction.member.id }); @@ -166,7 +177,7 @@ module.exports = { interaction.editReply({ content: 'Timed out', components: [] }); break; } - await interaction.message.edit({ embeds: [getSetupEmbed(voteSetup)], components: [generateVoteSetupButtons(bot)] }); + await interaction.update({ embeds: [getSetupEmbed(voteSetup)], components: [generateVoteSetupButtons(bot)] }); break; } default: @@ -192,11 +203,12 @@ class VoteSetup { * @param {string} options.settingsRoleName - The role name associated with the vote. * @param {Object} options.embedStyling - The embed styling for the vote. */ - constructor({ channel, feedbacks, members, role, settingsRoleName, embedStyling }) { + constructor({ channel, feedbacks, members, role, storedEmojis, settingsRoleName, embedStyling }) { this.channel = channel; this.feedbacks = feedbacks; this.members = members; this.role = role; + this.storedEmojis = storedEmojis; this.settingsRoleName = settingsRoleName; this.embedStyling = embedStyling; this.currentMemberIndex = 0; @@ -224,9 +236,9 @@ function getSetupEmbed(voteSetup) { `); const { includedFeedbacks, unidentifiedFeedbacks, otherFeedbacks } = sortMemberFeedbacks(voteSetup); const feedbackFields = [ - ...generateDisplayFields(includedFeedbacks, 1, 'Included Feedback:', getSetupDisplayString), - ...generateDisplayFields(unidentifiedFeedbacks, 1 + includedFeedbacks.length, 'Unidentified Feedback:', getSetupDisplayString), - ...generateDisplayFields(otherFeedbacks, 1 + includedFeedbacks.length + unidentifiedFeedbacks.length, 'Other Feedback:', getSetupDisplayString) + ...generateDisplayFields(includedFeedbacks, 1, 'Included Feedback:', getSetupDisplayString, voteSetup.storedEmojis), + ...generateDisplayFields(unidentifiedFeedbacks, 1 + includedFeedbacks.length, 'Unidentified Feedback:', getSetupDisplayString, voteSetup.storedEmojis), + ...generateDisplayFields(otherFeedbacks, 1 + includedFeedbacks.length + unidentifiedFeedbacks.length, 'Other Feedback:', getSetupDisplayString, voteSetup.storedEmojis) ]; embed.addFields(feedbackFields); return embed; @@ -250,17 +262,19 @@ function sortMemberFeedbacks(voteSetup) { return { includedFeedbacks, unidentifiedFeedbacks, otherFeedbacks }; } -function getSetupDisplayString(index, feedback) { - const tags = (`${feedback.dungeon?.tag || '??'} ${feedback.tier?.tag || '??'}`).padStart(11); - return `\`${index}.\` \`${tags}\` ${feedback.feedbackURL} `; +function getSetupDisplayString(index, feedback, storedEmojis) { + const emojiString = storedEmojis[feedback.dungeon?.emoji]?.text || '`??`'; + const tagString = `${feedback.tier?.tag || '??'}`.padStart(6); + return `\`${index}.\` ${emojiString} \`${tagString}\` ${feedback.feedbackURL} `; } -function getVoteDisplayString(index, feedback) { - const tags = (`${feedback.dungeon?.tag || '??'} ${feedback.tier?.tag || '??'}`).padStart(11); - return `\`${index}.\` \`${tags}\` ${feedback.feedbackURL} `; +function getVoteDisplayString(index, feedback, storedEmojis) { + const emojiString = storedEmojis[feedback.dungeon?.emoji]?.text || '`??`'; + const tagString = `${feedback.tier?.tag || '??'}`.padStart(6); + return `\`${index}.\` ${emojiString} \`${tagString}\` ${feedback.feedbackURL} `; } -function generateDisplayFields(feedbacks, startIndex, name, getDisplayString) { +function generateDisplayFields(feedbacks, startIndex, name, getDisplayString, storedEmojis) { const feedbackFields = []; let currentField = { name, @@ -268,7 +282,7 @@ function generateDisplayFields(feedbacks, startIndex, name, getDisplayString) { }; feedbacks.forEach(feedback => { - const feedbackString = getDisplayString(startIndex++, feedback); + const feedbackString = getDisplayString(startIndex++, feedback, storedEmojis); if (currentField.value.length + feedbackString.length > 1024) { feedbackFields.push(currentField); currentField = { @@ -293,7 +307,7 @@ async function sendVote(voteSetup) { .setDescription(`${member} \`${member.displayName}\``); if (voteSetup.embedStyling) { embed.setThumbnail(voteSetup.embedStyling.image); } const { includedFeedbacks } = sortMemberFeedbacks(voteSetup); - embed.addFields(generateDisplayFields(includedFeedbacks, 1, 'Feedback:', getVoteDisplayString)); + embed.addFields(generateDisplayFields(includedFeedbacks, 1, 'Feedback:', getVoteDisplayString, voteSetup.storedEmojis)); const voteMessage = await voteSetup.channel.send({ embeds: [embed] }); for (const emoji of ['✅', '❌', '👀']) { voteMessage.react(emoji); } } diff --git a/data/voteConfig.json b/data/voteConfig.json index 56f54d83..88c679e1 100644 --- a/data/voteConfig.json +++ b/data/voteConfig.json @@ -60,38 +60,44 @@ { "flags": ["void", "halls", "mbc"], "tag": "Void", + "emoji": "void", "roles": ["almostHallsBanner", "hallsBanner", "vetHallsBanner"] }, { "flags": ["cult"], "tag": "Cult", + "emoji": "cultist", "roles": ["almostHallsBanner", "hallsBanner", "vetHallsBanner"] }, { "flags": ["reading", "mapreading"], "tag": "Map", + "emoji": "map", "roles": ["almostHallsBanner", "hallsBanner", "vetHallsBanner", "fullskipBanner", "vetFullskipBanner"] }, { "flags": ["fullskip", "fs", "skip", "fskip"], "tag": "Fskip", + "emoji": "fullskipIcon", "roles": ["fullskipBanner", "vetFullskipBanner"] }, { "flags": ["oryx", "o3"], "tag": "Oryx", + "emoji": "oryx", "roles": ["almostOryxBanner", "oryxBanner", "veteranOryxBanner"] }, { "flags": ["shatters", "shatts"], "tag": "Shatts", + "emoji": "forgottenKing", "roles": ["almostShattersBanner", "shattersBanner", "vetShattersBanner"] }], "tier": [ { - "flags": ["trl"], + "flags": ["trl", "trial"], "tag": "TRL", - "roles": ["almostHallsBanner", "almostOryxBanner", "almostShattersBanner"] + "roles": ["almostHallsBanner", "almostOryxBanner", "almostShattersBanner", "fullskipBanner"] }, { "flags": ["arl"], @@ -109,14 +115,19 @@ "roles": ["almostHallsBanner", "almostOryxBanner", "almostShattersBanner", "hallsBanner", "oryxBanner", "shattersBanner", "vetHallsBanner", "veteranOryxBanner", "vetShattersBanner", "fullskipBanner", "vetFullskipBanner"] }, { - "flags": ["fullskip", "fs", "skip", "fskip"], - "tag": " ", - "roles": ["fullskipBanner", "vetFullskipBanner"] + "flags": ["vouch"], + "tag": "Vouch", + "roles": ["fullskipBanner", "vetFullskipBanner", "veteranOryxBanner", "vetShattersBanner", "vetHallsBanner"] }, { "flags": ["vtrl", "vrl", "rl"], "tag": "VTRL", "roles": ["vetHallsBanner", "veteranOryxBanner", "vetShattersBanner"] + }, + { + "flags": ["fullskip", "fs", "skip", "fskip"], + "tag": " ", + "roles": ["fullskipBanner", "vetFullskipBanner"] } ] } From d830ef00dcca2c2829a284c3aeaa640a1a7cede2 Mon Sep 17 00:00:00 2001 From: tro2 <62850247+tro2@users.noreply.github.com> Date: Fri, 23 Feb 2024 16:23:10 -0800 Subject: [PATCH 8/8] Update vote.js changed votesetup from class to an object with typedef improved documentation altered display of unidentified dungeon --- commands/vote.js | 246 ++++++++++++++++++++++++----------------------- 1 file changed, 128 insertions(+), 118 deletions(-) diff --git a/commands/vote.js b/commands/vote.js index 71916e60..a3f18088 100644 --- a/commands/vote.js +++ b/commands/vote.js @@ -19,6 +19,17 @@ const { slashArg, slashCommandJSON } = require('../utils.js'); * @property {number} timeStamp * @property {FeedbackState} feedbackState */ +/** + * @typedef VoteSetup + * @property {Discord.TextChannel} channel + * @property {FeedbackData[]} feedbacks + * @property {Discord.Role} role + * @property {Object} storedEmojis + * @property {string} settingsRoleName + * @property {Object} embedStyling + * @property {Discord.Member[]} members + * @property {number} currentMemberIndex + */ const FeedbackState = { Included: 0, Unidentified: 1, Other: 2 }; // notes: @@ -38,7 +49,7 @@ module.exports = { description: 'User to put up for vote', required: true }), - ...Array(14).fill(0).map((_, idx) => slashArg(SlashArgType.User, `user${idx + 2}`, { + ...Array(14).fill(0).map((_, index) => slashArg(SlashArgType.User, `user${index + 2}`, { description: 'User to put up for vote', required: false })), @@ -48,22 +59,22 @@ module.exports = { async execute(message, args, bot) { const { guild, channel } = message; const role = message.options.getRole('role'); - const settingsRoleName = Object.keys(bot.settings[guild.id].roles).find(roleName => bot.settings[guild.id].roles[roleName] == role.id) || null; - const embedStyling = voteConfig.templates.find(template => template.settingRole === settingsRoleName) || null; + const settingsRoleName = Object.keys(bot.settings[guild.id].roles).find(roleName => bot.settings[guild.id].roles[roleName] == role.id); + const embedStyling = voteConfig.templates.find(template => template.settingRole === settingsRoleName); - const voteSetup = new VoteSetup({ + const voteSetup = { channel, feedbacks: [], role, storedEmojis: bot.storedEmojis, settingsRoleName, embedStyling, - // members: message.options.getString('users').split(' ').concat(message.options.getVarargs() || []).map(member => guild.findMember(member)).filter(member => member != undefined) - members: [message.options.getMember('user'), ...Array(7).fill(0).map((_, idx) => message.options.getMember(`user${idx + 2}`))].filter(m => m) - }); + members: [message.options.getMember('user'), ...Array(14).fill(0).map((_, index) => message.options.getMember(`user${index + 2}`))].filter(m => m), + currentMemberIndex: 0 + }; if (voteSetup.members.length == 0) { return await message.reply('No members found.'); } - await getFeedbacks(guild, bot.settings[guild.id], voteSetup); + voteSetup.feedbacks = await getFeedbacks(guild, bot.settings[guild.id], voteSetup.members, settingsRoleName); const embed = getSetupEmbed(voteSetup); const voteSetupButtons = generateVoteSetupButtons(bot); @@ -81,6 +92,12 @@ module.exports = { }); interactionHandler.on('collect', interaction => this.interactionHandler(bot, interaction, voteSetup)); }, + /** + * Handles interactions for the vote setup buttons + * @param {Discord.Client} bot client with additions + * @param {Discord.ButtonInteraction} interaction Receieved interaction, guaranteed to be from the command author + * @param {VoteSetup} voteSetup vote setup data + */ async interactionHandler(bot, interaction, voteSetup) { switch (interaction.customId) { case 'voteSend': @@ -110,121 +127,91 @@ module.exports = { // also make sure first 24 feedbacks show up in the select panel, since last is custom const { includedFeedbacks, unidentifiedFeedbacks, otherFeedbacks } = sortMemberFeedbacks(voteSetup); const addableFeedbacks = unidentifiedFeedbacks.concat(otherFeedbacks).slice(0, 25); + interaction.deferUpdate(); if (addableFeedbacks.length == 0) { - interaction.deferUpdate(); break; } let index = includedFeedbacks.length + 1; - const addSelectionMenu = new Discord.StringSelectMenuBuilder() - .setCustomId('voteAddFeedbacks') - .setPlaceholder('Feedbacks to add') - .setMinValues(1) - .setMaxValues(addableFeedbacks.length) - .addOptions(addableFeedbacks.map(feedback => ({ - label: `${index++}. ${feedback.dungeon?.tag || '??'} ${feedback.tier?.tag || '??'}`, - value: feedback.messageID - }))); - const actionRow = new Discord.ActionRowBuilder() - .addComponents(addSelectionMenu); - const reply = await interaction.reply({ content: 'Select the feedbacks to add, times out after 2 minutes.', components: [actionRow], ephemeral: true, fetchReply: true }); - try { // what is CollectorOptions.dispose? https://discord.js.org/docs/packages/discord.js/14.14.1/CollectorOptions:Interface#dispose - const replyResponse = await reply.awaitMessageComponent({ componentType: Discord.ComponentType.StringSelect, time: 120000, filter: i => i.user.id == interaction.member.id }); - await replyResponse.deferUpdate(); - // array of message IDs - replyResponse.values.forEach(messageID => { - const feedback = voteSetup.feedbacks.find(feedback => feedback.messageID == messageID); - feedback.feedbackState = FeedbackState.Included; - }); - await interaction.deleteReply(reply.id); - } catch (error) { - interaction.editReply({ content: 'Timed out', components: [] }); - break; - } + const selectRow = new Discord.ActionRowBuilder().addComponents( + new Discord.StringSelectMenuBuilder() + .setCustomId('voteAddFeedbacks') + .setPlaceholder('Feedbacks to add') + .setMinValues(1) + .setMaxValues(addableFeedbacks.length) + .addOptions(addableFeedbacks.map(feedback => ({ + label: `${index++}. ${feedback.dungeon?.tag || '??'} ${feedback.tier?.tag || '??'}`, + value: feedback.messageID + })))); + const buttonRow = new Discord.ActionRowBuilder().addComponents( + new Discord.ButtonBuilder() + .setLabel('Back') + .setStyle(4) + .setCustomId('goBack')); + await interaction.message.edit({ embeds: [getSetupEmbed(voteSetup)], components: [buttonRow, selectRow] }); + try { + const selectMenuResponse = await interaction.message.awaitMessageComponent({ time: 120000, filter: i => i.user.id == interaction.member.id }); + await selectMenuResponse.deferUpdate(); + if (selectMenuResponse.customId == 'voteAddFeedbacks') { + selectMenuResponse.values.forEach(messageID => { + const feedback = voteSetup.feedbacks.find(feedback => feedback.messageID == messageID); + feedback.feedbackState = FeedbackState.Included; + }); + } + } catch (error) { } await interaction.message.edit({ embeds: [getSetupEmbed(voteSetup)], components: [generateVoteSetupButtons(bot)] }); break; } case 'voteRemoveFeedbacks': { const { includedFeedbacks } = sortMemberFeedbacks(voteSetup); + interaction.deferUpdate(); if (includedFeedbacks.length == 0) { - interaction.deferUpdate(); break; } let index = 1; - const addSelectionMenu = new Discord.StringSelectMenuBuilder() - .setCustomId('voteRemoveFeedbacks') - .setPlaceholder('Feedbacks to remove') - .setMinValues(1) - .setMaxValues(includedFeedbacks.length) - .addOptions(includedFeedbacks.map(feedback => ({ - label: `${index++}. ${feedback.dungeon?.tag || '??'} ${feedback.tier?.tag || '??'}`, - value: feedback.messageID - }))); - - const actionRow = new Discord.ActionRowBuilder() - .addComponents(addSelectionMenu); - - const reply = await interaction.reply({ content: 'Select the feedbacks to remove, times out after 2 minutes.', components: [actionRow], ephemeral: true, fetchReply: true }); + const selectRow = new Discord.ActionRowBuilder().addComponents( + new Discord.StringSelectMenuBuilder() + .setCustomId('voteRemoveFeedbacks') + .setPlaceholder('Feedbacks to remove') + .setMinValues(1) + .setMaxValues(includedFeedbacks.length) + .addOptions(includedFeedbacks.map(feedback => ({ + label: `${index++}. ${feedback.dungeon?.tag || '??'} ${feedback.tier?.tag || '??'}`, + value: feedback.messageID + })))); + const buttonRow = new Discord.ActionRowBuilder().addComponents( + new Discord.ButtonBuilder() + .setLabel('Back') + .setStyle(4) + .setCustomId('goBack')); + await interaction.message.edit({ embeds: [getSetupEmbed(voteSetup)], components: [buttonRow, selectRow] }); try { - const replyResponse = await reply.awaitMessageComponent({ componentType: Discord.ComponentType.StringSelect, time: 120000, filter: i => i.user.id == interaction.member.id }); - await replyResponse.deferUpdate(); - // array of message IDs - replyResponse.values.forEach(messageID => { - const feedback = voteSetup.feedbacks.find(feedback => feedback.messageID == messageID); - feedback.feedbackState = FeedbackState.Other; - }); - await interaction.deleteReply(reply.id); - } catch (error) { - interaction.editReply({ content: 'Timed out', components: [] }); - break; - } - await interaction.update({ embeds: [getSetupEmbed(voteSetup)], components: [generateVoteSetupButtons(bot)] }); + const selectMenuResponse = await interaction.message.awaitMessageComponent({ time: 120000, filter: i => i.user.id == interaction.member.id }); + await selectMenuResponse.deferUpdate(); + if (selectMenuResponse.customId == 'voteRemoveFeedbacks') { + selectMenuResponse.values.forEach(messageID => { + const feedback = voteSetup.feedbacks.find(feedback => feedback.messageID == messageID); + feedback.feedbackState = FeedbackState.Other; + }); + } + } catch (error) { } + await interaction.message.edit({ embeds: [getSetupEmbed(voteSetup)], components: [generateVoteSetupButtons(bot)] }); break; } default: - console.log('Invalid choice'); break; } } }; /** - * Represents vote data to be stored in case of restarts - * @class - */ -class VoteSetup { - /** - * Constructs a new VoteSetup object. - * @constructor - * @param {Object} options - The vote configuration options. - * @param {Channel} options.channel - The channel for the vote. - * @param {FeedbackData[]} options.feedbacks - the feedbacks for the vote. - * @param {Array} options.members - The members to be voted on. - * @param {Role} options.role - The role associated with the vote. - * @param {string} options.settingsRoleName - The role name associated with the vote. - * @param {Object} options.embedStyling - The embed styling for the vote. - */ - constructor({ channel, feedbacks, members, role, storedEmojis, settingsRoleName, embedStyling }) { - this.channel = channel; - this.feedbacks = feedbacks; - this.members = members; - this.role = role; - this.storedEmojis = storedEmojis; - this.settingsRoleName = settingsRoleName; - this.embedStyling = embedStyling; - this.currentMemberIndex = 0; - } -} - -/** - * Generates the confirmation message for the vote configuration. - * @param {Discord.Member} member - The member generating the confirmation message. - * @param {Object} emojiDatabase - The emoji database. - * @returns {string} The confirmation message for the vote configuration. + * Generates the embed for the vote setup + * @param {VoteSetup} voteSetup The vote setup data + * @returns {Discord.EmbedBuilder} The embed for the vote setup */ function getSetupEmbed(voteSetup) { const member = voteSetup.members[voteSetup.currentMemberIndex]; const embed = new Discord.EmbedBuilder() - .setColor(voteSetup.embedStyling ? voteSetup.embedStyling.embedColor : voteSetup.role.hexColor) + .setColor(voteSetup.embedStyling?.embedColor || voteSetup.role.hexColor) .setAuthor({ name: `Vote Configuration ${voteSetup.currentMemberIndex + 1}/${voteSetup.members.length}`, iconURL: member.user.displayAvatarURL({ dynamic: true }) @@ -236,14 +223,19 @@ function getSetupEmbed(voteSetup) { `); const { includedFeedbacks, unidentifiedFeedbacks, otherFeedbacks } = sortMemberFeedbacks(voteSetup); const feedbackFields = [ - ...generateDisplayFields(includedFeedbacks, 1, 'Included Feedback:', getSetupDisplayString, voteSetup.storedEmojis), - ...generateDisplayFields(unidentifiedFeedbacks, 1 + includedFeedbacks.length, 'Unidentified Feedback:', getSetupDisplayString, voteSetup.storedEmojis), - ...generateDisplayFields(otherFeedbacks, 1 + includedFeedbacks.length + unidentifiedFeedbacks.length, 'Other Feedback:', getSetupDisplayString, voteSetup.storedEmojis) + ...generateDisplayFields(includedFeedbacks, 1, 'Included Feedback:', voteSetup.storedEmojis), + ...generateDisplayFields(unidentifiedFeedbacks, 1 + includedFeedbacks.length, 'Unidentified Feedback:', voteSetup.storedEmojis), + ...generateDisplayFields(otherFeedbacks, 1 + includedFeedbacks.length + unidentifiedFeedbacks.length, 'Other Feedback:', voteSetup.storedEmojis) ]; embed.addFields(feedbackFields); return embed; } +/** + * Sorts the feedbacks for the current member, and returns the arrays in descending order + * @param {VoteSetup} voteSetup The vote setup data + * @returns {{includedFeedbacks: FeedbackData[], unidentifiedFeedbacks: FeedbackData[], otherFeedbacks: FeedbackData[]}} The sorted feedbacks + */ function sortMemberFeedbacks(voteSetup) { const member = voteSetup.members[voteSetup.currentMemberIndex]; const includedFeedbacks = []; @@ -262,31 +254,40 @@ function sortMemberFeedbacks(voteSetup) { return { includedFeedbacks, unidentifiedFeedbacks, otherFeedbacks }; } -function getSetupDisplayString(index, feedback, storedEmojis) { - const emojiString = storedEmojis[feedback.dungeon?.emoji]?.text || '`??`'; - const tagString = `${feedback.tier?.tag || '??'}`.padStart(6); - return `\`${index}.\` ${emojiString} \`${tagString}\` ${feedback.feedbackURL} `; -} - -function getVoteDisplayString(index, feedback, storedEmojis) { - const emojiString = storedEmojis[feedback.dungeon?.emoji]?.text || '`??`'; +/** + * Creates a string in the following format: `index. emoji tag feedbackURL timestamp` + * @param {number} index starting index + * @param {FeedbackData} feedback data for the feedback + * @param {*} storedEmojis emoji + * @returns {string} + */ +function getDisplayString(index, feedback, storedEmojis) { + const emojiString = storedEmojis[feedback.dungeon?.emoji]?.text || storedEmojis.blunder.text; const tagString = `${feedback.tier?.tag || '??'}`.padStart(6); return `\`${index}.\` ${emojiString} \`${tagString}\` ${feedback.feedbackURL} `; } -function generateDisplayFields(feedbacks, startIndex, name, getDisplayString, storedEmojis) { +/** + * Creates the fields for a type of feedback, respecting field size limits + * @param {FeedbackData[]} feedbacks filtered feedbacks to display + * @param {number} startIndex the starting index to list feedbacks by + * @param {string} fieldTitle title of the field + * @param {*} storedEmojis emoji cache + * @returns {Discord.EmbedField[]} The fields for the feedbacks + */ +function generateDisplayFields(feedbacks, startIndex, fieldTitle, storedEmojis) { const feedbackFields = []; let currentField = { - name, + name: fieldTitle, value: '' }; feedbacks.forEach(feedback => { const feedbackString = getDisplayString(startIndex++, feedback, storedEmojis); - if (currentField.value.length + feedbackString.length > 1024) { + if (currentField.value.length + feedbackString.length + 1 > 1024) { feedbackFields.push(currentField); currentField = { - name, + name: fieldTitle, value: feedbackString }; } else { @@ -295,23 +296,32 @@ function generateDisplayFields(feedbacks, startIndex, name, getDisplayString, st }); if (currentField.value !== '') feedbackFields.push(currentField); - else feedbackFields.push({ name, value: 'None' }); + else feedbackFields.push({ name: fieldTitle, value: 'None' }); return feedbackFields; } +/** + * send's the current member's vote + * @param {VoteSetup} voteSetup the feedback data + */ async function sendVote(voteSetup) { const member = voteSetup.members[voteSetup.currentMemberIndex]; const embed = new Discord.EmbedBuilder() - .setColor(voteSetup.embedStyling ? voteSetup.embedStyling.embedColor : voteSetup.role.hexColor) + .setColor(voteSetup.embedStyling?.embedColor || voteSetup.role.hexColor) .setAuthor({ name: `${member.displayName} to ${voteSetup.role.name}`, iconURL: member.user.displayAvatarURL({ dynamic: true }) }) .setDescription(`${member} \`${member.displayName}\``); if (voteSetup.embedStyling) { embed.setThumbnail(voteSetup.embedStyling.image); } const { includedFeedbacks } = sortMemberFeedbacks(voteSetup); - embed.addFields(generateDisplayFields(includedFeedbacks, 1, 'Feedback:', getVoteDisplayString, voteSetup.storedEmojis)); + embed.addFields(generateDisplayFields(includedFeedbacks, 1, 'Feedback:', voteSetup.storedEmojis)); const voteMessage = await voteSetup.channel.send({ embeds: [embed] }); for (const emoji of ['✅', '❌', '👀']) { voteMessage.react(emoji); } } +/** + * generates the buttons with ids for the vote setup + * @param {Discord.Client} bot client with additions + * @returns {Discord.ActionRowBuilder} + */ function generateVoteSetupButtons(bot) { return new Discord.ActionRowBuilder() .addComponents([ @@ -347,7 +357,7 @@ function generateVoteSetupButtons(bot) { * @param {*} bot * @returns {Promise} The feedbacks for the members. */ -async function getFeedbacks(guild, settings, voteSetup) { +async function getFeedbacks(guild, settings, members, settingsRoleName) { async function fetchMessages(limit) { const sumMessages = []; const feedbackChannel = guild.channels.cache.get(settings.channels.rlfeedback); @@ -362,9 +372,9 @@ async function getFeedbacks(guild, settings, voteSetup) { } const messages = await fetchMessages(500); - const filteredMessages = messages.filter(message => voteSetup.members.some(member => message.mentions.users.has(member.id))); + const filteredMessages = messages.filter(message => members.some(member => message.mentions.users.has(member.id))); //* @type {Feedback[]} */ - voteSetup.feedbacks = filteredMessages.map(message => { + return filteredMessages.map(message => { const mentionedIDs = message.mentions.users.map(user => user.id); const firstLine = message.content.split('\n')[0].toLowerCase(); // Convert to lowercase for searching const dungeon = voteConfig.feedbackTypes.dungeon.find(dungeon => dungeon.flags.some(flag => firstLine.includes(flag))); @@ -372,7 +382,7 @@ async function getFeedbacks(guild, settings, voteSetup) { let feedbackState = FeedbackState.Other; if (!tier || !dungeon) { feedbackState = FeedbackState.Unidentified; - } else if (tier.roles.includes(voteSetup.settingsRoleName) && dungeon.roles.includes(voteSetup.settingsRoleName)) { + } else if (tier.roles.includes(settingsRoleName) && dungeon.roles.includes(settingsRoleName)) { feedbackState = FeedbackState.Included; } return {