From f2c949c78a8b08aa012352f627aecca52aed7f72 Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Sat, 24 May 2025 10:04:24 +0100 Subject: [PATCH 1/8] Fix multidrop not handling externally hosted images correctly (#447) - Fix multidrop command not handling externally hosted images correctly - It was trying to find the `https://` link in the file system, which it couldn't find - Fixed by copying existing logic from the drop command into the multidrop command #442 Reviewed-on: https://git.vylpes.xyz/External/card-drop/pulls/447 Reviewed-by: VylpesTester Co-authored-by: Ethan Lane Co-committed-by: Ethan Lane --- src/buttonEvents/Multidrop.ts | 16 ++++++++++++---- src/commands/multidrop.ts | 15 +++++++++++---- tests/buttonEvents/Multidrop.test.ts | 13 +++++++++++++ tests/commands/multidrop.test.ts | 13 +++++++++++++ 4 files changed, 49 insertions(+), 8 deletions(-) create mode 100644 tests/buttonEvents/Multidrop.test.ts create mode 100644 tests/commands/multidrop.test.ts diff --git a/src/buttonEvents/Multidrop.ts b/src/buttonEvents/Multidrop.ts index e6ea7c2..e88d6ef 100644 --- a/src/buttonEvents/Multidrop.ts +++ b/src/buttonEvents/Multidrop.ts @@ -98,10 +98,18 @@ export default class Multidrop extends ButtonEvent { await interaction.deferUpdate(); 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 = ""; + + 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 attachment = new AttachmentBuilder(image, { name: imageFileName }); const inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, randomCard.card.id); const quantityClaimed = inventory ? inventory.Quantity : 0; @@ -112,7 +120,7 @@ export default class Multidrop extends ButtonEvent { await interaction.editReply({ embeds: [ embed ], - files: [ attachment ], + files: files, components: [ row ], }); } catch (e) { diff --git a/src/commands/multidrop.ts b/src/commands/multidrop.ts index aa42686..82d19fe 100644 --- a/src/commands/multidrop.ts +++ b/src/commands/multidrop.ts @@ -62,10 +62,17 @@ export default class Multidrop 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; @@ -76,7 +83,7 @@ export default class Multidrop extends Command { await interaction.editReply({ embeds: [ embed ], - files: [ attachment ], + files: files, components: [ row ], }); } catch (e) { diff --git a/tests/buttonEvents/Multidrop.test.ts b/tests/buttonEvents/Multidrop.test.ts new file mode 100644 index 0000000..5c9da42 --- /dev/null +++ b/tests/buttonEvents/Multidrop.test.ts @@ -0,0 +1,13 @@ +describe("execute", () => { + describe("GIVEN randomCard image is hosted locally", () => { + test.todo("EXPECT image to be uploaded directly"); + }); + + describe("GIVEN randomCard image is hosted via http", () => { + test.todo("EXPECT image link to be directly added to embed"); + }); + + describe("GIVEN randomCard image is hosted via https", () => { + test.todo("EXPECT image link to be directly added to embed"); + }); +}); diff --git a/tests/commands/multidrop.test.ts b/tests/commands/multidrop.test.ts new file mode 100644 index 0000000..5c9da42 --- /dev/null +++ b/tests/commands/multidrop.test.ts @@ -0,0 +1,13 @@ +describe("execute", () => { + describe("GIVEN randomCard image is hosted locally", () => { + test.todo("EXPECT image to be uploaded directly"); + }); + + describe("GIVEN randomCard image is hosted via http", () => { + test.todo("EXPECT image link to be directly added to embed"); + }); + + describe("GIVEN randomCard image is hosted via https", () => { + test.todo("EXPECT image link to be directly added to embed"); + }); +}); From ecc879cf13616bd2d280547130fa178e2e733f59 Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Sat, 24 May 2025 10:05:49 +0100 Subject: [PATCH 2/8] Fix sacrificing a dropped card taking from your inventory first (#448) - Fix sacrificing a dropped card trying to take it from your inventory instead of just directly from the drop - Did it by creating a separate subaction for the sacrifice command, which does the same logic except without checking your inventory, since it wont be coming from there. - Fix the sacrificed card image showing outside of the embed #435, #436 Reviewed-on: https://git.vylpes.xyz/External/card-drop/pulls/448 Reviewed-by: VylpesTester Co-authored-by: Ethan Lane Co-committed-by: Ethan Lane --- src/buttonEvents/Sacrifice.ts | 76 ++++++++++++++++++++++ src/helpers/DropHelpers/DropEmbedHelper.ts | 2 +- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/src/buttonEvents/Sacrifice.ts b/src/buttonEvents/Sacrifice.ts index de0bb77..1626cf6 100644 --- a/src/buttonEvents/Sacrifice.ts +++ b/src/buttonEvents/Sacrifice.ts @@ -5,6 +5,7 @@ import { CardRarityToString, GetSacrificeAmount } from "../constants/CardRarity" import EmbedColours from "../constants/EmbedColours"; import User from "../database/entities/app/User"; import GetCardsHelper from "../helpers/DropHelpers/GetCardsHelper"; +import CardConstants from "../constants/CardConstants"; export default class Sacrifice extends ButtonEvent { public override async execute(interaction: ButtonInteraction) { @@ -17,6 +18,8 @@ export default class Sacrifice extends ButtonEvent { case "cancel": await this.cancel(interaction); break; + case "give": + await this.give(interaction); } } @@ -168,4 +171,77 @@ export default class Sacrifice extends ButtonEvent { components: [ row ], }); } + + private async give(interaction: ButtonInteraction) { + const userId = interaction.customId.split(" ")[2]; + const cardNumber = interaction.customId.split(" ")[3]; + const quantity = Number(interaction.customId.split(" ")[4]) || 1; + + if (userId != interaction.user.id) { + await interaction.reply("Only the user who created this sacrifice can confirm it."); + return; + } + + const cardData = GetCardsHelper.GetCardByCardNumber(cardNumber); + + if (!cardData) { + await interaction.reply("Unable to find card in the database."); + return; + } + + let user = await User.FetchOneById(User, userId); + + if (!user) { + user = new User(userId, CardConstants.StartingCurrency); + } + + const cardInInventory = await Inventory.FetchOneByCardNumberAndUserId(userId, cardNumber); + let cardQuantity = 0; + + if (cardInInventory) { + cardQuantity = cardInInventory.Quantity; + } + + const cardValue = GetSacrificeAmount(cardData.card.type) * quantity; + const cardRarityString = CardRarityToString(cardData.card.type); + + user.AddCurrency(cardValue); + + await user.Save(User, user); + + const description = [ + `Card: ${cardData.card.name}`, + `Series: ${cardData.series.name}`, + `Rarity: ${cardRarityString}`, + `Quantity Owned: ${cardQuantity}`, + `Quantity To Sacrifice: ${quantity}`, + `Sacrifice Amount: ${cardValue}`, + ]; + + const embed = new EmbedBuilder() + .setTitle("Card Sacrificed") + .setDescription(description.join("\n")) + .setColor(EmbedColours.Green) + .setFooter({ text: `${interaction.user.username}` }); + + const row = new ActionRowBuilder() + .addComponents([ + new ButtonBuilder() + .setCustomId(`sacrifice confirm ${interaction.user.id} ${cardNumber}`) + .setLabel("Confirm") + .setStyle(ButtonStyle.Success) + .setDisabled(true), + new ButtonBuilder() + .setCustomId("sacrifice cancel") + .setLabel("Cancel") + .setStyle(ButtonStyle.Secondary) + .setDisabled(true), + ]); + + await interaction.update({ + embeds: [ embed ], + components: [ row ], + attachments: [], + }); + } } \ No newline at end of file diff --git a/src/helpers/DropHelpers/DropEmbedHelper.ts b/src/helpers/DropHelpers/DropEmbedHelper.ts index 08b5813..5956a27 100644 --- a/src/helpers/DropHelpers/DropEmbedHelper.ts +++ b/src/helpers/DropHelpers/DropEmbedHelper.ts @@ -77,7 +77,7 @@ export default class DropEmbedHelper { .setStyle(ButtonStyle.Success) .setDisabled(disabled), new ButtonBuilder() - .setCustomId(`sacrifice confirm ${userId} ${drop.card.id} 1`) + .setCustomId(`sacrifice give ${userId} ${drop.card.id} 1`) .setLabel(`Sacrifice`) .setStyle(ButtonStyle.Danger), new ButtonBuilder() From ead53ba062b3e0b27d4540e614af1cd78bbcb3a1 Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Sat, 24 May 2025 10:39:34 +0100 Subject: [PATCH 3/8] Add guards to the drop and reroll commands in case the card fetcher fails (#449) #446 Reviewed-on: https://git.vylpes.xyz/External/card-drop/pulls/449 Co-authored-by: Ethan Lane Co-committed-by: Ethan Lane --- src/buttonEvents/Reroll.ts | 15 ++++++++++++--- src/commands/drop.ts | 15 +++++++++++++-- src/helpers/DropHelpers/GetCardsHelper.ts | 2 +- .../DropHelpers/GetUnclaimedCardsHelper.ts | 17 ++++++++++++++--- 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/buttonEvents/Reroll.ts b/src/buttonEvents/Reroll.ts index 6d97024..6ad855e 100644 --- a/src/buttonEvents/Reroll.ts +++ b/src/buttonEvents/Reroll.ts @@ -11,6 +11,7 @@ import User from "../database/entities/app/User"; import CardConstants from "../constants/CardConstants"; import GetCardsHelper from "../helpers/DropHelpers/GetCardsHelper"; import DropEmbedHelper from "../helpers/DropHelpers/DropEmbedHelper"; +import {DropResult} from "../contracts/SeriesMetadata"; export default class Reroll extends ButtonEvent { public override async execute(interaction: ButtonInteraction) { @@ -32,7 +33,7 @@ export default class Reroll extends ButtonEvent { user = new User(interaction.user.id, CardConstants.StartingCurrency); await user.Save(User, user); - AppLogger.LogInfo("Commands/Drop", `New user (${interaction.user.id}) saved to the database`); + AppLogger.LogInfo("Button/Reroll", `New user (${interaction.user.id}) saved to the database`); } if (!user.RemoveCurrency(CardConstants.ClaimCost)) { @@ -40,9 +41,15 @@ export default class Reroll extends ButtonEvent { return; } - await user.Save(User, user); + let randomCard: DropResult | undefined; - const randomCard = await GetCardsHelper.FetchCard(interaction.user.id); + try { + randomCard = await GetCardsHelper.FetchCard(interaction.user.id); + } catch (e) { + AppLogger.CatchError("Button/Reroll", e); + + await interaction.reply("Unable to fetch card, please try again."); + } if (!randomCard) { await interaction.reply("Unable to fetch card, please try again."); @@ -75,6 +82,8 @@ export default class Reroll extends ButtonEvent { const row = DropEmbedHelper.GenerateDropButtons(randomCard, claimId, interaction.user.id); + await user.Save(User, user); + await interaction.editReply({ embeds: [ embed ], files: files, diff --git a/src/commands/drop.ts b/src/commands/drop.ts index ac8008c..b6ad224 100644 --- a/src/commands/drop.ts +++ b/src/commands/drop.ts @@ -12,6 +12,7 @@ import CardConstants from "../constants/CardConstants"; import ErrorMessages from "../constants/ErrorMessages"; import GetCardsHelper from "../helpers/DropHelpers/GetCardsHelper"; import DropEmbedHelper from "../helpers/DropHelpers/DropEmbedHelper"; +import {DropResult} from "../contracts/SeriesMetadata"; export default class Drop extends Command { constructor() { @@ -48,9 +49,16 @@ export default class Drop extends Command { return; } - await user.Save(User, user); + let randomCard: DropResult | undefined; + + try { + randomCard = await GetCardsHelper.FetchCard(interaction.user.id); + } catch (e) { + AppLogger.CatchError("Commands/Drop", e); + + await interaction.reply(ErrorMessages.UnableToFetchCard); + } - const randomCard = await GetCardsHelper.FetchCard(interaction.user.id); if (!randomCard) { AppLogger.LogWarn("Commands/Drop", ErrorMessages.UnableToFetchCard); @@ -82,6 +90,9 @@ export default class Drop extends Command { const row = DropEmbedHelper.GenerateDropButtons(randomCard, claimId, interaction.user.id); + await user.Save(User, user); + + await interaction.editReply({ embeds: [ embed ], files: files, diff --git a/src/helpers/DropHelpers/GetCardsHelper.ts b/src/helpers/DropHelpers/GetCardsHelper.ts index 8bc4fe7..4c8648f 100644 --- a/src/helpers/DropHelpers/GetCardsHelper.ts +++ b/src/helpers/DropHelpers/GetCardsHelper.ts @@ -55,7 +55,7 @@ export default class GetCardsHelper { .find(x => x.cards.includes(card)); if (!series) { - AppLogger.LogWarn("CardDropHelperMetadata/GetRandomCardByRarity", `Series not found for card ${card.id}`); + AppLogger.LogError("CardDropHelperMetadata/GetRandomCardByRarity", `Series not found for card ${card.id}`); return undefined; } diff --git a/src/helpers/DropHelpers/GetUnclaimedCardsHelper.ts b/src/helpers/DropHelpers/GetUnclaimedCardsHelper.ts index 87fafea..3264a98 100644 --- a/src/helpers/DropHelpers/GetUnclaimedCardsHelper.ts +++ b/src/helpers/DropHelpers/GetUnclaimedCardsHelper.ts @@ -31,7 +31,7 @@ export default class GetUnclaimedCardsHelper { public static async GetRandomCardByRarityUnclaimed(rarity: CardRarity, userId: string): Promise { const claimedCards = await Inventory.FetchAllByUserId(userId); - if (!claimedCards) { + if (!claimedCards || claimedCards.length == 0) { // They don't have any cards, so safe to get any random card return GetCardsHelper.GetRandomCardByRarity(rarity); } @@ -41,16 +41,27 @@ export default class GetUnclaimedCardsHelper { .filter(x => x.type == rarity) .filter(x => !claimedCards.find(y => y.CardNumber == x.id)); - if (!allCards) return undefined; + if (!allCards) { + AppLogger.LogError("CardDropHelperMetadata/GetRandomCardByRarityUnclaimed", `No cards found to randomise from, User Id: ${userId}, rarity: ${rarity}`); + + return undefined; + }; const randomCardIndex = Math.floor(Math.random() * allCards.length); const card = allCards[randomCardIndex]; + + if (!card) { + AppLogger.LogError("CardDropHelperMetadata/GetRandomCardByRarityUnclaimed", `Card not found in index, ${randomCardIndex} of ${allCards.length}, User Id: ${userId}, rarity: ${rarity}`); + + return undefined; + } + const series = CoreClient.Cards .find(x => x.cards.includes(card)); if (!series) { - AppLogger.LogWarn("CardDropHelperMetadata/GetRandomCardByRarityUnclaimed", `Series not found for card ${card.id}`); + AppLogger.LogError("CardDropHelperMetadata/GetRandomCardByRarityUnclaimed", `Series not found for card ${card.id}, User Id: ${userId}, rarity: ${rarity}`); return undefined; } From 8e63f26747667b86af98b061c428c63a3213d85e Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Sat, 24 May 2025 11:51:56 +0100 Subject: [PATCH 4/8] Update inventory dropdown logic to use a range around the current page so that we can remain within the 25 pages discord allows (#450) #443 Reviewed-on: https://git.vylpes.xyz/External/card-drop/pulls/450 Co-authored-by: Ethan Lane Co-committed-by: Ethan Lane --- src/helpers/InventoryHelper.ts | 43 +++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/src/helpers/InventoryHelper.ts b/src/helpers/InventoryHelper.ts index 3a7fbff..15c9c92 100644 --- a/src/helpers/InventoryHelper.ts +++ b/src/helpers/InventoryHelper.ts @@ -115,17 +115,48 @@ export default class InventoryHelper { let pageNum = 0; + const maxLength = 25; // Discord API limit + + const allPageOptions = pages.map(x => + new StringSelectMenuOptionBuilder() + .setLabel(`${x.name} (${x.seriesSubpage + 1})`.substring(0, 100)) + .setDescription(`Page ${pageNum + 1}`) + .setDefault(currentPage.id == x.id) + .setValue(`${userid} ${pageNum++}`)); + + const currentPageIndex = allPageOptions.indexOf(allPageOptions.find(x => x.data.default)!); + + let pageOptions: StringSelectMenuOptionBuilder[] = []; + + if (allPageOptions.length <= maxLength) { + pageOptions = allPageOptions; + } else { + let startIndex = currentPageIndex - Math.floor((maxLength - 1) / 2); + let endIndexOffset = 0; + + if (startIndex < 0) { + endIndexOffset = 0 - startIndex; + + startIndex = 0; + } + + let endIndex = currentPageIndex + Math.floor((maxLength - 1) / 2) + endIndexOffset; + + if (endIndex + 1 > allPageOptions.length) { + endIndex = allPageOptions.length; + } + + for (let i = startIndex; i < endIndex; i++) { + pageOptions.push(allPageOptions[i]); + } + } + const row2 = new ActionRowBuilder() .addComponents( new StringSelectMenuBuilder() .setCustomId("inventory") .setPlaceholder(`${currentPage.name} (${currentPage.seriesSubpage + 1})`) - .addOptions(...pages.map(x => - new StringSelectMenuOptionBuilder() - .setLabel(`${x.name} (${x.seriesSubpage + 1})`.substring(0, 100)) - .setDescription(`Page ${pageNum + 1}`) - .setDefault(currentPage.id == x.id) - .setValue(`${userid} ${pageNum++}`)))); + .addOptions(pageOptions)); const buffer = await ImageHelper.GenerateCardImageGrid(currentPage.cards.map(x => ({ id: x.id, path: x.path }))); const image = new AttachmentBuilder(buffer, { name: "page.png" }); From 7d9c1bda627d052b1a4cad9a34b9539eca6a1129 Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Sun, 25 May 2025 17:00:43 +0100 Subject: [PATCH 5/8] v0.9.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5a0ef42..84734e7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "card-drop", - "version": "0.9.0", + "version": "0.9.1", "main": "./dist/bot.js", "typings": "./dist", "scripts": { From 434f162a01b584b4f91546852a33c4b85f07cd1f Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Wed, 28 May 2025 16:07:38 +0100 Subject: [PATCH 6/8] Update unclaimed card filter to fallback to any card if all cards are claimed (#452) #451 Reviewed-on: https://git.vylpes.xyz/External/card-drop/pulls/452 Co-authored-by: Ethan Lane Co-committed-by: Ethan Lane --- src/helpers/DropHelpers/GetUnclaimedCardsHelper.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/helpers/DropHelpers/GetUnclaimedCardsHelper.ts b/src/helpers/DropHelpers/GetUnclaimedCardsHelper.ts index 3264a98..94ce894 100644 --- a/src/helpers/DropHelpers/GetUnclaimedCardsHelper.ts +++ b/src/helpers/DropHelpers/GetUnclaimedCardsHelper.ts @@ -39,12 +39,11 @@ export default class GetUnclaimedCardsHelper { const allCards = CoreClient.Cards .flatMap(x => x.cards) .filter(x => x.type == rarity) - .filter(x => !claimedCards.find(y => y.CardNumber == x.id)); + .filter(x => !claimedCards.find(y => y.CardNumber == x.id && y.Quantity > 0)); - if (!allCards) { - AppLogger.LogError("CardDropHelperMetadata/GetRandomCardByRarityUnclaimed", `No cards found to randomise from, User Id: ${userId}, rarity: ${rarity}`); - - return undefined; + if (!allCards || allCards.length == 0) { + // There is no card left unclaimed, fallback to any card + return GetCardsHelper.GetRandomCardByRarity(rarity); }; const randomCardIndex = Math.floor(Math.random() * allCards.length); From c9f7c443cfa9a8e83a376d7800b82a7fd026c1fd Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Wed, 28 May 2025 16:14:42 +0100 Subject: [PATCH 7/8] Fix effects helper returning an error when the buttons are disabled (#454) -This caused an issue with the updated embed after confirming to buy an effect it checking you had enough currency after taking the currency away #453 Reviewed-on: https://git.vylpes.xyz/External/card-drop/pulls/454 Co-authored-by: Ethan Lane Co-committed-by: Ethan Lane --- src/helpers/EffectHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/EffectHelper.ts b/src/helpers/EffectHelper.ts index 235ea08..09e096c 100644 --- a/src/helpers/EffectHelper.ts +++ b/src/helpers/EffectHelper.ts @@ -151,7 +151,7 @@ export default class EffectHelper { AppLogger.LogInfo("EffectHelper", `Created initial user entity for : ${userId}`); } - if (user.Currency < totalCost) { + if (!disabled && user.Currency < totalCost) { return `You don't have enough currency to buy this! You have \`${user.Currency} Currency\` and need \`${totalCost} Currency\`!`; } From fc3c98f1bba88e0da844561fb2c021ad8bec6a03 Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Wed, 28 May 2025 18:17:48 +0100 Subject: [PATCH 8/8] v0.9.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 84734e7..b8c7536 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "card-drop", - "version": "0.9.1", + "version": "0.9.2", "main": "./dist/bot.js", "typings": "./dist", "scripts": {