From f5d37a52f875eb6ca637e42131874d727c09a263 Mon Sep 17 00:00:00 2001 From: Cpt-Dingus <100243410+Cpt-Dingus@users.noreply.github.com> Date: Mon, 28 Aug 2023 22:28:28 +0200 Subject: [PATCH 1/5] Ducks: Added the ducks module (woo) --- src/core/api.ts | 1 + src/modules/duck/duck.ts | 910 ++++++++++++++++++++++++++++++ src/modules/duck/duck_quotes.json | 185 ++++++ 3 files changed, 1096 insertions(+) create mode 100644 src/modules/duck/duck.ts create mode 100644 src/modules/duck/duck_quotes.json diff --git a/src/core/api.ts b/src/core/api.ts index 24f50cb..41aa081 100644 --- a/src/core/api.ts +++ b/src/core/api.ts @@ -9,6 +9,7 @@ import {GatewayIntentBits, Client} from 'discord.js'; export const client = new Client({ intents: [ GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, GatewayIntentBits.GuildPresences, diff --git a/src/modules/duck/duck.ts b/src/modules/duck/duck.ts new file mode 100644 index 0000000..8741c6f --- /dev/null +++ b/src/modules/duck/duck.ts @@ -0,0 +1,910 @@ +/** + * @file + * Modules: + * - {@link duck} + * - Submodules: Friends, Killers, Record, Stats, Donate, Kill, Reset + */ + +import {readFileSync} from 'node:fs'; +import * as util from '../../core/util.js'; +import { + APIEmbedField, + Channel, + Colors, + EmbedBuilder, + GuildMember, + Snowflake, + TextChannel, + User, +} from 'discord.js'; +import {Collection, Db} from 'mongodb'; + +/** The main interface used in the DB + * @param user The user ID + * @param befriended The number of befriended ducks + * @param killed The number of killed ducks + * @param speedRecord The fastest duck interaction for the user + */ +interface duckUser { + user: Snowflake; + befriended: number; + killed: number; + speedRecord: number; +} + +/** The root 'duck' module definition */ +const duck = new util.RootModule('duck', 'Duck management commands', [ + util.mongo, +]); + +// -- Constants -- +const DUCK_COLLECTION_NAME = 'ducks'; +const DUCK_QUOTES: string[] = JSON.parse( + readFileSync('./src/modules/duck/duck_quotes.json', 'utf-8') +); + +// Config constants +const CHANNEL_IDS: string[] | undefined = duck.config.channels; +const MINIMUM_SPAWN: number | undefined = duck.config.minimumSpawn; +const MAXIMUM_SPAWN: number | undefined = duck.config.maximumSpawn; +const RUN_AWAY_TIME: number | undefined = duck.config.runAwayTime; +const COOLDOWN: number | undefined = duck.config.cooldown; +const FAIL_RATES = duck.config.failRates; + +const DUCK_PIC_URL = + 'https://cdn.icon-icons.com/icons2/1446/PNG/512/22276duck_98782.png'; +const BEFRIEND_PIC_URL = + 'https://cdn.icon-icons.com/icons2/603/PNG/512/heart_love_valentines_relationship_dating_date_icon-icons.com_55985.png'; +const KILL_PIC_URL = + 'https://cdn.icon-icons.com/icons2/1919/PNG/512/huntingtarget_122049.png'; + +const DUCK_EMBED: EmbedBuilder = new EmbedBuilder() + .setColor(Colors.Green) + .setTitle('Quack Quack!') + .setDescription('Befriend the duck with `bef` or shoot it with `bang`') + .setImage(DUCK_PIC_URL); + +const GOT_AWAY_EMBED: EmbedBuilder = new EmbedBuilder() + .setColor(Colors.Red) + .setTitle('A duck got away!') + .setDescription( + "Then he waddled away, waddle waddle, 'till the very next day" + ); + +// -- Helper functions -- + +/** Function to log a config error, done to save some lines + * @param message The message to send with the warning + */ +function configFail(message: string) { + util.logEvent(util.EventCategory.Warning, 'duck', message, 1); +} + +/** Function to get a random delay from the globally configured constants + * @returns A random delay between Max_spawn and Min_spawn + */ +function getRandomDelay(): number { + // Non null assertion - This is only called when the values aren't undefined + return Math.random() * (MAXIMUM_SPAWN! - MINIMUM_SPAWN!) + MINIMUM_SPAWN!; +} + +/** Function to get a random quote from the quote file + * @returns A random duck quote + */ +function getRandomQuote(): string { + return DUCK_QUOTES[Math.floor(Math.random() * DUCK_QUOTES.length)]; +} + +/** Function to get the duck record for an user by ID + * @param userId The ID of the user to get the record of + * + * @returns The record object if there is one, or null + */ +async function getRecord(userId: Snowflake): Promise { + const db: Db = util.mongo.fetchValue(); + const duckRecords: Collection = db.collection(DUCK_COLLECTION_NAME); + + return await duckRecords.findOne({ + user: userId, + }); +} + +/** Function to upsert a duck record in the DB + * @param duckUser The new entry + */ +async function updateRecord(newRecord: duckUser): Promise { + const db: Db = util.mongo.fetchValue(); + const duckRecords: Collection = db.collection(DUCK_COLLECTION_NAME); + + await duckRecords.updateOne( + {user: newRecord.user}, + { + $set: newRecord, + }, + {upsert: true} + ); +} + +/** Function to get the global speed record + * @returns The duckUser object with the record + */ +async function getGlobalSpeedRecord(): Promise { + const db: Db = util.mongo.fetchValue(); + const duckRecords: Collection = db.collection(DUCK_COLLECTION_NAME); + + return await duckRecords.find().sort({speedRecord: 1}).limit(1).next(); +} + +/** Function to get all befriended duck entries from the DB that don't equate to 0 + * @returns The array of duckUser objects, null if none are present + */ +async function getBefriendedRecords(): Promise { + const db: Db = util.mongo.fetchValue(); + const duckRecords: Collection = db.collection(DUCK_COLLECTION_NAME); + + return await duckRecords + .find({befriended: {$ne: 0}}) + .sort({befriended: -1}) + .toArray(); +} + +/** Function to get all killed duck entries from the DB that don't equate to 0 + * @returns The array of duckUser objects, null if none are present + */ +async function getKilledRecords(): Promise { + const db: Db = util.mongo.fetchValue(); + const duckRecords: Collection = db.collection(DUCK_COLLECTION_NAME); + + return await duckRecords + .find({killed: {$ne: 0}}) + .sort({killed: -1}) + .toArray(); +} + +// -- Core functions -- + +/** Function to add a befriended duck to the DB + * @param user The user who befriended the duck + * @param speed The time it took to befriend the duck + * @param channel The channel the duck was befriended in + */ +async function handleBefriend(user: User, speed: number, channel: TextChannel) { + const embed: EmbedBuilder = new EmbedBuilder() + .setColor(Colors.Blurple) + .setTitle('Duck befriended!') + .setThumbnail(BEFRIEND_PIC_URL); + + // Gets the user record from the db + const db: Db = util.mongo.fetchValue(); + const duckRecords: Collection = db.collection(DUCK_COLLECTION_NAME); + const locatedUser: duckUser | null = await duckRecords.findOne({ + user: user.id, + }); + + // If the user record was found, assign the value, otherwise leave it undefined + // This has to be done because ?? no worky with arithmetics + let updatedCount: number | undefined; + if (locatedUser !== null) { + updatedCount = locatedUser.befriended + 1; + } + + let updatedSpeed: number | undefined = locatedUser?.speedRecord; + + // The speed wasn't set (the person doesn't have a record) or the new speed record is faster + if (updatedSpeed === undefined || speed < updatedSpeed) { + updatedSpeed = speed; + + const oldRecord: number | null | undefined = (await getGlobalSpeedRecord()) + ?.speedRecord; + + if (oldRecord && oldRecord > updatedSpeed) { + embed.setFooter({ + text: `New personal record! Time: ${speed} seconds\nNew global record! previous global record: ${oldRecord} seconds`, + }); + } else { + embed.setFooter({text: `New personal record! Time: ${speed}`}); + } + } + + // The user has an entry, just append to it + await duckRecords.updateOne( + {user: user.id}, + { + $set: { + user: user.id, + befriended: updatedCount ?? 1, + killed: locatedUser?.killed ?? 0, + speedRecord: updatedSpeed, + }, + }, + {upsert: true} + ); + + embed.setDescription( + `<@!${user.id}> befriended the duck in ${speed} seconds!` + ); + embed.addFields([ + {name: 'Friends', value: updatedCount?.toString() ?? '1', inline: true}, + {name: 'Kills', value: locatedUser?.killed.toString() ?? '0', inline: true}, + ]); + duck.registerSubModule( + new util.SubModule( + 'record', + 'Gets the current global speed record', + [], + async () => { + const speedRecord: duckUser | null = await getGlobalSpeedRecord(); + + if (!speedRecord) { + return util.embed.errorEmbed('Noone has set a global record yet!'); + } + + const embed: EmbedBuilder = new EmbedBuilder() + .setColor(Colors.Green) + .setThumbnail(DUCK_PIC_URL) + .setTitle('Duck speed record') + .setFields([ + { + name: 'Time', + value: `${speedRecord.speedRecord.toString()} seconds`, + inline: true, + }, + { + name: 'Record holder', + value: `<@!${speedRecord.user}>`, + inline: true, + }, + ]); + + await channel.send({embeds: [embed]}); + } + ) + ); +} + +/** Function to add a killed duck to the DB + * @param user The user who killed the duck + * @param speed The time it took to kill the duck + * @param channel The channel the duck was killed in + */ +async function handleKill(user: User, speed: number, channel: TextChannel) { + const embed: EmbedBuilder = new EmbedBuilder() + .setColor(Colors.Red) + .setTitle('Duck killed!') + .setThumbnail(KILL_PIC_URL); + + // Gets the user record from the db + + const locatedUser: duckUser | null = await getRecord(user.id); + + // If the user record was found, assign the value, otherwise leave it undefined + // This has to be done because ?? no worky with arithmetics + let updatedCount: number | undefined; + if (locatedUser !== null) { + updatedCount = locatedUser.killed + 1; + } + + let updatedSpeed: number | undefined = locatedUser?.speedRecord; + + // The speed wasn't set (the person doesn't have a record) or the new speed record is faster + if (updatedSpeed === undefined || speed < updatedSpeed) { + updatedSpeed = speed; + + const oldRecord: number | null | undefined = (await getGlobalSpeedRecord()) + ?.speedRecord; + + if (oldRecord && oldRecord > updatedSpeed) { + embed.setFooter({ + text: `New personal record! Time: ${speed} seconds\nNew global record! previous global record: ${oldRecord} seconds`, + }); + } else { + embed.setFooter({text: `New personal record! Time: ${speed}`}); + } + } + + // Updates the existing record + await updateRecord({ + user: user.id, + befriended: locatedUser?.befriended ?? 0, + killed: updatedCount ?? 1, + speedRecord: updatedSpeed, + }).catch(err => { + return util.embed.errorEmbed( + `Database update call failed with error ${(err as Error).name}` + ); + }); + + embed + .setDescription(`<@!${user.id}> killed the duck in ${speed} seconds!`) + .addFields([ + { + name: 'Friends', + value: locatedUser?.befriended.toString() ?? '0', + inline: true, + }, + {name: 'Kills', value: updatedCount?.toString() ?? '1', inline: true}, + ]); + + await channel.send({embeds: [embed]}); +} + +/** Function to check whether the 'bef' or 'bang' missed + * @returns Whether the attempt missed + */ +async function miss( + member: GuildMember, + channel: TextChannel +): Promise { + if (Math.random() <= FAIL_RATES.interaction! / 100) { + return false; + } + const embed: EmbedBuilder = new EmbedBuilder() + .setColor(Colors.Red) + .setDescription(getRandomQuote()) + .setFooter({text: `Try again in ${COOLDOWN} seconds`}); + + const bot = channel.guild.members.cache.get(util.client.user!.id)!; + + if (bot.roles.highest.position > member.roles.highest.position) { + await member.timeout(COOLDOWN! * 1000, 'Missed a duck'); + } + + const timeoutMessage = await channel.send({embeds: [embed]}); + + // Deleted the timeout message after 5 seconds + setTimeout(async () => await timeoutMessage.delete(), COOLDOWN! * 1_000!); + return true; +} + +/** Function to send a duck and listen for a response */ +async function summonDuck(channel: TextChannel): Promise { + const duckMessage = await channel.send({embeds: [DUCK_EMBED]}); + const duckCollector = channel.createMessageCollector({ + time: duck.config.runAwayTime * 1_000, + filter: message => ['bef', 'bang'].includes(message.content), + }); + let caught = false; + + duckCollector.on('collect', async message => { + const time = + (message.createdTimestamp - duckMessage.createdTimestamp) / 1000; + + switch (message.content) { + case 'bef': + if (await miss(message.member!, channel)) { + break; + } else { + await duckMessage.delete(); + caught = true; + duckCollector.stop(); + + await handleBefriend(message.author, time, channel); + return; + } + + case 'bang': + if (await miss(message.member!, channel)) { + break; + } else { + await duckMessage.delete(); + caught = true; + duckCollector.stop(); + + await handleKill(message.author, time, channel); + return; + } + } + }); + + duckCollector.on('end', async () => { + // The duck wasn't caught using 'bef' or 'bang' + if (!caught) { + await duckMessage.delete(); + await channel.send({embeds: [GOT_AWAY_EMBED]}); + } + + // Restarts the duck loop with a random value + setTimeout(async () => { + void (await summonDuck(channel)); + }, getRandomDelay() * 1_000); + return; + }); +} + +duck.onInitialize(async () => { + // Verifies all config values are set (When not set, they are undefined) + if ( + typeof MINIMUM_SPAWN !== 'number' || + typeof MAXIMUM_SPAWN !== 'number' || + typeof RUN_AWAY_TIME !== 'number' || + typeof COOLDOWN !== 'number' || + typeof FAIL_RATES.interaction !== 'number' || + typeof FAIL_RATES.kill !== 'number' || + typeof FAIL_RATES.donate !== 'number' + ) { + configFail( + 'Config error: A config option is not set or is invalid, this module will be disabled.' + ); + return; + } + if (CHANNEL_IDS === undefined) { + configFail( + 'Config error: There are no valid channels set in the config, this mdule will be disabled.' + ); + return; + } + + // Gets all TextChannel objects + const channels: TextChannel[] = []; + + // Done to make sure all IDs are valid + for (const id of CHANNEL_IDS) { + const channel: Channel | undefined = util.client.channels.cache.get(id); + + if (channel === undefined) { + configFail( + `Config error: The channel ID ${id} is not a valid channel ID! Skipping it...` + ); + continue; + } + channels.push(channel as TextChannel); + } + + if (channels.length === 0) { + configFail( + 'Config error: There are no valid channels set in the config, this mdule will be disabled.' + ); + return; + } + + // There are valid channels, start all loops + for (const channel of channels) { + setTimeout(async () => { + void (await summonDuck(channel)); + }, getRandomDelay() * 1000); + } +}); + +// -- Module definitions -- + +duck.registerSubModule( + new util.SubModule( + 'stats', + 'Gets your current duck stats', + [ + { + type: util.ModuleOptionType.User, + name: 'user', + description: 'The user to get the stats of (Default: Yourself)', + required: false, + }, + ], + async (args, interaction) => { + let user: User; + + const userName: string | undefined = args + .find(arg => arg.name === 'user') + ?.value?.toString(); + + user = interaction.user!; + if (userName !== undefined) { + user = util.client.users.cache.get(userName)!; + } + + if (user.bot) { + return new EmbedBuilder() + .setColor(Colors.Red) + .setDescription( + "If it looks like a duck, quacks like a duck, it's a duck!" + ) + .toJSON(); + } + + const locatedUser: duckUser | null = await getRecord(user.id); + + if (locatedUser === null) { + if (user === interaction.user) { + return util.embed.errorEmbed( + 'You have not participated in the duck hunt yet' + ); + } + return util.embed.errorEmbed( + `<@!${user.id}> has not participated in the duck hunt yet!` + ); + } + + const embed: EmbedBuilder = new EmbedBuilder() + .setColor(Colors.Green) + .setThumbnail(DUCK_PIC_URL) + .setTitle('Duck stats') + .setDescription(`<@!${user.id}>`) + .setFields([ + { + name: 'Friends', + value: locatedUser.befriended.toString(), + inline: true, + }, + {name: 'Kills', value: locatedUser.killed.toString(), inline: true}, + ]); + + // The invoker holds the global record + if ( + (await getGlobalSpeedRecord())?.speedRecord === locatedUser.speedRecord + ) { + return embed + .setFooter({ + text: `Speed record: ${locatedUser.speedRecord} seconds\nYou hold the global record!`, + }) + .toJSON(); + } + + return embed + .setFooter({text: `Speed record: ${locatedUser.speedRecord} seconds`}) + .toJSON(); + } + ) +); + +duck.registerSubModule( + new util.SubModule( + 'record', + 'Gets the current global speed record', + [], + async () => { + const speedRecord: duckUser | null = await getGlobalSpeedRecord(); + + if (!speedRecord) { + return util.embed.errorEmbed('Noone has set a global record yet!'); + } + + const embed: EmbedBuilder = new EmbedBuilder() + .setColor(Colors.Green) + .setThumbnail(DUCK_PIC_URL) + .setTitle('Duck speed record') + .setFields([ + { + name: 'Time', + value: `${speedRecord.speedRecord.toString()} seconds`, + inline: true, + }, + { + name: 'Record holder', + value: `<@!${speedRecord.user}>`, + inline: true, + }, + ]); + + return embed.toJSON(); + } + ) +); + +duck.registerSubModule( + new util.SubModule( + 'friends', + 'Shows the global top befriended counts', + [], + async (_, interaction) => { + const entries: duckUser[] = await getBefriendedRecords(); + + if (entries.length === 0) { + return util.embed.errorEmbed('Noone has befriended a duck yet!'); + } + + // Gets the payloads for the pagination + let fieldNumber = 0; + const payloads = []; + + // The description has to be set to something, the speed record seems like the best choice + // Non-null assertion - people have befriended ducks, there is a record + const record: number = (await getGlobalSpeedRecord())!.speedRecord; + + let embed: EmbedBuilder = new EmbedBuilder() + .setColor(Colors.Green) + .setThumbnail(DUCK_PIC_URL) + .setTitle('Duck friendships') + .setDescription(`The current global record is: ${record} seconds`); + let fields: APIEmbedField[] = []; + + for (const entry of entries) { + // Limit of four entries per page + if (fieldNumber >= 4) { + embed.setFields(fields); + payloads.push({embeds: [embed.toJSON()]}); + + // Resets the embed and fields so they can be used for the next iteration + embed = new EmbedBuilder() + .setColor(Colors.Green) + .setThumbnail(DUCK_PIC_URL) + .setTitle('Duck friendships') + .setDescription(`The current global record is: ${record} seconds`); + + fields = []; + fieldNumber = 0; + } + + fieldNumber++; + + // Tries to get the user tag + const user: User | undefined = util.client.users.cache.get(entry.user); + + let tag = `Failed to get user tag, ID: ${entry.user}`; + // The user exists, use their tag + if (user) { + tag = user.tag; + } + fields.push({ + name: tag, + value: `Friends: \`${entry.befriended}\``, + }); + } + + // Makes sure fields are set if the iteration didn't finish + if (payloads.length % 4 !== 0) { + embed.setFields(fields); + payloads.push({embeds: [embed.toJSON()]}); + } + + new util.PaginatedMessage(interaction, payloads, 30); + } + ) +); + +duck.registerSubModule( + new util.SubModule( + 'killers', + 'Shows the global top killer counts', + [], + async (_, interaction) => { + const entries: duckUser[] = await getKilledRecords(); + + if (entries.length === 0) { + return util.embed.errorEmbed('Noone has killed a duck yet!'); + } + + // Gets the payloads for the pagination + let fieldNumber = 0; + const payloads = []; + + // The description has to be set to something, the speed record seems like the best choice + // Non-null assertion - people have killed ducks, there is a record + const record: number = (await getGlobalSpeedRecord())!.speedRecord; + + let embed: EmbedBuilder = new EmbedBuilder() + .setColor(Colors.Green) + .setThumbnail(DUCK_PIC_URL) + .setTitle('Duck kills') + .setDescription(`The current global record is: ${record} seconds`); + let fields: APIEmbedField[] = []; + + for (const entry of entries) { + // Limit of four entries per page + if (fieldNumber >= 4) { + embed.setFields(fields); + payloads.push({embeds: [embed.toJSON()]}); + + // Resets the embed and fields so they can be used for the next iteration + embed = new EmbedBuilder() + .setColor(Colors.Green) + .setThumbnail(DUCK_PIC_URL) + .setTitle('Duck kills') + .setDescription(`The current global record is: ${record} seconds`); + + fields = []; + fieldNumber = 0; + } + + fieldNumber++; + + // Tries to get the user tag + const user: User | undefined = util.client.users.cache.get(entry.user); + + let tag = `Failed to get user tag, ID: ${entry.user}`; + // The user exists, use their tag + if (user) { + tag = user.tag; + } + fields.push({ + name: tag, + value: `Kills: \`${entry.killed}\``, + }); + } + + // Makes sure fields are set if the iteration didn't finish + if (payloads.length % 4 !== 0) { + embed.setFields(fields); + payloads.push({embeds: [embed.toJSON()]}); + } + + new util.PaginatedMessage(interaction, payloads, 30); + } + ) +); + +duck.registerSubModule( + new util.SubModule( + 'donate', + 'Kills a befriended duck and adds it to your kill count.', + [ + { + type: util.ModuleOptionType.User, + name: 'user', + description: 'The user to donate the duck to', + required: true, + }, + ], + async (args, interaction) => { + const recipeeName: string = args + .find(arg => arg.name === 'user')! + .value!.toString(); + const recipee: User = util.client.users.cache.get(recipeeName)!; + + if (recipee.bot) { + return util.embed.errorEmbed( + 'The only ducks bots accept are plated with gold!' + ); + } + + const donorRecord = await getRecord(interaction.user.id); + const recipeeRecord = await getRecord(recipee.id); + + // Makes sure the command can be executed + + if (!donorRecord) { + return util.embed.errorEmbed( + 'You have not participated in the duck hunt yet' + ); + } + + if (!recipeeRecord) { + return util.embed.errorEmbed( + `<@!${recipee.id}> has not participated in the duck hunt yet!` + ); + } + + if (donorRecord.befriended === 0) { + return util.embed.errorEmbed('You have no ducks to donate!'); + } + + await updateRecord({ + user: donorRecord.user, + befriended: donorRecord.befriended - 1, + killed: donorRecord.killed, + speedRecord: donorRecord.speedRecord, + }).catch(err => { + return util.embed.errorEmbed( + `Database update call failed with error ${(err as Error).name}` + ); + }); + + // Fail chance + if (Math.random() <= FAIL_RATES.donate! / 100) { + return util.embed.errorEmbed( + `Oops, the duck broke out of its cage before it arrived. You have ${ + donorRecord.befriended - 1 + } ducks left.` + ); + } + + await updateRecord({ + user: recipeeRecord.user, + befriended: recipeeRecord.befriended + 1, + killed: recipeeRecord.killed, + speedRecord: recipeeRecord.speedRecord, + }).catch(err => { + return util.embed.errorEmbed( + `Database update call failed with error ${(err as Error).name}` + ); + }); + + return util.embed.successEmbed( + `How generous! You gave a duck to <@!${recipee.id}> and have ${ + donorRecord.befriended - 1 + } ducks remaning` + ); + } + ) +); + +duck.registerSubModule( + new util.SubModule( + 'kill', + 'Kills a befriended duck and adds it to your kill count.', + [], + async (_, interaction) => { + const userRecord = await getRecord(interaction.user.id); + + // Makes sure the command can be executed + + if (!userRecord) { + return util.embed.errorEmbed( + 'You have not participated in the duck hunt yet' + ); + } + + if (userRecord.befriended === 0) { + return util.embed.errorEmbed('You have no ducks left to kill'); + } + + // Fail chance + if (Math.random() <= FAIL_RATES.kill! / 100) { + await updateRecord({ + user: userRecord.user, + befriended: userRecord.befriended - 1, + killed: userRecord.killed, + speedRecord: userRecord.speedRecord, + }).catch(err => { + return util.embed.errorEmbed( + `Database update call failed with error ${(err as Error).name}` + ); + }); + + return util.embed.errorEmbed( + `Oops, the duck ran away before you could hurt it. You have ${ + userRecord.befriended - 1 + } ducks left.` + ); + } + + await updateRecord({ + user: userRecord.user, + befriended: userRecord.befriended - 1, + killed: userRecord.killed + 1, + speedRecord: userRecord.speedRecord, + }).catch(err => { + return util.embed.errorEmbed( + `Database update call failed with error ${(err as Error).name}` + ); + }); + + return util.embed.successEmbed( + `You monster! You have ${ + userRecord.befriended - 1 + } ducks remaning and ${userRecord.killed + 1} kills to your name` + ); + } + ) +); + +duck.registerSubModule( + new util.SubModule( + 'reset', + 'Resets an users duck commands', + [ + { + type: util.ModuleOptionType.User, + name: 'user', + description: ' The user to reset the duck stats of', + required: true, + }, + ], + async (args, interaction) => { + const userName: string = args + .find(arg => arg.name === 'user')! + .value!.toString(); + + const user: User = util.client.users.cache.get(userName)!; + const db: Db = util.mongo.fetchValue(); + const duckRecords: Collection = + db.collection(DUCK_COLLECTION_NAME); + + switch ( + await util.embed.confirmEmbed( + `Are you sure you want to reset the duck stats of <@!${user.id}>?`, + interaction + ) + ) { + case util.ConfirmEmbedResponse.Denied: + return util.embed.infoEmbed('The duck stats were NOT reset.'); + + case util.ConfirmEmbedResponse.Confirmed: + await duckRecords.deleteOne({user: user.id}); + return util.embed.successEmbed( + `Succesfully wiped the duck stats of <@!${user.id}>` + ); + } + } + ) +); + +export default duck; diff --git a/src/modules/duck/duck_quotes.json b/src/modules/duck/duck_quotes.json new file mode 100644 index 0000000..a549017 --- /dev/null +++ b/src/modules/duck/duck_quotes.json @@ -0,0 +1,185 @@ +[ + "\"...dissent, rebellion, and all-around hell-raising remain the true duty of patriots.\" - *Barbara Ehrenreich*", + "\"A citizen of America will cross the ocean to fight for democracy, but won't cross the street to vote...\" - *Bill Vaughan*", + "\"A hero is no braver than an ordinary man, but he is brave five minutes longer.\" - *Ralph Waldo Emerson*", + "\"A leader leads by example, not by force.\" - *Sun Tzu*", + "\"A man may die, nations may rise and fall, but an idea lives on.\" - *John F. Kennedy*", + "\"A man who would not risk his life for something does not deserve to live.\" - *Martin Luther King*", + "\"A man's feet must be planted in his country, but his eyes should survey the world.\" - *George Santayana*", + "\"A nation reveals itself not only by the men it produces but also by the men it honors, the men it remembers.\" - *John F. Kennedy*", + "\"A ship without Marines is like a garment without buttons.\" - *Admiral David D. Porter, USN*", + "\"A soldier will fight long and hard for a bit of colored ribbon.\" - *Napoleon Bonaparte*", + "\"Above all things, never be afraid. The enemy who forces you to retreat is himself afraid of you at that very moment.\" - *Andre Maurois*", + "\"Aim towards the Enemy.\" - *Instruction printed on US Rocket Launcher*", + "\"All that is necessary for evil to succeed is for good men to do nothing.\" - *Edmund Burke*", + "\"All warfare is based on deception.\" - *Sun Tzu*", + "\"All wars are civil wars, because all men are brothers.\" - *Francois Fenelon*", + "\"An eye for an eye makes the whole world blind.\" - *Gandhi*", + "\"Any military commander who is honest will admit he makes mistakes in the application of military power.\" - *Robert McNamara*", + "\"Any soldier worth his salt should be antiwar. And still, there are things worth fighting for.\" - *General Norman Schwarzkopf*", + "\"Anyone, who truly wants to go to war, has truly never been there before!\" - *Larry Reeves*", + "\"As we express our gratitude, we must never forget that the highest appreciation is not to utter words, but to live by them.\" - *John F Kennedy*", + "\"Ask not what your country can do for you, but what you can do for your country.\" - *John F Kennedy*", + "\"Battles are won by slaughter and maneuver. The greater the general, the more he contributes in maneuver, the less he demands in slaughter.\" - *Winston Churchill*", + "\"Before you embark on a journey of revenge, you should first dig two graves.\" - *Confucius*", + "\"Better to fight for something than live for nothing.\" - *General George S. Patton*", + "\"Cluster bombing from B-52s are very, very, accurate. The bombs are guaranteed to always hit the ground.\" - *USAF Ammo Troop*", + "\"Concentrated power has always been the enemy of liberty.\" - *Ronald Reagan*", + "\"Cost of a single AC-130U Gunship: $190 Million\" - *Unknown*", + "\"Cost of a single B-2 Bomber: $2.2 Billion\" - *Unknown*", + "\"Cost of a single F-117A Nighthawk: $122 Million\" - *Unknown*", + "\"Cost of a single F-22 Raptor: $135 Million\" - *Unknown*", + "\"Cost of a single Javelin Missile: $80,000\" - *Unknown*", + "\"Cost of a single Tomahawk Cruise Missile: $900,000\" - *Unknown*", + "\"Courage and perseverance have a magical talisman, before which difficulties disappear and obstacles vanish into air.\" - *John Quincy Adams*", + "\"Courage is being scared to death - but saddling up anyway.\" - *John Wayne*", + "\"Courage is fear holding on a minute longer.\" - *General George S. Patton*", + "\"Cowards die many times before their deaths; The valiant never taste of death but once.\" - *William Shakespeare, ‘’Julius Caesar’’*", + "\"Death is nothing, but to live defeated and inglorious is to die daily.\" - *Napoleon Bonaparte*", + "\"Death solves all problems - no man, no problem.\" - *Joseph Stalin*", + "\"Diplomats are just as essential in starting a war as soldiers are for finishing it.\" - *Will Rogers*", + "\"Don't get mad, get even.\" - *John F. Kennedy*", + "\"Every man's life ends the same way. It is only the details of how he lived and how he died that distinguish one man from another.\" - *Ernest Hemingway*", + "\"Every tyrant who has lived has believed in freedom - for himself.\" - *Elbert Hubbard*", + "\"Five second fuses only last three seconds.\" - *Infantry Journal*", + "\"For the Angel of Death spread his wings on the blast, And breathed in the face of the foe as he pass'd; And the eyes of the sleepers wax'd deadly and chill, And their hearts but once heaved, and for ever grew still!\" - *George Gordon Byron, The Destruction of Sennacherib*", + "\"Freedom is not free, but the U.S. Marine Corps will pay most of your share.\" - *Ned Dolan*", + "\"Friendly fire, isn't.\" - *Unknown*", + "\"From my rotting body, flowers shall grow and I am in them and that is eternity.\" - *Edvard Munch*", + "\"Future years will never know the seething hell and the black infernal background, the countless minor scenes and interiors of the secession war; and it is best they should not. The real war will never get in the books.\" - *Walt Whitman*", + "\"He conquers who endures.\" - *Persius*", + "\"He who fears being conquered is sure of defeat.\" - *Napoleon Bonaparte*", + "\"He who has a thousand friends has not a friend to spare, And he who has one enemy will meet him everywhere.\" - *Ali ibn-Abi-Talib*", + "\"Heroes may not be braver than anyone else. They're just brave five minutes longer.\" - *Ronald Reagan*", + "\"History will be kind to me for I intend to write it.\" - *Winston Churchill*", + "\"I have never advocated war except as a means of peace.\" - *Ulysses S. Grant*", + "\"I have never made but one prayer to God, a very short one: 'O Lord, make my enemies ridiculous.' And God granted it.\" - *Voltaire*", + "\"I know not with what weapons World War III will be fought, but World War IV will be fought with sticks and stones.\" - *Albert Einstein*", + "\"I love America more than any other country in this world; and, exactly for this reason, I insist on the right to criticize her perpetually.\" - *James Baldwin*", + "\"I only regret that I have but one life to give for my country.\" - *Nathan Hale*", + "\"I think that technologies are morally neutral until we apply them. It's only when we use them for good or for evil that they become good or evil.\" - *William Gibson*", + "\"I think the human race needs to think about killing. How much evil must we do to do good?\" - *Robert McNamara*", + "\"I was born an American; I will live an American; I shall die an American!\" - *Daniel Webster*", + "\"I will fight for my country, but I will not lie for her.\" - *Zora Neale Hurston*", + "\"I would not say that the future is necessarily less predictable than the past. I think the past was not predictable when it started.\" - *Donald Rumsfeld*", + "\"If a man has done his best, what else is there?\" - *General George S. Patton*", + "\"If an injury has to be done to a man it should be so severe that his vengeance need not be feared.\" - *Machiavelli*", + "\"If at first you don't succeed, call an air strike.\" - *Unknown*", + "\"If our country is worth dying for in time of war let us resolve that it is truly worth living for in time of peace.\" - *Hamilton Fish*", + "\"If the enemy is in range, so are you.\" - *Infantry Journal*", + "\"If the opposition disarms, well and good. If it refuses to disarm, we shall disarm it ourselves.\" - *Joseph Stalin*", + "\"If the wings are traveling faster than the fuselage, it's probably a helicopter, and therefore, unsafe.\" - *Unknown*", + "\"If we can't persuade nations with comparable values of the merits of our cause, we'd better reexamine our reasoning.\" - *Robert McNamara*", + "\"If we don't end war, war will end us.\" - *H. G. Wells*", + "\"If you are ashamed to stand by your colors, you had better seek another flag.\" - *Anonymous*", + "\"If you can't remember, the claymore is pointed toward you.\" - *Unknown*", + "\"If you know the enemy and know yourself you need not fear the results of a hundred battles.\" - *Sun Tzu*", + "\"If you want a symbolic gesture, don't burn the flag; wash it.\" - *Norman Thomas*", + "\"If your attack is going too well, you're walking into an ambush.\" - *Infantry Journal*", + "\"In peace, sons bury their fathers. In war, fathers bury their sons.\" - *Herodotus*", + "\"In the Soviet army it takes more courage to retreat than advance.\" - *Joseph Stalin*", + "\"In the end, it was luck. We were *this* close to nuclear war, and luck prevented it.\" - *Robert McNamara*", + "\"In war there is no prize for the runner-up.\" - *General Omar Bradley*", + "\"In war there is no substitute for victory.\" - *General Douglas MacArthur*", + "\"In war, truth is the first casualty\" - *Aeschylus*", + "\"In war, you win or lose, live or die and the difference is just an eyelash.\" - *General Douglas MacArthur*", + "\"Incoming fire has the right of way.\" - *Unknown*", + "\"It doesn't take a hero to order men into battle. It takes a hero to be one of those men who goes into battle.\" - *General Norman Schwarzkopf*", + "\"It is better to die on your feet than to live on your knees!\" - *Emiliano Zapata*", + "\"It is easy to take liberty for granted when you have never had it taken from you.\" - *Dick Cheney*", + "\"It is fatal to enter any war without the will to win it.\" - *General Douglas MacArthur*", + "\"It is foolish and wrong to mourn the men who died. Rather we should thank God that such men lived.\" - *General George S. Patton*", + "\"It is generally inadvisable to eject directly over the area you just bombed.\" - *U.S. Air Force Marshal*", + "\"It is lamentable, that to be a good patriot one must become the enemy of the rest of mankind.\" - *Voltaire*", + "\"It is well that war is so terrible, or we should get too fond of it.\" - *Robert E. Lee*", + "\"Keep looking below surface appearances. Don't shrink from doing so just because you might not like what you find.\" - *Colin Powell*", + "\"Let your plans be as dark and impenetrable as night, and when you move, fall like a thunderbolt.\" - *Sun Tzu*", + "\"Live as brave men; and if fortune is adverse, front its blows with brave hearts.\" - *Cicero*", + "\"Live well. It is the greatest revenge.\" - *The Talmud*", + "\"Mankind must put an end to war, or war will put an end to mankind.\" - *John F. Kennedy*", + "\"My first wish is to see this plague of mankind, war, banished from the earth.\" - *George Washington*", + "\"Nationalism is an infantile disease. It is the measles of mankind.\" - *Albert Einstein*", + "\"Nearly all men can stand adversity, but if you want to test a man's character, give him power.\" - *Abraham Lincoln*", + "\"Never forget that your weapon was made by the lowest bidder.\" - *Unknown*", + "\"Never in the field of human conflict was so much owed by so many to so few.\" - *Winston Churchill*", + "\"Never interrupt your enemy when he is making a mistake.\" - *Napoleon Bonaparte*", + "\"Never think that war, no matter how necessary, nor how justified, is not a crime.\" - *Ernest Hemingway*", + "\"No battle plan survives contact with the enemy.\" - *Colin Powell*", + "\"Nothing in life is so exhilarating as to be shot at without result.\" - *Winston Churchill*", + "\"Older men declare war. But it is the youth that must fight and die.\" - *Herbert Hoover*", + "\"One good act of vengeance deserves another.\" - *Jon Jefferson*", + "\"Only the dead have seen the end of war.\" - *Plato*", + "\"Our greatest glory is not in never failing, but in rising up every time we fail.\" - *Ralph Waldo Emerson*", + "\"Ours is a world of nuclear giants and ethical infants. We know more about war than we know about peace, more about killing than we know about living.\" - *General Omar Bradley*", + "\"Patriotism is an arbitrary veneration of real estate above principles.\" - *George Jean Nathan*", + "\"Patriotism is supporting your country all the time, and your government when it deserves it.\" - *Mark Twain*", + "\"Patriotism is your conviction that this country is superior to all others because you were born in it.\" - *George Bernard Shaw*", + "\"Patriotism ruins history.\" - *Goethe*", + "\"Patriotism varies, from a noble devotion to a moral lunacy.\" - *WR Inge*", + "\"Principle is OK up to a certain point, but principle doesn't do any good if you lose.\" - *Dick Cheney*", + "\"Revenge is profitable.\" - *Edward Gibbon*", + "\"Revenge, at first though sweet, Bitter ere long back on itself recoils.\" - *John Milton*", + "\"Safeguarding the rights of others is the most noble and beautiful end of a human being.\" - *Kahlil Gibran, ‘’The Voice of the Poet’’*", + "\"So long as there are men, there will be wars.\" - *Albert Einstein*", + "\"Soldiers usually win the battles and generals get the credit for them\" - *Napoleon Bonaparte*", + "\"Some people live an entire lifetime and wonder if they have ever made a difference in the world, but the Marines don't have that problem.\" - *Ronald Reagan*", + "\"Success is not final, failure is not fatal: it is the courage to continue that counts.\" - *Winston Churchill*", + "\"Teamwork is essential, it gives them other people to shoot at.\" - *Unknown*", + "\"The bursting radius of a hand-grenade is always one foot greater than your jumping range.\" - *Unknown*", + "\"The characteristic of a genuine heroism is its persistency. All men have wandering impulses, fits and starts of generosity. But when you have resolved to be great, abide by yourself, and do not weakly try to reconcile yourself with the world. The heroic cannot be the common, nor the common the heroic.\" - *Ralph Waldo Emerson*", + "\"The commander in the field is always right and the rear echelon is wrong, unless proved otherwise.\" - *Colin Powell*", + "\"The deadliest weapon in the world is a Marine and his rifle!\" - *General John J. Pershing*", + "\"The death of one man is a tragedy. The death of millions is a statistic.\" - *Joseph Stalin*", + "\"The indefinite combination of human infallibility and nuclear weapons will lead to the destruction of nations.\" - *Robert McNamara*", + "\"The more marines I have around, the better I like it.\" - *General Clark, U.S. Army*", + "\"The nation is divided, half patriots and half traitors, and no man can tell which from which.\" - *Mark Twain*", + "\"The object of war is not to die for your country but to make the other bastard die for his.\" - *General George S. Patton*", + "\"The press is our chief ideological weapon.\" - *Nikita Khrushchev*", + "\"The real and lasting victories are those of peace, and not of war.\" - *Ralph Waldo Emmerson*", + "\"The soldier above all others prays for peace, for it is the soldier who must suffer and bear the deepest wounds and scars of war.\" - *General Douglas MacArthur*", + "\"The tree of liberty must be refreshed from time to time with the blood of patriots and tyrants.\" - *Thomas Jefferson*", + "\"The truth of the matter is that you always know the right thing to do. The hard part is doing it.\" - *Norman Schwarzkopf*", + "\"The tyrant always talks as if he's preserving the best interests of his people when he actually acts to undermine them.\" - *Ramman Kenoun*", + "\"The world will not accept dictatorship or domination.\" - *Mikhail Gorbachev*", + "\"There are no atheists in foxholes, this isn't an argument against atheism, it's an argument against foxholes.\" - *James Morrow*", + "\"There are only two forces in the world, the sword and the spirit. In the long run the sword will always be conquered by the spirit.\" - *Napoleon Bonaparte*", + "\"There are only two kinds of people that understand Marines: Marines and the enemy. Everyone else has a secondhand opinion.\" - *General William Thornson*", + "\"There is many a boy here today who looks on war as all glory, but, boys, it is all hell. You can bear this warning voice to generations yet to come. I look upon war with horror.\" - *General William Tecumseh Sherman*", + "\"There will one day spring from the brain of science a machine or force so fearful in its potentialities, so absolutely terrifying, that even man, the fighter, who will dare torture and death in order to inflict torture and death, will be appalled, and so abandon war forever.\" - *Thomas A. Edison*", + "\"There's a graveyard in northern France where all the dead boys from D-Day are buried. The white crosses reach from one horizon to the other. I remember looking it over and thinking it was a forest of graves. But the rows were like this, dizzying, diagonal, perfectly straight, so after all it wasn't a forest but an orchard of graves. Nothing to do with nature, unless you count human nature.\" - *Barbara Kingsolver*", + "\"There's no honorable way to kill, no gentle way to destroy. There is nothing good in war. Except its ending.\" - *Abraham Lincoln*", + "\"They died hard, those savage men - like wounded wolves at bay. They were filthy, and they were lousy, and they stunk. And I loved them.\" - *General Douglas MacArthur*", + "\"They wrote in the old days that it is sweet and fitting to die for one's country. But in modern war, there is nothing sweet nor fitting in your dying. You will die like a dog for no good reason.\" - *Ernest Hemingway*", + "\"They'll be no learning period with nuclear weapons. Make one mistake and you're going to destroy nations.\" - *Robert McNamara*", + "\"Those who have long enjoyed such privileges as we enjoy forget in time that men have died to win them.\" - *Franklin D. Roosevelt*", + "\"Tracers work both ways.\" - *U.S. Army Ordinance*", + "\"Traditional nationalism cannot survive the fissioning of the atom. One world or none.\" - *Stuart Chase*", + "\"Try to look unimportant; they may be low on ammo.\" - *Infantry Journal*", + "\"Tyrants have always some slight shade of virtue; they support the laws before destroying them.\" - *Voltaire*", + "\"War does not determine who is right, only who is left.\" - *Bertrand Russell*", + "\"War is a series of catastrophes which result in victory.\" - *Georges Clemenceau*", + "\"War is an ugly thing, but not the ugliest of things. The decayed and degraded state of moral and patriotic feeling which thinks that nothing is worth war is much worse. The person who has nothing for which he is willing to fight, nothing which is more important than his own personal safety, is a miserable creature, and has no chance of being free unless made or kept so by the exertions of better men than himself.\" - *John Stuart Mill*", + "\"War is as much a punishment to the punisher as it is to the sufferer.\" - *Thomas Jefferson*", + "\"War is delightful to those who have not yet experienced it.\" - *Erasmus*", + "\"War is fear cloaked in courage.\" - *General William C. Westmoreland*", + "\"War would end if the dead could return.\" - *Stanley Baldwin*", + "\"We happy few, we band of brothers/For he today that sheds his blood with me/Shall be my brother.\" - *William Shakespeare,’’ King Henry V’’*", + "\"We know where they are. They're in the area around Tikrit and Baghdad and east, west, south and north somewhat.\" - *Donald Rumsfeld*", + "\"We must be prepared to make heroic sacrifices for the cause of peace that we make ungrudgingly for the cause of war. There is no task that is more important or closer to my heart.\" - *Albert Einstein*", + "\"We must not confuse dissent with disloyalty.\" - *Edward R Murrow*", + "\"We shall defend our island, whatever the cost may be, we shall fight on the beaches, we shall fight on the landing grounds, we shall fight in the fields and in the streets, we shall fight in the hills; we shall never surrender.\" - *Winston Churchill*", + "\"We sleep safely in our beds because rough men stand ready in the night to visit violence on those who would harm us.\" - *George Orwell (Misattributed, was actually written by Richard Grenier[1])*", + "\"We're in a world in which the possibility of terrorism, married up with technology, could make us very, very sorry we didn't act.\" - *Condoleeza Rice*", + "\"When the pin is pulled, Mr. Grenade is not our friend.\" - *U.S. Army Training Notice*", + "\"When you get to the end of your rope, tie a knot and hang on.\" - *Franklin D. Roosevelt*", + "\"When you have to kill a man it costs nothing to be polite.\" - *Winston Churchill*", + "\"Whether you like it or not, history is on our side. We will bury you!\" - *Nikita Khrushchev*", + "\"Whoever does not miss the Soviet Union has no heart. Whoever wants it back has no brain.\" - *Vladimir Putin*", + "\"Whoever said the pen is mightier than the sword obviously never encountered automatic weapons.\" - *General Douglas MacArthur*", + "\"Whoever stands by a just cause cannot possibly be called a terrorist.\" - *Yassar Arafat*", + "\"You can make a throne of bayonets, but you cant sit on it for long.\" - *Boris Yeltsin*", + "\"You can't say civilization don't advance, for in every war, they kill you in a new way.\" - *Will Rogers*", + "\"You cannot get ahead while you are getting even.\" - *Dick Armey*", + "\"You know the real meaning of peace only if you have been through the war.\" - *Kosovar*", + "\"You must not fight too often with one enemy, or you will teach him all your art of war.\" - *Napoleon Bonaparte*" +] From f84e597edf6d64555f716461d5536b1e78105f6d Mon Sep 17 00:00:00 2001 From: Cpt-Dingus <100243410+Cpt-Dingus@users.noreply.github.com> Date: Tue, 29 Aug 2023 00:17:14 +0200 Subject: [PATCH 2/5] Ducks: Added ducks to the default config, added a timeout Map --- config.default.jsonc | 17 ++++++++++ src/modules/duck/duck.ts | 70 +++++++++++++++++++--------------------- 2 files changed, 51 insertions(+), 36 deletions(-) diff --git a/config.default.jsonc b/config.default.jsonc index 7dd64dd..d491c0a 100644 --- a/config.default.jsonc +++ b/config.default.jsonc @@ -159,6 +159,23 @@ "maxLength": 100, "pasteFooterContent": "", "pasteApi": "" + }, + "duck": { + "enabled": true, + "channels": [], + // Times are specified in seconds + "minimumSpawn": 30, + "maximumSpawn": 180, + "runAwayTime": 20, + // Timeout on miss + "cooldown": 5, + // Failure percentages + "failRates": { + // Bef and bang + "interaction": 50, + "kill": 50, + "donate": 50 + } } } } diff --git a/src/modules/duck/duck.ts b/src/modules/duck/duck.ts index 8741c6f..1ede839 100644 --- a/src/modules/duck/duck.ts +++ b/src/modules/duck/duck.ts @@ -32,6 +32,13 @@ interface duckUser { speedRecord: number; } +/** Interface used for miss checks, where: + * User id: Unix timestamp of timeout end + */ +interface missMap { + [user: Snowflake]: number; +} + /** The root 'duck' module definition */ const duck = new util.RootModule('duck', 'Duck management commands', [ util.mongo, @@ -227,39 +234,8 @@ async function handleBefriend(user: User, speed: number, channel: TextChannel) { {name: 'Friends', value: updatedCount?.toString() ?? '1', inline: true}, {name: 'Kills', value: locatedUser?.killed.toString() ?? '0', inline: true}, ]); - duck.registerSubModule( - new util.SubModule( - 'record', - 'Gets the current global speed record', - [], - async () => { - const speedRecord: duckUser | null = await getGlobalSpeedRecord(); - - if (!speedRecord) { - return util.embed.errorEmbed('Noone has set a global record yet!'); - } - const embed: EmbedBuilder = new EmbedBuilder() - .setColor(Colors.Green) - .setThumbnail(DUCK_PIC_URL) - .setTitle('Duck speed record') - .setFields([ - { - name: 'Time', - value: `${speedRecord.speedRecord.toString()} seconds`, - inline: true, - }, - { - name: 'Record holder', - value: `<@!${speedRecord.user}>`, - inline: true, - }, - ]); - - await channel.send({embeds: [embed]}); - } - ) - ); + await channel.send({embeds: [embed.toJSON()]}); } /** Function to add a killed duck to the DB @@ -335,7 +311,7 @@ async function miss( member: GuildMember, channel: TextChannel ): Promise { - if (Math.random() <= FAIL_RATES.interaction! / 100) { + if (Math.random() >= FAIL_RATES.interaction! / 100) { return false; } const embed: EmbedBuilder = new EmbedBuilder() @@ -349,10 +325,8 @@ async function miss( await member.timeout(COOLDOWN! * 1000, 'Missed a duck'); } - const timeoutMessage = await channel.send({embeds: [embed]}); + await channel.send({embeds: [embed]}); - // Deleted the timeout message after 5 seconds - setTimeout(async () => await timeoutMessage.delete(), COOLDOWN! * 1_000!); return true; } @@ -363,15 +337,38 @@ async function summonDuck(channel: TextChannel): Promise { time: duck.config.runAwayTime * 1_000, filter: message => ['bef', 'bang'].includes(message.content), }); + const misses: missMap = {}; let caught = false; duckCollector.on('collect', async message => { const time = (message.createdTimestamp - duckMessage.createdTimestamp) / 1000; + // The person missed within seconds ago + if ( + misses[message.author.id] !== undefined && + Date.now() <= misses[message.author.id] + ) { + // All errors are catched since the only possible one is that the author has disabled dms + await message.author + .send(`I said to wait for ${COOLDOWN!} seconds! Resetting the timer...`) + .catch(); + // This informs the author that they got timed out regardless of their dm status + await message.react('🕗'); + // Resets the timer + misses[message.author.id] = Date.now() + COOLDOWN! * 1_000; + return; + } + + // The timeout has passed, just remove the value from the miss cache + if (misses[message.author.id] !== undefined) { + delete misses[message.author.id]; + } + switch (message.content) { case 'bef': if (await miss(message.member!, channel)) { + misses[message.author.id] = Date.now() + COOLDOWN! * 1_000; break; } else { await duckMessage.delete(); @@ -384,6 +381,7 @@ async function summonDuck(channel: TextChannel): Promise { case 'bang': if (await miss(message.member!, channel)) { + misses[message.author.id] = Date.now() + COOLDOWN! * 1_000; break; } else { await duckMessage.delete(); From 4dc0e5769653748f4bc57fa9345c3a3532edaf74 Mon Sep 17 00:00:00 2001 From: Cpt-Dingus <100243410+Cpt-Dingus@users.noreply.github.com> Date: Fri, 1 Sep 2023 13:10:14 +0200 Subject: [PATCH 3/5] fix: Cleaned the file up a bit --- src/modules/duck/duck.ts | 225 +++++++++++++++++++++------------------ 1 file changed, 123 insertions(+), 102 deletions(-) diff --git a/src/modules/duck/duck.ts b/src/modules/duck/duck.ts index 1ede839..79a2cef 100644 --- a/src/modules/duck/duck.ts +++ b/src/modules/duck/duck.ts @@ -12,30 +12,32 @@ import { Channel, Colors, EmbedBuilder, - GuildMember, + Message, Snowflake, TextChannel, User, } from 'discord.js'; import {Collection, Db} from 'mongodb'; -/** The main interface used in the DB +/** The main interface used to store duck records in the DB * @param user The user ID * @param befriended The number of befriended ducks * @param killed The number of killed ducks * @param speedRecord The fastest duck interaction for the user */ -interface duckUser { +interface DuckUser { user: Snowflake; befriended: number; killed: number; speedRecord: number; } -/** Interface used for miss checks, where: - * User id: Unix timestamp of timeout end +/** Interface used to store users who missed a duck interaction, important + * when people are admin and can't be timed out. + * Values: + * User id: Unix timestamp of the miss delays */ -interface missMap { +interface MissTimeoutMap { [user: Snowflake]: number; } @@ -45,18 +47,21 @@ const duck = new util.RootModule('duck', 'Duck management commands', [ ]); // -- Constants -- + const DUCK_COLLECTION_NAME = 'ducks'; const DUCK_QUOTES: string[] = JSON.parse( readFileSync('./src/modules/duck/duck_quotes.json', 'utf-8') ); // Config constants -const CHANNEL_IDS: string[] | undefined = duck.config.channels; -const MINIMUM_SPAWN: number | undefined = duck.config.minimumSpawn; -const MAXIMUM_SPAWN: number | undefined = duck.config.maximumSpawn; -const RUN_AWAY_TIME: number | undefined = duck.config.runAwayTime; -const COOLDOWN: number | undefined = duck.config.cooldown; -const FAIL_RATES = duck.config.failRates; +const channelIds: string[] | undefined = duck.config.channels; +const minimumSpawnTime: number | undefined = duck.config.minimumSpawn; +const maximumSpawnTime: number | undefined = duck.config.maximumSpawn; +const runAwayTime: number | undefined = duck.config.runAwayTime; +const missCooldown: number | undefined = duck.config.cooldown; +const failRates = duck.config.failRates; + +// Embed constants const DUCK_PIC_URL = 'https://cdn.icon-icons.com/icons2/1446/PNG/512/22276duck_98782.png'; @@ -87,12 +92,14 @@ function configFail(message: string) { util.logEvent(util.EventCategory.Warning, 'duck', message, 1); } -/** Function to get a random delay from the globally configured constants - * @returns A random delay between Max_spawn and Min_spawn +/** Function to get a random delay from the config + * @returns A random delay in seconds between maximumSpawnTime and minimumSpawnTime */ function getRandomDelay(): number { // Non null assertion - This is only called when the values aren't undefined - return Math.random() * (MAXIMUM_SPAWN! - MINIMUM_SPAWN!) + MINIMUM_SPAWN!; + return ( + Math.random() * (maximumSpawnTime! - minimumSpawnTime!) + minimumSpawnTime! + ); } /** Function to get a random quote from the quote file @@ -102,14 +109,14 @@ function getRandomQuote(): string { return DUCK_QUOTES[Math.floor(Math.random() * DUCK_QUOTES.length)]; } -/** Function to get the duck record for an user by ID +/** Function to get the duck record for an user by their ID * @param userId The ID of the user to get the record of * - * @returns The record object if there is one, or null + * @returns The DuckUser object, null if it wasn't found */ -async function getRecord(userId: Snowflake): Promise { +async function getRecord(userId: Snowflake): Promise { const db: Db = util.mongo.fetchValue(); - const duckRecords: Collection = db.collection(DUCK_COLLECTION_NAME); + const duckRecords: Collection = db.collection(DUCK_COLLECTION_NAME); return await duckRecords.findOne({ user: userId, @@ -117,11 +124,11 @@ async function getRecord(userId: Snowflake): Promise { } /** Function to upsert a duck record in the DB - * @param duckUser The new entry + * @param DuckUser The new DuckUser entry */ -async function updateRecord(newRecord: duckUser): Promise { +async function updateRecord(newRecord: DuckUser): Promise { const db: Db = util.mongo.fetchValue(); - const duckRecords: Collection = db.collection(DUCK_COLLECTION_NAME); + const duckRecords: Collection = db.collection(DUCK_COLLECTION_NAME); await duckRecords.updateOne( {user: newRecord.user}, @@ -132,22 +139,22 @@ async function updateRecord(newRecord: duckUser): Promise { ); } -/** Function to get the global speed record - * @returns The duckUser object with the record +/** + * @returns The DuckUser object with the record */ -async function getGlobalSpeedRecord(): Promise { +async function getGlobalSpeedRecord(): Promise { const db: Db = util.mongo.fetchValue(); - const duckRecords: Collection = db.collection(DUCK_COLLECTION_NAME); + const duckRecords: Collection = db.collection(DUCK_COLLECTION_NAME); return await duckRecords.find().sort({speedRecord: 1}).limit(1).next(); } -/** Function to get all befriended duck entries from the DB that don't equate to 0 - * @returns The array of duckUser objects, null if none are present +/** Function to get all DuckUser objects from the DB that don't have 0 befriended ducks + * @returns The array of DuckUser objects, null if none are present */ -async function getBefriendedRecords(): Promise { +async function getBefriendedRecords(): Promise { const db: Db = util.mongo.fetchValue(); - const duckRecords: Collection = db.collection(DUCK_COLLECTION_NAME); + const duckRecords: Collection = db.collection(DUCK_COLLECTION_NAME); return await duckRecords .find({befriended: {$ne: 0}}) @@ -155,12 +162,12 @@ async function getBefriendedRecords(): Promise { .toArray(); } -/** Function to get all killed duck entries from the DB that don't equate to 0 - * @returns The array of duckUser objects, null if none are present +/** Function to get all DuckUser objects from the DB that don't have 0 killed ducks + * @returns The array of DuckUser objects, null if none are present */ -async function getKilledRecords(): Promise { +async function getKilledRecords(): Promise { const db: Db = util.mongo.fetchValue(); - const duckRecords: Collection = db.collection(DUCK_COLLECTION_NAME); + const duckRecords: Collection = db.collection(DUCK_COLLECTION_NAME); return await duckRecords .find({killed: {$ne: 0}}) @@ -170,12 +177,11 @@ async function getKilledRecords(): Promise { // -- Core functions -- -/** Function to add a befriended duck to the DB - * @param user The user who befriended the duck +/** Function to handle a successful duck befriend event + * @param message The 'bef' message * @param speed The time it took to befriend the duck - * @param channel The channel the duck was befriended in */ -async function handleBefriend(user: User, speed: number, channel: TextChannel) { +async function handleBefriend(message: Message, speed: number) { const embed: EmbedBuilder = new EmbedBuilder() .setColor(Colors.Blurple) .setTitle('Duck befriended!') @@ -183,9 +189,9 @@ async function handleBefriend(user: User, speed: number, channel: TextChannel) { // Gets the user record from the db const db: Db = util.mongo.fetchValue(); - const duckRecords: Collection = db.collection(DUCK_COLLECTION_NAME); - const locatedUser: duckUser | null = await duckRecords.findOne({ - user: user.id, + const duckRecords: Collection = db.collection(DUCK_COLLECTION_NAME); + const locatedUser: DuckUser | null = await duckRecords.findOne({ + user: message.author.id, }); // If the user record was found, assign the value, otherwise leave it undefined @@ -215,10 +221,10 @@ async function handleBefriend(user: User, speed: number, channel: TextChannel) { // The user has an entry, just append to it await duckRecords.updateOne( - {user: user.id}, + {user: message.author.id}, { $set: { - user: user.id, + user: message.author.id, befriended: updatedCount ?? 1, killed: locatedUser?.killed ?? 0, speedRecord: updatedSpeed, @@ -228,22 +234,21 @@ async function handleBefriend(user: User, speed: number, channel: TextChannel) { ); embed.setDescription( - `<@!${user.id}> befriended the duck in ${speed} seconds!` + `<@!${message.author.id}> befriended the duck in ${speed} seconds!` ); embed.addFields([ {name: 'Friends', value: updatedCount?.toString() ?? '1', inline: true}, {name: 'Kills', value: locatedUser?.killed.toString() ?? '0', inline: true}, ]); - await channel.send({embeds: [embed.toJSON()]}); + await message.reply({embeds: [embed.toJSON()]}); } -/** Function to add a killed duck to the DB - * @param user The user who killed the duck +/** Function to handle a successful duck kill event + * @param message The 'bang' message * @param speed The time it took to kill the duck - * @param channel The channel the duck was killed in */ -async function handleKill(user: User, speed: number, channel: TextChannel) { +async function handleKill(message: Message, speed: number) { const embed: EmbedBuilder = new EmbedBuilder() .setColor(Colors.Red) .setTitle('Duck killed!') @@ -251,7 +256,7 @@ async function handleKill(user: User, speed: number, channel: TextChannel) { // Gets the user record from the db - const locatedUser: duckUser | null = await getRecord(user.id); + const locatedUser: DuckUser | null = await getRecord(message.author.id); // If the user record was found, assign the value, otherwise leave it undefined // This has to be done because ?? no worky with arithmetics @@ -280,7 +285,7 @@ async function handleKill(user: User, speed: number, channel: TextChannel) { // Updates the existing record await updateRecord({ - user: user.id, + user: message.author.id, befriended: locatedUser?.befriended ?? 0, killed: updatedCount ?? 1, speedRecord: updatedSpeed, @@ -291,7 +296,9 @@ async function handleKill(user: User, speed: number, channel: TextChannel) { }); embed - .setDescription(`<@!${user.id}> killed the duck in ${speed} seconds!`) + .setDescription( + `<@!${message.author.id}> killed the duck in ${speed} seconds!` + ) .addFields([ { name: 'Friends', @@ -301,32 +308,31 @@ async function handleKill(user: User, speed: number, channel: TextChannel) { {name: 'Kills', value: updatedCount?.toString() ?? '1', inline: true}, ]); - await channel.send({embeds: [embed]}); + await message.reply({embeds: [embed]}); } /** Function to check whether the 'bef' or 'bang' missed + * @param message The 'bef' or 'bang' message + * * @returns Whether the attempt missed */ -async function miss( - member: GuildMember, - channel: TextChannel -): Promise { - if (Math.random() >= FAIL_RATES.interaction! / 100) { +async function miss(message: Message): Promise { + if (Math.random() >= failRates.interaction! / 100) { return false; } const embed: EmbedBuilder = new EmbedBuilder() .setColor(Colors.Red) .setDescription(getRandomQuote()) - .setFooter({text: `Try again in ${COOLDOWN} seconds`}); + .setFooter({text: `Try again in ${missCooldown} seconds`}); - const bot = channel.guild.members.cache.get(util.client.user!.id)!; + // Times the user out if the bot has sufficient permissions + const bot = message.guild!.members.cache.get(util.client.user!.id)!; - if (bot.roles.highest.position > member.roles.highest.position) { - await member.timeout(COOLDOWN! * 1000, 'Missed a duck'); + if (bot.roles.highest.position > message.member!.roles.highest.position) { + await message.member!.timeout(missCooldown! * 1000, 'Missed a duck'); } - await channel.send({embeds: [embed]}); - + await message.reply({embeds: [embed]}); return true; } @@ -337,26 +343,28 @@ async function summonDuck(channel: TextChannel): Promise { time: duck.config.runAwayTime * 1_000, filter: message => ['bef', 'bang'].includes(message.content), }); - const misses: missMap = {}; + const misses: MissTimeoutMap = {}; let caught = false; duckCollector.on('collect', async message => { const time = (message.createdTimestamp - duckMessage.createdTimestamp) / 1000; - // The person missed within seconds ago + // The person missed within seconds ago if ( misses[message.author.id] !== undefined && Date.now() <= misses[message.author.id] ) { // All errors are catched since the only possible one is that the author has disabled dms await message.author - .send(`I said to wait for ${COOLDOWN!} seconds! Resetting the timer...`) + .send( + `I said to wait for ${missCooldown!} seconds! Resetting the timer...` + ) .catch(); // This informs the author that they got timed out regardless of their dm status await message.react('🕗'); // Resets the timer - misses[message.author.id] = Date.now() + COOLDOWN! * 1_000; + misses[message.author.id] = Date.now() + missCooldown! * 1_000; return; } @@ -367,33 +375,46 @@ async function summonDuck(channel: TextChannel): Promise { switch (message.content) { case 'bef': - if (await miss(message.member!, channel)) { - misses[message.author.id] = Date.now() + COOLDOWN! * 1_000; + if (await miss(message)) { + misses[message.author.id] = Date.now() + missCooldown! * 1_000; break; } else { - await duckMessage.delete(); + // Catches all errors, since the only possible one is unknownMessage, which would mean + // that someone else has caught a duck - return early. + try { + await duckMessage.delete(); + } catch { + // Someone caught the duck and the message is unknown, return early + return; + } caught = true; duckCollector.stop(); - await handleBefriend(message.author, time, channel); + await handleBefriend(message, time); return; } case 'bang': - if (await miss(message.member!, channel)) { - misses[message.author.id] = Date.now() + COOLDOWN! * 1_000; + if (await miss(message)) { + misses[message.author.id] = Date.now() + missCooldown! * 1_000; break; } else { - await duckMessage.delete(); + // Catches all errors, since the only possible one is unknownMessage, which would mean + // that someone else has caught a duck - return early. + try { + await duckMessage.delete(); + } catch { + // Someone caught the duck and the message is unknown, return early + return; + } caught = true; duckCollector.stop(); - await handleKill(message.author, time, channel); + await handleKill(message, time); return; } } }); - duckCollector.on('end', async () => { // The duck wasn't caught using 'bef' or 'bang' if (!caught) { @@ -412,20 +433,20 @@ async function summonDuck(channel: TextChannel): Promise { duck.onInitialize(async () => { // Verifies all config values are set (When not set, they are undefined) if ( - typeof MINIMUM_SPAWN !== 'number' || - typeof MAXIMUM_SPAWN !== 'number' || - typeof RUN_AWAY_TIME !== 'number' || - typeof COOLDOWN !== 'number' || - typeof FAIL_RATES.interaction !== 'number' || - typeof FAIL_RATES.kill !== 'number' || - typeof FAIL_RATES.donate !== 'number' + typeof minimumSpawnTime !== 'number' || + typeof maximumSpawnTime !== 'number' || + typeof runAwayTime !== 'number' || + typeof missCooldown !== 'number' || + typeof failRates.interaction !== 'number' || + typeof failRates.kill !== 'number' || + typeof failRates.donate !== 'number' ) { configFail( 'Config error: A config option is not set or is invalid, this module will be disabled.' ); return; } - if (CHANNEL_IDS === undefined) { + if (channelIds === undefined) { configFail( 'Config error: There are no valid channels set in the config, this mdule will be disabled.' ); @@ -436,7 +457,7 @@ duck.onInitialize(async () => { const channels: TextChannel[] = []; // Done to make sure all IDs are valid - for (const id of CHANNEL_IDS) { + for (const id of channelIds) { const channel: Channel | undefined = util.client.channels.cache.get(id); if (channel === undefined) { @@ -463,7 +484,7 @@ duck.onInitialize(async () => { } }); -// -- Module definitions -- +// -- Command definitions -- duck.registerSubModule( new util.SubModule( @@ -473,7 +494,7 @@ duck.registerSubModule( { type: util.ModuleOptionType.User, name: 'user', - description: 'The user to get the stats of (Default: Yourself)', + description: 'The user to get the stats of (Defaults to yourself)', required: false, }, ], @@ -498,7 +519,7 @@ duck.registerSubModule( .toJSON(); } - const locatedUser: duckUser | null = await getRecord(user.id); + const locatedUser: DuckUser | null = await getRecord(user.id); if (locatedUser === null) { if (user === interaction.user) { @@ -549,7 +570,7 @@ duck.registerSubModule( 'Gets the current global speed record', [], async () => { - const speedRecord: duckUser | null = await getGlobalSpeedRecord(); + const speedRecord: DuckUser | null = await getGlobalSpeedRecord(); if (!speedRecord) { return util.embed.errorEmbed('Noone has set a global record yet!'); @@ -583,9 +604,9 @@ duck.registerSubModule( 'Shows the global top befriended counts', [], async (_, interaction) => { - const entries: duckUser[] = await getBefriendedRecords(); + const entries: DuckUser[] = await getBefriendedRecords(); - if (entries.length === 0) { + if (entries === null) { return util.embed.errorEmbed('Noone has befriended a duck yet!'); } @@ -627,7 +648,7 @@ duck.registerSubModule( const user: User | undefined = util.client.users.cache.get(entry.user); let tag = `Failed to get user tag, ID: ${entry.user}`; - // The user exists, use their tag + // The user was found, use their tag if (user) { tag = user.tag; } @@ -637,7 +658,7 @@ duck.registerSubModule( }); } - // Makes sure fields are set if the iteration didn't finish + // Makes sure fields are added if the iteration didn't finish (the last 1-3 weren't added) if (payloads.length % 4 !== 0) { embed.setFields(fields); payloads.push({embeds: [embed.toJSON()]}); @@ -654,7 +675,7 @@ duck.registerSubModule( 'Shows the global top killer counts', [], async (_, interaction) => { - const entries: duckUser[] = await getKilledRecords(); + const entries: DuckUser[] = await getKilledRecords(); if (entries.length === 0) { return util.embed.errorEmbed('Noone has killed a duck yet!'); @@ -698,7 +719,7 @@ duck.registerSubModule( const user: User | undefined = util.client.users.cache.get(entry.user); let tag = `Failed to get user tag, ID: ${entry.user}`; - // The user exists, use their tag + // The user was found, use their tag if (user) { tag = user.tag; } @@ -708,7 +729,7 @@ duck.registerSubModule( }); } - // Makes sure fields are set if the iteration didn't finish + // Makes sure fields are added if the iteration didn't finish (the last 1-3 weren't added) if (payloads.length % 4 !== 0) { embed.setFields(fields); payloads.push({embeds: [embed.toJSON()]}); @@ -776,9 +797,9 @@ duck.registerSubModule( }); // Fail chance - if (Math.random() <= FAIL_RATES.donate! / 100) { + if (Math.random() <= failRates.donate! / 100) { return util.embed.errorEmbed( - `Oops, the duck broke out of its cage before it arrived. You have ${ + `Oops, the duck ran away before you could donate it. You have ${ donorRecord.befriended - 1 } ducks left.` ); @@ -825,7 +846,7 @@ duck.registerSubModule( } // Fail chance - if (Math.random() <= FAIL_RATES.kill! / 100) { + if (Math.random() <= failRates.kill! / 100) { await updateRecord({ user: userRecord.user, befriended: userRecord.befriended - 1, @@ -867,7 +888,7 @@ duck.registerSubModule( duck.registerSubModule( new util.SubModule( 'reset', - 'Resets an users duck commands', + 'Resets an users duck stats', [ { type: util.ModuleOptionType.User, @@ -883,7 +904,7 @@ duck.registerSubModule( const user: User = util.client.users.cache.get(userName)!; const db: Db = util.mongo.fetchValue(); - const duckRecords: Collection = + const duckRecords: Collection = db.collection(DUCK_COLLECTION_NAME); switch ( From a884de89494901a54853035c2abd89ecdce3e16c Mon Sep 17 00:00:00 2001 From: Cpt-Dingus <100243410+Cpt-Dingus@users.noreply.github.com> Date: Tue, 7 Nov 2023 18:19:07 +0100 Subject: [PATCH 4/5] fix: Fixed the merge, cleaned ducks up one last bit --- config.default.jsonc | 4 +-- src/modules/duck/duck.ts | 67 +++++++++++++++++++++------------------- 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/config.default.jsonc b/config.default.jsonc index 044ec73..870d89f 100644 --- a/config.default.jsonc +++ b/config.default.jsonc @@ -76,9 +76,7 @@ "roleIdOnApprove": "" }, "google": { - "enabled": true, - "ApiKey": "", - "CseId": "" + "enabled": true }, // API key is shared with google "youtube": { diff --git a/src/modules/duck/duck.ts b/src/modules/duck/duck.ts index 79a2cef..9c048fe 100644 --- a/src/modules/duck/duck.ts +++ b/src/modules/duck/duck.ts @@ -9,6 +9,7 @@ import {readFileSync} from 'node:fs'; import * as util from '../../core/util.js'; import { APIEmbedField, + BaseMessageOptions, Channel, Colors, EmbedBuilder, @@ -35,7 +36,7 @@ interface DuckUser { /** Interface used to store users who missed a duck interaction, important * when people are admin and can't be timed out. * Values: - * User id: Unix timestamp of the miss delays + * User id: Unix timestamp of the end of the miss delay */ interface MissTimeoutMap { [user: Snowflake]: number; @@ -85,17 +86,17 @@ const GOT_AWAY_EMBED: EmbedBuilder = new EmbedBuilder() // -- Helper functions -- -/** Function to log a config error, done to save some lines +/** Function to log a config error, done to save some lines in manual config verification * @param message The message to send with the warning */ function configFail(message: string) { util.logEvent(util.EventCategory.Warning, 'duck', message, 1); } -/** Function to get a random delay from the config - * @returns A random delay in seconds between maximumSpawnTime and minimumSpawnTime +/** Function to get a random spawn delay from the config + * @returns A random delay in seconds between minimumSpawnTime and maximumSpawnTime */ -function getRandomDelay(): number { +function getSpawnDelay(): number { // Non null assertion - This is only called when the values aren't undefined return ( Math.random() * (maximumSpawnTime! - minimumSpawnTime!) + minimumSpawnTime! @@ -336,7 +337,9 @@ async function miss(message: Message): Promise { return true; } -/** Function to send a duck and listen for a response */ +/** Function to send a duck and listen for a response + * @param channel The channel to summon the duck in + */ async function summonDuck(channel: TextChannel): Promise { const duckMessage = await channel.send({embeds: [DUCK_EMBED]}); const duckCollector = channel.createMessageCollector({ @@ -425,13 +428,14 @@ async function summonDuck(channel: TextChannel): Promise { // Restarts the duck loop with a random value setTimeout(async () => { void (await summonDuck(channel)); - }, getRandomDelay() * 1_000); + }, getSpawnDelay() * 1_000); return; }); } duck.onInitialize(async () => { // Verifies all config values are set (When not set, they are undefined) + // This should get replaced by config verification once it is implemented if ( typeof minimumSpawnTime !== 'number' || typeof maximumSpawnTime !== 'number' || @@ -480,7 +484,7 @@ duck.onInitialize(async () => { for (const channel of channels) { setTimeout(async () => { void (await summonDuck(channel)); - }, getRandomDelay() * 1000); + }, getSpawnDelay() * 1000); } }); @@ -499,12 +503,11 @@ duck.registerSubModule( }, ], async (args, interaction) => { - let user: User; - const userName: string | undefined = args .find(arg => arg.name === 'user') ?.value?.toString(); + let user: User; user = interaction.user!; if (userName !== undefined) { user = util.client.users.cache.get(userName)!; @@ -606,13 +609,13 @@ duck.registerSubModule( async (_, interaction) => { const entries: DuckUser[] = await getBefriendedRecords(); - if (entries === null) { + if (entries.length === 0) { return util.embed.errorEmbed('Noone has befriended a duck yet!'); } // Gets the payloads for the pagination let fieldNumber = 0; - const payloads = []; + const payloads: BaseMessageOptions[] = []; // The description has to be set to something, the speed record seems like the best choice // Non-null assertion - people have befriended ducks, there is a record @@ -644,11 +647,11 @@ duck.registerSubModule( fieldNumber++; - // Tries to get the user tag const user: User | undefined = util.client.users.cache.get(entry.user); + // Placeholder for an invalid tag + let tag = `Failed to get user tag, ID: ${entry.user}`; - // The user was found, use their tag if (user) { tag = user.tag; } @@ -659,7 +662,7 @@ duck.registerSubModule( } // Makes sure fields are added if the iteration didn't finish (the last 1-3 weren't added) - if (payloads.length % 4 !== 0) { + if (payloads.length <= 4 || payloads.length % 4 !== 0) { embed.setFields(fields); payloads.push({embeds: [embed.toJSON()]}); } @@ -683,7 +686,7 @@ duck.registerSubModule( // Gets the payloads for the pagination let fieldNumber = 0; - const payloads = []; + const payloads: BaseMessageOptions[] = []; // The description has to be set to something, the speed record seems like the best choice // Non-null assertion - people have killed ducks, there is a record @@ -715,9 +718,9 @@ duck.registerSubModule( fieldNumber++; - // Tries to get the user tag const user: User | undefined = util.client.users.cache.get(entry.user); + // Placeholder for an invalid tag let tag = `Failed to get user tag, ID: ${entry.user}`; // The user was found, use their tag if (user) { @@ -730,7 +733,7 @@ duck.registerSubModule( } // Makes sure fields are added if the iteration didn't finish (the last 1-3 weren't added) - if (payloads.length % 4 !== 0) { + if (payloads.length <= 4 || payloads.length % 4 !== 0) { embed.setFields(fields); payloads.push({embeds: [embed.toJSON()]}); } @@ -764,10 +767,7 @@ duck.registerSubModule( ); } - const donorRecord = await getRecord(interaction.user.id); - const recipeeRecord = await getRecord(recipee.id); - - // Makes sure the command can be executed + const donorRecord: DuckUser | null = await getRecord(interaction.user.id); if (!donorRecord) { return util.embed.errorEmbed( @@ -775,16 +775,18 @@ duck.registerSubModule( ); } + if (donorRecord.befriended === 0) { + return util.embed.errorEmbed('You have no ducks to donate!'); + } + + const recipeeRecord: DuckUser | null = await getRecord(recipee.id); + if (!recipeeRecord) { return util.embed.errorEmbed( `<@!${recipee.id}> has not participated in the duck hunt yet!` ); } - if (donorRecord.befriended === 0) { - return util.embed.errorEmbed('You have no ducks to donate!'); - } - await updateRecord({ user: donorRecord.user, befriended: donorRecord.befriended - 1, @@ -799,7 +801,7 @@ duck.registerSubModule( // Fail chance if (Math.random() <= failRates.donate! / 100) { return util.embed.errorEmbed( - `Oops, the duck ran away before you could donate it. You have ${ + `Oops, the duck flew away before you could donate it. You have ${ donorRecord.befriended - 1 } ducks left.` ); @@ -833,8 +835,6 @@ duck.registerSubModule( async (_, interaction) => { const userRecord = await getRecord(interaction.user.id); - // Makes sure the command can be executed - if (!userRecord) { return util.embed.errorEmbed( 'You have not participated in the duck hunt yet' @@ -859,7 +859,7 @@ duck.registerSubModule( }); return util.embed.errorEmbed( - `Oops, the duck ran away before you could hurt it. You have ${ + `Oops, the duck flew away before you could hurt it. You have ${ userRecord.befriended - 1 } ducks left.` ); @@ -917,7 +917,12 @@ duck.registerSubModule( return util.embed.infoEmbed('The duck stats were NOT reset.'); case util.ConfirmEmbedResponse.Confirmed: - await duckRecords.deleteOne({user: user.id}); + await duckRecords.deleteOne({user: user.id}).catch(err => { + return util.embed.errorEmbed( + `Database update call failed with error ${(err as Error).name}` + ); + }); + return util.embed.successEmbed( `Succesfully wiped the duck stats of <@!${user.id}>` ); From 2097b6f9e0d9a066d89fff407b09cb370cdb2f27 Mon Sep 17 00:00:00 2001 From: Cpt-Dingus <100243410+Cpt-Dingus@users.noreply.github.com> Date: Tue, 7 Nov 2023 18:23:34 +0100 Subject: [PATCH 5/5] fix: Fixed formatting in the mongo file --- src/core/mongo.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/core/mongo.ts b/src/core/mongo.ts index dd534ff..1233b56 100644 --- a/src/core/mongo.ts +++ b/src/core/mongo.ts @@ -13,9 +13,8 @@ import {Dependency} from './modules.js'; export const mongo = new Dependency('MongoDB', async () => { const mongoConfig = botConfig.secrets.mongodb; - let connectionString: string | undefined; - // Allows for empty authentication fields without erroring + // Allows for empty authentication fields without erroring if (mongoConfig.username === '' || mongoConfig.password === '') { // https://www.mongodb.com/docs/manual/reference/connection-string/ connectionString =