Compare commits

..

No commits in common. "renovate/jest-29.x-lockfile" and "main" have entirely different histories.

35 changed files with 319 additions and 1222 deletions

View file

@ -32,7 +32,6 @@ DB_AUTH_PASS=
DB_SYNC= DB_SYNC=
DB_LOGGING= DB_LOGGING=
DB_DATA_LOCATION=./.temp/database DB_DATA_LOCATION=./.temp/database
DB_ROOT_HOST=0.0.0.0
DB_CARD_FILE=:memory: DB_CARD_FILE=:memory:

View file

@ -16,7 +16,7 @@ jobs:
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20.x node-version: 18.x
- run: yarn install --frozen-lockfile - run: yarn install --frozen-lockfile
- run: yarn build - run: yarn build
- run: yarn test - run: yarn test
@ -30,7 +30,7 @@ jobs:
needs: build needs: build
runs-on: node runs-on: node
steps: steps:
- uses: https://github.com/appleboy/ssh-action@v1.1.0 - uses: https://github.com/appleboy/ssh-action@v1.0.3
env: env:
DB_NAME: ${{ secrets.PROD_DB_NAME }} DB_NAME: ${{ secrets.PROD_DB_NAME }}
DB_AUTH_USER: ${{ secrets.PROD_DB_AUTH_USER }} DB_AUTH_USER: ${{ secrets.PROD_DB_AUTH_USER }}

View file

@ -16,7 +16,7 @@ jobs:
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20.x node-version: 18.x
- run: yarn install --frozen-lockfile - run: yarn install --frozen-lockfile
- run: yarn build - run: yarn build
- run: yarn test - run: yarn test
@ -30,7 +30,7 @@ jobs:
needs: build needs: build
runs-on: node runs-on: node
steps: steps:
- uses: https://github.com/appleboy/ssh-action@v1.1.0 - uses: https://github.com/appleboy/ssh-action@v1.0.3
env: env:
DB_NAME: ${{ secrets.STAGE_DB_NAME }} DB_NAME: ${{ secrets.STAGE_DB_NAME }}
DB_AUTH_USER: ${{ secrets.STAGE_DB_AUTH_USER }} DB_AUTH_USER: ${{ secrets.STAGE_DB_AUTH_USER }}

View file

@ -18,7 +18,7 @@ jobs:
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20.x node-version: 18.x
- run: yarn install --frozen-lockfile - run: yarn install --frozen-lockfile
- run: yarn build - run: yarn build
- run: yarn test - run: yarn test

View file

