diff --git a/commands/vote.js b/commands/vote.js index bb1247a1..a3f18088 100644 --- a/commands/vote.js +++ b/commands/vote.js @@ -1,259 +1,398 @@ const Discord = require('discord.js'); -const ErrorLogger = require('../lib/logError'); -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'); +/** +* @typedef FeedbackType +* @property {string[]} flags +* @property {string} emoji +* @property {string[]} roles +*/ +/** + * @typedef FeedbackData + * @property {string} messageID + * @property {FeedbackType} tier + * @property {FeedbackType} dungeon + * @property {string[]} mentionedIDs + * @property {string} feedbackURL + * @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: +// 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', - 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(); - } -}; - -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 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 || {}; + args: [ + slashArg(SlashArgType.Role, 'role', { + description: 'The role that the vote is for', + required: true + }), + slashArg(SlashArgType.User, 'user', { + description: 'User to put up for vote', + required: true + }), + ...Array(14).fill(0).map((_, index) => slashArg(SlashArgType.User, `user${index + 2}`, { + description: 'User to put up for vote', + required: false + })), + ], + varargs: true, + 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); + const embedStyling = voteConfig.templates.find(template => template.settingRole === settingsRoleName); - // 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) + const voteSetup = { + channel, + feedbacks: [], + role, + storedEmojis: bot.storedEmojis, + settingsRoleName, + embedStyling, + members: [message.options.getMember('user'), ...Array(14).fill(0).map((_, index) => message.options.getMember(`user${index + 2}`))].filter(m => m), + currentMemberIndex: 0 }; - } - async startProcess() { - if (!this.voteConfiguration.role) { return await this.channel.send(`Could not find role \`${this.roleType}\``); } - if (this.voteConfiguration.members.length == 0) { return await this.channel.send('No members found.'); } - - this.getVoteConfigurationButtons(); - await this.message.delete(); - await this.sendConfirmationMessage(); - await this.updateConfirmationMessage(); - } - - async sendConfirmationMessage() { - this.embed = new Discord.EmbedBuilder() - .setColor(this.member.roles.highest.hexColor) - .setAuthor({ name: 'Vote Configuration', iconURL: this.member.user.displayAvatarURL({ dynamic: true }) }) - .setDescription('Loading...'); - this.voteConfigurationMessage = await this.channel.send({ embeds: [this.embed], components: [this.getVoteConfigurationButtons()] }); - - this.voteConfigurationMessageInteractionCollector = new Discord.InteractionCollector(this.bot, { message: this.voteConfigurationMessage, interactionType: Discord.InteractionType.MessageComponent, componentType: Discord.ComponentType.Button }); - this.voteConfigurationMessageInteractionCollector.on('collect', async (interaction) => await this.interactionHandler(interaction)); - } - - async updateConfirmationMessage() { - const voteConfigurationDescription = this.getVoteConfigurationDescription(); - this.embed = new Discord.EmbedBuilder() - .setColor(this.member.roles.highest.hexColor) - .setAuthor({ name: 'Vote Configuration', iconURL: this.member.user.displayAvatarURL({ dynamic: true }) }) - .setDescription(voteConfigurationDescription); - await this.voteConfigurationMessage.edit({ embeds: [this.embed], components: [this.getVoteConfigurationButtons()] }); - } - - getVoteConfigurationDescription() { - return ` - This vote will be for ${this.voteConfiguration.role}, inside of ${this.voteConfiguration.channel} - ${this.emojiDatabase.feedback.text} \`${this.voteConfiguration.maximumFeedbacks}\` - - ## Leaders - ${this.voteConfiguration.members.join(', ')} - `; - } - - getVoteConfigurationButtons() { - return new Discord.ActionRowBuilder() - .addComponents([ - new Discord.ButtonBuilder() - .setLabel('✅ Confirm') - .setStyle(3) - .setCustomId('voteConfirm'), - new Discord.ButtonBuilder() - .setLabel('Feedbacks') - .setStyle(2) - .setEmoji(this.emojiDatabase.feedback.id) - .setCustomId('voteFeedbackConfigure'), - new Discord.ButtonBuilder() - .setLabel('# Channel') - .setStyle(2) - .setCustomId('voteChannelConfigure'), - new Discord.ButtonBuilder() - .setLabel('@ Role') - .setStyle(2) - .setCustomId('voteRoleConfigure'), - new Discord.ButtonBuilder() - .setLabel('❌ Cancel') - .setStyle(4) - .setCustomId('voteCancel') - ]); - } + if (voteSetup.members.length == 0) { return await message.reply('No members found.'); } + voteSetup.feedbacks = await getFeedbacks(guild, bot.settings[guild.id], voteSetup.members, settingsRoleName); - async interactionHandler(interaction) { - if (interaction.member.id != this.member.id) { - return await interaction.reply({ content: 'You are not permitted to configure this', ephemeral: true }); - } + const embed = getSetupEmbed(voteSetup); + const voteSetupButtons = generateVoteSetupButtons(bot); + const voteSetupMessage = await message.channel.send({ embeds: [embed], components: [voteSetupButtons], fetchReply: true, ephemeral: true }); + if (!message.isInteraction) await message.delete(); + else { message.deferReply(); 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)); + }, + /** + * 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 'voteConfirm': - await this.buttonVoteConfirm(interaction); + case 'voteSend': + interaction.deferUpdate(); + await sendVote(voteSetup); + voteSetup.currentMemberIndex++; + if (voteSetup.currentMemberIndex >= voteSetup.members.length) { + await interaction.message.delete(); + } else { + await interaction.message.edit({ embeds: [getSetupEmbed(voteSetup)], components: [generateVoteSetupButtons(bot)] }); + } break; - case 'voteCancel': - await this.buttonVoteCancel(interaction); + case 'voteSkip': + voteSetup.currentMemberIndex++; + if (voteSetup.currentMemberIndex >= voteSetup.members.length) { + await interaction.message.delete(); + } else { + await interaction.message.edit({ embeds: [getSetupEmbed(voteSetup)], components: [generateVoteSetupButtons(bot)] }); + } break; - case 'voteFeedbackConfigure': - await this.buttonVoteFeedbackConfigure(interaction); + case 'voteAbort': + await interaction.message.delete(); break; - case 'voteChannelConfigure': - await this.buttonVoteChannelConfigure(interaction); + case 'voteAddFeedbacks': { + // 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); + const addableFeedbacks = unidentifiedFeedbacks.concat(otherFeedbacks).slice(0, 25); + interaction.deferUpdate(); + if (addableFeedbacks.length == 0) { + break; + } + let index = includedFeedbacks.length + 1; + 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 'voteRoleConfigure': - await this.buttonVoteRoleConfigure(interaction); + } + case 'voteRemoveFeedbacks': { + const { includedFeedbacks } = sortMemberFeedbacks(voteSetup); + interaction.deferUpdate(); + if (includedFeedbacks.length == 0) { + break; + } + let index = 1; + 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 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: - this.channel.send('How?'); break; } } +}; - async endVoteConfigurationPhase(interaction) { - await interaction.message.delete(); - this.voteConfigurationMessageInteractionCollector.stop(); - } +/** + * 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?.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'} - 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); + This vote will be for to <@${member.id}> to ${voteSetup.role} + `); + const { includedFeedbacks, unidentifiedFeedbacks, otherFeedbacks } = sortMemberFeedbacks(voteSetup); + const feedbackFields = [ + ...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 = []; + const unidentifiedFeedbacks = []; + const otherFeedbacks = []; + // sort by timestamp desc (after push the newest feedback is at the highest index) + 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) { + unidentifiedFeedbacks.push(feedback); + } else { + otherFeedbacks.push(feedback); } - } + }); + return { includedFeedbacks, unidentifiedFeedbacks, otherFeedbacks }; +} - 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); } - } +/** + * 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} `; +} - 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); - } +/** + * 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: fieldTitle, + value: '' + }; - async buttonVoteCancel(interaction) { - await interaction.reply({ content: 'You have decided to cancel the votes', ephemeral: true }); - await this.endVoteConfigurationPhase(interaction); - } + feedbacks.forEach(feedback => { + const feedbackString = getDisplayString(startIndex++, feedback, storedEmojis); + if (currentField.value.length + feedbackString.length + 1 > 1024) { + feedbackFields.push(currentField); + currentField = { + name: fieldTitle, + value: feedbackString + }; + } else { + currentField.value += feedbackString + '\n'; + } + }); - 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(); - } + if (currentField.value !== '') feedbackFields.push(currentField); + else feedbackFields.push({ name: fieldTitle, value: 'None' }); + return feedbackFields; +} - 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(); - } +/** + * 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?.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:', voteSetup.storedEmojis)); + const voteMessage = await voteSetup.channel.send({ embeds: [embed] }); + for (const emoji of ['✅', '❌', '👀']) { voteMessage.react(emoji); } +} - 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(); - } +/** + * 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([ + new Discord.ButtonBuilder() + .setLabel('✅ Send') + .setStyle(3) + .setCustomId('voteSend'), + new Discord.ButtonBuilder() + .setLabel('❌ Skip') + .setStyle(1) + .setCustomId('voteSkip'), + new Discord.ButtonBuilder() + .setLabel('Add') + .setStyle(2) + .setEmoji(bot.storedEmojis.feedback.id) + .setCustomId('voteAddFeedbacks'), + new Discord.ButtonBuilder() + .setLabel('Remove') + .setStyle(2) + .setEmoji(bot.storedEmojis.feedback.id) + .setCustomId('voteRemoveFeedbacks'), + new Discord.ButtonBuilder() + .setLabel('❌ Abort') + .setStyle(4) + .setCustomId('voteAbort') + ]); +} - getBaseEmbed() { - return new Discord.EmbedBuilder() - .setColor(this.member.roles.highest.hexColor) - .setAuthor({ - name: 'Vote Configuration', - iconURL: this.member.user.displayAvatarURL({ dynamic: true }) - }); +/** + * 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(guild, settings, members, settingsRoleName) { + async function fetchMessages(limit) { + const sumMessages = []; + 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 + 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 => members.some(member => message.mentions.users.has(member.id))); + //* @type {Feedback[]} */ + 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))); + const tier = voteConfig.feedbackTypes.tier.find(tier => tier.flags.some(flag => firstLine.includes(flag))); + let feedbackState = FeedbackState.Other; + if (!tier || !dungeon) { + feedbackState = FeedbackState.Unidentified; + } else if (tier.roles.includes(settingsRoleName) && dungeon.roles.includes(settingsRoleName)) { + feedbackState = FeedbackState.Included; + } + return { + messageID: message.id, + tier, + dungeon, + mentionedIDs, + feedbackURL: message.url, + timeStamp: (message.createdTimestamp / 1000).toFixed(0), + feedbackState + }; + }); } diff --git a/data/voteConfig.json b/data/voteConfig.json new file mode 100644 index 00000000..88c679e1 --- /dev/null +++ b/data/voteConfig.json @@ -0,0 +1,134 @@ +{ + "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", + "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", "trial"], + "tag": "TRL", + "roles": ["almostHallsBanner", "almostOryxBanner", "almostShattersBanner", "fullskipBanner"] + }, + { + "flags": ["arl"], + "tag": "ARL", + "roles": ["hallsBanner", "oryxBanner", "shattersBanner"] + }, + { + "flags": ["reading", "mapreading"], + "tag": "Lesson", + "roles": ["almostHallsBanner", "hallsBanner", "vetHallsBanner", "fullskipBanner", "vetFullskipBanner"] + }, + { + "flags": ["ghost"], + "tag": "Ghost", + "roles": ["almostHallsBanner", "almostOryxBanner", "almostShattersBanner", "hallsBanner", "oryxBanner", "shattersBanner", "vetHallsBanner", "veteranOryxBanner", "vetShattersBanner", "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"] + } + ] + } +} \ 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 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();