From 4a04c389f201f61eb557ae33be3866df46b7fecf Mon Sep 17 00:00:00 2001 From: Josh Sanchez Date: Fri, 24 Sep 2021 18:37:59 -0500 Subject: [PATCH 01/14] Add optional chain for potentially missing message in tenmans close --- src/commands/tenmans.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/tenmans.ts b/src/commands/tenmans.ts index 6532e0c..7e6ab71 100644 --- a/src/commands/tenmans.ts +++ b/src/commands/tenmans.ts @@ -134,7 +134,7 @@ class TenmansCloseSubcommand extends MessageExecutable { // Teardown - clear current queue tenmansQueue = [] - await activeTenmansMessage.delete(); + await activeTenmansMessage?.delete(); } } From 87c4c80773cd979cf8a082acbfff1c40e78eea54 Mon Sep 17 00:00:00 2001 From: Josh Sanchez Date: Fri, 24 Sep 2021 19:15:34 -0500 Subject: [PATCH 02/14] Generalize registered user error message for all ten mans commands --- src/commands/command.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/command.ts b/src/commands/command.ts index 6420ab2..f790e24 100644 --- a/src/commands/command.ts +++ b/src/commands/command.ts @@ -28,7 +28,7 @@ export abstract class RegisteredUserExecutable< if (!user) { this.interaction.reply({ content: - "Who are you? Copy of me?! You need to register with me before joining 10mans! Please visit #rules for more info.", + "Who are you? Copy of me?! You need to register with me before participating in 10mans! Please visit #rules for more info.", ephemeral: true, }); From 288e2857e28799d4b65da8be0aea10a3c6671716 Mon Sep 17 00:00:00 2001 From: Josh Sanchez Date: Fri, 24 Sep 2021 20:14:26 -0500 Subject: [PATCH 03/14] Add vote fields to bot config --- src/config/botConfig.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/config/botConfig.ts b/src/config/botConfig.ts index e78ae23..88f9378 100644 --- a/src/config/botConfig.ts +++ b/src/config/botConfig.ts @@ -1,7 +1,11 @@ import { Channel } from "discord.js"; class BotConfig { - constructor(public queueMsgChannel?: Channel) {} + constructor( + public queueMsgChannel?: Channel, + public minVoteCount?: number, + public hoursTillVoteClose?: number, + ) {} } export default new BotConfig(); From fdc5e0e635100361ecf5db022f5549b430416e30 Mon Sep 17 00:00:00 2001 From: Josh Sanchez Date: Sat, 25 Sep 2021 00:06:32 -0500 Subject: [PATCH 04/14] Add config data for voting sourced from env --- README.md | 2 ++ src/client.ts | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/README.md b/README.md index bf2d0c9..00977bd 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ In your `.env` file, you need to set the following variables: CYPHERBOT_CLIENT_ID= CYPHERBOT_TARGET_GUILD_ID= CYPHERBOT_TOKEN= +MIN_VOTE_COUNT= +HOURS_TO_CLOSE= DB_NAME= DB_CONN_STRING=mongodb+srv://:@/?retryWrites=true&w=majority ``` diff --git a/src/client.ts b/src/client.ts index a5e90cd..528e46a 100644 --- a/src/client.ts +++ b/src/client.ts @@ -20,15 +20,22 @@ async function initBotConfig(client: Client, db: Db) { // Persist default configuration data const defaultChannelId = process.env.DEFAULT_QUEUEMSG_CHANNELID; + const minVoteCount = process.env.MIN_VOTE_COUNT; + const hoursTillVoteClose = process.env.HOURS_TO_CLOSE; botConfigDoc = await botConfigCollection.insertOne({ configName: "bot", + minVoteCount: minVoteCount, + hoursTillVoteClose: hoursTillVoteClose, queueChannelId: defaultChannelId, }); } const channelId = botConfigDoc.queueChannelId; + botConfig.queueMsgChannel = client.channels.cache.get(channelId); + botConfig.minVoteCount = botConfigDoc.minVoteCount; + botConfig.hoursTillVoteClose = botConfigDoc.hoursTillVoteClose; } export default (db: Db) => { From f2a3c1e7e544f58cc5e2460ca45396d703042080 Mon Sep 17 00:00:00 2001 From: Josh Sanchez Date: Sat, 25 Sep 2021 00:20:40 -0500 Subject: [PATCH 05/14] Add vote subcommand to tenmans command --- src/commands/tenmans.ts | 76 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/src/commands/tenmans.ts b/src/commands/tenmans.ts index 7e6ab71..354b64a 100644 --- a/src/commands/tenmans.ts +++ b/src/commands/tenmans.ts @@ -23,6 +23,8 @@ import { let tenmansQueue: Member[] = []; let time: String | null; let activeTenmansMessage: Message | null; +let activeVoteMessage: Message | null; +let voteClosingTime: Date | null; abstract class QueueAction< T extends RepliableInteraction @@ -138,6 +140,62 @@ class TenmansCloseSubcommand extends MessageExecutable { } } +class TenmansVoteSubcommand extends RegisteredUserExecutable { + async afterUserExecute() { + if (activeTenmansMessage) { + this.interaction.reply({ + content: "You should have been looking more closely. There's already a queue - use that one instead!", + ephemeral: true + }); + + return; + } + + const queueChannel = botConfig.queueMsgChannel as TextChannel; + if (!queueChannel) { + this.interaction.reply({ + content: + "No queue message channel configured. Please set channel id using '/config defaultChannel'.", + ephemeral: true, + }); + + return; + } + + if (!tenmansQueue?.length) { + // init queue if it doesn't exist + voteClosingTime = new Date() + voteClosingTime.setHours(voteClosingTime.getHours() + botConfig.hoursTillVoteClose) + time = this.interaction.options.getString("time"); + + tenmansQueue = []; + } + tenmansQueue.push(this.user); + + if (tenmansQueue.length >= botConfig.minVoteCount) { + // Generate proper interactable queue once min votes reached + await activeVoteMessage.delete(); + voteClosingTime = null; + + activeTenmansMessage = await queueChannel.send({ + embeds: [createEmbed(time)] + }) + } else { + const votesStillNeeded = botConfig.minVoteCount - tenmansQueue.length; + + if (!activeVoteMessage) { + activeVoteMessage = await queueChannel.send({ + embeds: [createVoteEmbed(votesStillNeeded, time, voteClosingTime)], + }) + } else { + activeVoteMessage.edit({ + embeds: [createVoteEmbed(votesStillNeeded, time, voteClosingTime)] + }) + } + } + } +} + export async function cmd_tenmans(interaction, db: Db) { const commands: { [key: string]: { @@ -148,7 +206,8 @@ export async function cmd_tenmans(interaction, db: Db) { }; } = { start: SubcommandTenmansStart, - end: TenmansCloseSubcommand + end: TenmansCloseSubcommand, + vote: TenmansVoteSubcommand, }; // Wrap function call to pass same args to all methods @@ -214,6 +273,21 @@ const createEmbed = (time) => .setTimestamp() .setFooter("Last Updated"); +const createVoteEmbed = (votesStillNeeded: number, time, closingTime: Date) => + new MessageEmbed() + .setColor("#0099ff") + .setTitle(`Ten Mans Vote For ${time}`) + .addField("Votes needed to start queue: ", votesStillNeeded.toString(), true) + .addField( + "Discord Member", + tenmansQueue.length > 0 + ? tenmansQueue.map((member) => `<@${member.discordId}>`).join("\n") + : "No Players", + true + ) + .setTimestamp() + .setFooter("Vote ends at:", closingTime.toLocaleString()); + const createQueueActionRow = (queueId) => { return new MessageActionRow().addComponents( new MessageButton() From 7d8c4a4329f89b526deb3eaf8be66b4c89d17a24 Mon Sep 17 00:00:00 2001 From: Josh Sanchez Date: Sat, 25 Sep 2021 00:29:31 -0500 Subject: [PATCH 06/14] Add timeout mechanism for vote embed cleaning --- src/client.ts | 3 +++ src/commands/tenmans.ts | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/client.ts b/src/client.ts index 528e46a..38fd1ed 100644 --- a/src/client.ts +++ b/src/client.ts @@ -6,6 +6,7 @@ import cmd_register from "./commands/register"; import { cmd_tenmans, handleButton as tenmansHandleButton, + handleVoteCleaning, } from "./commands/tenmans"; import { cmdConfig } from "./commands/config"; import botConfig from "./config/botConfig"; @@ -36,6 +37,8 @@ async function initBotConfig(client: Client, db: Db) { botConfig.queueMsgChannel = client.channels.cache.get(channelId); botConfig.minVoteCount = botConfigDoc.minVoteCount; botConfig.hoursTillVoteClose = botConfigDoc.hoursTillVoteClose; + + setInterval(handleVoteCleaning, 300000); // Clean vote embeds every 5 minutes } export default (db: Db) => { diff --git a/src/commands/tenmans.ts b/src/commands/tenmans.ts index 354b64a..bfd8465 100644 --- a/src/commands/tenmans.ts +++ b/src/commands/tenmans.ts @@ -300,3 +300,15 @@ const createQueueActionRow = (queueId) => { .setStyle(Constants.MessageButtonStyles.DANGER) ); }; + +export async function handleVoteCleaning() { + if (activeVoteMessage) { + // Close vote if it has expired + if (voteClosingTime < new Date()) { + await activeVoteMessage.delete(); + + tenmansQueue = []; + voteClosingTime = null; + } + } +} From 8864b427645d82579320d85912185078e2b19f7f Mon Sep 17 00:00:00 2001 From: Josh Sanchez Date: Sun, 26 Sep 2021 19:41:32 -0500 Subject: [PATCH 07/14] Add config validation to vote command --- src/commands/tenmans.ts | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/commands/tenmans.ts b/src/commands/tenmans.ts index 5d43155..4cf3236 100644 --- a/src/commands/tenmans.ts +++ b/src/commands/tenmans.ts @@ -172,6 +172,7 @@ class TenmansCloseSubcommand extends MessageExecutable { class TenmansVoteSubcommand extends RegisteredUserExecutable { async afterUserExecute() { + // Verify queue not already active if (activeTenmansMessage) { this.interaction.reply({ content: "You should have been looking more closely. There's already a queue - use that one instead!", @@ -181,15 +182,18 @@ class TenmansVoteSubcommand extends RegisteredUserExecutable return; } - const queueChannel = botConfig.queueMsgChannel as TextChannel; - if (!queueChannel) { - this.interaction.reply({ - content: - "No queue message channel configured. Please set channel id using '/config defaultChannel'.", - ephemeral: true, - }); - - return; + // Verify bot config is valid + const required_configs = ["hoursTillVoteClose", "minVoteCount", "queueMsgChannel"]; + for (const setting in required_configs) { + if (!botConfig[setting]) { + this.interaction.reply({ + content: + `${setting} not configured. Please ask an admin to configure this value.`, + ephemeral: true, + }); + + return; + } } if (!tenmansQueue?.length) { @@ -202,6 +206,7 @@ class TenmansVoteSubcommand extends RegisteredUserExecutable } tenmansQueue.push(this.user); + const queueChannel = botConfig.queueMsgChannel as TextChannel; if (tenmansQueue.length >= botConfig.minVoteCount) { // Generate proper interactable queue once min votes reached await activeVoteMessage.delete(); From a595e35570295414a562592bd9614fc8cd96942f Mon Sep 17 00:00:00 2001 From: Josh Sanchez Date: Sun, 26 Sep 2021 20:25:14 -0500 Subject: [PATCH 08/14] Add role ping when vote subcmd creates a queue --- src/commands/tenmans.ts | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/commands/tenmans.ts b/src/commands/tenmans.ts index 4cf3236..d4036ae 100644 --- a/src/commands/tenmans.ts +++ b/src/commands/tenmans.ts @@ -8,7 +8,8 @@ import { Constants, ButtonInteraction, Interaction, - GuildApplicationCommandManager, + Role, + Collection, } from "discord.js"; import { Db } from "mongodb"; import botConfig from "../config/botConfig"; @@ -182,6 +183,26 @@ class TenmansVoteSubcommand extends RegisteredUserExecutable return; } + // Verify that a pingable role exists for 10 mans on this server + const roleId = await this.interaction.guild.roles.fetch() + .then((roles: Collection) => { + for (const role of roles.values()) { + if (role.name === "10 Mans") { + return role.id; + } + } + }) + .catch(console.error); + + if (!roleId) { + this.interaction.reply({ + content: "My camera is destroyed - cannot find a 10 mans role on this server. Message an admin.", + ephemeral: true, + }) + + return; + } + // Verify bot config is valid const required_configs = ["hoursTillVoteClose", "minVoteCount", "queueMsgChannel"]; for (const setting in required_configs) { @@ -215,6 +236,10 @@ class TenmansVoteSubcommand extends RegisteredUserExecutable activeTenmansMessage = await queueChannel.send({ embeds: [createEmbed(time)] }) + + await queueChannel.send({ + content: `<@${roleId}> that Radianite must be ours! A queue has been created!`, + }) } else { const votesStillNeeded = botConfig.minVoteCount - tenmansQueue.length; From 30cb304809def5c18072044c888d64afeec6995a Mon Sep 17 00:00:00 2001 From: Josh Sanchez Date: Sun, 26 Sep 2021 20:25:39 -0500 Subject: [PATCH 09/14] Cypherify settings validation error message in vote --- src/commands/tenmans.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/tenmans.ts b/src/commands/tenmans.ts index d4036ae..08f261a 100644 --- a/src/commands/tenmans.ts +++ b/src/commands/tenmans.ts @@ -209,7 +209,7 @@ class TenmansVoteSubcommand extends RegisteredUserExecutable if (!botConfig[setting]) { this.interaction.reply({ content: - `${setting} not configured. Please ask an admin to configure this value.`, + `Careful now. ${setting} not configured. Please ask an admin to configure this value.`, ephemeral: true, }); From 420a2e09c73ae173211d492e709baca3fddc003f Mon Sep 17 00:00:00 2001 From: Josh Sanchez Date: Sun, 26 Sep 2021 20:25:55 -0500 Subject: [PATCH 10/14] Clean up vote embed --- src/commands/tenmans.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/tenmans.ts b/src/commands/tenmans.ts index 08f261a..b1da144 100644 --- a/src/commands/tenmans.ts +++ b/src/commands/tenmans.ts @@ -349,7 +349,7 @@ const createVoteEmbed = (votesStillNeeded: number, time, closingTime: Date) => true ) .setTimestamp() - .setFooter("Vote ends at:", closingTime.toLocaleString()); + .setFooter("Vote ends at", closingTime.toLocaleString()); const createQueueActionRow = (queueId) => { return new MessageActionRow().addComponents( From db51037ee0dc73671c34e0b5187366575f240a97 Mon Sep 17 00:00:00 2001 From: Josh Sanchez Date: Sun, 26 Sep 2021 23:46:08 -0500 Subject: [PATCH 11/14] Refactor QueueAction to support generic precon and postupdate handlers --- src/commands/tenmans.ts | 41 ++++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/src/commands/tenmans.ts b/src/commands/tenmans.ts index b1da144..3dd69bf 100644 --- a/src/commands/tenmans.ts +++ b/src/commands/tenmans.ts @@ -27,33 +27,59 @@ let activeTenmansMessage: Message | null; let activeVoteMessage: Message | null; let voteClosingTime: Date | null; -abstract class QueueAction< +abstract class BaseQueueAction< T extends RepliableInteraction > extends RegisteredUserExecutable { constructor(interaction: T, db: Db, protected queueId: string) { super(interaction, db); } + /// Verify that the queue id stored in the constructor is correct. + /// If not, allow child class to throw custom error message and + /// fail gracefully. + abstract verifyQueueId() : string; + /// Perform actions after verifying queue id. Should not modify any + /// messages related to queue state. abstract updateQueue(); + /// Perform actions after updating queue state. This includes things + /// like updating related embed messages, etc. + abstract updateUserInterface(); + async afterUserExecute(): Promise { - if (activeTenmansMessage === null) { + const errorMessage = this.verifyQueueId(); + if (errorMessage) { this.interaction.reply({ - content: "I must wait a moment! There is no active 10mans queue.", + content: errorMessage, ephemeral: true, }); + return; } await this.updateQueue(); + await this.updateUserInterface(); + } +} + +abstract class StandardQueueAction< + T extends RepliableInteraction +> extends BaseQueueAction { + verifyQueueId() { + if (activeTenmansMessage === null) { + return "I must wait a moment! There is no active 10mans queue."; + } + } + + updateUserInterface() { activeTenmansMessage.edit({ embeds: [createEmbed(time)], }); } } -class JoinQueueButtonAction extends QueueAction { +class JoinQueueButtonAction extends StandardQueueAction { updateQueue() { tenmansQueue.push(this.user); this.interaction.reply({ @@ -63,7 +89,7 @@ class JoinQueueButtonAction extends QueueAction { } } -class LeaveQueueButtonAction extends QueueAction { +class LeaveQueueButtonAction extends StandardQueueAction { updateQueue() { tenmansQueue = tenmansQueue.filter( (member) => member.discordId !== this.user.discordId @@ -75,7 +101,7 @@ class LeaveQueueButtonAction extends QueueAction { } } -class ManualAddUserToQueue extends QueueAction { +class ManualAddUserToQueue extends StandardQueueAction { async updateQueue() { const targetUser = this.interaction.options.getUser("member"); const targetMember = (await this.db.collection("members").findOne({ @@ -89,7 +115,7 @@ class ManualAddUserToQueue extends QueueAction { } } -class ManualRemoveUserToQueue extends QueueAction { +class ManualRemoveUserToQueue extends StandardQueueAction { async updateQueue() { const targetUser = this.interaction.options.getUser("member"); const targetMember = (await this.db.collection("members").findOne({ @@ -315,6 +341,7 @@ export async function handleButton(interaction: ButtonInteraction, db: Db) { command.execute(); } +// TODO: Move embed generators into their own directory/module const createEmbed = (time) => new MessageEmbed() .setColor("#0099ff") From 26f29f890a219dd799f4433b996b321387b04f6a Mon Sep 17 00:00:00 2001 From: Josh Sanchez Date: Sun, 26 Sep 2021 23:47:45 -0500 Subject: [PATCH 12/14] Fix naming style issue for bot config validation on vote subcmd --- src/commands/tenmans.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/tenmans.ts b/src/commands/tenmans.ts index 3dd69bf..6e340ee 100644 --- a/src/commands/tenmans.ts +++ b/src/commands/tenmans.ts @@ -230,8 +230,8 @@ class TenmansVoteSubcommand extends RegisteredUserExecutable } // Verify bot config is valid - const required_configs = ["hoursTillVoteClose", "minVoteCount", "queueMsgChannel"]; - for (const setting in required_configs) { + const requiredConfigs = ["hoursTillVoteClose", "minVoteCount", "queueMsgChannel"]; + for (const setting in requiredConfigs) { if (!botConfig[setting]) { this.interaction.reply({ content: From 664e5f18cb18d2a35567c53b3c72dd3e8be87399 Mon Sep 17 00:00:00 2001 From: Josh Sanchez Date: Mon, 27 Sep 2021 00:09:35 -0500 Subject: [PATCH 13/14] Refactor vote logic to utilize RichEmbed to participate --- src/commands/tenmans.ts | 128 +++++++++++++++++++++++++--------------- 1 file changed, 82 insertions(+), 46 deletions(-) diff --git a/src/commands/tenmans.ts b/src/commands/tenmans.ts index 6e340ee..5d9a85b 100644 --- a/src/commands/tenmans.ts +++ b/src/commands/tenmans.ts @@ -79,6 +79,24 @@ abstract class StandardQueueAction< } } +abstract class VoteQueueAction< + T extends RepliableInteraction +> extends BaseQueueAction { + verifyQueueId() { + return ""; + } + + updateUserInterface () { + const votesStillNeeded = botConfig.minVoteCount - tenmansQueue.length; + + if (!!activeVoteMessage) { + activeVoteMessage.edit({ + embeds: [createVoteEmbed(votesStillNeeded, time, voteClosingTime)] + }); + } + } +} + class JoinQueueButtonAction extends StandardQueueAction { updateQueue() { tenmansQueue.push(this.user); @@ -101,6 +119,46 @@ class LeaveQueueButtonAction extends StandardQueueAction { } } +class VoteQueueButtonAction extends VoteQueueAction { + async updateQueue() { + const queueChannel = botConfig.queueMsgChannel as TextChannel; + + // Verify that a pingable role exists for 10 mans on this server + const roleId = await this.interaction.guild.roles.fetch() + .then((roles: Collection) => { + for (const role of roles.values()) { + if (role.name === "10 Mans") { + return role.id; + } + } + }) + .catch(console.error); + + if (!roleId) { + this.interaction.reply({ + content: "My camera is destroyed - cannot find a 10 mans role on this server. Message an admin.", + ephemeral: true, + }); + + return; + } + + if (tenmansQueue.length >= botConfig.minVoteCount) { + // Generate proper interactable queue once min votes reached + await activeVoteMessage.delete(); + voteClosingTime = null; + + activeTenmansMessage = await queueChannel.send({ + embeds: [createEmbed(time)] + }) + + await queueChannel.send({ + content: `<@${roleId}> that Radianite must be ours! A queue has been created!`, + }); + } + } +} + class ManualAddUserToQueue extends StandardQueueAction { async updateQueue() { const targetUser = this.interaction.options.getUser("member"); @@ -209,26 +267,6 @@ class TenmansVoteSubcommand extends RegisteredUserExecutable return; } - // Verify that a pingable role exists for 10 mans on this server - const roleId = await this.interaction.guild.roles.fetch() - .then((roles: Collection) => { - for (const role of roles.values()) { - if (role.name === "10 Mans") { - return role.id; - } - } - }) - .catch(console.error); - - if (!roleId) { - this.interaction.reply({ - content: "My camera is destroyed - cannot find a 10 mans role on this server. Message an admin.", - ephemeral: true, - }) - - return; - } - // Verify bot config is valid const requiredConfigs = ["hoursTillVoteClose", "minVoteCount", "queueMsgChannel"]; for (const setting in requiredConfigs) { @@ -250,34 +288,22 @@ class TenmansVoteSubcommand extends RegisteredUserExecutable time = this.interaction.options.getString("time"); tenmansQueue = []; - } - tenmansQueue.push(this.user); + tenmansQueue.push(this.user); - const queueChannel = botConfig.queueMsgChannel as TextChannel; - if (tenmansQueue.length >= botConfig.minVoteCount) { - // Generate proper interactable queue once min votes reached - await activeVoteMessage.delete(); - voteClosingTime = null; + const queueChannel = botConfig.queueMsgChannel as TextChannel; + const queueId = "stub"; - activeTenmansMessage = await queueChannel.send({ - embeds: [createEmbed(time)] - }) - - await queueChannel.send({ - content: `<@${roleId}> that Radianite must be ours! A queue has been created!`, - }) + activeVoteMessage = await queueChannel.send({ + embeds: [createVoteEmbed(botConfig.minVoteCount, time, voteClosingTime)], + components: [createVoteQueueActionRow(queueId)], + }); } else { - const votesStillNeeded = botConfig.minVoteCount - tenmansQueue.length; - - if (!activeVoteMessage) { - activeVoteMessage = await queueChannel.send({ - embeds: [createVoteEmbed(votesStillNeeded, time, voteClosingTime)], - }) - } else { - activeVoteMessage.edit({ - embeds: [createVoteEmbed(votesStillNeeded, time, voteClosingTime)] - }) - } + this.interaction.reply({ + content: "Cage triggered. A vote is already active, check it out!", + ephemeral: true, + }); + + return; } } } @@ -320,11 +346,12 @@ export async function handleButton(interaction: ButtonInteraction, db: Db) { interaction: ButtonInteraction, db: Db, queueId: string - ): QueueAction; + ): BaseQueueAction; }; } = { join: JoinQueueButtonAction, leave: LeaveQueueButtonAction, + vote: VoteQueueButtonAction, }; const actionParts = interaction.customId.split("."); const [commandName, queueId] = actionParts[actionParts.length - 1].split(":"); @@ -391,6 +418,15 @@ const createQueueActionRow = (queueId) => { ); }; +const createVoteQueueActionRow = (queueId) => { + return new MessageActionRow().addComponents( + new MessageButton() + .setCustomId(`tenmans.vote:${queueId}`) + .setLabel("Vote") + .setStyle(Constants.MessageButtonStyles.SUCCESS), + ); +}; + export async function handleVoteCleaning() { if (activeVoteMessage) { // Close vote if it has expired From 7c7055fc18e6bc6e4be39c43dbd05888d55f8cee Mon Sep 17 00:00:00 2001 From: Patrick Gallagher Date: Mon, 27 Sep 2021 01:38:46 -0500 Subject: [PATCH 14/14] Implement stability fixes for voting and queueing --- src/commands/command.ts | 2 +- src/commands/tenmans.ts | 149 ++++++++++++++++++++++++++++------------ 2 files changed, 107 insertions(+), 44 deletions(-) diff --git a/src/commands/command.ts b/src/commands/command.ts index f790e24..2ee0a0b 100644 --- a/src/commands/command.ts +++ b/src/commands/command.ts @@ -28,7 +28,7 @@ export abstract class RegisteredUserExecutable< if (!user) { this.interaction.reply({ content: - "Who are you? Copy of me?! You need to register with me before participating in 10mans! Please visit #rules for more info.", + "Who are you? You need to register with me before participating in 10mans! Please visit #rules for more info.", ephemeral: true, }); diff --git a/src/commands/tenmans.ts b/src/commands/tenmans.ts index 5d9a85b..c0f70ba 100644 --- a/src/commands/tenmans.ts +++ b/src/commands/tenmans.ts @@ -36,11 +36,11 @@ abstract class BaseQueueAction< /// Verify that the queue id stored in the constructor is correct. /// If not, allow child class to throw custom error message and /// fail gracefully. - abstract verifyQueueId() : string; + abstract verifyQueueId(): string; /// Perform actions after verifying queue id. Should not modify any /// messages related to queue state. - abstract updateQueue(); + abstract updateQueue(): Promise; /// Perform actions after updating queue state. This includes things /// like updating related embed messages, etc. @@ -57,9 +57,11 @@ abstract class BaseQueueAction< return; } - await this.updateQueue(); + const shouldRender = await this.updateQueue(); - await this.updateUserInterface(); + if (shouldRender) { + this.updateUserInterface(); + } } } @@ -86,29 +88,38 @@ abstract class VoteQueueAction< return ""; } - updateUserInterface () { + updateUserInterface() { const votesStillNeeded = botConfig.minVoteCount - tenmansQueue.length; - if (!!activeVoteMessage) { - activeVoteMessage.edit({ - embeds: [createVoteEmbed(votesStillNeeded, time, voteClosingTime)] - }); - } + activeVoteMessage.edit({ + embeds: [createVoteEmbed(votesStillNeeded, time, voteClosingTime)], + }); } } class JoinQueueButtonAction extends StandardQueueAction { - updateQueue() { + async updateQueue(): Promise { + if (tenmansQueue.some(queueUser => this.user.discordId === queueUser.discordId)) { + this.interaction.reply({ + content: "Who are you? Copy of me?! You're already in the queue!", + ephemeral: true, + }); + + return false; + } + tenmansQueue.push(this.user); this.interaction.reply({ content: "Greetings! You've been added to the queue.", ephemeral: true, }); + + return true; } } class LeaveQueueButtonAction extends StandardQueueAction { - updateQueue() { + async updateQueue(): Promise { tenmansQueue = tenmansQueue.filter( (member) => member.discordId !== this.user.discordId ); @@ -116,51 +127,77 @@ class LeaveQueueButtonAction extends StandardQueueAction { content: "This is no problem; You've been removed from the queue.", ephemeral: true, }); + + return true; } } class VoteQueueButtonAction extends VoteQueueAction { - async updateQueue() { + async updateQueue(): Promise { const queueChannel = botConfig.queueMsgChannel as TextChannel; // Verify that a pingable role exists for 10 mans on this server - const roleId = await this.interaction.guild.roles.fetch() - .then((roles: Collection) => { - for (const role of roles.values()) { - if (role.name === "10 Mans") { - return role.id; + const roleId = await this.interaction.guild.roles + .fetch() + .then((roles: Collection) => { + for (const role of roles.values()) { + if (role.name === "10 Mans") { + return role.id; + } } - } - }) - .catch(console.error); + }) + .catch(console.error); if (!roleId) { this.interaction.reply({ - content: "My camera is destroyed - cannot find a 10 mans role on this server. Message an admin.", + content: + "My camera is destroyed - cannot find a 10 mans role on this server. Message an admin.", ephemeral: true, }); - return; + return false; + } + + if (tenmansQueue.some(queueUser => this.user.discordId === queueUser.discordId)) { + this.interaction.reply({ + content: "Who are you? Copy of me?! You've already voted!", + ephemeral: true, + }); + + return false; } + tenmansQueue.push(this.user); + + this.interaction.reply({ + content: "Greetings! Your vote has been counted.", + ephemeral: true, + }); + if (tenmansQueue.length >= botConfig.minVoteCount) { // Generate proper interactable queue once min votes reached await activeVoteMessage.delete(); voteClosingTime = null; + activeVoteMessage = null; activeTenmansMessage = await queueChannel.send({ - embeds: [createEmbed(time)] - }) + embeds: [createEmbed(time)], + components: [createQueueActionRow(this.queueId)], + }); await queueChannel.send({ content: `<@${roleId}> that Radianite must be ours! A queue has been created!`, }); + + return false; } + + return true; } } class ManualAddUserToQueue extends StandardQueueAction { - async updateQueue() { + async updateQueue(): Promise { const targetUser = this.interaction.options.getUser("member"); const targetMember = (await this.db.collection("members").findOne({ discordId: targetUser.id, @@ -170,11 +207,13 @@ class ManualAddUserToQueue extends StandardQueueAction { content: `User \`${targetUser.username}\` added to the queue.`, ephemeral: true, }); + + return true; } } class ManualRemoveUserToQueue extends StandardQueueAction { - async updateQueue() { + async updateQueue(): Promise { const targetUser = this.interaction.options.getUser("member"); const targetMember = (await this.db.collection("members").findOne({ discordId: targetUser.id, @@ -186,6 +225,8 @@ class ManualRemoveUserToQueue extends StandardQueueAction { content: `User \`${targetUser.username}\` removed from the queue.`, ephemeral: true, }); + + return true; } } @@ -250,7 +291,7 @@ class TenmansCloseSubcommand extends MessageExecutable { } // Teardown - clear current queue - tenmansQueue = [] + tenmansQueue = []; await activeTenmansMessage?.delete(); } } @@ -260,31 +301,37 @@ class TenmansVoteSubcommand extends RegisteredUserExecutable // Verify queue not already active if (activeTenmansMessage) { this.interaction.reply({ - content: "You should have been looking more closely. There's already a queue - use that one instead!", - ephemeral: true + content: + "You should have been looking more closely. There's already a queue - use that one instead!", + ephemeral: true, }); return; } // Verify bot config is valid - const requiredConfigs = ["hoursTillVoteClose", "minVoteCount", "queueMsgChannel"]; - for (const setting in requiredConfigs) { + const requiredConfigs = [ + "hoursTillVoteClose", + "minVoteCount", + "queueMsgChannel", + ]; + for (const setting of requiredConfigs) { if (!botConfig[setting]) { this.interaction.reply({ - content: - `Careful now. ${setting} not configured. Please ask an admin to configure this value.`, + content: `Careful now. \`${setting}\` not configured. Please ask an admin to configure this value.`, ephemeral: true, }); - + return; } } if (!tenmansQueue?.length) { // init queue if it doesn't exist - voteClosingTime = new Date() - voteClosingTime.setHours(voteClosingTime.getHours() + botConfig.hoursTillVoteClose) + voteClosingTime = new Date(); + voteClosingTime.setHours( + voteClosingTime.getHours() + botConfig.hoursTillVoteClose + ); time = this.interaction.options.getString("time"); tenmansQueue = []; @@ -293,10 +340,18 @@ class TenmansVoteSubcommand extends RegisteredUserExecutable const queueChannel = botConfig.queueMsgChannel as TextChannel; const queueId = "stub"; + const votesStillNeeded = botConfig.minVoteCount - tenmansQueue.length; activeVoteMessage = await queueChannel.send({ - embeds: [createVoteEmbed(botConfig.minVoteCount, time, voteClosingTime)], + embeds: [ + createVoteEmbed(votesStillNeeded, time, voteClosingTime), + ], components: [createVoteQueueActionRow(queueId)], }); + + this.interaction.reply({ + content: "Vote started!", + ephemeral: true, + }); } else { this.interaction.reply({ content: "Cage triggered. A vote is already active, check it out!", @@ -335,7 +390,11 @@ export async function cmd_tenmans(interaction: CommandInteraction, db: Db) { }); return; } - const command = new Action(interaction, db, interaction.options.getString("queueId")); + const command = new Action( + interaction, + db, + interaction.options.getString("queueId") + ); command.execute(); } @@ -394,7 +453,11 @@ const createVoteEmbed = (votesStillNeeded: number, time, closingTime: Date) => new MessageEmbed() .setColor("#0099ff") .setTitle(`Ten Mans Vote For ${time}`) - .addField("Votes needed to start queue: ", votesStillNeeded.toString(), true) + .addField( + "Votes needed to start queue: ", + votesStillNeeded.toString(), + true + ) .addField( "Discord Member", tenmansQueue.length > 0 @@ -403,7 +466,7 @@ const createVoteEmbed = (votesStillNeeded: number, time, closingTime: Date) => true ) .setTimestamp() - .setFooter("Vote ends at", closingTime.toLocaleString()); + .setFooter(`Vote ends at ${closingTime.toLocaleString()}`); const createQueueActionRow = (queueId) => { return new MessageActionRow().addComponents( @@ -423,7 +486,7 @@ const createVoteQueueActionRow = (queueId) => { new MessageButton() .setCustomId(`tenmans.vote:${queueId}`) .setLabel("Vote") - .setStyle(Constants.MessageButtonStyles.SUCCESS), + .setStyle(Constants.MessageButtonStyles.SUCCESS) ); }; @@ -432,7 +495,7 @@ export async function handleVoteCleaning() { // Close vote if it has expired if (voteClosingTime < new Date()) { await activeVoteMessage.delete(); - + tenmansQueue = []; voteClosingTime = null; }