@ -1,35 +0,0 @@
# Google Drive Sync
The bot relies on an external sync between the local file system and Google
Drive in order to get newer cards to the bot. This is done using
[Rclone](https://rclone.org/).
The process for this is done by once the `/gdrivesync` command is executed by
an admin user of the bot, which calls the system shell to run rclone to the
card folder.
- The admins who can run the command is specifed in `$BOT_ADMINS`, which are
discord user ids separated by commas.
- The card folder is located at `$DATA_DIR/cards`.
- The source requires rclone's remote to be setup as `card-drop-gdrive`.
The exact command it runs is: `rclone sync card-drop-gdrive: $DATA_DIR/cards`.
Once it syncs the database will reread all the cards for updates and then load
them into the bot to be given.
## Safe Mode
Safe mode is a function of the bot which disables the `/drop` command function
and any other functions which rely on the card metadata. Safe mode is activated
upon failure to sync properly. It is disabled once errors are resolved.
The reason for safe mode is to ensure that the bot stays online for admins to
be able to resync the bot in case there's an error without it crashing.
## Google Drive
Please see the Rclone documentation on how to setup a remote using Google
Drive. You will need to make an app password for this.
- scope: `drive.readonly`
- root\_folder\_id: The folder id where the cards are located, this can be found
by looking at the url when viewing the folder in the browser in google drive.

View file

@ -29,7 +29,7 @@
"@types/clone-deep": "^4.0.4", "@types/clone-deep": "^4.0.4",
"@types/express": "^4.17.20", "@types/express": "^4.17.20",
"@types/jest": "^29.0.0", "@types/jest": "^29.0.0",
"@types/uuid": "^10.0.0", "@types/uuid": "^9.0.0",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"canvas": "^2.11.2", "canvas": "^2.11.2",
"clone-deep": "^4.0.1", "clone-deep": "^4.0.1",
@ -37,11 +37,11 @@
"discord.js": "^14.15.3", "discord.js": "^14.15.3",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"express": "^4.18.2", "express": "^4.18.2",
"fuse.js": "^7.0.0", "glob": "^10.3.10",
"glob": "^11.0.0",
"jest": "^29.0.0", "jest": "^29.0.0",
"jest-mock-extended": "^3.0.0", "jest-mock-extended": "^3.0.0",
"jimp": "^0.22.12", "jimp": "^0.22.12",
"minimatch": "9.0.5",
"mysql": "^2.18.1", "mysql": "^2.18.1",
"ts-jest": "^29.0.0", "ts-jest": "^29.0.0",
"typeorm": "0.3.20", "typeorm": "0.3.20",

View file

@ -38,7 +38,6 @@ const client = new CoreClient([
Registry.RegisterCommands(); Registry.RegisterCommands();
Registry.RegisterButtonEvents(); Registry.RegisterButtonEvents();
Registry.RegisterStringDropdownEvents();
if (!existsSync(`${process.env.DATA_DIR}/cards`) && process.env.GDRIVESYNC_AUTO && process.env.GDRIVESYNC_AUTO == "true") { if (!existsSync(`${process.env.DATA_DIR}/cards`) && process.env.GDRIVESYNC_AUTO && process.env.GDRIVESYNC_AUTO == "true") {
console.log("Card directory not found, syncing..."); console.log("Card directory not found, syncing...");

View file

@ -58,7 +58,7 @@ export default class Claim extends ButtonEvent {
if (!inventory) { if (!inventory) {
inventory = new Inventory(userId, cardNumber, 1); inventory = new Inventory(userId, cardNumber, 1);
} else { } else {
inventory.AddQuantity(1); inventory.SetQuantity(inventory.Quantity + 1);
} }
await inventory.Save(Inventory, inventory); await inventory.Save(Inventory, inventory);

View file

@ -34,7 +34,7 @@ export default class Inventory extends ButtonEvent {
await interaction.editReply({ await interaction.editReply({
files: [ embed.image ], files: [ embed.image ],
embeds: [ embed.embed ], embeds: [ embed.embed ],
components: [ embed.row1, embed.row2 ], components: [ embed.row ],
}); });
} catch (e) { } catch (e) {
AppLogger.LogError("Button/Inventory", `Error generating inventory page for ${member.user.username} with id ${member.user.id}: ${e}`); AppLogger.LogError("Button/Inventory", `Error generating inventory page for ${member.user.username} with id ${member.user.id}: ${e}`);

View file

@ -1,213 +0,0 @@
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";
import path from "path";
import ErrorMessages from "../constants/ErrorMessages";
import User from "../database/entities/app/User";
import { GetSacrificeAmount } from "../constants/CardRarity";
export default class Multidrop extends ButtonEvent {
public override async execute(interaction: ButtonInteraction) {
const action = interaction.customId.split(" ")[1];
switch (action) {
case "keep":
await this.Keep(interaction);
break;
case "sacrifice":
await this.Sacrifice(interaction);
break;
default:
await interaction.reply("Invalid action");
AppLogger.LogError("Button/Multidrop", `Invalid action, ${action}`);
}
}
private async Keep(interaction: ButtonInteraction) {
const cardNumber = interaction.customId.split(" ")[2];
let cardsRemaining = Number(interaction.customId.split(" ")[3]) || 0;
const userId = interaction.customId.split(" ")[4];
if (interaction.user.id != userId) {
await interaction.reply("You're not the user this drop was made for!");
return;
}
const card = CardDropHelperMetadata.GetCardByCardNumber(cardNumber);
if (!card) {
await interaction.reply("Unable to find card.");
AppLogger.LogWarn("Button/Multidrop/Keep", `Card not found, ${cardNumber}`);
return;
}
if (cardsRemaining < 0) {
await interaction.reply("Your multidrop has ran out! Please buy a new one!");
return;
}
const user = await User.FetchOneById(User, interaction.user.id);
if (!user) {
AppLogger.LogWarn("Button/Multidrop/Keep", ErrorMessages.UnableToFetchUser);
await interaction.reply(ErrorMessages.UnableToFetchUser);
return;
}
// Claim
let inventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, cardNumber);
if (!inventory) {
inventory = new Inventory(interaction.user.id, cardNumber, 1);
} else {
inventory.AddQuantity(1);
}
await inventory.Save(Inventory, inventory);
// Pack has ran out
if (cardsRemaining == 0) {
const embed = new EmbedBuilder()
.setDescription("Your multidrop has ran out! Please buy a new one!")
.setColor(EmbedColours.Ok);
await interaction.update({
embeds: [ embed ],
attachments: [],
components: [],
});
return;
}
// Drop next card
const randomCard = CardDropHelperMetadata.GetRandomCard();
cardsRemaining -= 1;
if (!randomCard) {
AppLogger.LogWarn("Button/Multidrop/Keep", ErrorMessages.UnableToFetchCard);
await interaction.reply(ErrorMessages.UnableToFetchCard);
return;
}
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 attachment = new AttachmentBuilder(image, { name: imageFileName });
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 row = CardDropHelperMetadata.GenerateMultidropButtons(randomCard, cardsRemaining, interaction.user.id, cardsRemaining < 0);
await interaction.editReply({
embeds: [ embed ],
files: [ attachment ],
components: [ row ],
});
} catch (e) {
AppLogger.LogError("Button/Multidrop/Keep", `Error sending next drop for card ${randomCard.card.id}: ${e}`);
await interaction.editReply(`Unable to send next drop. Please try again, and report this if it keeps happening. (${randomCard.card.id})`);
}
}
private async Sacrifice(interaction: ButtonInteraction) {
const cardNumber = interaction.customId.split(" ")[2];
let cardsRemaining = Number(interaction.customId.split(" ")[3]) || 0;
const userId = interaction.customId.split(" ")[4];
if (interaction.user.id != userId) {
await interaction.reply("You're not the user this drop was made for!");
return;
}
const card = CardDropHelperMetadata.GetCardByCardNumber(cardNumber);
if (!card) {
await interaction.reply("Unable to find card.");
AppLogger.LogWarn("Button/Multidrop/Sacrifice", `Card not found, ${cardNumber}`);
return;
}
if (cardsRemaining < 0) {
await interaction.reply("Your multidrop has ran out! Please buy a new one!");
return;
}
const user = await User.FetchOneById(User, interaction.user.id);
if (!user) {
AppLogger.LogWarn("Button/Multidrop/Sacrifice", ErrorMessages.UnableToFetchUser);
await interaction.reply(ErrorMessages.UnableToFetchUser);
return;
}
// Sacrifice
const sacrificeAmount = GetSacrificeAmount(card.card.type);
user.AddCurrency(sacrificeAmount);
await user.Save(User, user);
// Pack has ran out
if (cardsRemaining == 0) {
const embed = new EmbedBuilder()
.setDescription("Your multidrop has ran out! Please buy a new one!")
.setColor(EmbedColours.Ok);
await interaction.update({
embeds: [ embed ],
attachments: [],
components: [],
});
return;
}
// Drop next card
const randomCard = CardDropHelperMetadata.GetRandomCard();
cardsRemaining -= 1;
if (!randomCard) {
AppLogger.LogWarn("Button/Multidrop/Sacrifice", ErrorMessages.UnableToFetchCard);
await interaction.reply(ErrorMessages.UnableToFetchCard);
return;
}
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 attachment = new AttachmentBuilder(image, { name: imageFileName });
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 row = CardDropHelperMetadata.GenerateMultidropButtons(randomCard, cardsRemaining, interaction.user.id, cardsRemaining < 0);
await interaction.editReply({
embeds: [ embed ],
files: [ attachment ],
components: [ row ],
});
} catch (e) {
AppLogger.LogError("Button/Multidrop/Sacrifice", `Error sending next drop for card ${randomCard.card.id}: ${e}`);
await interaction.editReply(`Unable to send next drop. Please try again, and report this if it keeps happening. (${randomCard.card.id})`);
}
}
}

View file

@ -23,7 +23,6 @@ export default class Sacrifice extends ButtonEvent {
private async confirm(interaction: ButtonInteraction) { private async confirm(interaction: ButtonInteraction) {
const userId = interaction.customId.split(" ")[2]; const userId = interaction.customId.split(" ")[2];
const cardNumber = interaction.customId.split(" ")[3]; const cardNumber = interaction.customId.split(" ")[3];
const quantity = Number(interaction.customId.split(" ")[4]) || 1;
if (userId != interaction.user.id) { if (userId != interaction.user.id) {
await interaction.reply("Only the user who created this sacrifice can confirm it."); await interaction.reply("Only the user who created this sacrifice can confirm it.");
@ -32,16 +31,11 @@ export default class Sacrifice extends ButtonEvent {
const cardInInventory = await Inventory.FetchOneByCardNumberAndUserId(userId, cardNumber); const cardInInventory = await Inventory.FetchOneByCardNumberAndUserId(userId, cardNumber);
if (!cardInInventory || cardInInventory.Quantity == 0) { if (!cardInInventory) {
await interaction.reply("Unable to find card in inventory."); await interaction.reply("Unable to find card in inventory.");
return; return;
} }
if (cardInInventory.Quantity < quantity) {
await interaction.reply("You can only sacrifice what you own.");
return;
}
const cardData = CardDropHelperMetadata.GetCardByCardNumber(cardNumber); const cardData = CardDropHelperMetadata.GetCardByCardNumber(cardNumber);
if (!cardData) { if (!cardData) {
@ -56,11 +50,11 @@ export default class Sacrifice extends ButtonEvent {
return; return;
} }
cardInInventory.RemoveQuantity(quantity); cardInInventory.RemoveQuantity(1);
await cardInInventory.Save(Inventory, cardInInventory); await cardInInventory.Save(Inventory, cardInInventory);
const cardValue = GetSacrificeAmount(cardData.card.type) * quantity; const cardValue = GetSacrificeAmount(cardData.card.type);
const cardRarityString = CardRarityToString(cardData.card.type); const cardRarityString = CardRarityToString(cardData.card.type);
user.AddCurrency(cardValue); user.AddCurrency(cardValue);
@ -72,7 +66,6 @@ export default class Sacrifice extends ButtonEvent {
`Series: ${cardData.series.name}`, `Series: ${cardData.series.name}`,
`Rarity: ${cardRarityString}`, `Rarity: ${cardRarityString}`,
`Quantity Owned: ${cardInInventory.Quantity}`, `Quantity Owned: ${cardInInventory.Quantity}`,
`Quantity To Sacrifice: ${quantity}`,
`Sacrifice Amount: ${cardValue}`, `Sacrifice Amount: ${cardValue}`,
]; ];
@ -105,7 +98,6 @@ export default class Sacrifice extends ButtonEvent {
private async cancel(interaction: ButtonInteraction) { private async cancel(interaction: ButtonInteraction) {
const userId = interaction.customId.split(" ")[2]; const userId = interaction.customId.split(" ")[2];
const cardNumber = interaction.customId.split(" ")[3]; const cardNumber = interaction.customId.split(" ")[3];
const quantity = Number(interaction.customId.split(" ")[4]) || 1;
if (userId != interaction.user.id) { if (userId != interaction.user.id) {
await interaction.reply("Only the user who created this sacrifice can cancel it."); await interaction.reply("Only the user who created this sacrifice can cancel it.");
@ -114,16 +106,11 @@ export default class Sacrifice extends ButtonEvent {
const cardInInventory = await Inventory.FetchOneByCardNumberAndUserId(userId, cardNumber); const cardInInventory = await Inventory.FetchOneByCardNumberAndUserId(userId, cardNumber);
if (!cardInInventory || cardInInventory.Quantity == 0) { if (!cardInInventory) {
await interaction.reply("Unable to find card in inventory."); await interaction.reply("Unable to find card in inventory.");
return; return;
} }
if (cardInInventory.Quantity < quantity) {
await interaction.reply("You can only sacrifice what you own.");
return;
}
const cardData = CardDropHelperMetadata.GetCardByCardNumber(cardNumber); const cardData = CardDropHelperMetadata.GetCardByCardNumber(cardNumber);
if (!cardData) { if (!cardData) {
@ -131,7 +118,7 @@ export default class Sacrifice extends ButtonEvent {
return; return;
} }
const cardValue = GetSacrificeAmount(cardData.card.type) * quantity; const cardValue = GetSacrificeAmount(cardData.card.type);
const cardRarityString = CardRarityToString(cardData.card.type); const cardRarityString = CardRarityToString(cardData.card.type);
const description = [ const description = [
@ -139,7 +126,6 @@ export default class Sacrifice extends ButtonEvent {
`Series: ${cardData.series.name}`, `Series: ${cardData.series.name}`,
`Rarity: ${cardRarityString}`, `Rarity: ${cardRarityString}`,
`Quantity Owned: ${cardInInventory.Quantity}`, `Quantity Owned: ${cardInInventory.Quantity}`,
`Quantity To Sacrifice: ${quantity}`,
`Sacrifice Amount: ${cardValue}`, `Sacrifice Amount: ${cardValue}`,
]; ];

View file

@ -28,10 +28,8 @@ export default class Trade extends ButtonEvent {
const user2CardNumber = interaction.customId.split(" ")[5]; const user2CardNumber = interaction.customId.split(" ")[5];
const expiry = interaction.customId.split(" ")[6]; const expiry = interaction.customId.split(" ")[6];
const timeoutId = interaction.customId.split(" ")[7]; const timeoutId = interaction.customId.split(" ")[7];
const user1Quantity = Number(interaction.customId.split(" ")[8]) || 1;
const user2Quantity = Number(interaction.customId.split(" ")[9]) || 1;
AppLogger.LogSilly("Button/Trade/AcceptTrade", `Parameters: user1UserId=${user1UserId}, user2UserId=${user2UserId}, user1CardNumber=${user1CardNumber}, user2CardNumber=${user2CardNumber}, expiry=${expiry}, timeoutId=${timeoutId} user1Quantity=${user1Quantity} user2Quantity=${user2Quantity}`); AppLogger.LogSilly("Button/Trade/AcceptTrade", `Parameters: user1UserId=${user1UserId}, user2UserId=${user2UserId}, user1CardNumber=${user1CardNumber}, user2CardNumber=${user2CardNumber}, expiry=${expiry}, timeoutId=${timeoutId}`);
const expiryDate = new Date(expiry); const expiryDate = new Date(expiry);
@ -69,13 +67,13 @@ export default class Trade extends ButtonEvent {
return; return;
} }
if (user1UserInventory1.Quantity < user1Quantity || user2UserInventory1.Quantity < user2Quantity) { if (user1UserInventory1.Quantity < 1 || user2UserInventory1.Quantity < 1) {
await interaction.reply("One or more of the items you are trying to trade does not exist."); await interaction.reply("One or more of the items you are trying to trade does not exist.");
return; return;
} }
user1UserInventory1.RemoveQuantity(user1Quantity); user1UserInventory1.RemoveQuantity(1);
user2UserInventory1.RemoveQuantity(user2Quantity); user2UserInventory1.RemoveQuantity(1);
await user1UserInventory1.Save(Inventory, user1UserInventory1); await user1UserInventory1.Save(Inventory, user1UserInventory1);
await user2UserInventory1.Save(Inventory, user2UserInventory1); await user2UserInventory1.Save(Inventory, user2UserInventory1);
@ -84,15 +82,15 @@ export default class Trade extends ButtonEvent {
let user2UserInventory2 = await Inventory.FetchOneByCardNumberAndUserId(user2UserId, user1CardNumber); let user2UserInventory2 = await Inventory.FetchOneByCardNumberAndUserId(user2UserId, user1CardNumber);
if (!user1UserInventory2) { if (!user1UserInventory2) {
user1UserInventory2 = new Inventory(user1UserId, user2CardNumber, user2Quantity); user1UserInventory2 = new Inventory(user1UserId, user2CardNumber, 1);
} else { } else {
user1UserInventory2.AddQuantity(user2Quantity); user1UserInventory2.AddQuantity(1);
} }
if (!user2UserInventory2) { if (!user2UserInventory2) {
user2UserInventory2 = new Inventory(user2UserId, user1CardNumber, user1Quantity); user2UserInventory2 = new Inventory(user2UserId, user1CardNumber, 1);
} else { } else {
user2UserInventory2.AddQuantity(user1Quantity); user2UserInventory2.AddQuantity(1);
} }
await user1UserInventory2.Save(Inventory, user1UserInventory2); await user1UserInventory2.Save(Inventory, user1UserInventory2);
@ -108,12 +106,12 @@ export default class Trade extends ButtonEvent {
.addFields([ .addFields([
{ {
name: `${user1User.username} Receives`, name: `${user1User.username} Receives`,
value: `${user2Item.id}: ${user2Item.name} x${user2Quantity}`, value: `${user2Item.id}: ${user2Item.name}`,
inline: true, inline: true,
}, },
{ {
name: `${user2User.username} Receives`, name: `${user2User.username} Receives`,
value: `${user1Item.id}: ${user1Item.name} x${user1Quantity}`, value: `${user1Item.id}: ${user1Item.name}`,
inline: true, inline: true,
}, },
{ {
@ -146,8 +144,6 @@ export default class Trade extends ButtonEvent {
const user2CardNumber = interaction.customId.split(" ")[5]; const user2CardNumber = interaction.customId.split(" ")[5];
// No need to get expiry date // No need to get expiry date
const timeoutId = interaction.customId.split(" ")[7]; const timeoutId = interaction.customId.split(" ")[7];
const user1Quantity = Number(interaction.customId.split(" ")[8]) || 1;
const user2Quantity = Number(interaction.customId.split(" ")[9]) || 1;
AppLogger.LogSilly("Button/Trade/DeclineTrade", `Parameters: user1UserId=${user1UserId}, user2UserId=${user2UserId}, user1CardNumber=${user1CardNumber}, user2CardNumber=${user2CardNumber}, timeoutId=${timeoutId}`); AppLogger.LogSilly("Button/Trade/DeclineTrade", `Parameters: user1UserId=${user1UserId}, user2UserId=${user2UserId}, user1CardNumber=${user1CardNumber}, user2CardNumber=${user2CardNumber}, timeoutId=${timeoutId}`);
@ -182,12 +178,12 @@ export default class Trade extends ButtonEvent {
.addFields([ .addFields([
{ {
name: `${user1User.username} Receives`, name: `${user1User.username} Receives`,
value: `${user2Item.id}: ${user2Item.name} x${user2Quantity}`, value: `${user2Item.id}: ${user2Item.name}`,
inline: true, inline: true,
}, },
{ {
name: `${user2User.username} Receives`, name: `${user2User.username} Receives`,
value: `${user1Item.id}: ${user1Item.name} x${user1Quantity}`, value: `${user1Item.id}: ${user1Item.name}`,
inline: true, inline: true,
}, },
{ {

View file

@ -1,25 +0,0 @@
import {ButtonInteraction} from "discord.js";
import {ButtonEvent} from "../type/buttonEvent.js";
import CardSearchHelper from "../helpers/CardSearchHelper.js";
export default class View extends ButtonEvent {
public override async execute(interaction: ButtonInteraction) {
const page = interaction.customId.split(" ")[1];
const results = interaction.customId.split(" ").splice(2);
await interaction.deferUpdate();
const searchResult = await CardSearchHelper.GenerateSearchPageFromQuery(results, interaction.user.id, Number(page));
if (!searchResult) {
await interaction.followUp("No results found");
return;
}
await interaction.editReply({
embeds: [ searchResult.embed ],
components: [ searchResult.row ],
files: [ searchResult.attachment ],
});
}
}

View file

@ -17,14 +17,11 @@ import AppLogger from "./appLogger";
import TimerHelper from "../helpers/TimerHelper"; import TimerHelper from "../helpers/TimerHelper";
import GiveCurrency from "../timers/GiveCurrency"; import GiveCurrency from "../timers/GiveCurrency";
import PurgeClaims from "../timers/PurgeClaims"; import PurgeClaims from "../timers/PurgeClaims";
import StringDropdownEventItem from "../contracts/StringDropdownEventItem";
import {StringDropdownEvent} from "../type/stringDropdownEvent";
export class CoreClient extends Client { export class CoreClient extends Client {
private static _commandItems: ICommandItem[]; private static _commandItems: ICommandItem[];
private static _eventExecutors: EventExecutors; private static _eventExecutors: EventExecutors;
private static _buttonEvents: IButtonEventItem[]; private static _buttonEvents: IButtonEventItem[];
private static _stringDropdowns: StringDropdownEventItem[];
private _events: Events; private _events: Events;
private _util: Util; private _util: Util;
@ -48,10 +45,6 @@ export class CoreClient extends Client {
return this._buttonEvents; return this._buttonEvents;
} }
public static get stringDropdowns(): StringDropdownEventItem[] {
return this._stringDropdowns;
}
constructor(intents: number[]) { constructor(intents: number[]) {
super({ intents: intents }); super({ intents: intents });
dotenv.config(); dotenv.config();
@ -66,7 +59,6 @@ export class CoreClient extends Client {
CoreClient._commandItems = []; CoreClient._commandItems = [];
CoreClient._buttonEvents = []; CoreClient._buttonEvents = [];
CoreClient._stringDropdowns = [];
this._events = new Events(); this._events = new Events();
this._util = new Util(); this._util = new Util();
@ -416,19 +408,4 @@ export class CoreClient extends Client {
AppLogger.LogVerbose("Client", `Registered Button Event: ${buttonId}`); AppLogger.LogVerbose("Client", `Registered Button Event: ${buttonId}`);
} }
} }
public static RegisterStringDropdownEvent(dropdownId: string, event: StringDropdownEvent, environment: Environment = Environment.All) {
const item: StringDropdownEventItem = {
DropdownId: dropdownId,
Event: event,
Environment: environment,
};
if ((environment & CoreClient.Environment) == CoreClient.Environment) {
CoreClient._stringDropdowns.push(item);
AppLogger.LogVerbose("Client", `Registered String Dropdown Event: ${dropdownId}`);
} }
}
}

View file

@ -3,7 +3,6 @@ import ChatInputCommand from "./interactionCreate/ChatInputCommand";
import Button from "./interactionCreate/Button"; import Button from "./interactionCreate/Button";
import AppLogger from "./appLogger"; import AppLogger from "./appLogger";
import NewUserDiscovery from "./interactionCreate/middleware/NewUserDiscovery"; import NewUserDiscovery from "./interactionCreate/middleware/NewUserDiscovery";
import StringDropdown from "./interactionCreate/StringDropdown";
export class Events { export class Events {
public async onInteractionCreate(interaction: Interaction) { public async onInteractionCreate(interaction: Interaction) {
@ -20,11 +19,6 @@ export class Events {
AppLogger.LogVerbose("Client", `Button: ${interaction.customId}`); AppLogger.LogVerbose("Client", `Button: ${interaction.customId}`);
Button.onButtonClicked(interaction); Button.onButtonClicked(interaction);
} }
if (interaction.isStringSelectMenu()) {
AppLogger.LogVerbose("Client", `StringDropdown: ${interaction.customId}`);
StringDropdown.onStringDropdownSelected(interaction);
}
} }
// Emit when bot is logged in and ready to use // Emit when bot is logged in and ready to use

View file

@ -1,29 +0,0 @@
import {StringSelectMenuInteraction} from "discord.js";
import {CoreClient} from "../client";
import AppLogger from "../appLogger";
export default class StringDropdown {
public static async onStringDropdownSelected(interaction: StringSelectMenuInteraction) {
if (!interaction.isStringSelectMenu()) return;
const item = CoreClient.stringDropdowns.find(x => x.DropdownId == interaction.customId.split(" ")[0]);
if (!item) {
AppLogger.LogVerbose("StringDropdown", `Event not found: ${interaction.customId}`);
await interaction.reply("Event not found");
return;
}
try {
AppLogger.LogDebug("StringDropdown", `Executing ${interaction.customId}`);
item.Event.execute(interaction);
} catch (e) {
AppLogger.LogError("StringDropdown", `Error occurred while executing event: ${interaction.customId}`);
AppLogger.LogError("StringDropdown", e as string);
await interaction.reply("An error occurred while executing the event");
}
}
}

View file

@ -10,7 +10,6 @@ import path from "path";
import AppLogger from "../client/appLogger"; import AppLogger from "../client/appLogger";
import User from "../database/entities/app/User"; import User from "../database/entities/app/User";
import CardConstants from "../constants/CardConstants"; import CardConstants from "../constants/CardConstants";
import ErrorMessages from "../constants/ErrorMessages";
export default class Drop extends Command { export default class Drop extends Command {
constructor() { constructor() {
@ -23,13 +22,14 @@ export default class Drop extends Command {
public override async execute(interaction: CommandInteraction) { public override async execute(interaction: CommandInteraction) {
if (!CoreClient.AllowDrops) { if (!CoreClient.AllowDrops) {
await interaction.reply(ErrorMessages.BotSyncing); await interaction.reply("Bot is currently syncing, please wait until its done.");
return; return;
} }
if (await Config.GetValue("safemode") == "true") { if (await Config.GetValue("safemode") == "true") {
AppLogger.LogWarn("Commands/Drop", ErrorMessages.SafeMode); AppLogger.LogWarn("Commands/Drop", "Safe Mode is active, refusing to send next drop.");
await interaction.reply(ErrorMessages.SafeMode);
await interaction.reply("Safe Mode has been activated, please resync to continue.");
return; return;
} }
@ -43,15 +43,16 @@ export default class Drop extends Command {
} }
if (user.Currency < CardConstants.ClaimCost) { if (user.Currency < CardConstants.ClaimCost) {
await interaction.reply(ErrorMessages.NotEnoughCurrency(CardConstants.ClaimCost, user.Currency)); await interaction.reply(`Not enough currency! You need ${CardConstants.ClaimCost} currency, you have ${user.Currency}!`);
return; return;
} }
const randomCard = CardDropHelperMetadata.GetRandomCard(); const randomCard = CardDropHelperMetadata.GetRandomCard();
if (!randomCard) { if (!randomCard) {
AppLogger.LogWarn("Commands/Drop", ErrorMessages.UnableToFetchCard); AppLogger.LogWarn("Commands/Drop", "Unable to fetch card, please try again. (randomCard is null)");
await interaction.reply(ErrorMessages.UnableToFetchCard);
await interaction.reply("Unable to fetch card, please try again.");
return; return;
} }

View file

@ -1,82 +0,0 @@
import { AttachmentBuilder, CommandInteraction, DiscordAPIError, SlashCommandBuilder } from "discord.js";
import { Command } from "../type/command";
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";
export default class Id extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName("id")
.setDescription("View a specific command by its id")
.addStringOption(x =>
x
.setName("cardnumber")
.setDescription("The card number to view")
.setRequired(true));
}
public override async execute(interaction: CommandInteraction) {
const cardNumber = interaction.options.get("cardnumber");
AppLogger.LogSilly("Commands/View", `Parameters: cardNumber=${cardNumber?.value}`);
if (!cardNumber || !cardNumber.value) {
await interaction.reply("Card number is required.");
return;
}
const card = CoreClient.Cards
.flatMap(x => x.cards)
.find(x => x.id == cardNumber.value);
if (!card) {
await interaction.reply("Card not found.");
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 {
AppLogger.LogError("Commands/View", `Unable to fetch image for card ${card.id}.`);
await interaction.reply(`Unable to fetch image for card ${card.id}.`);
return;
}
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);
try {
await interaction.editReply({
embeds: [ embed ],
files: [ attachment ],
});
} catch (e) {
AppLogger.LogError("Commands/View", `Error sending view for card ${card.id}: ${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.");
}
}
}
}

View file

@ -47,7 +47,7 @@ export default class Inventory extends Command {
await interaction.followUp({ await interaction.followUp({
files: [ embed.image ], files: [ embed.image ],
embeds: [ embed.embed ], embeds: [ embed.embed ],
components: [ embed.row1, embed.row2 ], components: [ embed.row ],
}); });
} catch (e) { } catch (e) {
AppLogger.LogError("Commands/Inventory", e as string); AppLogger.LogError("Commands/Inventory", e as string);

View file

@ -1,87 +0,0 @@
import { AttachmentBuilder, CommandInteraction, SlashCommandBuilder } from "discord.js";
import { Command } from "../type/command";
import { CoreClient } from "../client/client";
import ErrorMessages from "../constants/ErrorMessages";
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";
export default class Multidrop extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName("multidrop")
.setDescription("Drop 11 cards for the price of 10!");
}
public override async execute(interaction: CommandInteraction) {
if (!CoreClient.AllowDrops) {
await interaction.reply(ErrorMessages.BotSyncing);
return;
}
if (await Config.GetValue("safemode") == "true") {
AppLogger.LogWarn("Commands/Multidrop", ErrorMessages.SafeMode);
await interaction.reply(ErrorMessages.SafeMode);
return;
}
let user = await User.FetchOneById(User, interaction.user.id);
if (!user) {
user = new User(interaction.user.id, CardConstants.StartingCurrency);
await user.Save(User, user);
AppLogger.LogInfo("Commands/Multidrop", `New user (${interaction.user.id}) saved to the database`);
}
if (user.Currency < CardConstants.MultidropCost) {
await interaction.reply(ErrorMessages.NotEnoughCurrency(CardConstants.MultidropCost, user.Currency));
return;
}
user.RemoveCurrency(CardConstants.MultidropCost);
await user.Save(User, user);
const randomCard = CardDropHelperMetadata.GetRandomCard();
const cardsRemaining = CardConstants.MultidropQuantity - 1;
if (!randomCard) {
AppLogger.LogWarn("Commands/Multidrop", ErrorMessages.UnableToFetchCard);
await interaction.reply(ErrorMessages.UnableToFetchCard);
return;
}
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 attachment = new AttachmentBuilder(image, { name: imageFileName });
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 row = CardDropHelperMetadata.GenerateMultidropButtons(randomCard, cardsRemaining, interaction.user.id);
await interaction.editReply({
embeds: [ embed ],
files: [ attachment ],
components: [ row ],
});
} catch (e) {
AppLogger.LogError("Commands/Multidrop", `Error sending next drop for card ${randomCard.card.id}: ${e}`);
await interaction.editReply(`Unable to send next drop. Please try again, and report this if it keeps happening. (${randomCard.card.id})`);
}
}
}

View file

@ -16,18 +16,11 @@ export default class Sacrifice extends Command {
x x
.setName("cardnumber") .setName("cardnumber")
.setDescription("The card to sacrifice from your inventory") .setDescription("The card to sacrifice from your inventory")
.setRequired(true)) .setRequired(true));
.addNumberOption(x =>
x
.setName("quantity")
.setDescription("The amount to sacrifice (default 1)"));
} }
public override async execute(interaction: CommandInteraction<CacheType>): Promise<void> { public override async execute(interaction: CommandInteraction<CacheType>): Promise<void> {
const cardnumber = interaction.options.get("cardnumber", true); const cardnumber = interaction.options.get("cardnumber", true);
const quantityInput = interaction.options.get("quantity")?.value ?? 1;
const quantity = Number(quantityInput) || 1;
const cardInInventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, cardnumber.value! as string); const cardInInventory = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, cardnumber.value! as string);
@ -36,11 +29,6 @@ export default class Sacrifice extends Command {
return; return;
} }
if (cardInInventory.Quantity < quantity) {
await interaction.reply(`You can only sacrifice what you own! You have ${cardInInventory.Quantity} of this card`);
return;
}
const cardData = CardDropHelperMetadata.GetCardByCardNumber(cardnumber.value! as string); const cardData = CardDropHelperMetadata.GetCardByCardNumber(cardnumber.value! as string);
if (!cardData) { if (!cardData) {
@ -48,7 +36,7 @@ export default class Sacrifice extends Command {
return; return;
} }
const cardValue = GetSacrificeAmount(cardData.card.type) * quantity; const cardValue = GetSacrificeAmount(cardData.card.type);
const cardRarityString = CardRarityToString(cardData.card.type); const cardRarityString = CardRarityToString(cardData.card.type);
const description = [ const description = [
@ -56,7 +44,6 @@ export default class Sacrifice extends Command {
`Series: ${cardData.series.name}`, `Series: ${cardData.series.name}`,
`Rarity: ${cardRarityString}`, `Rarity: ${cardRarityString}`,
`Quantity Owned: ${cardInInventory.Quantity}`, `Quantity Owned: ${cardInInventory.Quantity}`,
`Quantity To Sacrifice: ${quantity}`,
`Sacrifice Amount: ${cardValue}`, `Sacrifice Amount: ${cardValue}`,
]; ];
@ -69,11 +56,11 @@ export default class Sacrifice extends Command {
const row = new ActionRowBuilder<ButtonBuilder>() const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents([ .addComponents([
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`sacrifice confirm ${interaction.user.id} ${cardnumber.value!} ${quantity}`) .setCustomId(`sacrifice confirm ${interaction.user.id} ${cardnumber.value!}`)
.setLabel("Confirm") .setLabel("Confirm")
.setStyle(ButtonStyle.Success), .setStyle(ButtonStyle.Success),
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`sacrifice cancel ${interaction.user.id} ${cardnumber.value!} ${quantity}`) .setCustomId(`sacrifice cancel ${interaction.user.id} ${cardnumber.value!}`)
.setLabel("Cancel") .setLabel("Cancel")
.setStyle(ButtonStyle.Secondary), .setStyle(ButtonStyle.Secondary),
]); ]);

View file

@ -26,26 +26,13 @@ export default class Trade extends Command {
x x
.setName("receive") .setName("receive")
.setDescription("Item to receive") .setDescription("Item to receive")
.setRequired(true)) .setRequired(true));
.addNumberOption(x =>
x
.setName("givequantity")
.setDescription("Amount to give"))
.addNumberOption(x =>
x
.setName("receivequantity")
.setDescription("Amount to receive"));
} }
public override async execute(interaction: CommandInteraction) { public override async execute(interaction: CommandInteraction) {
const user = interaction.options.get("user", true).user!; const user = interaction.options.get("user", true).user!;
const give = interaction.options.get("give", true); const give = interaction.options.get("give", true);
const receive = interaction.options.get("receive", true); const receive = interaction.options.get("receive", true);
const givequantityInput = interaction.options.get("givequantity")?.value ?? 1;
const receivequantityInput = interaction.options.get("receivequantity")?.value ?? 1;
const givequantity = Number(givequantityInput) || 1;
const receivequantity = Number(receivequantityInput) || 1;
AppLogger.LogSilly("Commands/Trade", `Parameters: user=${user.id}, give=${give.value}, receive=${receive.value}`); AppLogger.LogSilly("Commands/Trade", `Parameters: user=${user.id}, give=${give.value}, receive=${receive.value}`);
@ -57,12 +44,12 @@ export default class Trade extends Command {
const user1ItemEntity = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, give.value!.toString()); const user1ItemEntity = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, give.value!.toString());
const user2ItemEntity = await Inventory.FetchOneByCardNumberAndUserId(user.id, receive.value!.toString()); const user2ItemEntity = await Inventory.FetchOneByCardNumberAndUserId(user.id, receive.value!.toString());
if (!user1ItemEntity || user1ItemEntity.Quantity < givequantity) { if (!user1ItemEntity) {
await interaction.reply("You do not have the item you are trying to trade."); await interaction.reply("You do not have the item you are trying to trade.");
return; return;
} }
if (!user2ItemEntity || user2ItemEntity.Quantity < receivequantity) { if (!user2ItemEntity) {
await interaction.reply("The user you are trying to trade with does not have the item you are trying to trade for."); await interaction.reply("The user you are trying to trade with does not have the item you are trying to trade for.");
return; return;
} }
@ -91,12 +78,12 @@ export default class Trade extends Command {
.addFields([ .addFields([
{ {
name: `${interaction.user.username} Receives`, name: `${interaction.user.username} Receives`,
value: `${user2Item.id}: ${user2Item.name} x${receivequantity}`, value: `${user2Item.id}: ${user2Item.name}`,
inline: true, inline: true,
}, },
{ {
name: `${user.username} Receives`, name: `${user.username} Receives`,
value: `${user1Item.id}: ${user1Item.name} x${givequantity}`, value: `${user1Item.id}: ${user1Item.name}`,
inline: true, inline: true,
}, },
{ {
@ -105,16 +92,16 @@ export default class Trade extends Command {
} }
]); ]);
const timeoutId = setTimeout(async () => this.autoDecline(interaction, interaction.user.username, user.username, user1Item.id, user2Item.id, user1Item.name, user2Item.name, givequantity, receivequantity), 1000 * 60 * 15); // 15 minutes const timeoutId = setTimeout(async () => this.autoDecline(interaction, interaction.user.username, user.username, user1Item.id, user2Item.id, user1Item.name, user2Item.name), 1000 * 60 * 15); // 15 minutes
const row = new ActionRowBuilder<ButtonBuilder>() const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents([ .addComponents([
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`trade accept ${interaction.user.id} ${user.id} ${user1Item.id} ${user2Item.id} ${expiry} ${timeoutId} ${givequantity} ${receivequantity}`) .setCustomId(`trade accept ${interaction.user.id} ${user.id} ${user1Item.id} ${user2Item.id} ${expiry} ${timeoutId}`)
.setLabel("Accept") .setLabel("Accept")
.setStyle(ButtonStyle.Success), .setStyle(ButtonStyle.Success),
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`trade decline ${interaction.user.id} ${user.id} ${user1Item.id} ${user2Item.id} ${expiry} ${timeoutId} ${givequantity} ${receivequantity}`) .setCustomId(`trade decline ${interaction.user.id} ${user.id} ${user1Item.id} ${user2Item.id} ${expiry} ${timeoutId}`)
.setLabel("Decline") .setLabel("Decline")
.setStyle(ButtonStyle.Danger), .setStyle(ButtonStyle.Danger),
]); ]);
@ -122,7 +109,7 @@ export default class Trade extends Command {
await interaction.reply({ content: `${user}`, embeds: [ tradeEmbed ], components: [ row ] }); await interaction.reply({ content: `${user}`, embeds: [ tradeEmbed ], components: [ row ] });
} }
private async autoDecline(interaction: CommandInteraction, user1Username: string, user2Username: string, user1CardNumber: string, user2CardNumber: string, user1CardName: string, user2CardName: string, user1Quantity: number, user2Quantity: number) { private async autoDecline(interaction: CommandInteraction, user1Username: string, user2Username: string, user1CardNumber: string, user2CardNumber: string, user1CardName: string, user2CardName: string) {
AppLogger.LogSilly("Commands/Trade/AutoDecline", `Auto declining trade between ${user1Username} and ${user2Username}`); AppLogger.LogSilly("Commands/Trade/AutoDecline", `Auto declining trade between ${user1Username} and ${user2Username}`);
const tradeEmbed = new EmbedBuilder() const tradeEmbed = new EmbedBuilder()
@ -133,12 +120,12 @@ export default class Trade extends Command {
.addFields([ .addFields([
{ {
name: `${user1Username} Receives`, name: `${user1Username} Receives`,
value: `${user2CardNumber}: ${user2CardName} x${user2Quantity}`, value: `${user2CardNumber}: ${user2CardName}`,
inline: true, inline: true,
}, },
{ {
name: `${user2Username} Receives`, name: `${user2Username} Receives`,
value: `${user1CardNumber}: ${user1CardName} x${user1Quantity}`, value: `${user1CardNumber}: ${user1CardName}`,
inline: true, inline: true,
}, },
{ {

View file

@ -1,7 +1,11 @@
import { CommandInteraction, SlashCommandBuilder } from "discord.js"; import { AttachmentBuilder, CommandInteraction, DiscordAPIError, SlashCommandBuilder } from "discord.js";
import { Command } from "../type/command"; import { Command } from "../type/command";
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 AppLogger from "../client/appLogger";
import CardSearchHelper from "../helpers/CardSearchHelper";
export default class View extends Command { export default class View extends Command {
constructor() { constructor() {
@ -9,32 +13,70 @@ export default class View extends Command {
this.CommandBuilder = new SlashCommandBuilder() this.CommandBuilder = new SlashCommandBuilder()
.setName("view") .setName("view")
.setDescription("Search for a card by its name") .setDescription("View a specific command")
.addStringOption(x => .addStringOption(x =>
x x
.setName("name") .setName("cardnumber")
.setDescription("The card name to search for") .setDescription("The card number to view")
.setRequired(true)); .setRequired(true));
} }
public override async execute(interaction: CommandInteraction) { public override async execute(interaction: CommandInteraction) {
const name = interaction.options.get("name", true); const cardNumber = interaction.options.get("cardnumber");
AppLogger.LogSilly("Commands/View", `Parameters: name=${name.value}`); AppLogger.LogSilly("Commands/View", `Parameters: cardNumber=${cardNumber?.value}`);
await interaction.deferReply(); if (!cardNumber || !cardNumber.value) {
await interaction.reply("Card number is required.");
const searchResult = await CardSearchHelper.GenerateSearchQuery(name.value!.toString(), interaction.user.id, 7);
if (!searchResult) {
await interaction.editReply("No results found");
return; return;
} }
const card = CoreClient.Cards
.flatMap(x => x.cards)
.find(x => x.id == cardNumber.value);
if (!card) {
await interaction.reply("Card not found.");
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 {
AppLogger.LogError("Commands/View", `Unable to fetch image for card ${card.id}.`);
await interaction.reply(`Unable to fetch image for card ${card.id}.`);
return;
}
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);
try {
await interaction.editReply({ await interaction.editReply({
embeds: [ searchResult.embed ], embeds: [ embed ],
components: [ searchResult.row ], files: [ attachment ],
files: [ searchResult.attachment ],
}); });
} catch (e) {
AppLogger.LogError("Commands/View", `Error sending view for card ${card.id}: ${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.");
}
}
} }
} }

View file

@ -3,8 +3,4 @@ export default class CardConstants {
public static readonly TimerGiveAmount = 10; public static readonly TimerGiveAmount = 10;
public static readonly DailyCurrency = 100; public static readonly DailyCurrency = 100;
public static readonly StartingCurrency = 300; public static readonly StartingCurrency = 300;
// Multidrop
public static readonly MultidropCost = this.ClaimCost * 10;
public static readonly MultidropQuantity = 11;
} }

View file

@ -1,8 +0,0 @@
export default class ErrorMessages {
public static readonly BotSyncing = "Bot is currently syncing, please wait until its done.";
public static readonly SafeMode = "Safe Mode has been activated, please resync to continue.";
public static readonly UnableToFetchCard = "Unable to fetch card, please try again.";
public static readonly UnableToFetchUser = "Unable to fetch user, please try again.";
public static readonly NotEnoughCurrency = (need: number, have: number) => `Not enough currency! You need ${need} currency, you have ${have}!`;
}

View file

@ -1,10 +0,0 @@
import {Environment} from "../constants/Environment";
import {StringDropdownEvent} from "../type/stringDropdownEvent";
interface StringDropdownEventItem {
DropdownId: string,
Event: StringDropdownEvent,
Environment: Environment,
}
export default StringDropdownEventItem;

View file

@ -29,16 +29,16 @@ export default class Inventory extends AppBaseEntity {
this.Quantity = quantity; this.Quantity = quantity;
} }
public AddQuantity(amount: number) {
this.Quantity += amount;
}
public RemoveQuantity(amount: number) { public RemoveQuantity(amount: number) {
if (this.Quantity < amount) return; if (this.Quantity < amount) return;
this.Quantity -= amount; this.Quantity -= amount;
} }
public AddQuantity(amount: number) {
this.Quantity += amount;
}
public AddClaim(claim: Claim) { public AddClaim(claim: Claim) {
this.Claims.push(claim); this.Claims.push(claim);
} }

View file

@ -13,7 +13,7 @@ export default class User extends AppBaseEntity {
@Column() @Column()
Currency: number; Currency: number;
@Column({ nullable: true }) @Column()
LastUsedDaily?: Date; LastUsedDaily?: Date;
public AddCurrency(amount: number) { public AddCurrency(amount: number) {

View file

@ -1,5 +1,5 @@
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js"; import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
import { CardRarity, CardRarityToColour, CardRarityToString, GetSacrificeAmount } from "../constants/CardRarity"; import { CardRarity, CardRarityToColour, CardRarityToString } from "../constants/CardRarity";
import CardRarityChances from "../constants/CardRarityChances"; import CardRarityChances from "../constants/CardRarityChances";
import { DropResult } from "../contracts/SeriesMetadata"; import { DropResult } from "../contracts/SeriesMetadata";
import { CoreClient } from "../client/client"; import { CoreClient } from "../client/client";
@ -149,26 +149,4 @@ export default class CardDropHelperMetadata {
.setLabel("Reroll") .setLabel("Reroll")
.setStyle(ButtonStyle.Secondary)); .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<ButtonBuilder> {
return new ActionRowBuilder<ButtonBuilder>()
.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));
}
} }

View file

@ -1,117 +0,0 @@
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";
interface ReturnedPage {
embed: EmbedBuilder,
row: ActionRowBuilder<ButtonBuilder>,
attachment: AttachmentBuilder,
results: string[],
}
export default class CardSearchHelper {
public static async GenerateSearchQuery(query: string, userid: string, pages: number): Promise<ReturnedPage | undefined> {
AppLogger.LogSilly("CardSearchHelper/GenerateSearchQuery", `Parameters: query=${query}, userid=${userid}, pages=${pages}`);
const fzf = new Fuse(CoreClient.Cards.flatMap(x => x.cards), { keys: ["name"] });
const entries = fzf.search(query)
.splice(0, pages);
const entry = entries[0];
const results = entries
.flatMap(x => x.item.id);
if (!entry) {
AppLogger.LogVerbose("CardSearchHelper/GenerateSearchQuery", `Unable to find entry: ${query}`);
return undefined;
}
const card = CardDropHelperMetadata.GetCardByCardNumber(entry.item.id);
if (!card) return undefined;
let image: Buffer;
const imageFileName = card.card.path.split("/").pop()!;
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;
}
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 row = new ActionRowBuilder<ButtonBuilder>()
.addComponents(
new ButtonBuilder()
.setCustomId(`view 0 ${results.join(" ")}`)
.setLabel("Previous")
.setStyle(ButtonStyle.Primary)
.setDisabled(true),
new ButtonBuilder()
.setCustomId(`view 2 ${results.join(" ")}`)
.setLabel("Next")
.setStyle(ButtonStyle.Primary)
.setDisabled(pages == 1));
return { embed, row, attachment, results };
}
public static async GenerateSearchPageFromQuery(results: string[], userid: string, page: number): Promise<ReturnedPage | undefined> {
const currentPageId = results[page - 1];
const card = CardDropHelperMetadata.GetCardByCardNumber(currentPageId);
if (!card) {
AppLogger.LogError("CardSearchHelper/GenerateSearchPageFromQuery", `Unable to find card by id: ${currentPageId}.`);
return undefined;
}
let image: Buffer;
const imageFileName = card.card.path.split("/").pop()!;
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}.`);
return undefined;
}
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 row = new ActionRowBuilder<ButtonBuilder>()
.addComponents(
new ButtonBuilder()
.setCustomId(`view ${page - 1} ${results.join(" ")}`)
.setLabel("Previous")
.setStyle(ButtonStyle.Primary)
.setDisabled(page - 1 == 0),
new ButtonBuilder()
.setCustomId(`view ${page + 1} ${results.join(" ")}`)
.setLabel("Next")
.setStyle(ButtonStyle.Primary)
.setDisabled(page == results.length));
return { embed, row, attachment, results };
}
}

View file

@ -1,4 +1,4 @@
import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from "discord.js"; import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
import Inventory from "../database/entities/app/Inventory"; import Inventory from "../database/entities/app/Inventory";
import { CoreClient } from "../client/client"; import { CoreClient } from "../client/client";
import EmbedColours from "../constants/EmbedColours"; import EmbedColours from "../constants/EmbedColours";
@ -24,8 +24,7 @@ interface InventoryPageCards {
interface ReturnedInventoryPage { interface ReturnedInventoryPage {
embed: EmbedBuilder, embed: EmbedBuilder,
row1: ActionRowBuilder<ButtonBuilder>, row: ActionRowBuilder<ButtonBuilder>,
row2: ActionRowBuilder<StringSelectMenuBuilder>,
image: AttachmentBuilder, image: AttachmentBuilder,
} }
@ -100,7 +99,7 @@ export default class InventoryHelper {
.setColor(EmbedColours.Ok) .setColor(EmbedColours.Ok)
.setImage("attachment://page.png"); .setImage("attachment://page.png");
const row1 = new ActionRowBuilder<ButtonBuilder>() const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`inventory ${userid} ${page - 1}`) .setCustomId(`inventory ${userid} ${page - 1}`)
@ -113,23 +112,9 @@ export default class InventoryHelper {
.setStyle(ButtonStyle.Primary) .setStyle(ButtonStyle.Primary)
.setDisabled(page + 1 == pages.length)); .setDisabled(page + 1 == pages.length));
let pageNum = 0;
const row2 = new ActionRowBuilder<StringSelectMenuBuilder>()
.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++}`))));
const buffer = await ImageHelper.GenerateCardImageGrid(currentPage.cards.map(x => ({ id: x.id, path: x.path }))); const buffer = await ImageHelper.GenerateCardImageGrid(currentPage.cards.map(x => ({ id: x.id, path: x.path })));
const image = new AttachmentBuilder(buffer, { name: "page.png" }); const image = new AttachmentBuilder(buffer, { name: "page.png" });
return { embed, row1, row2, image }; return { embed, row, image };
} }
} }

View file

@ -9,9 +9,7 @@ import Daily from "./commands/daily";
import Drop from "./commands/drop"; import Drop from "./commands/drop";
import Gdrivesync from "./commands/gdrivesync"; import Gdrivesync from "./commands/gdrivesync";
import Give from "./commands/give"; import Give from "./commands/give";
import Id from "./commands/id";
import Inventory from "./commands/inventory"; import Inventory from "./commands/inventory";
import Multidrop from "./commands/multidrop";
import Resync from "./commands/resync"; import Resync from "./commands/resync";
import Sacrifice from "./commands/sacrifice"; import Sacrifice from "./commands/sacrifice";
import Series from "./commands/series"; import Series from "./commands/series";
@ -26,15 +24,10 @@ import Droprarity from "./commands/stage/droprarity";
// Button Event Imports // Button Event Imports
import Claim from "./buttonEvents/Claim"; import Claim from "./buttonEvents/Claim";
import InventoryButtonEvent from "./buttonEvents/Inventory"; import InventoryButtonEvent from "./buttonEvents/Inventory";
import MultidropButtonEvent from "./buttonEvents/Multidrop";
import Reroll from "./buttonEvents/Reroll"; import Reroll from "./buttonEvents/Reroll";
import SacrificeButtonEvent from "./buttonEvents/Sacrifice"; import SacrificeButtonEvent from "./buttonEvents/Sacrifice";
import SeriesEvent from "./buttonEvents/Series"; import SeriesEvent from "./buttonEvents/Series";
import TradeButtonEvent from "./buttonEvents/Trade"; import TradeButtonEvent from "./buttonEvents/Trade";
import ViewButtonEvent from "./buttonEvents/View";
// String Dropdown Event Imports
import InventoryStringDropdown from "./stringDropdowns/Inventory";
export default class Registry { export default class Registry {
public static RegisterCommands() { public static RegisterCommands() {
@ -46,9 +39,7 @@ export default class Registry {
CoreClient.RegisterCommand("drop", new Drop()); CoreClient.RegisterCommand("drop", new Drop());
CoreClient.RegisterCommand("gdrivesync", new Gdrivesync()); CoreClient.RegisterCommand("gdrivesync", new Gdrivesync());
CoreClient.RegisterCommand("give", new Give()); CoreClient.RegisterCommand("give", new Give());
CoreClient.RegisterCommand("id", new Id());
CoreClient.RegisterCommand("inventory", new Inventory()); CoreClient.RegisterCommand("inventory", new Inventory());
CoreClient.RegisterCommand("multidrop", new Multidrop());
CoreClient.RegisterCommand("resync", new Resync()); CoreClient.RegisterCommand("resync", new Resync());
CoreClient.RegisterCommand("sacrifice", new Sacrifice()); CoreClient.RegisterCommand("sacrifice", new Sacrifice());
CoreClient.RegisterCommand("series", new Series()); CoreClient.RegisterCommand("series", new Series());
@ -64,15 +55,9 @@ export default class Registry {
public static RegisterButtonEvents() { public static RegisterButtonEvents() {
CoreClient.RegisterButtonEvent("claim", new Claim()); CoreClient.RegisterButtonEvent("claim", new Claim());
CoreClient.RegisterButtonEvent("inventory", new InventoryButtonEvent()); CoreClient.RegisterButtonEvent("inventory", new InventoryButtonEvent());
CoreClient.RegisterButtonEvent("multidrop", new MultidropButtonEvent());
CoreClient.RegisterButtonEvent("reroll", new Reroll()); CoreClient.RegisterButtonEvent("reroll", new Reroll());
CoreClient.RegisterButtonEvent("sacrifice", new SacrificeButtonEvent()); CoreClient.RegisterButtonEvent("sacrifice", new SacrificeButtonEvent());
CoreClient.RegisterButtonEvent("series", new SeriesEvent()); CoreClient.RegisterButtonEvent("series", new SeriesEvent());
CoreClient.RegisterButtonEvent("trade", new TradeButtonEvent()); CoreClient.RegisterButtonEvent("trade", new TradeButtonEvent());
CoreClient.RegisterButtonEvent("view", new ViewButtonEvent());
}
public static RegisterStringDropdownEvents() {
CoreClient.RegisterStringDropdownEvent("inventory", new InventoryStringDropdown());
} }
} }

View file

@ -1,43 +0,0 @@
import {StringSelectMenuInteraction} from "discord.js";
import {StringDropdownEvent} from "../type/stringDropdownEvent";
import AppLogger from "../client/appLogger";
import InventoryHelper from "../helpers/InventoryHelper";
export default class Inventory extends StringDropdownEvent {
public override async execute(interaction: StringSelectMenuInteraction) {
if (!interaction.guild) return;
const userid = interaction.values[0].split(" ")[0];
const page = interaction.values[0].split(" ")[1];
AppLogger.LogDebug("StringDropdown/Inventory", `Parameters: userid=${userid}, page=${page}`);
await interaction.deferUpdate();
const member = interaction.guild.members.cache.find(x => x.id == userid) || await interaction.guild.members.fetch(userid);
if (!member) {
await interaction.reply("Unable to find user.");
return;
}
try {
const embed = await InventoryHelper.GenerateInventoryPage(member.user.username, member.user.id, Number(page));
if (!embed) {
await interaction.followUp("No page for user found.");
return;
}
await interaction.editReply({
files: [ embed.image ],
embeds: [ embed.embed ],
components: [ embed.row1, embed.row2 ],
});
} catch (e) {
AppLogger.LogError("StringDropdown/Inventory", `Error generating inventory page for ${member.user.username} with id ${member.user.id}: ${e}`);
await interaction.followUp("An error has occurred running this command.");
}
}
}

View file

@ -1,5 +0,0 @@
import {StringSelectMenuInteraction} from "discord.js";
export abstract class StringDropdownEvent {
abstract execute(interaction: StringSelectMenuInteraction): Promise<void>;
}

553
yarn.lock

File diff suppressed because it is too large Load diff