diff --git a/.env.example b/.env.example index add48a5..d25e432 100644 --- a/.env.example +++ b/.env.example @@ -7,7 +7,7 @@ # any secret values. BOT_TOKEN= -BOT_VER=0.8.2 +BOT_VER=0.8.4 BOT_AUTHOR=Vylpes BOT_OWNERID=147392775707426816 BOT_CLIENTID=682942374040961060 @@ -34,8 +34,6 @@ DB_LOGGING= DB_DATA_LOCATION=./.temp/database DB_ROOT_HOST=0.0.0.0 -DB_CARD_FILE=:memory: - EXPRESS_PORT=3302 -GDRIVESYNC_AUTO=true +GDRIVESYNC_AUTO=false diff --git a/.forgejo/workflows/production.yml b/.forgejo/workflows/production.yml index d7b014b..cb4ed46 100644 --- a/.forgejo/workflows/production.yml +++ b/.forgejo/workflows/production.yml @@ -23,7 +23,7 @@ jobs: - run: yarn lint - name: "Copy files over to location" - run: cp -r . ${{ secrets.PROD_REPO_PATH }} + run: rsync -rvzP . ${{ secrets.PROD_REPO_PATH }} deploy: environment: prod diff --git a/.forgejo/workflows/stage.yml b/.forgejo/workflows/stage.yml index 60712ca..b6c27da 100644 --- a/.forgejo/workflows/stage.yml +++ b/.forgejo/workflows/stage.yml @@ -23,7 +23,7 @@ jobs: - run: yarn lint - name: "Copy files over to location" - run: cp -r . ${{ secrets.STAGE_REPO_PATH }} + run: rsync -rvzP . ${{ secrets.STAGE_REPO_PATH }} deploy: environment: prod diff --git a/package.json b/package.json index 0dc2630..137ca15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "card-drop", - "version": "0.8.2", + "version": "0.8.4", "main": "./dist/bot.js", "typings": "./dist", "scripts": { @@ -30,6 +30,7 @@ "@types/express": "^5.0.0", "@types/jest": "^29.5.14", "@types/uuid": "^10.0.0", + "axios": "^1.8.4", "body-parser": "^1.20.2", "canvas": "^2.11.2", "clone-deep": "^4.0.1", @@ -42,6 +43,7 @@ "jest": "^29.0.0", "jest-mock-extended": "^3.0.0", "jimp": "^1.6.0", + "minimatch": "9.0.5", "mysql": "^2.18.1", "ts-jest": "^29.0.0", "typeorm": "0.3.20", diff --git a/src/buttonEvents/Claim.ts b/src/buttonEvents/Claim.ts index 522ab40..d139f42 100644 --- a/src/buttonEvents/Claim.ts +++ b/src/buttonEvents/Claim.ts @@ -1,12 +1,12 @@ import { ButtonInteraction } from "discord.js"; import { ButtonEvent } from "../type/buttonEvent"; import Inventory from "../database/entities/app/Inventory"; -import { CoreClient } from "../client/client"; import { default as eClaim } from "../database/entities/app/Claim"; import AppLogger from "../client/appLogger"; -import CardDropHelperMetadata from "../helpers/CardDropHelperMetadata"; import User from "../database/entities/app/User"; import CardConstants from "../constants/CardConstants"; +import GetCardsHelper from "../helpers/DropHelpers/GetCardsHelper"; +import DropEmbedHelper from "../helpers/DropHelpers/DropEmbedHelper"; export default class Claim extends ButtonEvent { public override async execute(interaction: ButtonInteraction) { @@ -22,10 +22,10 @@ export default class Claim extends ButtonEvent { const userId = interaction.user.id; const whenDropped = interaction.message.createdAt; - const lastClaimableDate = new Date(Date.now() - (1000 * 60 * 5)); // 5 minutes ago + const lastClaimableDate = new Date(Date.now() - (1000 * 60 * 2)); // 2 minutes ago if (whenDropped < lastClaimableDate) { - await interaction.channel.send(`${interaction.user}, Cards can only be claimed within 5 minutes of it being dropped!`); + await interaction.channel.send(`${interaction.user}, Cards can only be claimed within 2 minutes of it being dropped!`); return; } @@ -35,11 +35,6 @@ export default class Claim extends ButtonEvent { AppLogger.LogSilly("Button/Claim", `${user.Id} has ${user.Currency} currency`); - if (!user.RemoveCurrency(CardConstants.ClaimCost)) { - await interaction.channel.send(`${interaction.user}, Not enough currency! You need ${CardConstants.ClaimCost} currency, you have ${user.Currency}!`); - return; - } - const claimed = await eClaim.FetchOneByClaimId(claimId); if (claimed) { @@ -47,13 +42,6 @@ export default class Claim extends ButtonEvent { return; } - if (claimId == CoreClient.ClaimId && userId != droppedBy) { - await interaction.channel.send(`${interaction.user}, The latest dropped card can only be claimed by the user who dropped it!`); - return; - } - - await user.Save(User, user); - let inventory = await Inventory.FetchOneByCardNumberAndUserId(userId, cardNumber); if (!inventory) { @@ -69,16 +57,18 @@ export default class Claim extends ButtonEvent { await claim.Save(eClaim, claim); - const card = CardDropHelperMetadata.GetCardByCardNumber(cardNumber); + const card = GetCardsHelper.GetCardByCardNumber(cardNumber); if (!card) { + AppLogger.LogError("Button/Claim", `Unable to find card, ${cardNumber}`); + return; } const imageFileName = card.card.path.split("/").pop()!; - const embed = CardDropHelperMetadata.GenerateDropEmbed(card, inventory.Quantity, imageFileName, interaction.user.username, user.Currency); - const row = CardDropHelperMetadata.GenerateDropButtons(card, claimId, interaction.user.id, true); + const embed = DropEmbedHelper.GenerateDropEmbed(card, inventory.Quantity, imageFileName, interaction.user.username, user.Currency); + const row = DropEmbedHelper.GenerateDropButtons(card, claimId, interaction.user.id, true); await interaction.editReply({ embeds: [ embed ], diff --git a/src/buttonEvents/Effects.ts b/src/buttonEvents/Effects.ts new file mode 100644 index 0000000..cd1a765 --- /dev/null +++ b/src/buttonEvents/Effects.ts @@ -0,0 +1,26 @@ +import { ButtonInteraction } from "discord.js"; +import { ButtonEvent } from "../type/buttonEvent"; +import List from "./Effects/List"; +import Use from "./Effects/Use"; +import AppLogger from "../client/appLogger"; +import Buy from "./Effects/Buy"; + +export default class Effects extends ButtonEvent { + public override async execute(interaction: ButtonInteraction) { + const action = interaction.customId.split(" ")[1]; + + switch (action) { + case "list": + await List(interaction); + break; + case "use": + await Use.Execute(interaction); + break; + case "buy": + await Buy.Execute(interaction); + break; + default: + AppLogger.LogError("Buttons/Effects", `Unknown action, ${action}`); + } + } +} diff --git a/src/buttonEvents/Effects/Buy.ts b/src/buttonEvents/Effects/Buy.ts new file mode 100644 index 0000000..49cc75f --- /dev/null +++ b/src/buttonEvents/Effects/Buy.ts @@ -0,0 +1,120 @@ +import {ButtonInteraction} from "discord.js"; +import AppLogger from "../../client/appLogger"; +import EffectHelper from "../../helpers/EffectHelper"; +import EmbedColours from "../../constants/EmbedColours"; +import User from "../../database/entities/app/User"; +import {EffectDetails} from "../../constants/EffectDetails"; + +export default class Buy { + public static async Execute(interaction: ButtonInteraction) { + const subaction = interaction.customId.split(" ")[2]; + + switch (subaction) { + case "confirm": + await this.Confirm(interaction); + break; + case "cancel": + await this.Cancel(interaction); + break; + default: + AppLogger.LogError("Buy", `Unknown subaction, effects ${subaction}`); + } + } + + private static async Confirm(interaction: ButtonInteraction) { + const id = interaction.customId.split(" ")[3]; + const quantity = interaction.customId.split(" ")[4]; + + if (!id || !quantity) { + AppLogger.LogError("Buy Confirm", "Not enough parameters"); + return; + } + + const effectDetail = EffectDetails.get(id); + + if (!effectDetail) { + AppLogger.LogError("Buy Confirm", "Effect detail not found!"); + return; + } + + const quantityNumber = Number(quantity); + + if (!quantityNumber || quantityNumber < 1) { + AppLogger.LogError("Buy Confirm", "Invalid number"); + return; + } + + const totalCost = effectDetail.cost * quantityNumber; + + const user = await User.FetchOneById(User, interaction.user.id); + + if (!user) { + AppLogger.LogError("Buy Confirm", "Unable to find user"); + return; + } + + if (user.Currency < totalCost) { + interaction.reply(`You don't have enough currency to buy this! You have \`${user.Currency} Currency\` and need \`${totalCost} Currency\`!`); + return; + } + + user.RemoveCurrency(totalCost); + await user.Save(User, user); + + await EffectHelper.AddEffectToUserInventory(interaction.user.id, id, quantityNumber); + + const generatedEmbed = await EffectHelper.GenerateEffectBuyEmbed(interaction.user.id, id, quantityNumber, true); + + if (typeof generatedEmbed == "string") { + await interaction.reply(generatedEmbed); + return; + } + + generatedEmbed.embed.setColor(EmbedColours.Success); + generatedEmbed.embed.setFooter({ text: "Purchased" }); + + await interaction.update({ + embeds: [ generatedEmbed.embed ], + components: [ generatedEmbed.row ], + }); + } + + private static async Cancel(interaction: ButtonInteraction) { + const id = interaction.customId.split(" ")[3]; + const quantity = interaction.customId.split(" ")[4]; + + if (!id || !quantity) { + AppLogger.LogError("Buy Cancel", "Not enough parameters"); + return; + } + + const effectDetail = EffectDetails.get(id); + + if (!effectDetail) { + AppLogger.LogError("Buy Cancel", "Effect detail not found!"); + return; + } + + const quantityNumber = Number(quantity); + + if (!quantityNumber || quantityNumber < 1) { + AppLogger.LogError("Buy Cancel", "Invalid number"); + return; + } + + const generatedEmbed = await EffectHelper.GenerateEffectBuyEmbed(interaction.user.id, id, quantityNumber, true); + + if (typeof generatedEmbed == "string") { + await interaction.reply(generatedEmbed); + return; + } + + generatedEmbed.embed.setColor(EmbedColours.Error); + generatedEmbed.embed.setFooter({ text: "Cancelled" }); + + await interaction.update({ + embeds: [ generatedEmbed.embed ], + components: [ generatedEmbed.row ], + }); + } +} diff --git a/src/buttonEvents/Effects/List.ts b/src/buttonEvents/Effects/List.ts new file mode 100644 index 0000000..d86dfce --- /dev/null +++ b/src/buttonEvents/Effects/List.ts @@ -0,0 +1,20 @@ +import { ButtonInteraction } from "discord.js"; +import EffectHelper from "../../helpers/EffectHelper"; + +export default async function List(interaction: ButtonInteraction) { + const pageOption = interaction.customId.split(" ")[2]; + + const page = Number(pageOption); + + if (!page) { + await interaction.reply("Page option is not a valid number"); + return; + } + + const result = await EffectHelper.GenerateEffectListEmbed(interaction.user.id, page); + + await interaction.update({ + embeds: [ result.embed ], + components: [ result.row ], + }); +} \ No newline at end of file diff --git a/src/buttonEvents/Effects/Use.ts b/src/buttonEvents/Effects/Use.ts new file mode 100644 index 0000000..5bfe2fd --- /dev/null +++ b/src/buttonEvents/Effects/Use.ts @@ -0,0 +1,132 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, EmbedBuilder } from "discord.js"; +import { EffectDetails } from "../../constants/EffectDetails"; +import EffectHelper from "../../helpers/EffectHelper"; +import EmbedColours from "../../constants/EmbedColours"; +import TimeLengthInput from "../../helpers/TimeLengthInput"; +import AppLogger from "../../client/appLogger"; + +export default class Use { + public static async Execute(interaction: ButtonInteraction) { + const subaction = interaction.customId.split(" ")[2]; + + switch (subaction) { + case "confirm": + await this.UseConfirm(interaction); + break; + case "cancel": + await this.UseCancel(interaction); + break; + } + } + + private static async UseConfirm(interaction: ButtonInteraction) { + const id = interaction.customId.split(" ")[3]; + + const effectDetail = EffectDetails.get(id); + + if (!effectDetail) { + AppLogger.LogError("Button/Effects/Use", `Effect not found, ${id}`); + + await interaction.reply("Effect not found in system!"); + return; + } + + const now = new Date(); + + const whenExpires = new Date(now.getTime() + effectDetail.duration); + + const result = await EffectHelper.UseEffect(interaction.user.id, id, whenExpires); + + if (!result) { + await interaction.reply("Unable to use effect! Please make sure you have it in your inventory and is not on cooldown"); + return; + } + + const embed = new EmbedBuilder() + .setTitle("Effect Used") + .setDescription("You now have an active effect!") + .setColor(EmbedColours.Green) + .addFields([ + { + name: "Effect", + value: effectDetail.friendlyName, + inline: true, + }, + { + name: "Expires", + value: ``, + inline: true, + }, + ]); + + const row = new ActionRowBuilder() + .addComponents([ + new ButtonBuilder() + .setLabel("Confirm") + .setCustomId(`effects use confirm ${effectDetail.id}`) + .setStyle(ButtonStyle.Primary) + .setDisabled(true), + new ButtonBuilder() + .setLabel("Cancel") + .setCustomId(`effects use cancel ${effectDetail.id}`) + .setStyle(ButtonStyle.Danger) + .setDisabled(true), + ]); + + await interaction.update({ + embeds: [ embed ], + components: [ row ], + }); + } + + private static async UseCancel(interaction: ButtonInteraction) { + const id = interaction.customId.split(" ")[3]; + + const effectDetail = EffectDetails.get(id); + + if (!effectDetail) { + AppLogger.LogError("Button/Effects/Cancel", `Effect not found, ${id}`); + + await interaction.reply("Effect not found in system!"); + return; + } + + const timeLengthInput = TimeLengthInput.ConvertFromMilliseconds(effectDetail.duration); + + const embed = new EmbedBuilder() + .setTitle("Effect Use Cancelled") + .setDescription("The effect from your inventory has not been used") + .setColor(EmbedColours.Grey) + .addFields([ + { + name: "Effect", + value: effectDetail.friendlyName, + inline: true, + }, + { + name: "Expires", + value: timeLengthInput.GetLengthShort(), + inline: true, + }, + ]); + + const row = new ActionRowBuilder() + .addComponents([ + new ButtonBuilder() + .setLabel("Confirm") + .setCustomId(`effects use confirm ${effectDetail.id}`) + .setStyle(ButtonStyle.Primary) + .setDisabled(true), + new ButtonBuilder() + .setLabel("Cancel") + .setCustomId(`effects use cancel ${effectDetail.id}`) + .setStyle(ButtonStyle.Danger) + .setDisabled(true), + ]); + + await interaction.update({ + embeds: [ embed ], + components: [ row ], + }); + } +} diff --git a/src/buttonEvents/Multidrop.ts b/src/buttonEvents/Multidrop.ts index 604f02c..e6ea7c2 100644 --- a/src/buttonEvents/Multidrop.ts +++ b/src/buttonEvents/Multidrop.ts @@ -1,7 +1,6 @@ import { AttachmentBuilder, ButtonInteraction, EmbedBuilder } from "discord.js"; import { ButtonEvent } from "../type/buttonEvent"; import AppLogger from "../client/appLogger"; -import CardDropHelperMetadata from "../helpers/CardDropHelperMetadata"; import Inventory from "../database/entities/app/Inventory"; import EmbedColours from "../constants/EmbedColours"; import { readFileSync } from "fs"; @@ -9,6 +8,8 @@ import path from "path"; import ErrorMessages from "../constants/ErrorMessages"; import User from "../database/entities/app/User"; import { GetSacrificeAmount } from "../constants/CardRarity"; +import GetCardsHelper from "../helpers/DropHelpers/GetCardsHelper"; +import MultidropEmbedHelper from "../helpers/DropHelpers/MultidropEmbedHelper"; export default class Multidrop extends ButtonEvent { public override async execute(interaction: ButtonInteraction) { @@ -37,7 +38,7 @@ export default class Multidrop extends ButtonEvent { return; } - const card = CardDropHelperMetadata.GetCardByCardNumber(cardNumber); + const card = GetCardsHelper.GetCardByCardNumber(cardNumber); if (!card) { await interaction.reply("Unable to find card."); @@ -85,7 +86,7 @@ export default class Multidrop extends ButtonEvent { } // Drop next card - const randomCard = CardDropHelperMetadata.GetRandomCard(); + const randomCard = GetCardsHelper.GetRandomCard(); cardsRemaining -= 1; if (!randomCard) { @@ -105,9 +106,9 @@ export default class Multidrop extends ButtonEvent { const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, randomCard.card.id); const quantityClaimed = inventory ? inventory.Quantity : 0; - const embed = CardDropHelperMetadata.GenerateMultidropEmbed(randomCard, quantityClaimed, imageFileName, cardsRemaining, undefined, user.Currency); + const embed = MultidropEmbedHelper.GenerateMultidropEmbed(randomCard, quantityClaimed, imageFileName, cardsRemaining, undefined, user.Currency); - const row = CardDropHelperMetadata.GenerateMultidropButtons(randomCard, cardsRemaining, interaction.user.id, cardsRemaining < 0); + const row = MultidropEmbedHelper.GenerateMultidropButtons(randomCard, cardsRemaining, interaction.user.id, cardsRemaining < 0); await interaction.editReply({ embeds: [ embed ], @@ -131,7 +132,7 @@ export default class Multidrop extends ButtonEvent { return; } - const card = CardDropHelperMetadata.GetCardByCardNumber(cardNumber); + const card = GetCardsHelper.GetCardByCardNumber(cardNumber); if (!card) { await interaction.reply("Unable to find card."); @@ -175,7 +176,7 @@ export default class Multidrop extends ButtonEvent { } // Drop next card - const randomCard = CardDropHelperMetadata.GetRandomCard(); + const randomCard = GetCardsHelper.GetRandomCard(); cardsRemaining -= 1; if (!randomCard) { @@ -195,9 +196,9 @@ export default class Multidrop extends ButtonEvent { const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, randomCard.card.id); const quantityClaimed = inventory ? inventory.Quantity : 0; - const embed = CardDropHelperMetadata.GenerateMultidropEmbed(randomCard, quantityClaimed, imageFileName, cardsRemaining, undefined, user.Currency); + const embed = MultidropEmbedHelper.GenerateMultidropEmbed(randomCard, quantityClaimed, imageFileName, cardsRemaining, undefined, user.Currency); - const row = CardDropHelperMetadata.GenerateMultidropButtons(randomCard, cardsRemaining, interaction.user.id, cardsRemaining < 0); + const row = MultidropEmbedHelper.GenerateMultidropButtons(randomCard, cardsRemaining, interaction.user.id, cardsRemaining < 0); await interaction.editReply({ embeds: [ embed ], diff --git a/src/buttonEvents/Reroll.ts b/src/buttonEvents/Reroll.ts index 12578db..6d97024 100644 --- a/src/buttonEvents/Reroll.ts +++ b/src/buttonEvents/Reroll.ts @@ -5,11 +5,12 @@ import { v4 } from "uuid"; import { CoreClient } from "../client/client"; import Inventory from "../database/entities/app/Inventory"; import Config from "../database/entities/app/Config"; -import CardDropHelperMetadata from "../helpers/CardDropHelperMetadata"; import path from "path"; import AppLogger from "../client/appLogger"; import User from "../database/entities/app/User"; import CardConstants from "../constants/CardConstants"; +import GetCardsHelper from "../helpers/DropHelpers/GetCardsHelper"; +import DropEmbedHelper from "../helpers/DropHelpers/DropEmbedHelper"; export default class Reroll extends ButtonEvent { public override async execute(interaction: ButtonInteraction) { @@ -34,12 +35,14 @@ export default class Reroll extends ButtonEvent { AppLogger.LogInfo("Commands/Drop", `New user (${interaction.user.id}) saved to the database`); } - if (user.Currency < CardConstants.ClaimCost) { + if (!user.RemoveCurrency(CardConstants.ClaimCost)) { await interaction.reply(`Not enough currency! You need ${CardConstants.ClaimCost} currency, you have ${user.Currency}!`); return; } - const randomCard = CardDropHelperMetadata.GetRandomCard(); + await user.Save(User, user); + + const randomCard = await GetCardsHelper.FetchCard(interaction.user.id); if (!randomCard) { await interaction.reply("Unable to fetch card, please try again."); @@ -51,27 +54,32 @@ export default class Reroll extends ButtonEvent { try { AppLogger.LogVerbose("Button/Reroll", `Sending next drop: ${randomCard.card.id} (${randomCard.card.name})`); - const image = readFileSync(path.join(process.env.DATA_DIR!, "cards", randomCard.card.path)); - const imageFileName = randomCard.card.path.split("/").pop()!; + const files = []; + let imageFileName = ""; - const attachment = new AttachmentBuilder(image, { name: imageFileName }); + if (!(randomCard.card.path.startsWith("http://") || randomCard.card.path.startsWith("https://"))) { + const image = readFileSync(path.join(process.env.DATA_DIR!, "cards", randomCard.card.path)); + imageFileName = randomCard.card.path.split("/").pop()!; + + const attachment = new AttachmentBuilder(image, { name: imageFileName }); + + files.push(attachment); + } const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, randomCard.card.id); const quantityClaimed = inventory ? inventory.Quantity : 0; - const embed = CardDropHelperMetadata.GenerateDropEmbed(randomCard, quantityClaimed, imageFileName, undefined, user.Currency); + const embed = DropEmbedHelper.GenerateDropEmbed(randomCard, quantityClaimed, imageFileName, undefined, user.Currency); const claimId = v4(); - const row = CardDropHelperMetadata.GenerateDropButtons(randomCard, claimId, interaction.user.id); + const row = DropEmbedHelper.GenerateDropButtons(randomCard, claimId, interaction.user.id); await interaction.editReply({ embeds: [ embed ], - files: [ attachment ], + files: files, components: [ row ], }); - - CoreClient.ClaimId = claimId; } catch (e) { AppLogger.LogError("Button/Reroll", `Error sending next drop for card ${randomCard.card.id}: ${e}`); diff --git a/src/buttonEvents/Sacrifice.ts b/src/buttonEvents/Sacrifice.ts index 463ca1f..de0bb77 100644 --- a/src/buttonEvents/Sacrifice.ts +++ b/src/buttonEvents/Sacrifice.ts @@ -1,10 +1,10 @@ import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, EmbedBuilder } from "discord.js"; import { ButtonEvent } from "../type/buttonEvent"; import Inventory from "../database/entities/app/Inventory"; -import CardDropHelperMetadata from "../helpers/CardDropHelperMetadata"; import { CardRarityToString, GetSacrificeAmount } from "../constants/CardRarity"; import EmbedColours from "../constants/EmbedColours"; import User from "../database/entities/app/User"; +import GetCardsHelper from "../helpers/DropHelpers/GetCardsHelper"; export default class Sacrifice extends ButtonEvent { public override async execute(interaction: ButtonInteraction) { @@ -42,7 +42,7 @@ export default class Sacrifice extends ButtonEvent { return; } - const cardData = CardDropHelperMetadata.GetCardByCardNumber(cardNumber); + const cardData = GetCardsHelper.GetCardByCardNumber(cardNumber); if (!cardData) { await interaction.reply("Unable to find card in the database."); @@ -124,7 +124,7 @@ export default class Sacrifice extends ButtonEvent { return; } - const cardData = CardDropHelperMetadata.GetCardByCardNumber(cardNumber); + const cardData = GetCardsHelper.GetCardByCardNumber(cardNumber); if (!cardData) { await interaction.reply("Unable to find card in the database."); diff --git a/src/buttonEvents/View.ts b/src/buttonEvents/View.ts index f79a1af..007a911 100644 --- a/src/buttonEvents/View.ts +++ b/src/buttonEvents/View.ts @@ -19,7 +19,7 @@ export default class View extends ButtonEvent { await interaction.editReply({ embeds: [ searchResult.embed ], components: [ searchResult.row ], - files: [ searchResult.attachment ], + files: searchResult.attachments, }); } } diff --git a/src/client/appLogger.ts b/src/client/appLogger.ts index f1ba1dc..6c33609 100644 --- a/src/client/appLogger.ts +++ b/src/client/appLogger.ts @@ -86,4 +86,12 @@ export default class AppLogger { public static LogSilly(label: string, message: string) { AppLogger.Logger.silly({ label, message }); } + + public static CatchError(label: string, error: unknown) { + if (error instanceof Error) { + AppLogger.Logger.error({ label, message: error.message }); + } else { + AppLogger.Logger.error({ label, message: error }); + } + } } diff --git a/src/client/client.ts b/src/client/client.ts index 87b496e..57551fb 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -31,7 +31,6 @@ export class CoreClient extends Client { private _webhooks: Webhooks; private _timerHelper: TimerHelper; - public static ClaimId: string; public static Environment: Environment; public static AllowDrops: boolean; public static Cards: SeriesMetadata[]; diff --git a/src/commands/drop.ts b/src/commands/drop.ts index 6799a2c..ac8008c 100644 --- a/src/commands/drop.ts +++ b/src/commands/drop.ts @@ -5,12 +5,13 @@ import { CoreClient } from "../client/client"; import { v4 } from "uuid"; import Inventory from "../database/entities/app/Inventory"; import Config from "../database/entities/app/Config"; -import CardDropHelperMetadata from "../helpers/CardDropHelperMetadata"; import path from "path"; import AppLogger from "../client/appLogger"; import User from "../database/entities/app/User"; import CardConstants from "../constants/CardConstants"; import ErrorMessages from "../constants/ErrorMessages"; +import GetCardsHelper from "../helpers/DropHelpers/GetCardsHelper"; +import DropEmbedHelper from "../helpers/DropHelpers/DropEmbedHelper"; export default class Drop extends Command { constructor() { @@ -42,12 +43,14 @@ export default class Drop extends Command { AppLogger.LogInfo("Commands/Drop", `New user (${interaction.user.id}) saved to the database`); } - if (user.Currency < CardConstants.ClaimCost) { + if (!user.RemoveCurrency(CardConstants.ClaimCost)) { await interaction.reply(ErrorMessages.NotEnoughCurrency(CardConstants.ClaimCost, user.Currency)); return; } - const randomCard = CardDropHelperMetadata.GetRandomCard(); + await user.Save(User, user); + + const randomCard = await GetCardsHelper.FetchCard(interaction.user.id); if (!randomCard) { AppLogger.LogWarn("Commands/Drop", ErrorMessages.UnableToFetchCard); @@ -58,28 +61,33 @@ export default class Drop extends Command { await interaction.deferReply(); try { - const image = readFileSync(path.join(process.env.DATA_DIR!, "cards", randomCard.card.path)); - const imageFileName = randomCard.card.path.split("/").pop()!; + const files = []; + let imageFileName = ""; - const attachment = new AttachmentBuilder(image, { name: imageFileName }); + if (!(randomCard.card.path.startsWith("http://") || randomCard.card.path.startsWith("https://"))) { + const image = readFileSync(path.join(process.env.DATA_DIR!, "cards", randomCard.card.path)); + imageFileName = randomCard.card.path.split("/").pop()!; + + const attachment = new AttachmentBuilder(image, { name: imageFileName }); + + files.push(attachment); + } const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, randomCard.card.id); const quantityClaimed = inventory ? inventory.Quantity : 0; - const embed = CardDropHelperMetadata.GenerateDropEmbed(randomCard, quantityClaimed, imageFileName, undefined, user.Currency); + const embed = DropEmbedHelper.GenerateDropEmbed(randomCard, quantityClaimed, imageFileName, undefined, user.Currency); const claimId = v4(); - const row = CardDropHelperMetadata.GenerateDropButtons(randomCard, claimId, interaction.user.id); + const row = DropEmbedHelper.GenerateDropButtons(randomCard, claimId, interaction.user.id); await interaction.editReply({ embeds: [ embed ], - files: [ attachment ], + files: files, components: [ row ], }); - CoreClient.ClaimId = claimId; - } catch (e) { AppLogger.LogError("Commands/Drop", `Error sending next drop for card ${randomCard.card.id}: ${e}`); diff --git a/src/commands/effects.ts b/src/commands/effects.ts new file mode 100644 index 0000000..cd6d1d4 --- /dev/null +++ b/src/commands/effects.ts @@ -0,0 +1,64 @@ +import { CommandInteraction, SlashCommandBuilder } from "discord.js"; +import { Command } from "../type/command"; +import { EffectChoices } from "../constants/EffectDetails"; +import AppLogger from "../client/appLogger"; +import List from "./effects/List"; +import Use from "./effects/Use"; +import Buy from "./effects/Buy"; + +export default class Effects extends Command { + constructor() { + super(); + + this.CommandBuilder = new SlashCommandBuilder() + .setName("effects") + .setDescription("Effects") + .addSubcommand(x => x + .setName("list") + .setDescription("List all effects I have") + .addNumberOption(x => x + .setName("page") + .setDescription("The page number") + .setMinValue(1))) + .addSubcommand(x => x + .setName("use") + .setDescription("Use an effect in your inventory") + .addStringOption(y => y + .setName("id") + .setDescription("The effect id to use") + .setRequired(true) + .setChoices(EffectChoices))) + .addSubcommand(x => x + .setName("buy") + .setDescription("Buy more effects") + .addStringOption(y => y + .setName("id") + .setDescription("The effect id to buy") + .setRequired(true) + .setChoices(EffectChoices)) + .addNumberOption(y => y + .setName("quantity") + .setDescription("The amount to buy") + .setMinValue(1))); + } + + public override async execute(interaction: CommandInteraction) { + if (!interaction.isChatInputCommand()) return; + + const subcommand = interaction.options.getSubcommand(); + + switch (subcommand) { + case "list": + await List(interaction); + break; + case "use": + await Use(interaction); + break; + case "buy": + await Buy(interaction); + break; + default: + AppLogger.LogError("Commands/Effects", `Invalid subcommand: ${subcommand}`); + } + } +} diff --git a/src/commands/effects/Buy.ts b/src/commands/effects/Buy.ts new file mode 100644 index 0000000..3ebf587 --- /dev/null +++ b/src/commands/effects/Buy.ts @@ -0,0 +1,22 @@ +import { CommandInteraction } from "discord.js"; +import EffectHelper from "../../helpers/EffectHelper"; + +export default async function Buy(interaction: CommandInteraction) { + const id = interaction.options.get("id", true).value!; + const quantity = interaction.options.get("quantity")?.value ?? 1; + + const idValue = id.toString(); + const quantityValue = Number(quantity); + + const result = await EffectHelper.GenerateEffectBuyEmbed(interaction.user.id, idValue, quantityValue, false); + + if (typeof result == "string") { + await interaction.reply(result); + return; + } + + await interaction.reply({ + embeds: [ result.embed ], + components: [ result.row ], + }); +} \ No newline at end of file diff --git a/src/commands/effects/List.ts b/src/commands/effects/List.ts new file mode 100644 index 0000000..14e6085 --- /dev/null +++ b/src/commands/effects/List.ts @@ -0,0 +1,15 @@ +import { CommandInteraction } from "discord.js"; +import EffectHelper from "../../helpers/EffectHelper"; + +export default async function List(interaction: CommandInteraction) { + const pageOption = interaction.options.get("page"); + + const page = !isNaN(Number(pageOption?.value)) ? Number(pageOption?.value) : 1; + + const result = await EffectHelper.GenerateEffectListEmbed(interaction.user.id, page); + + await interaction.reply({ + embeds: [ result.embed ], + components: [ result.row ], + }); +} \ No newline at end of file diff --git a/src/commands/effects/Use.ts b/src/commands/effects/Use.ts new file mode 100644 index 0000000..9f72ae0 --- /dev/null +++ b/src/commands/effects/Use.ts @@ -0,0 +1,62 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder } from "discord.js"; +import { EffectDetails } from "../../constants/EffectDetails"; +import AppLogger from "../../client/appLogger"; +import EffectHelper from "../../helpers/EffectHelper"; +import TimeLengthInput from "../../helpers/TimeLengthInput"; +import EmbedColours from "../../constants/EmbedColours"; + +export default async function Use(interaction: CommandInteraction) { + const id = interaction.options.get("id", true).value!.toString(); + + const effectDetail = EffectDetails.get(id); + + if (!effectDetail) { + AppLogger.LogWarn("Commands/Effects", `Unable to find effect details for ${id}`); + + await interaction.reply("Unable to find effect!"); + return; + } + + const canUseEffect = await EffectHelper.CanUseEffect(interaction.user.id, id); + + if (!canUseEffect) { + await interaction.reply("Unable to use effect! Please make sure you have it in your inventory and is not on cooldown"); + return; + } + + const timeLengthInput = TimeLengthInput.ConvertFromMilliseconds(effectDetail.duration); + + const embed = new EmbedBuilder() + .setTitle("Effect Confirmation") + .setDescription("Would you like to use this effect?") + .setColor(EmbedColours.Ok) + .addFields([ + { + name: "Effect", + value: effectDetail.friendlyName, + inline: true, + }, + { + name: "Length", + value: timeLengthInput.GetLengthShort(), + inline: true, + }, + ]); + + const row = new ActionRowBuilder() + .addComponents([ + new ButtonBuilder() + .setLabel("Confirm") + .setCustomId(`effects use confirm ${effectDetail.id}`) + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setLabel("Cancel") + .setCustomId(`effects use cancel ${effectDetail.id}`) + .setStyle(ButtonStyle.Danger), + ]); + + await interaction.reply({ + embeds: [ embed ], + components: [ row ], + }); +} \ No newline at end of file diff --git a/src/commands/give.ts b/src/commands/give.ts index 3ffbe8f..35dfa04 100644 --- a/src/commands/give.ts +++ b/src/commands/give.ts @@ -2,10 +2,10 @@ import { CacheType, CommandInteraction, PermissionsBitField, SlashCommandBuilder import { Command } from "../type/command"; import { CoreClient } from "../client/client"; import Config from "../database/entities/app/Config"; -import CardDropHelperMetadata from "../helpers/CardDropHelperMetadata"; import Inventory from "../database/entities/app/Inventory"; import AppLogger from "../client/appLogger"; import User from "../database/entities/app/User"; +import GetCardsHelper from "../helpers/DropHelpers/GetCardsHelper"; export default class Give extends Command { constructor() { @@ -81,7 +81,7 @@ export default class Give extends Command { AppLogger.LogSilly("Commands/Give/GiveCard", `Parameters: cardNumber=${cardNumber.value}, user=${user.id}`); - const card = CardDropHelperMetadata.GetCardByCardNumber(cardNumber.value!.toString()); + const card = GetCardsHelper.GetCardByCardNumber(cardNumber.value!.toString()); if (!card) { await interaction.reply("Unable to fetch card, please try again."); diff --git a/src/commands/id.ts b/src/commands/id.ts index ae924a6..0f11aaa 100644 --- a/src/commands/id.ts +++ b/src/commands/id.ts @@ -4,8 +4,8 @@ import { CoreClient } from "../client/client"; import { readFileSync } from "fs"; import path from "path"; import Inventory from "../database/entities/app/Inventory"; -import CardDropHelperMetadata from "../helpers/CardDropHelperMetadata"; import AppLogger from "../client/appLogger"; +import DropEmbedHelper from "../helpers/DropHelpers/DropEmbedHelper"; export default class Id extends Command { constructor() { @@ -43,31 +43,29 @@ export default class Id extends Command { const series = CoreClient.Cards .find(x => x.cards.includes(card))!; - let image: Buffer; - const imageFileName = card.path.split("/").pop()!; + const files = []; + let imageFileName = ""; - try { - image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.path)); - } catch { - AppLogger.LogError("Commands/View", `Unable to fetch image for card ${card.id}.`); + if (!(card.path.startsWith("http://") || card.path.startsWith("https://"))) { + const image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.path)); + imageFileName = card.path.split("/").pop()!; - await interaction.reply(`Unable to fetch image for card ${card.id}.`); - return; + const attachment = new AttachmentBuilder(image, { name: imageFileName }); + + files.push(attachment); } await interaction.deferReply(); - const attachment = new AttachmentBuilder(image, { name: imageFileName }); - const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, card.id); const quantityClaimed = inventory ? inventory.Quantity : 0; - const embed = CardDropHelperMetadata.GenerateDropEmbed({ card, series }, quantityClaimed, imageFileName); + const embed = DropEmbedHelper.GenerateDropEmbed({ card, series }, quantityClaimed, imageFileName); try { await interaction.editReply({ embeds: [ embed ], - files: [ attachment ], + files: files, }); } catch (e) { AppLogger.LogError("Commands/View", `Error sending view for card ${card.id}: ${e}`); diff --git a/src/commands/multidrop.ts b/src/commands/multidrop.ts index f35b921..aa42686 100644 --- a/src/commands/multidrop.ts +++ b/src/commands/multidrop.ts @@ -6,10 +6,11 @@ import Config from "../database/entities/app/Config"; import AppLogger from "../client/appLogger"; import User from "../database/entities/app/User"; import CardConstants from "../constants/CardConstants"; -import CardDropHelperMetadata from "../helpers/CardDropHelperMetadata"; import { readFileSync } from "fs"; import path from "path"; import Inventory from "../database/entities/app/Inventory"; +import GetCardsHelper from "../helpers/DropHelpers/GetCardsHelper"; +import MultidropEmbedHelper from "../helpers/DropHelpers/MultidropEmbedHelper"; export default class Multidrop extends Command { constructor() { @@ -49,7 +50,7 @@ export default class Multidrop extends Command { user.RemoveCurrency(CardConstants.MultidropCost); await user.Save(User, user); - const randomCard = CardDropHelperMetadata.GetRandomCard(); + const randomCard = GetCardsHelper.GetRandomCard(); const cardsRemaining = CardConstants.MultidropQuantity - 1; if (!randomCard) { @@ -69,9 +70,9 @@ export default class Multidrop extends Command { const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, randomCard.card.id); const quantityClaimed = inventory ? inventory.Quantity : 0; - const embed = CardDropHelperMetadata.GenerateMultidropEmbed(randomCard, quantityClaimed, imageFileName, cardsRemaining, undefined, user.Currency); + const embed = MultidropEmbedHelper.GenerateMultidropEmbed(randomCard, quantityClaimed, imageFileName, cardsRemaining, undefined, user.Currency); - const row = CardDropHelperMetadata.GenerateMultidropButtons(randomCard, cardsRemaining, interaction.user.id); + const row = MultidropEmbedHelper.GenerateMultidropButtons(randomCard, cardsRemaining, interaction.user.id); await interaction.editReply({ embeds: [ embed ], diff --git a/src/commands/sacrifice.ts b/src/commands/sacrifice.ts index 9683714..a44a69e 100644 --- a/src/commands/sacrifice.ts +++ b/src/commands/sacrifice.ts @@ -2,8 +2,8 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CacheType, CommandInterac import { Command } from "../type/command"; import Inventory from "../database/entities/app/Inventory"; import { CardRarityToString, GetSacrificeAmount } from "../constants/CardRarity"; -import CardDropHelperMetadata from "../helpers/CardDropHelperMetadata"; import EmbedColours from "../constants/EmbedColours"; +import GetCardsHelper from "../helpers/DropHelpers/GetCardsHelper"; export default class Sacrifice extends Command { constructor() { @@ -41,7 +41,7 @@ export default class Sacrifice extends Command { return; } - const cardData = CardDropHelperMetadata.GetCardByCardNumber(cardnumber.value! as string); + const cardData = GetCardsHelper.GetCardByCardNumber(cardnumber.value! as string); if (!cardData) { await interaction.reply("Unable to find card in the database."); diff --git a/src/commands/series.ts b/src/commands/series.ts index e268db9..16ae0db 100644 --- a/src/commands/series.ts +++ b/src/commands/series.ts @@ -60,13 +60,18 @@ export default class Series extends Command { return; } - const embed = await SeriesHelper.GenerateSeriesViewPage(series.id, 0, interaction.user.id); + try { + const embed = await SeriesHelper.GenerateSeriesViewPage(series.id, 0, interaction.user.id); - await interaction.followUp({ - embeds: [ embed!.embed ], - components: [ embed!.row ], - files: [ embed!.image ], - }); + await interaction.followUp({ + embeds: [ embed!.embed ], + components: [ embed!.row ], + files: [ embed!.image ], + }); + } catch (e) { + await interaction.followUp("An error has occured generating the series grid."); + AppLogger.CatchError("Series", e); + } } private async ListSeries(interaction: CommandInteraction) { diff --git a/src/commands/stage/dropnumber.ts b/src/commands/stage/dropnumber.ts index 0642327..c61ed43 100644 --- a/src/commands/stage/dropnumber.ts +++ b/src/commands/stage/dropnumber.ts @@ -1,11 +1,12 @@ -import { AttachmentBuilder, CacheType, CommandInteraction, DiscordAPIError, SlashCommandBuilder } from "discord.js"; +import { AttachmentBuilder, CacheType, CommandInteraction, SlashCommandBuilder } from "discord.js"; import { Command } from "../../type/command"; import { readFileSync } from "fs"; import Inventory from "../../database/entities/app/Inventory"; import { v4 } from "uuid"; import { CoreClient } from "../../client/client"; import path from "path"; -import CardDropHelperMetadata from "../../helpers/CardDropHelperMetadata"; +import DropEmbedHelper from "../../helpers/DropHelpers/DropEmbedHelper"; +import AppLogger from "../../client/appLogger"; export default class Dropnumber extends Command { constructor() { @@ -40,48 +41,40 @@ export default class Dropnumber extends Command { return; } - const series = CoreClient.Cards - .find(x => x.cards.includes(card))!; - - let image: Buffer; - const imageFileName = card.path.split("/").pop()!; - - try { - image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.path)); - } catch { - await interaction.reply(`Unable to fetch image for card ${card.id}`); - return; - } - + const claimId = v4(); await interaction.deferReply(); - const attachment = new AttachmentBuilder(image, { name: imageFileName }); + try { + const files = []; + let imageFileName = ""; + + if (!(card.path.startsWith("http://") || card.path.startsWith("https://"))) { + const image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.path)); + imageFileName = card.path.split("/").pop()!; + + const attachment = new AttachmentBuilder(image, { name: imageFileName }); + + files.push(attachment); + } + + const series = CoreClient.Cards + .find(x => x.cards.includes(card))!; const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, card.id); const quantityClaimed = inventory ? inventory.Quantity : 0; - const embed = CardDropHelperMetadata.GenerateDropEmbed({ card, series }, quantityClaimed, imageFileName); + const embed = DropEmbedHelper.GenerateDropEmbed({ card, series }, quantityClaimed, imageFileName); - const claimId = v4(); + const row = DropEmbedHelper.GenerateDropButtons({ card, series }, claimId, interaction.user.id); - const row = CardDropHelperMetadata.GenerateDropButtons({ card, series }, claimId, interaction.user.id); - - try { - await interaction.editReply({ - embeds: [ embed ], - files: [ attachment ], - components: [ row ], - }); + await interaction.editReply({ + embeds: [ embed ], + files: files, + components: [ row ], + }); } catch (e) { - console.error(e); - - if (e instanceof DiscordAPIError) { - await interaction.editReply(`Unable to send next drop. Please try again, and report this if it keeps happening. Code: ${e.code}`); - } else { - await interaction.editReply("Unable to send next drop. Please try again, and report this if it keeps happening. Code: UNKNOWN"); - } + AppLogger.CatchError("Dropnumber", e); + await interaction.editReply("Unable to send next drop. Please try again, and report this if it keeps happening"); } - - CoreClient.ClaimId = claimId; } } \ No newline at end of file diff --git a/src/commands/stage/droprarity.ts b/src/commands/stage/droprarity.ts index be0a62d..f776930 100644 --- a/src/commands/stage/droprarity.ts +++ b/src/commands/stage/droprarity.ts @@ -1,12 +1,13 @@ -import { AttachmentBuilder, CacheType, CommandInteraction, DiscordAPIError, SlashCommandBuilder } from "discord.js"; +import { AttachmentBuilder, CacheType, CommandInteraction, SlashCommandBuilder } from "discord.js"; import { Command } from "../../type/command"; -import { CardRarity, CardRarityParse } from "../../constants/CardRarity"; +import { CardRarity, CardRarityChoices, CardRarityParse } from "../../constants/CardRarity"; import { readFileSync } from "fs"; import Inventory from "../../database/entities/app/Inventory"; import { v4 } from "uuid"; -import { CoreClient } from "../../client/client"; -import CardDropHelperMetadata from "../../helpers/CardDropHelperMetadata"; import path from "path"; +import GetCardsHelper from "../../helpers/DropHelpers/GetCardsHelper"; +import DropEmbedHelper from "../../helpers/DropHelpers/DropEmbedHelper"; +import AppLogger from "../../client/appLogger"; export default class Droprarity extends Command { constructor() { @@ -19,7 +20,8 @@ export default class Droprarity extends Command { x .setName("rarity") .setDescription("The rarity you want to summon") - .setRequired(true)); + .setRequired(true) + .setChoices(CardRarityChoices)); } public override async execute(interaction: CommandInteraction) { @@ -39,52 +41,44 @@ export default class Droprarity extends Command { return; } - const card = await CardDropHelperMetadata.GetRandomCardByRarity(rarityType); + const card = GetCardsHelper.GetRandomCardByRarity(rarityType); if (!card) { await interaction.reply("Card not found"); return; } - let image: Buffer; - const imageFileName = card.card.path.split("/").pop()!; - - try { - image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.card.path)); - } catch { - await interaction.reply(`Unable to fetch image for card ${card.card.id}`); - return; - } - + const claimId = v4(); await interaction.deferReply(); - const attachment = new AttachmentBuilder(image, { name: imageFileName }); - - const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, card.card.id); - const quantityClaimed = inventory ? inventory.Quantity : 0; - - const embed = CardDropHelperMetadata.GenerateDropEmbed(card, quantityClaimed, imageFileName); - - const claimId = v4(); - - const row = CardDropHelperMetadata.GenerateDropButtons(card, claimId, interaction.user.id); - try { + const files = []; + let imageFileName = ""; + + if (!(card.card.path.startsWith("http://") || card.card.path.startsWith("https://"))) { + const image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.card.path)); + imageFileName = card.card.path.split("/").pop()!; + + const attachment = new AttachmentBuilder(image, { name: imageFileName }); + + files.push(attachment); + } + + const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, card.card.id); + const quantityClaimed = inventory ? inventory.Quantity : 0; + + const embed = DropEmbedHelper.GenerateDropEmbed(card, quantityClaimed, imageFileName); + + const row = DropEmbedHelper.GenerateDropButtons(card, claimId, interaction.user.id); + await interaction.editReply({ embeds: [ embed ], - files: [ attachment ], + files: files, components: [ row ], }); } catch (e) { - console.error(e); - - if (e instanceof DiscordAPIError) { - await interaction.editReply(`Unable to send next drop. Please try again, and report this if it keeps happening. Code: ${e.code}`); - } else { - await interaction.editReply("Unable to send next drop. Please try again, and report this if it keeps happening. Code: UNKNOWN"); - } + AppLogger.CatchError("Droprarity", e); + await interaction.editReply("Unable to send next drop. Please try again, and report this if it keeps happening"); } - - CoreClient.ClaimId = claimId; } } \ No newline at end of file diff --git a/src/commands/view.ts b/src/commands/view.ts index 3ba5621..9a1d447 100644 --- a/src/commands/view.ts +++ b/src/commands/view.ts @@ -34,7 +34,7 @@ export default class View extends Command { await interaction.editReply({ embeds: [ searchResult.embed ], components: [ searchResult.row ], - files: [ searchResult.attachment ], + files: searchResult.attachments, }); } } \ No newline at end of file diff --git a/src/constants/CardConstants.ts b/src/constants/CardConstants.ts index 3f9723b..60a7f59 100644 --- a/src/constants/CardConstants.ts +++ b/src/constants/CardConstants.ts @@ -7,4 +7,7 @@ export default class CardConstants { // Multidrop public static readonly MultidropCost = this.ClaimCost * 10; public static readonly MultidropQuantity = 11; + + // Effects + public static readonly UnusedChanceUpChance = 0.5; } \ No newline at end of file diff --git a/src/constants/CardRarity.ts b/src/constants/CardRarity.ts index 8817172..0fcdec7 100644 --- a/src/constants/CardRarity.ts +++ b/src/constants/CardRarity.ts @@ -9,6 +9,29 @@ export enum CardRarity { Legendary, } +export const CardRarityChoices = [ + { + name: "Bronze", + value: "bronze", + }, + { + name: "Silver", + value: "silver", + }, + { + name: "Gold", + value: "gold", + }, + { + name: "Manga", + value: "manga", + }, + { + name: "Legendary", + value: "legendary", + }, +]; + export function CardRarityToString(rarity: CardRarity): string { switch (rarity) { case CardRarity.Unknown: diff --git a/src/constants/EffectDetails.ts b/src/constants/EffectDetails.ts new file mode 100644 index 0000000..9d1f2b6 --- /dev/null +++ b/src/constants/EffectDetails.ts @@ -0,0 +1,23 @@ +class EffectDetail { + public readonly id: string; + public readonly friendlyName: string; + public readonly duration: number; + public readonly cost: number; + public readonly cooldown: number; + + constructor(id: string, friendlyName: string, duration: number, cost: number, cooldown: number) { + this.id = id; + this.friendlyName = friendlyName; + this.duration = duration; + this.cost = cost; + this.cooldown = cooldown; + } +}; + +export const EffectDetails = new Map([ + [ "unclaimed", new EffectDetail("unclaimed", "Unclaimed Chance Up", 10 * 60 * 1000, 100, 3 * 60 * 60 * 1000) ], +]); + +export const EffectChoices = [ + { name: "Unclaimed Chance Up", value: "unclaimed" }, +]; diff --git a/src/database/entities/app/UserEffect.ts b/src/database/entities/app/UserEffect.ts index fa1b584..447b9c5 100644 --- a/src/database/entities/app/UserEffect.ts +++ b/src/database/entities/app/UserEffect.ts @@ -57,4 +57,30 @@ export default class UserEffect extends AppBaseEntity { return single; } + + public static async FetchAllByUserIdPaginated(userId: string, page: number = 0, itemsPerPage: number = 10): Promise<[UserEffect[], number]> { + const repository = AppDataSource.getRepository(UserEffect); + + const query = await repository.createQueryBuilder("effect") + .where("effect.UserId = :userId", { userId }) + .andWhere("effect.Unused > 0") + .orderBy("effect.Name", "ASC") + .skip(page * itemsPerPage) + .take(itemsPerPage) + .getManyAndCount(); + + return query; + } + + public static async FetchActiveEffectByUserId(userId: string): Promise { + const repository = AppDataSource.getRepository(UserEffect); + + const query = await repository.createQueryBuilder("effect") + .where("effect.UserId = :userId", { userId }) + .andWhere("effect.WhenExpires IS NOT NULL") + .andWhere("effect.WhenExpires > :now", { now: new Date() }) + .getOne(); + + return query; + } } diff --git a/src/helpers/CardDropHelperMetadata.ts b/src/helpers/CardDropHelperMetadata.ts deleted file mode 100644 index 8b9b273..0000000 --- a/src/helpers/CardDropHelperMetadata.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js"; -import { CardRarity, CardRarityToColour, CardRarityToString, GetSacrificeAmount } from "../constants/CardRarity"; -import CardRarityChances from "../constants/CardRarityChances"; -import { DropResult } from "../contracts/SeriesMetadata"; -import { CoreClient } from "../client/client"; -import AppLogger from "../client/appLogger"; -import CardConstants from "../constants/CardConstants"; -import StringTools from "./StringTools"; - -export default class CardDropHelperMetadata { - public static GetRandomCard(): DropResult | undefined { - const randomRarity = Math.random() * 100; - - let cardRarity: CardRarity; - - const bronzeChance = CardRarityChances.Bronze; - const silverChance = bronzeChance + CardRarityChances.Silver; - const goldChance = silverChance + CardRarityChances.Gold; - const mangaChance = goldChance + CardRarityChances.Manga; - - if (randomRarity < bronzeChance) cardRarity = CardRarity.Bronze; - else if (randomRarity < silverChance) cardRarity = CardRarity.Silver; - else if (randomRarity < goldChance) cardRarity = CardRarity.Gold; - else if (randomRarity < mangaChance) cardRarity = CardRarity.Manga; - else cardRarity = CardRarity.Legendary; - - const randomCard = this.GetRandomCardByRarity(cardRarity); - - AppLogger.LogSilly("CardDropHelperMetadata/GetRandomCard", `Random card: ${randomCard?.card.id} ${randomCard?.card.name}`); - - return randomCard; - } - - public static GetRandomCardByRarity(rarity: CardRarity): DropResult | undefined { - AppLogger.LogSilly("CardDropHelperMetadata/GetRandomCardByRarity", `Parameters: rarity=${rarity}`); - - const allCards = CoreClient.Cards - .flatMap(x => x.cards) - .filter(x => x.type == rarity); - - const randomCardIndex = Math.floor(Math.random() * allCards.length); - - const card = allCards[randomCardIndex]; - const series = CoreClient.Cards - .find(x => x.cards.includes(card)); - - if (!series) { - AppLogger.LogWarn("CardDropHelperMetadata/GetRandomCardByRarity", `Series not found for card ${card.id}`); - - return undefined; - } - - AppLogger.LogSilly("CardDropHelperMetadata/GetRandomCardByRarity", `Random card: ${card.id} ${card.name}`); - - return { - series: series, - card: card, - }; - } - - public static GetCardByCardNumber(cardNumber: string): DropResult | undefined { - AppLogger.LogSilly("CardDropHelperMetadata/GetCardByCardNumber", `Parameters: cardNumber=${cardNumber}`); - - const card = CoreClient.Cards - .flatMap(x => x.cards) - .find(x => x.id == cardNumber); - - const series = CoreClient.Cards - .find(x => x.cards.find(y => y.id == card?.id)); - - AppLogger.LogSilly("CardDropHelperMetadata/GetCardByCardNumber", `Card: ${card?.id} ${card?.name}`); - AppLogger.LogSilly("CardDropHelperMetadata/GetCardByCardNumber", `Series: ${series?.id} ${series?.name}`); - - if (!card || !series) { - AppLogger.LogVerbose("CardDropHelperMetadata/GetCardByCardNumber", `Unable to find card metadata: ${cardNumber}`); - return undefined; - } - - return { card, series }; - } - - public static GenerateDropEmbed(drop: DropResult, quantityClaimed: number, imageFileName: string, claimedBy?: string, currency?: number): EmbedBuilder { - AppLogger.LogSilly("CardDropHelperMetadata/GenerateDropEmbed", `Parameters: drop=${drop.card.id}, quantityClaimed=${quantityClaimed}, imageFileName=${imageFileName}`); - - const description = drop.card.subseries ?? drop.series.name; - let colour = CardRarityToColour(drop.card.type); - - if (drop.card.colour && StringTools.IsHexCode(drop.card.colour)) { - const hexCode = Number("0x" + drop.card.colour); - - if (hexCode) { - colour = hexCode; - } else { - AppLogger.LogWarn("CardDropHelperMetadata/GenerateDropEmbed", `Card's colour override is invalid: ${drop.card.id}, ${drop.card.colour}`); - } - } else if (drop.card.colour) { - AppLogger.LogWarn("CardDropHelperMetadata/GenerateDropEmbed", `Card's colour override is invalid: ${drop.card.id}, ${drop.card.colour}`); - } - - const embed = new EmbedBuilder() - .setTitle(drop.card.name) - .setDescription(description) - .setFooter({ text: `${CardRarityToString(drop.card.type)} · ${drop.card.id}` }) - .setColor(colour) - .setImage(`attachment://${imageFileName}`) - .addFields([ - { - name: "Claimed", - value: `${quantityClaimed}`, - inline: true, - } - ]); - - if (claimedBy != null) { - embed.addFields([ - { - name: "Claimed by", - value: claimedBy, - inline: true, - } - ]); - } - - if (currency != null) { - embed.addFields([ - { - name: "Currency", - value: `${currency}`, - inline: true, - } - ]); - } - - return embed; - } - - public static GenerateDropButtons(drop: DropResult, claimId: string, userId: string, disabled: boolean = false): ActionRowBuilder { - AppLogger.LogSilly("CardDropHelperMetadata/GenerateDropButtons", `Parameters: drop=${drop.card.id}, claimId=${claimId}, userId=${userId}`); - - return new ActionRowBuilder() - .addComponents( - new ButtonBuilder() - .setCustomId(`claim ${drop.card.id} ${claimId} ${userId}`) - .setLabel(`Claim (${CardConstants.ClaimCost} 🪙)`) - .setStyle(ButtonStyle.Primary) - .setDisabled(disabled), - new ButtonBuilder() - .setCustomId("reroll") - .setLabel("Reroll") - .setStyle(ButtonStyle.Secondary)); - } - - public static GenerateMultidropEmbed(drop: DropResult, quantityClaimed: number, imageFileName: string, cardsRemaining: number, claimedBy?: string, currency?: number): EmbedBuilder { - const dropEmbed = this.GenerateDropEmbed(drop, quantityClaimed, imageFileName, claimedBy, currency); - - dropEmbed.setFooter({ text: `${dropEmbed.data.footer?.text} · ${cardsRemaining} Remaining`}); - - return dropEmbed; - } - - public static GenerateMultidropButtons(drop: DropResult, cardsRemaining: number, userId: string, disabled = false): ActionRowBuilder { - return new ActionRowBuilder() - .addComponents( - new ButtonBuilder() - .setCustomId(`multidrop keep ${drop.card.id} ${cardsRemaining} ${userId}`) - .setLabel("Keep") - .setStyle(ButtonStyle.Primary) - .setDisabled(disabled), - new ButtonBuilder() - .setCustomId(`multidrop sacrifice ${drop.card.id} ${cardsRemaining} ${userId}`) - .setLabel(`Sacrifice (+${GetSacrificeAmount(drop.card.type)} 🪙)`) - .setStyle(ButtonStyle.Secondary)); - } -} diff --git a/src/helpers/CardSearchHelper.ts b/src/helpers/CardSearchHelper.ts index 29014f7..df0265c 100644 --- a/src/helpers/CardSearchHelper.ts +++ b/src/helpers/CardSearchHelper.ts @@ -1,16 +1,17 @@ import {ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder} from "discord.js"; import Fuse from "fuse.js"; import {CoreClient} from "../client/client.js"; -import CardDropHelperMetadata from "./CardDropHelperMetadata.js"; import Inventory from "../database/entities/app/Inventory.js"; import {readFileSync} from "fs"; import path from "path"; import AppLogger from "../client/appLogger.js"; +import GetCardsHelper from "./DropHelpers/GetCardsHelper.js"; +import DropEmbedHelper from "./DropHelpers/DropEmbedHelper.js"; interface ReturnedPage { embed: EmbedBuilder, row: ActionRowBuilder, - attachment: AttachmentBuilder, + attachments: AttachmentBuilder[], results: string[], } @@ -32,27 +33,26 @@ export default class CardSearchHelper { return undefined; } - const card = CardDropHelperMetadata.GetCardByCardNumber(entry.item.id); + const card = GetCardsHelper.GetCardByCardNumber(entry.item.id); if (!card) return undefined; - let image: Buffer; - const imageFileName = card.card.path.split("/").pop()!; + const attachments = []; + let imageFileName = ""; - try { - image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.card.path)); - } catch { - AppLogger.LogError("CardSearchHelper/GenerateSearchQuery", `Unable to fetch image for card ${card.card.id}.`); - - return undefined; + if (!(card.card.path.startsWith("http://") || card.card.path.startsWith("https://"))) { + const image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.card.path)); + imageFileName = card.card.path.split("/").pop()!; + + const attachment = new AttachmentBuilder(image, { name: imageFileName }); + + attachments.push(attachment); } - const attachment = new AttachmentBuilder(image, { name: imageFileName }); - const inventory = await Inventory.FetchOneByCardNumberAndUserId(userid, card.card.id); const quantityClaimed = inventory?.Quantity ?? 0; - const embed = CardDropHelperMetadata.GenerateDropEmbed(card, quantityClaimed, imageFileName); + const embed = DropEmbedHelper.GenerateDropEmbed(card, quantityClaimed, imageFileName); const row = new ActionRowBuilder() .addComponents( @@ -67,13 +67,13 @@ export default class CardSearchHelper { .setStyle(ButtonStyle.Primary) .setDisabled(pages == 1)); - return { embed, row, attachment, results }; + return { embed, row, attachments, results }; } public static async GenerateSearchPageFromQuery(results: string[], userid: string, page: number): Promise { const currentPageId = results[page - 1]; - const card = CardDropHelperMetadata.GetCardByCardNumber(currentPageId); + const card = GetCardsHelper.GetCardByCardNumber(currentPageId); if (!card) { AppLogger.LogError("CardSearchHelper/GenerateSearchPageFromQuery", `Unable to find card by id: ${currentPageId}.`); @@ -81,23 +81,22 @@ export default class CardSearchHelper { return undefined; } - let image: Buffer; - const imageFileName = card.card.path.split("/").pop()!; + const attachments = []; + let imageFileName = ""; - try { - image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.card.path)); - } catch { - AppLogger.LogError("CardSearchHelper/GenerateSearchPageFromQuery", `Unable to fetch image for card ${card.card.id}.`); + if (!(card.card.path.startsWith("http://") || card.card.path.startsWith("https://"))) { + const image = readFileSync(path.join(process.env.DATA_DIR!, "cards", card.card.path)); + imageFileName = card.card.path.split("/").pop()!; - return undefined; + const attachment = new AttachmentBuilder(image, { name: imageFileName }); + + attachments.push(attachment); } - const attachment = new AttachmentBuilder(image, { name: imageFileName }); - const inventory = await Inventory.FetchOneByCardNumberAndUserId(userid, card.card.id); const quantityClaimed = inventory?.Quantity ?? 0; - const embed = CardDropHelperMetadata.GenerateDropEmbed(card, quantityClaimed, imageFileName); + const embed = DropEmbedHelper.GenerateDropEmbed(card, quantityClaimed, imageFileName); const row = new ActionRowBuilder() .addComponents( @@ -112,6 +111,6 @@ export default class CardSearchHelper { .setStyle(ButtonStyle.Primary) .setDisabled(page == results.length)); - return { embed, row, attachment, results }; + return { embed, row, attachments, results }; } } diff --git a/src/helpers/DropHelpers/DropEmbedHelper.ts b/src/helpers/DropHelpers/DropEmbedHelper.ts new file mode 100644 index 0000000..08b5813 --- /dev/null +++ b/src/helpers/DropHelpers/DropEmbedHelper.ts @@ -0,0 +1,88 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js"; +import { DropResult } from "../../contracts/SeriesMetadata"; +import AppLogger from "../../client/appLogger"; +import { CardRarityToColour, CardRarityToString } from "../../constants/CardRarity"; +import StringTools from "../StringTools"; + +export default class DropEmbedHelper { + public static GenerateDropEmbed(drop: DropResult, quantityClaimed: number, imageFileName: string, claimedBy?: string, currency?: number): EmbedBuilder { + AppLogger.LogSilly("CardDropHelperMetadata/GenerateDropEmbed", `Parameters: drop=${drop.card.id}, quantityClaimed=${quantityClaimed}, imageFileName=${imageFileName}`); + + const description = drop.card.subseries ?? drop.series.name; + let colour = CardRarityToColour(drop.card.type); + + if (drop.card.colour && StringTools.IsHexCode(drop.card.colour)) { + const hexCode = Number("0x" + drop.card.colour); + + if (hexCode) { + colour = hexCode; + } else { + AppLogger.LogWarn("CardDropHelperMetadata/GenerateDropEmbed", `Card's colour override is invalid: ${drop.card.id}, ${drop.card.colour}`); + } + } else if (drop.card.colour) { + AppLogger.LogWarn("CardDropHelperMetadata/GenerateDropEmbed", `Card's colour override is invalid: ${drop.card.id}, ${drop.card.colour}`); + } + + let imageUrl = `attachment://${imageFileName}`; + + if (drop.card.path.startsWith("http://") || drop.card.path.startsWith("https://")) { + imageUrl = drop.card.path; + } + + const embed = new EmbedBuilder() + .setTitle(drop.card.name) + .setDescription(description) + .setFooter({ text: `${CardRarityToString(drop.card.type)} · ${drop.card.id}` }) + .setColor(colour) + .setImage(imageUrl) + .addFields([ + { + name: "Claimed", + value: `${quantityClaimed}`, + inline: true, + } + ]); + + if (claimedBy != null) { + embed.addFields([ + { + name: "Claimed by", + value: claimedBy, + inline: true, + } + ]); + } + + if (currency != null) { + embed.addFields([ + { + name: "Currency", + value: `${currency}`, + inline: true, + } + ]); + } + + return embed; + } + + public static GenerateDropButtons(drop: DropResult, claimId: string, userId: string, disabled: boolean = false): ActionRowBuilder { + AppLogger.LogSilly("CardDropHelperMetadata/GenerateDropButtons", `Parameters: drop=${drop.card.id}, claimId=${claimId}, userId=${userId}`); + + return new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId(`claim ${drop.card.id} ${claimId} ${userId}`) + .setLabel("Claim") + .setStyle(ButtonStyle.Success) + .setDisabled(disabled), + new ButtonBuilder() + .setCustomId(`sacrifice confirm ${userId} ${drop.card.id} 1`) + .setLabel(`Sacrifice`) + .setStyle(ButtonStyle.Danger), + new ButtonBuilder() + .setCustomId("reroll") + .setEmoji("🔁") + .setStyle(ButtonStyle.Primary),); + } +} \ No newline at end of file diff --git a/src/helpers/DropHelpers/GetCardsHelper.ts b/src/helpers/DropHelpers/GetCardsHelper.ts new file mode 100644 index 0000000..8bc4fe7 --- /dev/null +++ b/src/helpers/DropHelpers/GetCardsHelper.ts @@ -0,0 +1,91 @@ +import AppLogger from "../../client/appLogger"; +import { CoreClient } from "../../client/client"; +import CardConstants from "../../constants/CardConstants"; +import { CardRarity } from "../../constants/CardRarity"; +import CardRarityChances from "../../constants/CardRarityChances"; +import { DropResult } from "../../contracts/SeriesMetadata"; +import EffectHelper from "../EffectHelper"; +import GetUnclaimedCardsHelper from "./GetUnclaimedCardsHelper"; + +export default class GetCardsHelper { + public static async FetchCard(userId: string): Promise { + const hasChanceUpEffect = await EffectHelper.HasEffect(userId, "unclaimed"); + + if (hasChanceUpEffect && Math.random() <= CardConstants.UnusedChanceUpChance) { + return await GetUnclaimedCardsHelper.GetRandomCardUnclaimed(userId); + } + + return this.GetRandomCard(); + } + + public static GetRandomCard(): DropResult | undefined { + const randomRarity = Math.random() * 100; + + let cardRarity: CardRarity; + + const bronzeChance = CardRarityChances.Bronze; + const silverChance = bronzeChance + CardRarityChances.Silver; + const goldChance = silverChance + CardRarityChances.Gold; + const mangaChance = goldChance + CardRarityChances.Manga; + + if (randomRarity < bronzeChance) cardRarity = CardRarity.Bronze; + else if (randomRarity < silverChance) cardRarity = CardRarity.Silver; + else if (randomRarity < goldChance) cardRarity = CardRarity.Gold; + else if (randomRarity < mangaChance) cardRarity = CardRarity.Manga; + else cardRarity = CardRarity.Legendary; + + const randomCard = this.GetRandomCardByRarity(cardRarity); + + AppLogger.LogSilly("CardDropHelperMetadata/GetRandomCard", `Random card: ${randomCard?.card.id} ${randomCard?.card.name}`); + + return randomCard; + } + + public static GetRandomCardByRarity(rarity: CardRarity): DropResult | undefined { + AppLogger.LogSilly("CardDropHelperMetadata/GetRandomCardByRarity", `Parameters: rarity=${rarity}`); + + const allCards = CoreClient.Cards + .flatMap(x => x.cards) + .filter(x => x.type == rarity); + + const randomCardIndex = Math.floor(Math.random() * allCards.length); + + const card = allCards[randomCardIndex]; + const series = CoreClient.Cards + .find(x => x.cards.includes(card)); + + if (!series) { + AppLogger.LogWarn("CardDropHelperMetadata/GetRandomCardByRarity", `Series not found for card ${card.id}`); + + return undefined; + } + + AppLogger.LogSilly("CardDropHelperMetadata/GetRandomCardByRarity", `Random card: ${card.id} ${card.name}`); + + return { + series: series, + card: card, + }; + } + + public static GetCardByCardNumber(cardNumber: string): DropResult | undefined { + AppLogger.LogSilly("CardDropHelperMetadata/GetCardByCardNumber", `Parameters: cardNumber=${cardNumber}`); + + const card = CoreClient.Cards + .flatMap(x => x.cards) + .find(x => x.id == cardNumber); + + const series = CoreClient.Cards + .find(x => x.cards.find(y => y.id == card?.id)); + + AppLogger.LogSilly("CardDropHelperMetadata/GetCardByCardNumber", `Card: ${card?.id} ${card?.name}`); + AppLogger.LogSilly("CardDropHelperMetadata/GetCardByCardNumber", `Series: ${series?.id} ${series?.name}`); + + if (!card || !series) { + AppLogger.LogVerbose("CardDropHelperMetadata/GetCardByCardNumber", `Unable to find card metadata: ${cardNumber}`); + return undefined; + } + + return { card, series }; + } +} diff --git a/src/helpers/DropHelpers/GetUnclaimedCardsHelper.ts b/src/helpers/DropHelpers/GetUnclaimedCardsHelper.ts new file mode 100644 index 0000000..87fafea --- /dev/null +++ b/src/helpers/DropHelpers/GetUnclaimedCardsHelper.ts @@ -0,0 +1,63 @@ +import AppLogger from "../../client/appLogger"; +import { CoreClient } from "../../client/client"; +import { CardRarity } from "../../constants/CardRarity"; +import CardRarityChances from "../../constants/CardRarityChances"; +import { DropResult } from "../../contracts/SeriesMetadata"; +import Inventory from "../../database/entities/app/Inventory"; +import GetCardsHelper from "./GetCardsHelper"; + +export default class GetUnclaimedCardsHelper { + public static async GetRandomCardUnclaimed(userId: string): Promise { + const randomRarity = Math.random() * 100; + + let cardRarity: CardRarity; + + const bronzeChance = CardRarityChances.Bronze; + const silverChance = bronzeChance + CardRarityChances.Silver; + const goldChance = silverChance + CardRarityChances.Gold; + const mangaChance = goldChance + CardRarityChances.Manga; + + if (randomRarity < bronzeChance) cardRarity = CardRarity.Bronze; + else if (randomRarity < silverChance) cardRarity = CardRarity.Silver; + else if (randomRarity < goldChance) cardRarity = CardRarity.Gold; + else if (randomRarity < mangaChance) cardRarity = CardRarity.Manga; + else cardRarity = CardRarity.Legendary; + + const randomCard = await this.GetRandomCardByRarityUnclaimed(cardRarity, userId); + + return randomCard; + } + + public static async GetRandomCardByRarityUnclaimed(rarity: CardRarity, userId: string): Promise { + const claimedCards = await Inventory.FetchAllByUserId(userId); + + if (!claimedCards) { + // They don't have any cards, so safe to get any random card + return GetCardsHelper.GetRandomCardByRarity(rarity); + } + + const allCards = CoreClient.Cards + .flatMap(x => x.cards) + .filter(x => x.type == rarity) + .filter(x => !claimedCards.find(y => y.CardNumber == x.id)); + + if (!allCards) return undefined; + + const randomCardIndex = Math.floor(Math.random() * allCards.length); + + const card = allCards[randomCardIndex]; + const series = CoreClient.Cards + .find(x => x.cards.includes(card)); + + if (!series) { + AppLogger.LogWarn("CardDropHelperMetadata/GetRandomCardByRarityUnclaimed", `Series not found for card ${card.id}`); + + return undefined; + } + + return { + series: series, + card: card, + }; + } +} diff --git a/src/helpers/DropHelpers/MultidropEmbedHelper.ts b/src/helpers/DropHelpers/MultidropEmbedHelper.ts new file mode 100644 index 0000000..526993e --- /dev/null +++ b/src/helpers/DropHelpers/MultidropEmbedHelper.ts @@ -0,0 +1,28 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js"; +import { DropResult } from "../../contracts/SeriesMetadata"; +import { GetSacrificeAmount } from "../../constants/CardRarity"; +import DropEmbedHelper from "./DropEmbedHelper"; + +export default class MultidropEmbedHelper { + public static GenerateMultidropEmbed(drop: DropResult, quantityClaimed: number, imageFileName: string, cardsRemaining: number, claimedBy?: string, currency?: number): EmbedBuilder { + const dropEmbed = DropEmbedHelper.GenerateDropEmbed(drop, quantityClaimed, imageFileName, claimedBy, currency); + + dropEmbed.setFooter({ text: `${dropEmbed.data.footer?.text} · ${cardsRemaining} Remaining`}); + + return dropEmbed; + } + + public static GenerateMultidropButtons(drop: DropResult, cardsRemaining: number, userId: string, disabled = false): ActionRowBuilder { + return new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId(`multidrop keep ${drop.card.id} ${cardsRemaining} ${userId}`) + .setLabel("Keep") + .setStyle(ButtonStyle.Primary) + .setDisabled(disabled), + new ButtonBuilder() + .setCustomId(`multidrop sacrifice ${drop.card.id} ${cardsRemaining} ${userId}`) + .setLabel(`Sacrifice (+${GetSacrificeAmount(drop.card.type)} 🪙)`) + .setStyle(ButtonStyle.Secondary)); + } +} diff --git a/src/helpers/EffectHelper.ts b/src/helpers/EffectHelper.ts index 14c2f43..235ea08 100644 --- a/src/helpers/EffectHelper.ts +++ b/src/helpers/EffectHelper.ts @@ -1,4 +1,10 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js"; import UserEffect from "../database/entities/app/UserEffect"; +import EmbedColours from "../constants/EmbedColours"; +import { EffectDetails } from "../constants/EffectDetails"; +import User from "../database/entities/app/User"; +import CardConstants from "../constants/CardConstants"; +import AppLogger from "../client/appLogger"; export default class EffectHelper { public static async AddEffectToUserInventory(userId: string, name: string, quantity: number = 1) { @@ -14,6 +20,20 @@ export default class EffectHelper { } public static async UseEffect(userId: string, name: string, whenExpires: Date): Promise { + const canUseEffect = await this.CanUseEffect(userId, name); + + if (!canUseEffect) return false; + + const effect = await UserEffect.FetchOneByUserIdAndName(userId, name); + + effect!.UseEffect(whenExpires); + + await effect!.Save(UserEffect, effect!); + + return true; + } + + public static async CanUseEffect(userId: string, name: string): Promise { const effect = await UserEffect.FetchOneByUserIdAndName(userId, name); const now = new Date(); @@ -21,13 +41,15 @@ export default class EffectHelper { return false; } - if (effect.WhenExpires && now < effect.WhenExpires) { + const effectDetail = EffectDetails.get(effect.Name); + + if (!effectDetail) { return false; } - effect.UseEffect(whenExpires); - - await effect.Save(UserEffect, effect); + if (effect.WhenExpires && now < new Date(effect.WhenExpires.getTime() + effectDetail.cooldown)) { + return false; + } return true; } @@ -46,4 +68,127 @@ export default class EffectHelper { return true; } + + public static async GenerateEffectListEmbed(userId: string, page: number): Promise<{ + embed: EmbedBuilder, + row: ActionRowBuilder, + }> { + const itemsPerPage = 10; + + const query = await UserEffect.FetchAllByUserIdPaginated(userId, page - 1, itemsPerPage); + const activeEffect = await UserEffect.FetchActiveEffectByUserId(userId); + + const effects = query[0]; + const count = query[1]; + + const totalPages = count > 0 ? Math.ceil(count / itemsPerPage) : 1; + + let description = "*none*"; + + if (effects.length > 0) { + description = effects.map(x => `${EffectDetails.get(x.Name)?.friendlyName} x${x.Unused}`).join("\n"); + } + + const embed = new EmbedBuilder() + .setTitle("Effects") + .setDescription(description) + .setColor(EmbedColours.Ok) + .setFooter({ text: `Page ${page} of ${totalPages}` }); + + if (activeEffect) { + embed.addFields([ + { + name: "Active", + value: `${EffectDetails.get(activeEffect.Name)?.friendlyName}`, + inline: true, + }, + { + name: "Expires", + value: ``, + inline: true, + }, + ]); + } + + const row = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId(`effects list ${page - 1}`) + .setLabel("Previous") + .setStyle(ButtonStyle.Primary) + .setDisabled(page == 1), + new ButtonBuilder() + .setCustomId(`effects list ${page + 1}`) + .setLabel("Next") + .setStyle(ButtonStyle.Primary) + .setDisabled(page == totalPages), + ); + + return { + embed, + row, + }; + } + + public static async GenerateEffectBuyEmbed(userId: string, id: string, quantity: number, disabled: boolean): Promise<{ + embed: EmbedBuilder, + row: ActionRowBuilder, + } | string> { + const effectDetail = EffectDetails.get(id); + + if (!effectDetail) { + return "Effect detail not found!"; + } + + const totalCost = effectDetail.cost * quantity; + + let user = await User.FetchOneById(User, userId); + + if (!user) { + user = new User(userId, CardConstants.StartingCurrency); + await user.Save(User, user); + + AppLogger.LogInfo("EffectHelper", `Created initial user entity for : ${userId}`); + } + + if (user.Currency < totalCost) { + return `You don't have enough currency to buy this! You have \`${user.Currency} Currency\` and need \`${totalCost} Currency\`!`; + } + + const embed = new EmbedBuilder() + .setTitle("Buy Effect") + .setDescription(effectDetail.friendlyName) + .setColor(EmbedColours.Ok) + .addFields([ + { + name: "Cost", + value: `${totalCost}`, + inline: true, + }, + { + name: "Quantity", + value: `${quantity}`, + inline: true, + }, + ]); + + const row = new ActionRowBuilder() + .addComponents([ + new ButtonBuilder() + .setCustomId(`effects buy confirm ${id} ${quantity}`) + .setLabel("Confirm") + .setStyle(ButtonStyle.Success) + .setDisabled(disabled), + new ButtonBuilder() + .setCustomId(`effects buy cancel ${id} ${quantity}`) + .setLabel("Cancel") + .setStyle(ButtonStyle.Danger) + .setDisabled(disabled), + ]); + + return { + embed, + row, + } + } } diff --git a/src/helpers/ImageHelper.ts b/src/helpers/ImageHelper.ts index 4e43977..6156938 100644 --- a/src/helpers/ImageHelper.ts +++ b/src/helpers/ImageHelper.ts @@ -3,7 +3,8 @@ import path from "path"; import AppLogger from "../client/appLogger"; import {existsSync} from "fs"; import Inventory from "../database/entities/app/Inventory"; -import {Jimp} from "jimp"; +import { Bitmap, Jimp } from "jimp"; +import axios from "axios"; interface CardInput { id: string; @@ -25,36 +26,52 @@ export default class ImageHelper { const ctx = canvas.getContext("2d"); for (let i = 0; i < cards.length; i++) { - const card = cards[i]; + try { + const card = cards[i]; - const filePath = path.join(process.env.DATA_DIR!, "cards", card.path); + const filePath = path.join(process.env.DATA_DIR!, "cards", card.path); - const exists = existsSync(filePath); + let bitmap: Bitmap; - if (!exists) { - AppLogger.LogError("ImageHelper/GenerateCardImageGrid", `Failed to load image from path ${card.path}`); - continue; - } + if (existsSync(filePath)) { + const data = await Jimp.read(filePath); - const imageData = await Jimp.read(filePath); + bitmap = data.bitmap; + } else if (card.path.startsWith("http://") || card.path.startsWith("https://")) { + const response = await axios.get(card.path, { responseType: "arraybuffer" }); + const buffer = Buffer.from(response.data); + const data = await Jimp.fromBuffer(buffer); - if (userId != null) { - const claimed = await Inventory.FetchOneByCardNumberAndUserId(userId, card.id); - - if (!claimed || claimed.Quantity == 0) { - imageData.greyscale(); + bitmap = data.bitmap; + } else { + AppLogger.LogError("ImageHelper/GenerateCardImageGrid", `Failed to load image from path ${card.path}`); + continue; } + + const imageData = Jimp.fromBitmap(bitmap); + + if (userId != null) { + const claimed = await Inventory.FetchOneByCardNumberAndUserId(userId, card.id); + + if (!claimed || claimed.Quantity == 0) { + imageData.greyscale(); + } + } + + const image = await loadImage(await imageData.getBuffer("image/png")); + + const x = i % gridWidth; + const y = Math.floor(i / gridWidth); + + const imageX = imageWidth * x; + const imageY = imageHeight * y; + + ctx.drawImage(image, imageX, imageY); + } + catch { + // TODO: Enable once we've investigated a fix + //AppLogger.CatchError("ImageHelper", e); } - - const image = await loadImage(await imageData.getBuffer("image/png")); - - const x = i % gridWidth; - const y = Math.floor(i / gridWidth); - - const imageX = imageWidth * x; - const imageY = imageHeight * y; - - ctx.drawImage(image, imageX, imageY); } return canvas.toBuffer(); diff --git a/src/helpers/TimeLengthInput.ts b/src/helpers/TimeLengthInput.ts index d1d8734..aef58ba 100644 --- a/src/helpers/TimeLengthInput.ts +++ b/src/helpers/TimeLengthInput.ts @@ -118,4 +118,19 @@ export default class TimeLengthInput { return desNumber; } + + public static ConvertFromMilliseconds(ms: number): TimeLengthInput { + const seconds = Math.floor(ms / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + const remainingSeconds = seconds % 60; + const remainingMinutes = minutes % 60; + const remainingHours = hours % 24; + + const timeString = `${days}d ${remainingHours}h ${remainingMinutes}m ${remainingSeconds}s`; + + return new TimeLengthInput(timeString); + } } \ No newline at end of file diff --git a/src/registry.ts b/src/registry.ts index e4e5d64..3ae885d 100644 --- a/src/registry.ts +++ b/src/registry.ts @@ -7,6 +7,7 @@ import AllBalance from "./commands/allbalance"; import Balance from "./commands/balance"; import Daily from "./commands/daily"; import Drop from "./commands/drop"; +import Effects from "./commands/effects"; import Gdrivesync from "./commands/gdrivesync"; import Give from "./commands/give"; import Id from "./commands/id"; @@ -25,6 +26,7 @@ import Droprarity from "./commands/stage/droprarity"; // Button Event Imports import Claim from "./buttonEvents/Claim"; +import EffectsButtonEvent from "./buttonEvents/Effects"; import InventoryButtonEvent from "./buttonEvents/Inventory"; import MultidropButtonEvent from "./buttonEvents/Multidrop"; import Reroll from "./buttonEvents/Reroll"; @@ -44,6 +46,7 @@ export default class Registry { CoreClient.RegisterCommand("balance", new Balance()); CoreClient.RegisterCommand("daily", new Daily()); CoreClient.RegisterCommand("drop", new Drop()); + CoreClient.RegisterCommand("effects", new Effects()); CoreClient.RegisterCommand("gdrivesync", new Gdrivesync()); CoreClient.RegisterCommand("give", new Give()); CoreClient.RegisterCommand("id", new Id()); @@ -63,6 +66,7 @@ export default class Registry { public static RegisterButtonEvents() { CoreClient.RegisterButtonEvent("claim", new Claim()); + CoreClient.RegisterButtonEvent("effects", new EffectsButtonEvent()); CoreClient.RegisterButtonEvent("inventory", new InventoryButtonEvent()); CoreClient.RegisterButtonEvent("multidrop", new MultidropButtonEvent()); CoreClient.RegisterButtonEvent("reroll", new Reroll()); diff --git a/src/timers/PurgeClaims.ts b/src/timers/PurgeClaims.ts index a0ed9d0..4ca8902 100644 --- a/src/timers/PurgeClaims.ts +++ b/src/timers/PurgeClaims.ts @@ -4,7 +4,7 @@ import Claim from "../database/entities/app/Claim"; export default async function PurgeClaims() { const claims = await Claim.FetchAll(Claim); - const whenLastClaimable = new Date(Date.now() - (1000 * 60 * 5)); // 5 minutes ago + const whenLastClaimable = new Date(Date.now() - (1000 * 60 * 2)); // 2 minutes ago const expiredClaims = claims.filter(x => x.WhenCreated < whenLastClaimable); diff --git a/tests/.gitkeep b/tests/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/__functions__/discord.js/GenerateButtonInteractionMock.ts b/tests/__functions__/discord.js/GenerateButtonInteractionMock.ts new file mode 100644 index 0000000..2199477 --- /dev/null +++ b/tests/__functions__/discord.js/GenerateButtonInteractionMock.ts @@ -0,0 +1,23 @@ +import { ButtonInteraction } from "../../__types__/discord.js"; + +export default function GenerateButtonInteractionMock(): ButtonInteraction { + return { + guild: {}, + guildId: "guildId", + channel: { + isSendable: jest.fn().mockReturnValue(true), + send: jest.fn(), + }, + deferUpdate: jest.fn(), + editReply: jest.fn(), + message: { + createdAt: new Date(1000 * 60 * 27), + }, + user: { + id: "userId", + }, + customId: "customId", + update: jest.fn(), + reply: jest.fn(), + }; +} \ No newline at end of file diff --git a/tests/__functions__/discord.js/GenerateCommandInteractionMock.ts b/tests/__functions__/discord.js/GenerateCommandInteractionMock.ts new file mode 100644 index 0000000..128c25b --- /dev/null +++ b/tests/__functions__/discord.js/GenerateCommandInteractionMock.ts @@ -0,0 +1,17 @@ +import { CommandInteraction } from "../../__types__/discord.js"; + +export default function GenerateCommandInteractionMock(options?: { + subcommand?: string, +}): CommandInteraction { + return { + deferReply: jest.fn(), + editReply: jest.fn(), + isChatInputCommand: jest.fn().mockReturnValue(true), + options: { + getSubcommand: jest.fn().mockReturnValue(options?.subcommand), + }, + user: { + id: "userId", + }, + }; +} \ No newline at end of file diff --git a/tests/__types__/discord.js.ts b/tests/__types__/discord.js.ts new file mode 100644 index 0000000..3304af4 --- /dev/null +++ b/tests/__types__/discord.js.ts @@ -0,0 +1,31 @@ +export type ButtonInteraction = { + guild: object | null, + guildId: string | null, + channel: { + isSendable: jest.Func, + send: jest.Func, + } | null, + deferUpdate: jest.Func, + editReply: jest.Func, + message: { + createdAt: Date, + } | null, + user: { + id: string, + } | null, + customId: string, + update: jest.Func, + reply: jest.Func, +} + +export type CommandInteraction = { + deferReply: jest.Func, + editReply: jest.Func, + isChatInputCommand: jest.Func, + options: { + getSubcommand: jest.Func, + }, + user: { + id: string, + }, +} \ No newline at end of file diff --git a/tests/buttonEvents/Claim.test.ts b/tests/buttonEvents/Claim.test.ts new file mode 100644 index 0000000..1e7027c --- /dev/null +++ b/tests/buttonEvents/Claim.test.ts @@ -0,0 +1,90 @@ +import { ButtonInteraction, TextChannel } from "discord.js"; +import Claim from "../../src/buttonEvents/Claim"; +import { ButtonInteraction as ButtonInteractionType } from "../__types__/discord.js"; +import GenerateButtonInteractionMock from "../__functions__/discord.js/GenerateButtonInteractionMock"; + +jest.mock("../../src/client/appLogger"); + +let interaction: ButtonInteractionType; + +beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(1000 * 60 * 30); + + interaction = GenerateButtonInteractionMock(); + interaction.customId = "claim cardNumber claimId droppedBy userId"; +}); + +afterAll(() => { + jest.useRealTimers(); +}); + +test("GIVEN interaction.guild is null, EXPECT nothing to happen", async () => { + // Arrange + interaction.guild = null; + + // Act + const claim = new Claim(); + await claim.execute(interaction as unknown as ButtonInteraction); + + // Assert + expect(interaction.deferUpdate).not.toHaveBeenCalled(); + expect(interaction.editReply).not.toHaveBeenCalled(); + expect((interaction.channel as TextChannel).send).not.toHaveBeenCalled(); +}); + +test("GIVEN interaction.guildId is null, EXPECT nothing to happen", async () => { + // Arrange + interaction.guildId = null; + + // Act + const claim = new Claim(); + await claim.execute(interaction as unknown as ButtonInteraction); + + // Assert + expect(interaction.deferUpdate).not.toHaveBeenCalled(); + expect(interaction.editReply).not.toHaveBeenCalled(); + expect((interaction.channel as TextChannel).send).not.toHaveBeenCalled(); +}); + +test("GIVEN interaction.channel is null, EXPECT nothing to happen", async () => { + // Arrange + interaction.channel = null; + + // Act + const claim = new Claim(); + await claim.execute(interaction as unknown as ButtonInteraction); + + // Assert + expect(interaction.deferUpdate).not.toHaveBeenCalled(); + expect(interaction.editReply).not.toHaveBeenCalled(); +}); + +test("GIVEN channel is not sendable, EXPECT nothing to happen", async () => { + // Arrange + interaction.channel!.isSendable = jest.fn().mockReturnValue(false); + + // Act + const claim = new Claim(); + await claim.execute(interaction as unknown as ButtonInteraction); + + // Assert + expect(interaction.deferUpdate).not.toHaveBeenCalled(); + expect(interaction.editReply).not.toHaveBeenCalled(); + expect((interaction.channel as TextChannel).send).not.toHaveBeenCalled(); +}); + +test("GIVEN interaction.message was created more than 5 minutes ago, EXPECT error", async () => { + // Arrange + interaction.message!.createdAt = new Date(0); + + // Act + const claim = new Claim(); + await claim.execute(interaction as unknown as ButtonInteraction); + + // Assert + expect(interaction.channel!.send).toHaveBeenCalledTimes(1); + expect(interaction.channel!.send).toHaveBeenCalledWith("[object Object], Cards can only be claimed within 2 minutes of it being dropped!"); + + expect(interaction.editReply).not.toHaveBeenCalled(); +}); \ No newline at end of file diff --git a/tests/buttonEvents/Effects.test.ts b/tests/buttonEvents/Effects.test.ts new file mode 100644 index 0000000..8fb1023 --- /dev/null +++ b/tests/buttonEvents/Effects.test.ts @@ -0,0 +1,68 @@ +import { ButtonInteraction } from "discord.js"; +import Effects from "../../src/buttonEvents/Effects"; +import GenerateButtonInteractionMock from "../__functions__/discord.js/GenerateButtonInteractionMock"; +import { ButtonInteraction as ButtonInteractionType } from "../__types__/discord.js"; +import List from "../../src/buttonEvents/Effects/List"; +import Use from "../../src/buttonEvents/Effects/Use"; +import AppLogger from "../../src/client/appLogger"; + +jest.mock("../../src/client/appLogger"); +jest.mock("../../src/buttonEvents/Effects/List"); +jest.mock("../../src/buttonEvents/Effects/Use"); + +let interaction: ButtonInteractionType; + +beforeEach(() => { + jest.resetAllMocks(); + + interaction = GenerateButtonInteractionMock(); + interaction.customId = "effects"; +}); + +test("GIVEN action is list, EXPECT list function to be called", async () => { + // Arrange + interaction.customId = "effects list"; + + // Act + const effects = new Effects(); + await effects.execute(interaction as unknown as ButtonInteraction); + + // Assert + expect(List).toHaveBeenCalledTimes(1); + expect(List).toHaveBeenCalledWith(interaction); + + expect(Use.Execute).not.toHaveBeenCalled(); +}); + +test("GIVEN action is use, EXPECT use function to be called", async () => { + // Arrange + interaction.customId = "effects use"; + + // Act + const effects = new Effects(); + await effects.execute(interaction as unknown as ButtonInteraction); + + // Assert + expect(Use.Execute).toHaveBeenCalledTimes(1); + expect(Use.Execute).toHaveBeenCalledWith(interaction); + + expect(List).not.toHaveBeenCalled(); +}); + +test.todo("GIVEN action is buy, EXPECT buy function to be called"); + +test("GIVEN action is invalid, EXPECT nothing to be called", async () => { + // Arrange + interaction.customId = "effects invalid"; + + // Act + const effects = new Effects(); + await effects.execute(interaction as unknown as ButtonInteraction); + + // Assert + expect(List).not.toHaveBeenCalled(); + expect(Use.Execute).not.toHaveBeenCalled(); + + expect(AppLogger.LogError).toHaveBeenCalledTimes(1); + expect(AppLogger.LogError).toHaveBeenCalledWith("Buttons/Effects", "Unknown action, invalid"); +}); diff --git a/tests/buttonEvents/Effects/Buy.test.ts b/tests/buttonEvents/Effects/Buy.test.ts new file mode 100644 index 0000000..3898504 --- /dev/null +++ b/tests/buttonEvents/Effects/Buy.test.ts @@ -0,0 +1,350 @@ +import {ButtonInteraction} from "discord.js"; +import Buy from "../../../src/buttonEvents/Effects/Buy"; +import GenerateButtonInteractionMock from "../../__functions__/discord.js/GenerateButtonInteractionMock"; +import { ButtonInteraction as ButtonInteractionType } from "../../__types__/discord.js"; +import AppLogger from "../../../src/client/appLogger"; +import EffectHelper from "../../../src/helpers/EffectHelper"; +import EmbedColours from "../../../src/constants/EmbedColours"; +import User from "../../../src/database/entities/app/User"; + +jest.mock("../../../src/client/appLogger"); +jest.mock("../../../src/helpers/EffectHelper"); +jest.mock("../../../src/database/entities/app/User"); + +let interaction: ButtonInteractionType; + +beforeEach(() => { + jest.resetAllMocks(); + + interaction = GenerateButtonInteractionMock(); + interaction.customId = "effects buy"; + +}); + +describe("Execute", () => { + test("GIVEN subaction is invalid, EXPECT error logged", async () => { + // Arrange + interaction.customId += " invalid"; + + // Act + await Buy.Execute(interaction as unknown as ButtonInteraction); + + // Assert + expect(AppLogger.LogError).toHaveBeenCalledTimes(1); + expect(AppLogger.LogError).toHaveBeenCalledWith("Buy", "Unknown subaction, effects invalid"); + }); +}); + +describe("Confirm", () => { + let user: User; + + beforeEach(() => { + interaction.customId += " confirm"; + + user = { + Currency: 1000, + Save: jest.fn(), + RemoveCurrency: jest.fn(), + } as unknown as User; + + (User.FetchOneById as jest.Mock).mockResolvedValue(user); + }); + + test("EXPECT success embed generated", async () => { + // Assert + interaction.customId += " unclaimed 1"; + + const embed = { + id: "embed", + setColor: jest.fn(), + setFooter: jest.fn(), + }; + const row = { + id: "row", + }; + + (EffectHelper.GenerateEffectBuyEmbed as jest.Mock).mockResolvedValue({ + embed, + row, + }); + + // Act + await Buy.Execute(interaction as unknown as ButtonInteraction); + + // Assert + expect(interaction.update).toHaveBeenCalledTimes(1); + expect(interaction.update).toHaveBeenCalledWith({ + embeds: [ embed ], + components: [ row ], + }); + + expect(EffectHelper.GenerateEffectBuyEmbed).toHaveBeenCalledTimes(1); + expect(EffectHelper.GenerateEffectBuyEmbed).toHaveBeenCalledWith("userId", "unclaimed", 1, true); + + expect(embed.setColor).toHaveBeenCalledTimes(1); + expect(embed.setColor).toHaveBeenCalledWith(EmbedColours.Success); + + expect(embed.setFooter).toHaveBeenCalledTimes(1); + expect(embed.setFooter).toHaveBeenCalledWith({ text: "Purchased" }); + + expect(interaction.reply).not.toHaveBeenCalled(); + expect(AppLogger.LogError).not.toHaveBeenCalled(); + + expect(User.FetchOneById).toHaveBeenCalledTimes(1); + expect(User.FetchOneById).toHaveBeenCalledWith(User, "userId"); + + expect(user.RemoveCurrency).toHaveBeenCalledTimes(1); + expect(user.RemoveCurrency).toHaveBeenCalledWith(100); + + expect(user.Save).toHaveBeenCalledTimes(1); + expect(user.Save).toHaveBeenCalledWith(User, user); + + expect(EffectHelper.AddEffectToUserInventory).toHaveBeenCalledTimes(1); + expect(EffectHelper.AddEffectToUserInventory).toHaveBeenCalledWith("userId", "unclaimed", 1); + }); + + test("GIVEN id is not supplied, EXPECT error", async () => { + // Act + await Buy.Execute(interaction as unknown as ButtonInteraction); + + // Assert + expect(AppLogger.LogError).toHaveBeenCalledTimes(1); + expect(AppLogger.LogError).toHaveBeenCalledWith("Buy Confirm", "Not enough parameters"); + + expect(EffectHelper.GenerateEffectBuyEmbed).not.toHaveBeenCalled(); + expect(interaction.reply).not.toHaveBeenCalled(); + expect(interaction.update).not.toHaveBeenCalled(); + }); + + test("GIVEN quantity is not supplied, EXPECT error", async () => { + // Assert + interaction.customId += " unclaimed"; + + // Act + await Buy.Execute(interaction as unknown as ButtonInteraction); + + // Assert + expect(AppLogger.LogError).toHaveBeenCalledTimes(1); + expect(AppLogger.LogError).toHaveBeenCalledWith("Buy Confirm", "Not enough parameters"); + + expect(EffectHelper.GenerateEffectBuyEmbed).not.toHaveBeenCalled(); + expect(interaction.reply).not.toHaveBeenCalled(); + expect(interaction.update).not.toHaveBeenCalled(); + }); + + test("GIVEN quantity is not a number, EXPECT error", async () => { + // Assert + interaction.customId += " unclaimed invalid"; + + // Act + await Buy.Execute(interaction as unknown as ButtonInteraction); + + // Assert + expect(AppLogger.LogError).toHaveBeenCalledTimes(1); + expect(AppLogger.LogError).toHaveBeenCalledWith("Buy Confirm", "Invalid number"); + + expect(EffectHelper.GenerateEffectBuyEmbed).not.toHaveBeenCalled(); + expect(interaction.reply).not.toHaveBeenCalled(); + expect(interaction.update).not.toHaveBeenCalled(); + }); + + test("GIVEN quantity is 0, EXPECT error", async () => { + // Assert + interaction.customId += " unclaimed 0"; + + // Act + await Buy.Execute(interaction as unknown as ButtonInteraction); + + // Assert + expect(AppLogger.LogError).toHaveBeenCalledTimes(1); + expect(AppLogger.LogError).toHaveBeenCalledWith("Buy Confirm", "Invalid number"); + + expect(EffectHelper.GenerateEffectBuyEmbed).not.toHaveBeenCalled(); + expect(interaction.reply).not.toHaveBeenCalled(); + expect(interaction.update).not.toHaveBeenCalled(); + }); + + test("GIVEN user is not found, EXPECT error", async () => { + // Assert + interaction.customId += " unclaimed 1"; + + (User.FetchOneById as jest.Mock).mockResolvedValue(undefined); + + // Act + await Buy.Execute(interaction as unknown as ButtonInteraction); + + // Assert + expect(AppLogger.LogError).toHaveBeenCalledTimes(1); + expect(AppLogger.LogError).toHaveBeenCalledWith("Buy Confirm", "Unable to find user"); + + expect(EffectHelper.AddEffectToUserInventory).not.toHaveBeenCalled(); + + expect(interaction.update).not.toHaveBeenCalled(); + expect(interaction.reply).not.toHaveBeenCalled(); + }); + + test("GIVEN user does not have enough currency, EXPECT error", async () => { + // Assert + interaction.customId += " unclaimed 1"; + + user.Currency = 0; + + // Act + await Buy.Execute(interaction as unknown as ButtonInteraction); + + // Assert + expect(interaction.reply).toHaveBeenCalledTimes(1); + expect(interaction.reply).toHaveBeenCalledWith("You don't have enough currency to buy this! You have `0 Currency` and need `100 Currency`!"); + + expect(EffectHelper.AddEffectToUserInventory).not.toHaveBeenCalled(); + + expect(interaction.update).not.toHaveBeenCalled(); + + expect(AppLogger.LogError).not.toHaveBeenCalled(); + }); + + test("GIVEN GenerateEffectBuyEmbed returns with a string, EXPECT error replied", async () => { + // Assert + interaction.customId += " unclaimed 1"; + + (EffectHelper.GenerateEffectBuyEmbed as jest.Mock).mockResolvedValue("Test error"); + + // Act + await Buy.Execute(interaction as unknown as ButtonInteraction); + + // Assert + expect(interaction.reply).toHaveBeenCalledTimes(1); + expect(interaction.reply).toHaveBeenCalledWith("Test error"); + + expect(EffectHelper.GenerateEffectBuyEmbed).toHaveBeenCalledTimes(1); + + expect(interaction.update).not.toHaveBeenCalled(); + expect(AppLogger.LogError).not.toHaveBeenCalled(); + }); +}); + +describe("Cancel", () => { + beforeEach(() => { + interaction.customId += " cancel"; + }); + + test("EXPECT embed generated", async () => { + // Assert + interaction.customId += " unclaimed 1"; + + const embed = { + id: "embed", + setColor: jest.fn(), + setFooter: jest.fn(), + }; + const row = { + id: "row", + }; + + (EffectHelper.GenerateEffectBuyEmbed as jest.Mock).mockResolvedValue({ + embed, + row, + }); + + // Act + await Buy.Execute(interaction as unknown as ButtonInteraction); + + // Assert + expect(interaction.update).toHaveBeenCalledTimes(1); + expect(interaction.update).toHaveBeenCalledWith({ + embeds: [ embed ], + components: [ row ], + }); + + expect(EffectHelper.GenerateEffectBuyEmbed).toHaveBeenCalledTimes(1); + expect(EffectHelper.GenerateEffectBuyEmbed).toHaveBeenCalledWith("userId", "unclaimed", 1, true); + + expect(embed.setColor).toHaveBeenCalledTimes(1); + expect(embed.setColor).toHaveBeenCalledWith(EmbedColours.Error); + + expect(embed.setFooter).toHaveBeenCalledTimes(1); + expect(embed.setFooter).toHaveBeenCalledWith({ text: "Cancelled" }); + + expect(interaction.reply).not.toHaveBeenCalled(); + expect(AppLogger.LogError).not.toHaveBeenCalled(); + }); + + test("GIVEN id is not supplied, EXPECT error", async () => { + // Act + await Buy.Execute(interaction as unknown as ButtonInteraction); + + // Assert + expect(AppLogger.LogError).toHaveBeenCalledTimes(1); + expect(AppLogger.LogError).toHaveBeenCalledWith("Buy Cancel", "Not enough parameters"); + + expect(interaction.reply).not.toHaveBeenCalled(); + expect(interaction.update).not.toHaveBeenCalled(); + }); + + test("GIVEN quantity is not supplied, EXPECT error", async () => { + // Assert + interaction.customId += " unclaimed"; + + // Act + await Buy.Execute(interaction as unknown as ButtonInteraction); + + // Assert + expect(AppLogger.LogError).toHaveBeenCalledTimes(1); + expect(AppLogger.LogError).toHaveBeenCalledWith("Buy Cancel", "Not enough parameters"); + + expect(EffectHelper.GenerateEffectBuyEmbed).not.toHaveBeenCalled(); + expect(interaction.reply).not.toHaveBeenCalled(); + expect(interaction.update).not.toHaveBeenCalled(); + }); + + test("GIVEN quantity is not a number, EXPECT error", async () => { + // Assert + interaction.customId += " unclaimed invalid"; + + // Act + await Buy.Execute(interaction as unknown as ButtonInteraction); + + // Assert + expect(AppLogger.LogError).toHaveBeenCalledTimes(1); + expect(AppLogger.LogError).toHaveBeenCalledWith("Buy Cancel", "Invalid number"); + + expect(EffectHelper.GenerateEffectBuyEmbed).not.toHaveBeenCalled(); + expect(interaction.reply).not.toHaveBeenCalled(); + expect(interaction.update).not.toHaveBeenCalled(); + }); + + test("GIVEN quantity is 0, EXPECT error", async () => { + // Assert + interaction.customId += " unclaimed 0"; + + // Act + await Buy.Execute(interaction as unknown as ButtonInteraction); + + // Assert + expect(AppLogger.LogError).toHaveBeenCalledTimes(1); + expect(AppLogger.LogError).toHaveBeenCalledWith("Buy Cancel", "Invalid number"); + + expect(EffectHelper.GenerateEffectBuyEmbed).not.toHaveBeenCalled(); + expect(interaction.reply).not.toHaveBeenCalled(); + expect(interaction.update).not.toHaveBeenCalled(); + }); + + test("GIVEN GenerateEffectBuyEmbed returns with a string, EXPECT error replied", async () => { + // Assert + interaction.customId += " unclaimed 1"; + + (EffectHelper.GenerateEffectBuyEmbed as jest.Mock).mockResolvedValue("Test error"); + + // Act + await Buy.Execute(interaction as unknown as ButtonInteraction); + + // Assert + expect(interaction.reply).toHaveBeenCalledTimes(1); + expect(interaction.reply).toHaveBeenCalledWith("Test error"); + + expect(EffectHelper.GenerateEffectBuyEmbed).toHaveBeenCalledTimes(1); + + expect(interaction.update).not.toHaveBeenCalled(); + expect(AppLogger.LogError).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/buttonEvents/Effects/List.test.ts b/tests/buttonEvents/Effects/List.test.ts new file mode 100644 index 0000000..5b42c61 --- /dev/null +++ b/tests/buttonEvents/Effects/List.test.ts @@ -0,0 +1,50 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, EmbedBuilder } from "discord.js"; +import List from "../../../src/buttonEvents/Effects/List"; +import EffectHelper from "../../../src/helpers/EffectHelper"; +import { mock } from "jest-mock-extended"; + +jest.mock("../../../src/helpers/EffectHelper"); + +let interaction: ReturnType>; + +beforeEach(() => { + jest.resetAllMocks(); + + (EffectHelper.GenerateEffectListEmbed as jest.Mock).mockResolvedValue({ + embed: mock(), + row: mock>(), + }); + + interaction = mock(); + interaction.user.id = "userId"; + interaction.customId = "effects list 1"; +}); + +test("GIVEN pageOption is NOT a number, EXPECT error", async () => { + // Arrange + interaction.customId = "effects list invalid"; + + // Act + await List(interaction); + + // Assert + expect(interaction.reply).toHaveBeenCalledTimes(1); + expect(interaction.reply).toHaveBeenCalledWith("Page option is not a valid number") + + expect(EffectHelper.GenerateEffectListEmbed).not.toHaveBeenCalled(); + expect(interaction.update).not.toHaveBeenCalled(); +}); + +test("GIVEN pageOption is a number, EXPECT interaction updated", async () => { + // Arrange + interaction.customId = "effects list 1"; + + // Act + await List(interaction); + + // Assert + expect(EffectHelper.GenerateEffectListEmbed).toHaveBeenCalledTimes(1); + expect(EffectHelper.GenerateEffectListEmbed).toHaveBeenCalledWith("userId", 1); + + expect(interaction.update).toHaveBeenCalledTimes(1); +}); \ No newline at end of file diff --git a/tests/buttonEvents/Effects/Use.test.ts b/tests/buttonEvents/Effects/Use.test.ts new file mode 100644 index 0000000..86391cc --- /dev/null +++ b/tests/buttonEvents/Effects/Use.test.ts @@ -0,0 +1,148 @@ +import { ButtonInteraction, InteractionResponse, InteractionUpdateOptions, MessagePayload } from "discord.js"; +import Use from "../../../src/buttonEvents/Effects/Use"; +import { mock } from "jest-mock-extended"; +import AppLogger from "../../../src/client/appLogger"; +import EffectHelper from "../../../src/helpers/EffectHelper"; + +jest.mock("../../../src/client/appLogger"); +jest.mock("../../../src/helpers/EffectHelper"); + +beforeEach(() => { + jest.resetAllMocks(); + + jest.useFakeTimers(); + jest.setSystemTime(0); +}); + +afterAll(() => { + jest.useRealTimers(); +}); + +describe("Execute", () => { + test("GIVEN subaction is unknown, EXPECT nothing to be called", async () => { + // Arrange + const interaction = mock(); + interaction.customId = "effects use invalud"; + + // Act + await Use.Execute(interaction); + + // Assert + expect(interaction.reply).not.toHaveBeenCalled(); + expect(interaction.update).not.toHaveBeenCalled(); + }); +}); + +describe("UseConfirm", () => { + let interaction = mock(); + + beforeEach(() => { + interaction = mock(); + interaction.customId = "effects use confirm"; + }); + + test("GIVEN effectDetail is not found, EXPECT error", async () => { + // Arrange + interaction.customId += " invalid"; + + // Act + await Use.Execute(interaction); + + // Assert + expect(AppLogger.LogError).toHaveBeenCalledTimes(1); + expect(AppLogger.LogError).toHaveBeenCalledWith("Button/Effects/Use", "Effect not found, invalid"); + + expect(interaction.reply).toHaveBeenCalledTimes(1); + expect(interaction.reply).toHaveBeenCalledWith("Effect not found in system!"); + }); + + test("GIVEN EffectHelper.UseEffect failed, EXPECT error", async () => { + // Arrange + interaction.customId += " unclaimed"; + interaction.user.id = "userId"; + + (EffectHelper.UseEffect as jest.Mock).mockResolvedValue(false); + + const whenExpires = new Date(Date.now() + 10 * 60 * 1000); + + // Act + await Use.Execute(interaction); + + // Assert + expect(EffectHelper.UseEffect).toHaveBeenCalledTimes(1); + expect(EffectHelper.UseEffect).toHaveBeenCalledWith("userId", "unclaimed", whenExpires); + + expect(interaction.reply).toHaveBeenCalledTimes(1); + expect(interaction.reply).toHaveBeenCalledWith("Unable to use effect! Please make sure you have it in your inventory and is not on cooldown"); + }); + + test("GIVEN EffectHelper.UseEffect succeeded, EXPECT interaction updated", async () => { + let updatedWith; + + // Arrange + interaction.customId += " unclaimed"; + interaction.user.id = "userId"; + interaction.update.mockImplementation(async (opts: string | MessagePayload | InteractionUpdateOptions) => { + updatedWith = opts; + + return mock>(); + }); + + (EffectHelper.UseEffect as jest.Mock).mockResolvedValue(true); + + const whenExpires = new Date(Date.now() + 10 * 60 * 1000); + + // Act + await Use.Execute(interaction); + + // Assert + expect(EffectHelper.UseEffect).toHaveBeenCalledTimes(1); + expect(EffectHelper.UseEffect).toHaveBeenCalledWith("userId", "unclaimed", whenExpires); + + expect(interaction.update).toHaveBeenCalledTimes(1); + expect(updatedWith).toMatchSnapshot(); + }); +}); + +describe("UseCancel", () => { + let interaction = mock(); + + beforeEach(() => { + interaction = mock(); + interaction.customId = "effects use cancel"; + }); + + test("GIVEN effectDetail is not found, EXPECT error", async () => { + // Arrange + interaction.customId += " invalid"; + + // Act + await Use.Execute(interaction); + + // Assert + expect(AppLogger.LogError).toHaveBeenCalledTimes(1); + expect(AppLogger.LogError).toHaveBeenCalledWith("Button/Effects/Cancel", "Effect not found, invalid"); + + expect(interaction.reply).toHaveBeenCalledTimes(1); + expect(interaction.reply).toHaveBeenCalledWith("Effect not found in system!"); + }); + + test("GIVEN effectDetail is found, EXPECT interaction updated", async () => { + let updatedWith; + + // Arrange + interaction.customId += " unclaimed"; + interaction.user.id = "userId"; + interaction.update.mockImplementation(async (opts: string | MessagePayload | InteractionUpdateOptions) => { + updatedWith = opts; + + return mock>(); + }); + // Act + await Use.Execute(interaction); + + // Assert + expect(interaction.update).toHaveBeenCalledTimes(1); + expect(updatedWith).toMatchSnapshot(); + }); +}); \ No newline at end of file diff --git a/tests/buttonEvents/Effects/__snapshots__/Use.test.ts.snap b/tests/buttonEvents/Effects/__snapshots__/Use.test.ts.snap new file mode 100644 index 0000000..6cec0f4 --- /dev/null +++ b/tests/buttonEvents/Effects/__snapshots__/Use.test.ts.snap @@ -0,0 +1,95 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UseCancel GIVEN effectDetail is found, EXPECT interaction updated 1`] = ` +{ + "components": [ + { + "components": [ + { + "custom_id": "effects use confirm unclaimed", + "disabled": true, + "emoji": undefined, + "label": "Confirm", + "style": 1, + "type": 2, + }, + { + "custom_id": "effects use cancel unclaimed", + "disabled": true, + "emoji": undefined, + "label": "Cancel", + "style": 4, + "type": 2, + }, + ], + "type": 1, + }, + ], + "embeds": [ + { + "color": 13882323, + "description": "The effect from your inventory has not been used", + "fields": [ + { + "inline": true, + "name": "Effect", + "value": "Unclaimed Chance Up", + }, + { + "inline": true, + "name": "Expires", + "value": "10m", + }, + ], + "title": "Effect Use Cancelled", + }, + ], +} +`; + +exports[`UseConfirm GIVEN EffectHelper.UseEffect succeeded, EXPECT interaction updated 1`] = ` +{ + "components": [ + { + "components": [ + { + "custom_id": "effects use confirm unclaimed", + "disabled": true, + "emoji": undefined, + "label": "Confirm", + "style": 1, + "type": 2, + }, + { + "custom_id": "effects use cancel unclaimed", + "disabled": true, + "emoji": undefined, + "label": "Cancel", + "style": 4, + "type": 2, + }, + ], + "type": 1, + }, + ], + "embeds": [ + { + "color": 2263842, + "description": "You now have an active effect!", + "fields": [ + { + "inline": true, + "name": "Effect", + "value": "Unclaimed Chance Up", + }, + { + "inline": true, + "name": "Expires", + "value": "", + }, + ], + "title": "Effect Used", + }, + ], +} +`; diff --git a/tests/buttonEvents/Reroll.test.ts b/tests/buttonEvents/Reroll.test.ts new file mode 100644 index 0000000..2021aac --- /dev/null +++ b/tests/buttonEvents/Reroll.test.ts @@ -0,0 +1,7 @@ +describe("GIVEN valid conditions", () => { + test.todo("EXPECT user.RemoveCurrency to be called"); + + test.todo("GIVEN user is saved"); +}); + +test.todo("GIVEN user.RemoveCurrency fails, EXPECT error replied"); \ No newline at end of file diff --git a/tests/commands/__snapshots__/effects.test.ts.snap b/tests/commands/__snapshots__/effects.test.ts.snap new file mode 100644 index 0000000..474b505 --- /dev/null +++ b/tests/commands/__snapshots__/effects.test.ts.snap @@ -0,0 +1,106 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EXPECT CommandBuilder to be defined 1`] = ` +{ + "contexts": undefined, + "default_member_permissions": undefined, + "default_permission": undefined, + "description": "Effects", + "description_localizations": undefined, + "dm_permission": undefined, + "integration_types": undefined, + "name": "effects", + "name_localizations": undefined, + "nsfw": undefined, + "options": [ + { + "description": "List all effects I have", + "description_localizations": undefined, + "name": "list", + "name_localizations": undefined, + "options": [ + { + "autocomplete": undefined, + "choices": undefined, + "description": "The page number", + "description_localizations": undefined, + "max_value": undefined, + "min_value": 1, + "name": "page", + "name_localizations": undefined, + "required": false, + "type": 10, + }, + ], + "type": 1, + }, + { + "description": "Use an effect in your inventory", + "description_localizations": undefined, + "name": "use", + "name_localizations": undefined, + "options": [ + { + "autocomplete": undefined, + "choices": [ + { + "name": "Unclaimed Chance Up", + "name_localizations": undefined, + "value": "unclaimed", + }, + ], + "description": "The effect id to use", + "description_localizations": undefined, + "max_length": undefined, + "min_length": undefined, + "name": "id", + "name_localizations": undefined, + "required": true, + "type": 3, + }, + ], + "type": 1, + }, + { + "description": "Buy more effects", + "description_localizations": undefined, + "name": "buy", + "name_localizations": undefined, + "options": [ + { + "autocomplete": undefined, + "choices": [ + { + "name": "Unclaimed Chance Up", + "name_localizations": undefined, + "value": "unclaimed", + }, + ], + "description": "The effect id to buy", + "description_localizations": undefined, + "max_length": undefined, + "min_length": undefined, + "name": "id", + "name_localizations": undefined, + "required": true, + "type": 3, + }, + { + "autocomplete": undefined, + "choices": undefined, + "description": "The amount to buy", + "description_localizations": undefined, + "max_value": undefined, + "min_value": 1, + "name": "quantity", + "name_localizations": undefined, + "required": false, + "type": 10, + }, + ], + "type": 1, + }, + ], + "type": 1, +} +`; diff --git a/tests/commands/drop.test.ts b/tests/commands/drop.test.ts new file mode 100644 index 0000000..81b5f4a --- /dev/null +++ b/tests/commands/drop.test.ts @@ -0,0 +1,142 @@ +import { CommandInteraction } from "discord.js"; +import Drop from "../../src/commands/drop"; +import GenerateCommandInteractionMock from "../__functions__/discord.js/GenerateCommandInteractionMock"; +import { CommandInteraction as CommandInteractionMock } from "../__types__/discord.js"; +import { CoreClient } from "../../src/client/client"; +import Config from "../../src/database/entities/app/Config"; +import User from "../../src/database/entities/app/User"; +import GetCardsHelper from "../../src/helpers/DropHelpers/GetCardsHelper"; +import Inventory from "../../src/database/entities/app/Inventory"; +import DropEmbedHelper from "../../src/helpers/DropHelpers/DropEmbedHelper"; +import CardConstants from "../../src/constants/CardConstants"; +import * as uuid from "uuid"; + +jest.mock("../../src/database/entities/app/Config"); +jest.mock("../../src/database/entities/app/User"); +jest.mock("../../src/helpers/DropHelpers/GetCardsHelper"); +jest.mock("../../src/database/entities/app/Inventory"); +jest.mock("../../src/helpers/DropHelpers/DropEmbedHelper"); + +jest.mock("uuid"); + +beforeEach(() => { + (Config.GetValue as jest.Mock).mockResolvedValue("false"); +}); + +describe("execute", () => { + describe("GIVEN user is in the database", () => { + let interaction: CommandInteractionMock; + let user: User; + const randomCard = { + card: { + id: "cardId", + path: "https://google.com/", + } + }; + + beforeAll(async () => { + // Arrange + CoreClient.AllowDrops = true; + + interaction = GenerateCommandInteractionMock(); + + user = { + Currency: 500, + RemoveCurrency: jest.fn().mockReturnValue(true), + Save: jest.fn(), + } as unknown as User; + + (User.FetchOneById as jest.Mock).mockResolvedValue(user); + (GetCardsHelper.FetchCard as jest.Mock).mockResolvedValue(randomCard); + (Inventory.FetchOneByCardNumberAndUserId as jest.Mock).mockResolvedValue({ + Quantity: 1, + }); + (DropEmbedHelper.GenerateDropEmbed as jest.Mock).mockReturnValue({ + type: "Embed", + }); + (DropEmbedHelper.GenerateDropButtons as jest.Mock).mockReturnValue({ + type: "Button", + }); + + (uuid.v4 as jest.Mock).mockReturnValue("uuid"); + + // Act + const drop = new Drop(); + await drop.execute(interaction as unknown as CommandInteraction); + }); + + test("EXPECT user to be fetched", () => { + expect(User.FetchOneById).toHaveBeenCalledTimes(1); + expect(User.FetchOneById).toHaveBeenCalledWith(User, "userId"); + }); + + test("EXPECT user.RemoveCurrency to be called", () => { + expect(user.RemoveCurrency).toHaveBeenCalledTimes(1); + expect(user.RemoveCurrency).toHaveBeenCalledWith(CardConstants.ClaimCost); + }); + + test("EXPECT user to be saved", () => { + expect(user.Save).toHaveBeenCalledTimes(1); + expect(user.Save).toHaveBeenCalledWith(User, user); + }); + + test("EXPECT random card to be fetched", () => { + expect(GetCardsHelper.FetchCard).toHaveBeenCalledTimes(1); + expect(GetCardsHelper.FetchCard).toHaveBeenCalledWith("userId"); + }); + + test("EXPECT interaction to be deferred", () => { + expect(interaction.deferReply).toHaveBeenCalledTimes(1); + }); + + test("EXPECT Inventory.FetchOneByCardNumberAndUserId to be called", () => { + expect(Inventory.FetchOneByCardNumberAndUserId).toHaveBeenCalledTimes(1); + expect(Inventory.FetchOneByCardNumberAndUserId).toHaveBeenCalledWith("userId", "cardId"); + }); + + test("EXPECT DropEmbedHelper.GenerateDropEmbed to be called", () => { + expect(DropEmbedHelper.GenerateDropEmbed).toHaveBeenCalledTimes(1); + expect(DropEmbedHelper.GenerateDropEmbed).toHaveBeenCalledWith(randomCard, 1, "", undefined, 500); + }); + + test("EXPECT DropEmbedHelper.GenerateDropButtons to be called", () => { + expect(DropEmbedHelper.GenerateDropButtons).toHaveBeenCalledTimes(1); + expect(DropEmbedHelper.GenerateDropButtons).toHaveBeenCalledWith(randomCard, "uuid", "userId"); + }); + + test("EXPECT interaction to be edited", () => { + expect(interaction.editReply).toHaveBeenCalledTimes(1); + expect(interaction.editReply).toHaveBeenCalledWith({ + embeds: [ { type: "Embed" } ], + files: [], + components: [ { type: "Button" } ], + }); + }); + + describe("AND randomCard path is not a url", () => { + test.todo("EXPECT image read from file system"); + + test.todo("EXPECT files on the embed to contain the image as an attachment"); + }); + }); + + describe("GIVEN user is not in the database", () => { + test.todo("EXPECT new user to be created"); + }); + + describe("GIVEN user.RemoveCurrency fails", () => { + test.todo("EXPECT error replied"); + }); + + describe("GIVEN randomCard returns null", () => { + test.todo("EXPECT error logged"); + + test.todo("EXPECT error replied"); + }); + + describe("GIVEN the code throws an error", () => { + test.todo("EXPECT error logged"); + + test.todo("EXPECT interaction edited with error"); + }); +}); \ No newline at end of file diff --git a/tests/commands/effects.test.ts b/tests/commands/effects.test.ts new file mode 100644 index 0000000..33f612d --- /dev/null +++ b/tests/commands/effects.test.ts @@ -0,0 +1,105 @@ +import Effects from "../../src/commands/effects"; +import List from "../../src/commands/effects/List"; +import Use from "../../src/commands/effects/Use"; +import Buy from "../../src/commands/effects/Buy"; +import AppLogger from "../../src/client/appLogger"; +import GenerateCommandInteractionMock from "../__functions__/discord.js/GenerateCommandInteractionMock"; +import { CommandInteraction } from "discord.js"; + +jest.mock("../../src/commands/effects/List"); +jest.mock("../../src/commands/effects/Use"); +jest.mock("../../src/commands/effects/Buy"); +jest.mock("../../src/client/appLogger"); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +test("EXPECT CommandBuilder to be defined", async () => { + // Act + const effects = new Effects(); + + // Assert + expect(effects.CommandBuilder).toMatchSnapshot(); +}); + +describe("execute", () => { + test("GIVEN interaction subcommand is list, EXPECT buy function called", async () => { + // Arrange + const interaction = GenerateCommandInteractionMock({ + subcommand: "list", + }); + + // Act + const effects = new Effects(); + await effects.execute(interaction as unknown as CommandInteraction); + + // Assert + expect(List).toHaveBeenCalledTimes(1); + expect(List).toHaveBeenCalledWith(interaction); + + expect(Use).not.toHaveBeenCalled(); + expect(Buy).not.toHaveBeenCalled(); + + expect(AppLogger.LogError).not.toHaveBeenCalled(); + }); + + test("GIVEN interaction subcommand is use, EXPECT buy function called", async () => { + // Arrange + const interaction = GenerateCommandInteractionMock({ + subcommand: "use", + }); + + // Act + const effects = new Effects(); + await effects.execute(interaction as unknown as CommandInteraction); + + // Assert + expect(Use).toHaveBeenCalledTimes(1); + expect(Use).toHaveBeenCalledWith(interaction); + + expect(List).not.toHaveBeenCalled(); + expect(Buy).not.toHaveBeenCalled(); + + expect(AppLogger.LogError).not.toHaveBeenCalled(); + }); + + test("GIVEN interaction subcommand is buy, EXPECT buy function called", async () => { + // Arrange + const interaction = GenerateCommandInteractionMock({ + subcommand: "buy", + }); + + // Act + const effects = new Effects(); + await effects.execute(interaction as unknown as CommandInteraction); + + // Assert + expect(Buy).toHaveBeenCalledTimes(1); + expect(Buy).toHaveBeenCalledWith(interaction); + + expect(List).not.toHaveBeenCalled(); + expect(Use).not.toHaveBeenCalled(); + + expect(AppLogger.LogError).not.toHaveBeenCalled(); + }); + + test("GIVEN interaction subcommand is invalid, EXPECT error logged", async () => { + // Arrange + const interaction = GenerateCommandInteractionMock({ + subcommand: "invalid", + }); + + // Act + const effects = new Effects(); + await effects.execute(interaction as unknown as CommandInteraction); + + // Assert + expect(AppLogger.LogError).toHaveBeenCalledTimes(1); + expect(AppLogger.LogError).toHaveBeenCalledWith("Commands/Effects", "Invalid subcommand: invalid"); + + expect(List).not.toHaveBeenCalled(); + expect(Use).not.toHaveBeenCalled(); + expect(Buy).not.toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/tests/commands/effects/Buy.test.ts b/tests/commands/effects/Buy.test.ts new file mode 100644 index 0000000..87e4219 --- /dev/null +++ b/tests/commands/effects/Buy.test.ts @@ -0,0 +1,9 @@ +jest.mock("../../../src/helpers/EffectHelper"); + +describe("Buy", () => { + test.todo("GIVEN result returns a string, EXPECT interaction replied with string"); + + test.todo("GIVEN result returns an embed, EXPECT interaction replied with embed and row"); + + test.todo("GIVEN quantity option is not supplied, EXPECT quantity to default to 1"); +}); diff --git a/tests/database/entities/app/UserEffect.test.ts b/tests/database/entities/app/UserEffect.test.ts deleted file mode 100644 index 66992ec..0000000 --- a/tests/database/entities/app/UserEffect.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import UserEffect from "../../../../src/database/entities/app/UserEffect"; - -let userEffect: UserEffect; -const now = new Date(); - -beforeEach(() => { - userEffect = new UserEffect("name", "userId", 1); -}); - -describe("AddUnused", () => { - beforeEach(() => { - userEffect.AddUnused(1); - }); - - test("EXPECT unused to be the amount more", () => { - expect(userEffect.Unused).toBe(2); - }); -}); - -describe("UseEffect", () => { - describe("GIVEN Unused is 0", () => { - let result: boolean; - - beforeEach(() => { - userEffect.Unused = 0; - - result = userEffect.UseEffect(now); - }); - - test("EXPECT false returned", () => { - expect(result).toBe(false); - }); - - test("EXPECT details not to be changed", () => { - expect(userEffect.Unused).toBe(0); - expect(userEffect.WhenExpires).toBeUndefined(); - }); - }); - - describe("GIVEN Unused is greater than 0", () => { - let result: boolean; - - beforeEach(() => { - result = userEffect.UseEffect(now); - }); - - test("EXPECT true returned", () => { - expect(result).toBe(true); - }); - - test("EXPECT Unused to be subtracted by 1", () => { - expect(userEffect.Unused).toBe(0); - }); - - test("EXPECT WhenExpires to be set", () => { - expect(userEffect.WhenExpires).toBe(now); - }); - }); -}); - -describe("IsEffectActive", () => { - describe("GIVEN WhenExpires is null", () => { - let result: boolean; - - beforeEach(() => { - result = userEffect.IsEffectActive(); - }); - - test("EXPECT false returned", () => { - expect(result).toBe(false); - }); - }); - - describe("GIVEN WhenExpires is defined", () => { - describe("AND WhenExpires is in the past", () => { - let result: boolean; - - beforeEach(() => { - userEffect.WhenExpires = new Date(now.getTime() - 100); - - result = userEffect.IsEffectActive(); - }); - - test("EXPECT false returned", () => { - expect(result).toBe(false); - }); - }); - - describe("AND WhenExpires is in the future", () => { - let result: boolean; - - beforeEach(() => { - userEffect.WhenExpires = new Date(now.getTime() + 100); - - result = userEffect.IsEffectActive(); - }); - - test("EXPECT true returned", () => { - expect(result).toBe(true); - }); - }); - }); -}); diff --git a/tests/helpers/DropHelpers/DropEmbedHelper.test.ts b/tests/helpers/DropHelpers/DropEmbedHelper.test.ts new file mode 100644 index 0000000..743f6c7 --- /dev/null +++ b/tests/helpers/DropHelpers/DropEmbedHelper.test.ts @@ -0,0 +1,3 @@ +describe("GenerateDropButtons", () => { + test.todo("EXPECT row to be returned"); +}); \ No newline at end of file diff --git a/tests/helpers/DropHelpers/GetCardsHelper.test.ts b/tests/helpers/DropHelpers/GetCardsHelper.test.ts new file mode 100644 index 0000000..421d4ef --- /dev/null +++ b/tests/helpers/DropHelpers/GetCardsHelper.test.ts @@ -0,0 +1,68 @@ +import GetCardsHelper from "../../../src/helpers/DropHelpers/GetCardsHelper"; +import EffectHelper from "../../../src/helpers/EffectHelper"; +import GetUnclaimedCardsHelper from "../../../src/helpers/DropHelpers/GetUnclaimedCardsHelper"; +import CardConstants from "../../../src/constants/CardConstants"; + +jest.mock("../../../src/helpers/EffectHelper"); +jest.mock("../../../src/helpers/DropHelpers/GetUnclaimedCardsHelper"); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe("FetchCard", () => { + test("GIVEN user has the unclaimed effect AND unused chance is within constraint, EXPECT unclaimed card returned", async () => { + // Arrange + (EffectHelper.HasEffect as jest.Mock).mockResolvedValue(true); + GetCardsHelper.GetRandomCard = jest.fn(); + Math.random = jest.fn().mockReturnValue(CardConstants.UnusedChanceUpChance - 0.1); + + // Act + await GetCardsHelper.FetchCard("userId"); + + // Assert + expect(EffectHelper.HasEffect).toHaveBeenCalledTimes(1); + expect(EffectHelper.HasEffect).toHaveBeenCalledWith("userId", "unclaimed"); + + expect(GetUnclaimedCardsHelper.GetRandomCardUnclaimed).toHaveBeenCalledTimes(1); + expect(GetUnclaimedCardsHelper.GetRandomCardUnclaimed).toHaveBeenCalledWith("userId"); + + expect(GetCardsHelper.GetRandomCard).not.toHaveBeenCalled(); + }); + + test("GIVEN user has unclaimed effect AND unused chance is NOT within constraint, EXPECT random card returned", async () => { + // Arrange + (EffectHelper.HasEffect as jest.Mock).mockResolvedValue(true); + GetCardsHelper.GetRandomCard = jest.fn(); + Math.random = jest.fn().mockReturnValue(CardConstants.UnusedChanceUpChance + 0.1); + + // Act + await GetCardsHelper.FetchCard("userId"); + + // Assert + expect(EffectHelper.HasEffect).toHaveBeenCalledTimes(1); + expect(EffectHelper.HasEffect).toHaveBeenCalledWith("userId", "unclaimed"); + + expect(GetCardsHelper.GetRandomCard).toHaveBeenCalledTimes(1); + + expect(GetUnclaimedCardsHelper.GetRandomCardUnclaimed).not.toHaveBeenCalled(); + }); + + test("GIVEN user does NOT have unclaimed effect, EXPECT random card returned", async () => { + // Arrange + (EffectHelper.HasEffect as jest.Mock).mockResolvedValue(false); + GetCardsHelper.GetRandomCard = jest.fn(); + Math.random = jest.fn().mockReturnValue(CardConstants.UnusedChanceUpChance + 0.1); + + // Act + await GetCardsHelper.FetchCard("userId"); + + // Assert + expect(EffectHelper.HasEffect).toHaveBeenCalledTimes(1); + expect(EffectHelper.HasEffect).toHaveBeenCalledWith("userId", "unclaimed"); + + expect(GetCardsHelper.GetRandomCard).toHaveBeenCalledTimes(1); + + expect(GetUnclaimedCardsHelper.GetRandomCardUnclaimed).not.toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/tests/helpers/EffectHelper.test.ts b/tests/helpers/EffectHelper.test.ts index 343f06c..dcab744 100644 --- a/tests/helpers/EffectHelper.test.ts +++ b/tests/helpers/EffectHelper.test.ts @@ -1,281 +1,127 @@ -import UserEffect from "../../src/database/entities/app/UserEffect"; import EffectHelper from "../../src/helpers/EffectHelper"; +import UserEffect from "../../src/database/entities/app/UserEffect"; -describe("AddEffectToUserInventory", () => { - describe("GIVEN effect is in database", () => { - const effectMock = { - AddUnused: jest.fn(), - Save: jest.fn(), - }; +jest.mock("../../src/database/entities/app/UserEffect"); - beforeAll(async () => { - UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(effectMock); +describe("GenerateEffectListEmbed", () => { + test("GIVEN user has an effect, EXPECT detailed embed to be returned", async () => { + // Arrange + (UserEffect.FetchAllByUserIdPaginated as jest.Mock).mockResolvedValue([ + [ + { + Name: "unclaimed", + Unused: 1, + } + ], + 1, + ]); - await EffectHelper.AddEffectToUserInventory("userId", "name", 1); - }); + // Act + const result = await EffectHelper.GenerateEffectListEmbed("userId", 1); - test("EXPECT database to be fetched", () => { - expect(UserEffect.FetchOneByUserIdAndName).toHaveBeenCalledTimes(1); - expect(UserEffect.FetchOneByUserIdAndName).toHaveBeenCalledWith("userId", "name"); - }); - - test("EXPECT effect to be updated", () => { - expect(effectMock.AddUnused).toHaveBeenCalledTimes(1); - expect(effectMock.AddUnused).toHaveBeenCalledWith(1); - }); - - test("EXPECT effect to be saved", () => { - expect(effectMock.Save).toHaveBeenCalledTimes(1); - expect(effectMock.Save).toHaveBeenCalledWith(UserEffect, effectMock); - }); + // Assert + expect(result).toMatchSnapshot(); }); - describe("GIVEN effect is not in database", () => { - beforeAll(async () => { - UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(null); - UserEffect.prototype.Save = jest.fn(); + test("GIVEN user has more than 1 page of effects, EXPECT pagination enabled", async () => { + const effects: { + Name: string, + Unused: number, + }[] = []; - await EffectHelper.AddEffectToUserInventory("userId", "name", 1); + for (let i = 0; i < 15; i++) { + effects.push({ + Name: "unclaimed", + Unused: 1, + }); + } + + // Arrange + (UserEffect.FetchAllByUserIdPaginated as jest.Mock).mockResolvedValue([ + effects, + 15, + ]); + + // Act + const result = await EffectHelper.GenerateEffectListEmbed("userId", 1); + + // Assert + expect(result).toMatchSnapshot(); + }); + + test("GIVEN user is on a page other than 1, EXPECT pagination enabled", async () => { + const effects: { + Name: string, + Unused: number, + }[] = []; + + for (let i = 0; i < 15; i++) { + effects.push({ + Name: "unclaimed", + Unused: 1, + }); + } + + // Arrange + (UserEffect.FetchAllByUserIdPaginated as jest.Mock).mockResolvedValue([ + effects, + 15, + ]); + + // Act + const result = await EffectHelper.GenerateEffectListEmbed("userId", 2); + + // Assert + expect(result).toMatchSnapshot(); + }); + + test("GIVEN user does NOT have an effect, EXPECT empty embed to be returned", async () => { + // Arrange + (UserEffect.FetchAllByUserIdPaginated as jest.Mock).mockResolvedValue([ + [], + 0, + ]); + + // Act + const result = await EffectHelper.GenerateEffectListEmbed("userId", 1); + + // Assert + expect(result).toMatchSnapshot(); + }); + + test("GIVEN there is an active effect, EXPECT field added", async () => { + // Arrange + (UserEffect.FetchAllByUserIdPaginated as jest.Mock).mockResolvedValue([ + [ + { + Name: "unclaimed", + Unused: 1, + } + ], + 1, + ]); + + (UserEffect.FetchActiveEffectByUserId as jest.Mock).mockResolvedValue({ + Name: "unclaimed", + WhenExpires: new Date(1738174052), }); - test("EXPECT effect to be saved", () => { - expect(UserEffect.prototype.Save).toHaveBeenCalledTimes(1); - expect(UserEffect.prototype.Save).toHaveBeenCalledWith(UserEffect, expect.any(UserEffect)); - }); + // Act + const result = await EffectHelper.GenerateEffectListEmbed("userId", 1); + + // Assert + expect(result).toMatchSnapshot(); }); }); -describe("UseEffect", () => { - describe("GIVEN effect is in database", () => { - describe("GIVEN now is before effect.WhenExpires", () => { - let result: boolean | undefined; +describe("GenerateEffectBuyEmbed", () => { + test.todo("GIVEN Effect Details are not found, EXPECT error"); - // nowMock < whenExpires - const nowMock = new Date(2024, 11, 3, 13, 30); - const whenExpires = new Date(2024, 11, 3, 14, 0); + test.todo("GIVEN user is not in database, EXPECT blank user created"); - const userEffect = { - Unused: 1, - WhenExpires: whenExpires, - }; + test.todo("GIVEN user does not have enough currency, EXPECT error"); - beforeAll(async () => { - jest.setSystemTime(nowMock); + test.todo("GIVEN user does have enough currency, EXPECT embed returned"); - UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect); - - result = await EffectHelper.UseEffect("userId", "name", new Date()); - }); - - test("EXPECT false returned", () => { - expect(result).toBe(false); - }); - }); - - describe("GIVEN currently used effect is inactive", () => { - let result: boolean | undefined; - - // nowMock > whenExpires - const nowMock = new Date(2024, 11, 3, 13, 30); - const whenExpires = new Date(2024, 11, 3, 13, 0); - const whenExpiresNew = new Date(2024, 11, 3, 15, 0); - - const userEffect = { - Unused: 1, - WhenExpires: whenExpires, - UseEffect: jest.fn(), - Save: jest.fn(), - }; - - beforeAll(async () => { - jest.setSystemTime(nowMock); - - UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect); - - result = await EffectHelper.UseEffect("userId", "name", whenExpiresNew); - }); - - test("EXPECT UseEffect to be called", () => { - expect(userEffect.UseEffect).toHaveReturnedTimes(1); - expect(userEffect.UseEffect).toHaveBeenCalledWith(whenExpiresNew); - }); - - test("EXPECT effect to be saved", () => { - expect(userEffect.Save).toHaveBeenCalledTimes(1); - expect(userEffect.Save).toHaveBeenCalledWith(UserEffect, userEffect); - }); - - test("EXPECT true returned", () => { - expect(result).toBe(true); - }); - }); - - describe("GIVEN effect.WhenExpires is null", () => { - let result: boolean | undefined; - - // nowMock > whenExpires - const nowMock = new Date(2024, 11, 3, 13, 30); - const whenExpiresNew = new Date(2024, 11, 3, 15, 0); - - const userEffect = { - Unused: 1, - WhenExpires: null, - UseEffect: jest.fn(), - Save: jest.fn(), - }; - - beforeAll(async () => { - jest.setSystemTime(nowMock); - - UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect); - - result = await EffectHelper.UseEffect("userId", "name", whenExpiresNew); - }); - - test("EXPECT UseEffect to be called", () => { - expect(userEffect.UseEffect).toHaveBeenCalledTimes(1); - expect(userEffect.UseEffect).toHaveBeenCalledWith(whenExpiresNew); - }); - - test("EXPECT effect to be saved", () => { - expect(userEffect.Save).toHaveBeenCalledTimes(1); - expect(userEffect.Save).toHaveBeenCalledWith(UserEffect, userEffect); - }); - - test("EXPECT true returned", () => { - expect(result).toBe(true); - }); - }); - }); - - describe("GIVEN effect is not in database", () => { - let result: boolean | undefined; - - // nowMock > whenExpires - const nowMock = new Date(2024, 11, 3, 13, 30); - const whenExpiresNew = new Date(2024, 11, 3, 15, 0); - - beforeAll(async () => { - jest.setSystemTime(nowMock); - - UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(null); - - result = await EffectHelper.UseEffect("userId", "name", whenExpiresNew); - }); - - test("EXPECT false returned", () => { - expect(result).toBe(false); - }); - }); - - describe("GIVEN effect.Unused is 0", () => { - let result: boolean | undefined; - - // nowMock > whenExpires - const nowMock = new Date(2024, 11, 3, 13, 30); - const whenExpiresNew = new Date(2024, 11, 3, 15, 0); - - const userEffect = { - Unused: 0, - WhenExpires: null, - UseEffect: jest.fn(), - Save: jest.fn(), - }; - - beforeAll(async () => { - jest.setSystemTime(nowMock); - - UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect); - - result = await EffectHelper.UseEffect("userId", "name", whenExpiresNew); - }); - - test("EXPECT false returned", () => { - expect(result).toBe(false); - }); - }); -}); - -describe("HasEffect", () => { - describe("GIVEN effect is in database", () => { - describe("GIVEN effect.WhenExpires is defined", () => { - describe("GIVEN now is before effect.WhenExpires", () => { - let result: boolean | undefined; - - const nowMock = new Date(2024, 11, 3, 13, 30); - const whenExpires = new Date(2024, 11, 3, 15, 0); - - const userEffect = { - WhenExpires: whenExpires, - }; - - beforeAll(async () => { - jest.setSystemTime(nowMock); - - UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect); - - result = await EffectHelper.HasEffect("userId", "name"); - }); - - test("EXPECT true returned", () => { - expect(result).toBe(true); - }); - }); - - describe("GIVEN now is after effect.WhenExpires", () => { - let result: boolean | undefined; - - const nowMock = new Date(2024, 11, 3, 16, 30); - const whenExpires = new Date(2024, 11, 3, 15, 0); - - const userEffect = { - WhenExpires: whenExpires, - }; - - beforeAll(async () => { - jest.setSystemTime(nowMock); - - UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect); - - result = await EffectHelper.HasEffect("userId", "name"); - }); - - test("EXPECT false returned", () => { - expect(result).toBe(false); - }); - }); - }); - - describe("GIVEN effect.WhenExpires is undefined", () => { - let result: boolean | undefined; - - const userEffect = { - WhenExpires: undefined, - }; - - beforeAll(async () => { - UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect); - - result = await EffectHelper.HasEffect("userId", "name"); - }); - - test("EXPECT false returned", () => { - expect(result).toBe(false); - }); - }); - }); - - describe("GIVEN effect is not in database", () => { - let result: boolean | undefined; - - beforeAll(async () => { - UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(null); - - result = await EffectHelper.HasEffect("userId", "name"); - }); - - test("EXPECT false returned", () => { - expect(result).toBe(false); - }); - }); -}); + test.todo("GIVEN disabled boolean is true, EXPECT buttons to be disabled"); +}); \ No newline at end of file diff --git a/tests/helpers/TimeLengthInput.test.ts b/tests/helpers/TimeLengthInput.test.ts new file mode 100644 index 0000000..6a23d67 --- /dev/null +++ b/tests/helpers/TimeLengthInput.test.ts @@ -0,0 +1,38 @@ +import TimeLengthInput from "../../src/helpers/TimeLengthInput"; + +describe("ConvertFromMilliseconds", () => { + test("EXPECT 1000ms to be outputted as a second", () => { + const timeLength = TimeLengthInput.ConvertFromMilliseconds(1000); + expect(timeLength.GetLengthShort()).toBe("1s"); + }); + + test("EXPECT 60000ms to be outputted as a minute", () => { + const timeLength = TimeLengthInput.ConvertFromMilliseconds(60000); + expect(timeLength.GetLengthShort()).toBe("1m"); + }); + + test("EXPECT 3600000ms to be outputted as an hour", () => { + const timeLength = TimeLengthInput.ConvertFromMilliseconds(3600000); + expect(timeLength.GetLengthShort()).toBe("1h"); + }); + + test("EXPECT 86400000ms to be outputted as a day", () => { + const timeLength = TimeLengthInput.ConvertFromMilliseconds(86400000); + expect(timeLength.GetLengthShort()).toBe("1d"); + }); + + test("EXPECT a combination to be outputted correctly", () => { + const timeLength = TimeLengthInput.ConvertFromMilliseconds(90061000); + expect(timeLength.GetLengthShort()).toBe("1d 1h 1m 1s"); + }); + + test("EXPECT 0ms to be outputted as empty", () => { + const timeLength = TimeLengthInput.ConvertFromMilliseconds(0); + expect(timeLength.GetLengthShort()).toBe(""); + }); + + test("EXPECT 123456789ms to be outputted correctly", () => { + const timeLength = TimeLengthInput.ConvertFromMilliseconds(123456789); + expect(timeLength.GetLengthShort()).toBe("1d 10h 17m 36s"); + }); +}); \ No newline at end of file diff --git a/tests/helpers/__snapshots__/EffectHelper.test.ts.snap b/tests/helpers/__snapshots__/EffectHelper.test.ts.snap new file mode 100644 index 0000000..fc3317e --- /dev/null +++ b/tests/helpers/__snapshots__/EffectHelper.test.ts.snap @@ -0,0 +1,216 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GenerateEffectListEmbed GIVEN there is an active effect, EXPECT field added 1`] = ` +{ + "embed": { + "color": 3166394, + "description": "Unclaimed Chance Up x1", + "fields": [ + { + "inline": true, + "name": "Active", + "value": "Unclaimed Chance Up", + }, + { + "inline": true, + "name": "Expires", + "value": "", + }, + ], + "footer": { + "icon_url": undefined, + "text": "Page 1 of 1", + }, + "title": "Effects", + }, + "row": { + "components": [ + { + "custom_id": "effects list 0", + "disabled": true, + "emoji": undefined, + "label": "Previous", + "style": 1, + "type": 2, + }, + { + "custom_id": "effects list 2", + "disabled": true, + "emoji": undefined, + "label": "Next", + "style": 1, + "type": 2, + }, + ], + "type": 1, + }, +} +`; + +exports[`GenerateEffectListEmbed GIVEN user does NOT have an effect, EXPECT empty embed to be returned 1`] = ` +{ + "embed": { + "color": 3166394, + "description": "*none*", + "footer": { + "icon_url": undefined, + "text": "Page 1 of 1", + }, + "title": "Effects", + }, + "row": { + "components": [ + { + "custom_id": "effects list 0", + "disabled": true, + "emoji": undefined, + "label": "Previous", + "style": 1, + "type": 2, + }, + { + "custom_id": "effects list 2", + "disabled": true, + "emoji": undefined, + "label": "Next", + "style": 1, + "type": 2, + }, + ], + "type": 1, + }, +} +`; + +exports[`GenerateEffectListEmbed GIVEN user has an effect, EXPECT detailed embed to be returned 1`] = ` +{ + "embed": { + "color": 3166394, + "description": "Unclaimed Chance Up x1", + "footer": { + "icon_url": undefined, + "text": "Page 1 of 1", + }, + "title": "Effects", + }, + "row": { + "components": [ + { + "custom_id": "effects list 0", + "disabled": true, + "emoji": undefined, + "label": "Previous", + "style": 1, + "type": 2, + }, + { + "custom_id": "effects list 2", + "disabled": true, + "emoji": undefined, + "label": "Next", + "style": 1, + "type": 2, + }, + ], + "type": 1, + }, +} +`; + +exports[`GenerateEffectListEmbed GIVEN user has more than 1 page of effects, EXPECT pagination enabled 1`] = ` +{ + "embed": { + "color": 3166394, + "description": "Unclaimed Chance Up x1 +Unclaimed Chance Up x1 +Unclaimed Chance Up x1 +Unclaimed Chance Up x1 +Unclaimed Chance Up x1 +Unclaimed Chance Up x1 +Unclaimed Chance Up x1 +Unclaimed Chance Up x1 +Unclaimed Chance Up x1 +Unclaimed Chance Up x1 +Unclaimed Chance Up x1 +Unclaimed Chance Up x1 +Unclaimed Chance Up x1 +Unclaimed Chance Up x1 +Unclaimed Chance Up x1", + "footer": { + "icon_url": undefined, + "text": "Page 1 of 2", + }, + "title": "Effects", + }, + "row": { + "components": [ + { + "custom_id": "effects list 0", + "disabled": true, + "emoji": undefined, + "label": "Previous", + "style": 1, + "type": 2, + }, + { + "custom_id": "effects list 2", + "disabled": false, + "emoji": undefined, + "label": "Next", + "style": 1, + "type": 2, + }, + ], + "type": 1, + }, +} +`; + +exports[`GenerateEffectListEmbed GIVEN user is on a page other than 1, EXPECT pagination enabled 1`] = ` +{ + "embed": { + "color": 3166394, + "description": "Unclaimed Chance Up x1 +Unclaimed Chance Up x1 +Unclaimed Chance Up x1 +Unclaimed Chance Up x1 +Unclaimed Chance Up x1 +Unclaimed Chance Up x1 +Unclaimed Chance Up x1 +Unclaimed Chance Up x1 +Unclaimed Chance Up x1 +Unclaimed Chance Up x1 +Unclaimed Chance Up x1 +Unclaimed Chance Up x1 +Unclaimed Chance Up x1 +Unclaimed Chance Up x1 +Unclaimed Chance Up x1", + "footer": { + "icon_url": undefined, + "text": "Page 2 of 2", + }, + "title": "Effects", + }, + "row": { + "components": [ + { + "custom_id": "effects list 1", + "disabled": false, + "emoji": undefined, + "label": "Previous", + "style": 1, + "type": 2, + }, + { + "custom_id": "effects list 3", + "disabled": true, + "emoji": undefined, + "label": "Next", + "style": 1, + "type": 2, + }, + ], + "type": 1, + }, +} +`; diff --git a/tests/timers/PurgeClaims.test.ts b/tests/timers/PurgeClaims.test.ts new file mode 100644 index 0000000..6d0d09b --- /dev/null +++ b/tests/timers/PurgeClaims.test.ts @@ -0,0 +1,7 @@ +describe("PurgeClaims", () => { + test.todo("EXPECT claims to be fetched"); + + test.todo("EXPECT Claim.RemoveMany to remove the claims older than 2 minutes"); + + test.todo("EXPECT info logged"); +}); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 0313723..c63e681 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1591,6 +1591,15 @@ await-to-js@^3.0.0: resolved "https://registry.yarnpkg.com/await-to-js/-/await-to-js-3.0.0.tgz#70929994185616f4675a91af6167eb61cc92868f" integrity sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g== +axios@^1.8.4: + version "1.8.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.4.tgz#78990bb4bc63d2cae072952d374835950a82f447" + integrity sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-jest@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" @@ -1794,6 +1803,14 @@ bytes@3.1.2: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + call-bind@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" @@ -2378,6 +2395,15 @@ dotenv@^16.0.0, dotenv@^16.0.3: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" @@ -2454,12 +2480,39 @@ es-define-property@^1.0.0: dependencies: get-intrinsic "^1.2.4" +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + es-errors@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== -escalade@^3.1.1, escalade@^3.2.0: +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +escalade@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== + +escalade@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== @@ -2874,6 +2927,11 @@ fn.name@1.x.x: resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + foreground-child@^3.1.0: version "3.3.0" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" @@ -2891,6 +2949,16 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.2.tgz#35cabbdd30c3ce73deb2c42d3c8d3ed9ca51794c" + integrity sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" + mime-types "^2.1.12" + formidable@^1.2.1: version "1.2.6" resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168" @@ -2974,11 +3042,35 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: has-symbols "^1.0.3" hasown "^2.0.0" +get-intrinsic@^1.2.6: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + get-stream@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" @@ -3087,6 +3179,11 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + graceful-fs@4.2.10: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" @@ -3136,6 +3233,18 @@ has-symbols@^1.0.3: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + has-unicode@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -4328,6 +4437,11 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -4418,6 +4532,13 @@ mimic-response@^2.0.0: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== +minimatch@9.0.5, minimatch@^9.0.0, minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + minimatch@^10.0.0: version "10.0.1" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.1.tgz#ce0521856b453c86e25f2c4c0d03e6ff7ddc440b" @@ -4439,13 +4560,6 @@ minimatch@^5.0.1: dependencies: brace-expansion "^2.0.1" -minimatch@^9.0.0, minimatch@^9.0.4: - version "9.0.5" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" - integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== - dependencies: - brace-expansion "^2.0.1" - minimist@^1.2.0: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" @@ -5007,7 +5121,12 @@ peek-readable@^4.1.0: resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-4.1.0.tgz#4ece1111bf5c2ad8867c314c81356847e8a62e72" integrity sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg== -picocolors@^1.0.0, picocolors@^1.1.0: +picocolors@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== + +picocolors@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== @@ -5098,6 +5217,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + punycode@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" @@ -5997,7 +6121,12 @@ type-fest@^3.0.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.13.1.tgz#bb744c1f0678bea7543a2d1ec24e83e68e8c8706" integrity sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g== -type-fest@^4.18.2, type-fest@^4.21.0, type-fest@^4.6.0, type-fest@^4.7.1: +type-fest@^4.18.2: + version "4.40.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.40.0.tgz#62bc09caccb99a75e1ad6b9b4653e8805e5e1eee" + integrity sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw== + +type-fest@^4.21.0, type-fest@^4.6.0, type-fest@^4.7.1: version "4.26.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.26.1.tgz#a4a17fa314f976dd3e6d6675ef6c775c16d7955e" integrity sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg== @@ -6412,6 +6541,6 @@ yoctocolors-cjs@^2.1.2: integrity sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA== zod@^3.23.8: - version "3.23.8" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" - integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== + version "3.24.3" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.3.tgz#1f40f750a05e477396da64438e0e1c0995dafd87" + integrity sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==