Compare commits

..

2 commits

Author SHA1 Message Date
d9d0243c3c Use node 20
All checks were successful
Deploy To Production / build (push) Successful in 39s
Deploy To Production / deploy (push) Successful in 15s
2024-12-10 11:07:55 +00:00
c4abf21013 Use rsync -rvzP 2024-12-10 10:39:24 +00:00
47 changed files with 2106 additions and 2804 deletions

View file

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

45
.eslintrc.json Normal file
View file

@ -0,0 +1,45 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
"indent": [
"error",
4
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"double"
],
"semi": [
"error",
"always"
]
},
"globals": {
"jest": true,
"require": true,
"exports": true,
"process": true
},
"ignorePatterns": [
"dist/**/*"
]
}

View file

@ -23,14 +23,14 @@ 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
needs: build
runs-on: node
steps:
- uses: https://github.com/appleboy/ssh-action@v1.1.0
- uses: https://github.com/appleboy/ssh-action@v1.0.3
env:
DB_NAME: ${{ secrets.PROD_DB_NAME }}
DB_AUTH_USER: ${{ secrets.PROD_DB_AUTH_USER }}

View file

@ -23,14 +23,14 @@ 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
needs: build
runs-on: node
steps:
- uses: https://github.com/appleboy/ssh-action@v1.1.0
- uses: https://github.com/appleboy/ssh-action@v1.0.3
env:
DB_NAME: ${{ secrets.STAGE_DB_NAME }}
DB_AUTH_USER: ${{ secrets.STAGE_DB_AUTH_USER }}

View file

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

View file

@ -1 +0,0 @@
DROP TABLE `user_effect`;

View file

@ -1,10 +0,0 @@
CREATE TABLE `user_effect` (
`Id` varchar(255) NOT NULL,
`WhenCreated` datetime NOT NULL,
`WhenUpdated` datetime NOT NULL,
`Name` varchar(255) NOT NULL,
`UserId` varchar(255) NOT NULL,
`Unused` int NOT NULL DEFAULT 0,
`WhenExpires` datetime NULL,
PRIMARY KEY (`Id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

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

@ -1,55 +0,0 @@
import js from "@eslint/js";
import ts from "typescript-eslint";
export default [
{
ignores: [
"**/dist/",
"eslint.config.mjs",
"jest.config.cjs",
"jest.setup.js",
"**/.temp/**/*"
],
},
js.configs.recommended,
...ts.configs.recommended,
{
languageOptions: {
globals: {
exports: "writable",
module: "writable",
require: "writable",
process: "writable",
console: "writable",
jest: "writable",
},
ecmaVersion: 6,
sourceType: "script",
},
files: [
"./src",
"./tests"
],
rules: {
camelcase: "error",
"brace-style": ["error", "1tbs"],
"comma-dangle": ["error", "never"],
"comma-spacing": ["error", {
before: false,
after: true,
}],
"comma-style": ["error", "last"],
"arrow-body-style": ["error", "as-needed"],
"arrow-parens": ["error", "as-needed"],
"arrow-spacing": "error",
"no-var": "error",
"prefer-template": "error",
"prefer-const": "error",
},
}
];

View file

@ -1,4 +1,3 @@
jest.setTimeout(1 * 1000); // 1 second
jest.resetModules();
jest.resetAllMocks();
jest.useFakeTimers();
jest.resetAllMocks();

View file

@ -7,7 +7,7 @@
"clean": "rm -rf node_modules/ dist/",
"build": "tsc",
"start": "node ./dist/bot.js",
"test": "jest",
"test": "echo true",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"db:up": "typeorm migration:run -d dist/database/dataSources/appDataSource.js",
@ -27,36 +27,37 @@
"dependencies": {
"@discordjs/rest": "^2.0.0",
"@types/clone-deep": "^4.0.4",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/uuid": "^10.0.0",
"@types/express": "^4.17.20",
"@types/jest": "^29.0.0",
"@types/uuid": "^9.0.0",
"body-parser": "^1.20.2",
"canvas": "^2.11.2",
"clone-deep": "^4.0.1",
"cron": "^3.1.7",
"discord.js": "^14.16.3",
"discord.js": "^14.15.3",
"dotenv": "^16.0.0",
"express": "^4.18.2",
"fuse.js": "^7.0.0",
"glob": "^11.0.0",
"glob": "^10.3.10",
"jest": "^29.0.0",
"jest-mock-extended": "^3.0.0",
"jimp": "^1.6.0",
"jimp": "^0.22.12",
"minimatch": "9.0.5",
"mysql": "^2.18.1",
"ts-jest": "^29.0.0",
"typeorm": "0.3.20",
"winston": "^3.15.0",
"winston": "^3.11.0",
"winston-daily-rotate-file": "^5.0.0",
"winston-discord-transport": "^1.3.0"
},
"resolutions": {},
"resolutions": {
"**/ws": "^8.17.1"
},
"devDependencies": {
"@types/node": "^22.8.1",
"@typescript-eslint/eslint-plugin": "^8.11.0",
"@typescript-eslint/parser": "^8.11.0",
"eslint": "^9.13.0",
"np": "^10.0.7",
"typescript": "^5.0.0",
"typescript-eslint": "^8.11.0"
"@types/node": "^20.0.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^6.16.0",
"eslint": "^8.56.0",
"np": "^9.0.0",
"typescript": "^5.0.0"
}
}

View file

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

View file

@ -12,7 +12,6 @@ export default class Claim extends ButtonEvent {
public override async execute(interaction: ButtonInteraction) {
if (!interaction.guild || !interaction.guildId) return;
if (!interaction.channel) return;
if (!interaction.channel.isSendable()) return;
await interaction.deferUpdate();
@ -59,7 +58,7 @@ export default class Claim extends ButtonEvent {
if (!inventory) {
inventory = new Inventory(userId, cardNumber, 1);
} else {
inventory.AddQuantity(1);
inventory.SetQuantity(inventory.Quantity + 1);
}
await inventory.Save(Inventory, inventory);

View file

@ -11,7 +11,7 @@ export default class Inventory extends ButtonEvent {
const page = interaction.customId.split(" ")[2];
AppLogger.LogSilly("Button/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);
@ -34,7 +34,7 @@ export default class Inventory extends ButtonEvent {
await interaction.editReply({
files: [ embed.image ],
embeds: [ embed.embed ],
components: [ embed.row1, embed.row2 ],
components: [ embed.row ],
});
} catch (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) {
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.");
@ -32,16 +31,11 @@ export default class Sacrifice extends ButtonEvent {
const cardInInventory = await Inventory.FetchOneByCardNumberAndUserId(userId, cardNumber);
if (!cardInInventory || cardInInventory.Quantity == 0) {
if (!cardInInventory) {
await interaction.reply("Unable to find card in inventory.");
return;
}
if (cardInInventory.Quantity < quantity) {
await interaction.reply("You can only sacrifice what you own.");
return;
}
const cardData = CardDropHelperMetadata.GetCardByCardNumber(cardNumber);
if (!cardData) {
@ -56,11 +50,11 @@ export default class Sacrifice extends ButtonEvent {
return;
}
cardInInventory.RemoveQuantity(quantity);
cardInInventory.RemoveQuantity(1);
await cardInInventory.Save(Inventory, cardInInventory);
const cardValue = GetSacrificeAmount(cardData.card.type) * quantity;
const cardValue = GetSacrificeAmount(cardData.card.type);
const cardRarityString = CardRarityToString(cardData.card.type);
user.AddCurrency(cardValue);
@ -72,7 +66,6 @@ export default class Sacrifice extends ButtonEvent {
`Series: ${cardData.series.name}`,
`Rarity: ${cardRarityString}`,
`Quantity Owned: ${cardInInventory.Quantity}`,
`Quantity To Sacrifice: ${quantity}`,
`Sacrifice Amount: ${cardValue}`,
];
@ -105,7 +98,6 @@ export default class Sacrifice extends ButtonEvent {
private async cancel(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 cancel it.");
@ -114,16 +106,11 @@ export default class Sacrifice extends ButtonEvent {
const cardInInventory = await Inventory.FetchOneByCardNumberAndUserId(userId, cardNumber);
if (!cardInInventory || cardInInventory.Quantity == 0) {
if (!cardInInventory) {
await interaction.reply("Unable to find card in inventory.");
return;
}
if (cardInInventory.Quantity < quantity) {
await interaction.reply("You can only sacrifice what you own.");
return;
}
const cardData = CardDropHelperMetadata.GetCardByCardNumber(cardNumber);
if (!cardData) {
@ -131,7 +118,7 @@ export default class Sacrifice extends ButtonEvent {
return;
}
const cardValue = GetSacrificeAmount(cardData.card.type) * quantity;
const cardValue = GetSacrificeAmount(cardData.card.type);
const cardRarityString = CardRarityToString(cardData.card.type);
const description = [
@ -139,7 +126,6 @@ export default class Sacrifice extends ButtonEvent {
`Series: ${cardData.series.name}`,
`Rarity: ${cardRarityString}`,
`Quantity Owned: ${cardInInventory.Quantity}`,
`Quantity To Sacrifice: ${quantity}`,
`Sacrifice Amount: ${cardValue}`,
];

View file

@ -28,10 +28,8 @@ export default class Trade extends ButtonEvent {
const user2CardNumber = interaction.customId.split(" ")[5];
const expiry = interaction.customId.split(" ")[6];
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);
@ -69,13 +67,13 @@ export default class Trade extends ButtonEvent {
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.");
return;
}
user1UserInventory1.RemoveQuantity(user1Quantity);
user2UserInventory1.RemoveQuantity(user2Quantity);
user1UserInventory1.RemoveQuantity(1);
user2UserInventory1.RemoveQuantity(1);
await user1UserInventory1.Save(Inventory, user1UserInventory1);
await user2UserInventory1.Save(Inventory, user2UserInventory1);
@ -84,15 +82,15 @@ export default class Trade extends ButtonEvent {
let user2UserInventory2 = await Inventory.FetchOneByCardNumberAndUserId(user2UserId, user1CardNumber);
if (!user1UserInventory2) {
user1UserInventory2 = new Inventory(user1UserId, user2CardNumber, user2Quantity);
user1UserInventory2 = new Inventory(user1UserId, user2CardNumber, 1);
} else {
user1UserInventory2.AddQuantity(user2Quantity);
user1UserInventory2.AddQuantity(1);
}
if (!user2UserInventory2) {
user2UserInventory2 = new Inventory(user2UserId, user1CardNumber, user1Quantity);
user2UserInventory2 = new Inventory(user2UserId, user1CardNumber, 1);
} else {
user2UserInventory2.AddQuantity(user1Quantity);
user2UserInventory2.AddQuantity(1);
}
await user1UserInventory2.Save(Inventory, user1UserInventory2);
@ -108,12 +106,12 @@ export default class Trade extends ButtonEvent {
.addFields([
{
name: `${user1User.username} Receives`,
value: `${user2Item.id}: ${user2Item.name} x${user2Quantity}`,
value: `${user2Item.id}: ${user2Item.name}`,
inline: true,
},
{
name: `${user2User.username} Receives`,
value: `${user1Item.id}: ${user1Item.name} x${user1Quantity}`,
value: `${user1Item.id}: ${user1Item.name}`,
inline: true,
},
{
@ -146,8 +144,6 @@ export default class Trade extends ButtonEvent {
const user2CardNumber = interaction.customId.split(" ")[5];
// No need to get expiry date
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}`);
@ -182,12 +178,12 @@ export default class Trade extends ButtonEvent {
.addFields([
{
name: `${user1User.username} Receives`,
value: `${user2Item.id}: ${user2Item.name} x${user2Quantity}`,
value: `${user2Item.id}: ${user2Item.name}`,
inline: true,
},
{
name: `${user2User.username} Receives`,
value: `${user1Item.id}: ${user1Item.name} x${user1Quantity}`,
value: `${user1Item.id}: ${user1Item.name}`,
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 GiveCurrency from "../timers/GiveCurrency";
import PurgeClaims from "../timers/PurgeClaims";
import StringDropdownEventItem from "../contracts/StringDropdownEventItem";
import {StringDropdownEvent} from "../type/stringDropdownEvent";
export class CoreClient extends Client {
private static _commandItems: ICommandItem[];
private static _eventExecutors: EventExecutors;
private static _buttonEvents: IButtonEventItem[];
private static _stringDropdowns: StringDropdownEventItem[];
private _events: Events;
private _util: Util;
@ -48,10 +45,6 @@ export class CoreClient extends Client {
return this._buttonEvents;
}
public static get stringDropdowns(): StringDropdownEventItem[] {
return this._stringDropdowns;
}
constructor(intents: number[]) {
super({ intents: intents });
dotenv.config();
@ -66,7 +59,6 @@ export class CoreClient extends Client {
CoreClient._commandItems = [];
CoreClient._buttonEvents = [];
CoreClient._stringDropdowns = [];
this._events = new Events();
this._util = new Util();
@ -416,19 +408,4 @@ export class CoreClient extends Client {
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 AppLogger from "./appLogger";
import NewUserDiscovery from "./interactionCreate/middleware/NewUserDiscovery";
import StringDropdown from "./interactionCreate/StringDropdown";
export class Events {
public async onInteractionCreate(interaction: Interaction) {
@ -20,11 +19,6 @@ export class Events {
AppLogger.LogVerbose("Client", `Button: ${interaction.customId}`);
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

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 User from "../database/entities/app/User";
import CardConstants from "../constants/CardConstants";
import ErrorMessages from "../constants/ErrorMessages";
export default class Drop extends Command {
constructor() {
@ -23,13 +22,14 @@ export default class Drop extends Command {
public override async execute(interaction: CommandInteraction) {
if (!CoreClient.AllowDrops) {
await interaction.reply(ErrorMessages.BotSyncing);
await interaction.reply("Bot is currently syncing, please wait until its done.");
return;
}
if (await Config.GetValue("safemode") == "true") {
AppLogger.LogWarn("Commands/Drop", ErrorMessages.SafeMode);
await interaction.reply(ErrorMessages.SafeMode);
AppLogger.LogWarn("Commands/Drop", "Safe Mode is active, refusing to send next drop.");
await interaction.reply("Safe Mode has been activated, please resync to continue.");
return;
}
@ -43,15 +43,16 @@ export default class Drop extends Command {
}
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;
}
const randomCard = CardDropHelperMetadata.GetRandomCard();
if (!randomCard) {
AppLogger.LogWarn("Commands/Drop", ErrorMessages.UnableToFetchCard);
await interaction.reply(ErrorMessages.UnableToFetchCard);
AppLogger.LogWarn("Commands/Drop", "Unable to fetch card, please try again. (randomCard is null)");
await interaction.reply("Unable to fetch card, please try again.");
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({
files: [ embed.image ],
embeds: [ embed.embed ],
components: [ embed.row1, embed.row2 ],
components: [ embed.row ],
});
} catch (e) {
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
.setName("cardnumber")
.setDescription("The card to sacrifice from your inventory")
.setRequired(true))
.addNumberOption(x =>
x
.setName("quantity")
.setDescription("The amount to sacrifice (default 1)"));
.setRequired(true));
}
public override async execute(interaction: CommandInteraction<CacheType>): Promise<void> {
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);
@ -36,11 +29,6 @@ export default class Sacrifice extends Command {
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);
if (!cardData) {
@ -48,7 +36,7 @@ export default class Sacrifice extends Command {
return;
}
const cardValue = GetSacrificeAmount(cardData.card.type) * quantity;
const cardValue = GetSacrificeAmount(cardData.card.type);
const cardRarityString = CardRarityToString(cardData.card.type);
const description = [
@ -56,7 +44,6 @@ export default class Sacrifice extends Command {
`Series: ${cardData.series.name}`,
`Rarity: ${cardRarityString}`,
`Quantity Owned: ${cardInInventory.Quantity}`,
`Quantity To Sacrifice: ${quantity}`,
`Sacrifice Amount: ${cardValue}`,
];
@ -69,11 +56,11 @@ export default class Sacrifice extends Command {
const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents([
new ButtonBuilder()
.setCustomId(`sacrifice confirm ${interaction.user.id} ${cardnumber.value!} ${quantity}`)
.setCustomId(`sacrifice confirm ${interaction.user.id} ${cardnumber.value!}`)
.setLabel("Confirm")
.setStyle(ButtonStyle.Success),
new ButtonBuilder()
.setCustomId(`sacrifice cancel ${interaction.user.id} ${cardnumber.value!} ${quantity}`)
.setCustomId(`sacrifice cancel ${interaction.user.id} ${cardnumber.value!}`)
.setLabel("Cancel")
.setStyle(ButtonStyle.Secondary),
]);

View file

@ -26,26 +26,13 @@ export default class Trade extends Command {
x
.setName("receive")
.setDescription("Item to receive")
.setRequired(true))
.addNumberOption(x =>
x
.setName("givequantity")
.setDescription("Amount to give"))
.addNumberOption(x =>
x
.setName("receivequantity")
.setDescription("Amount to receive"));
.setRequired(true));
}
public override async execute(interaction: CommandInteraction) {
const user = interaction.options.get("user", true).user!;
const give = interaction.options.get("give", 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}`);
@ -57,12 +44,12 @@ export default class Trade extends Command {
const user1ItemEntity = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, give.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.");
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.");
return;
}
@ -91,12 +78,12 @@ export default class Trade extends Command {
.addFields([
{
name: `${interaction.user.username} Receives`,
value: `${user2Item.id}: ${user2Item.name} x${receivequantity}`,
value: `${user2Item.id}: ${user2Item.name}`,
inline: true,
},
{
name: `${user.username} Receives`,
value: `${user1Item.id}: ${user1Item.name} x${givequantity}`,
value: `${user1Item.id}: ${user1Item.name}`,
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>()
.addComponents([
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")
.setStyle(ButtonStyle.Success),
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")
.setStyle(ButtonStyle.Danger),
]);
@ -122,7 +109,7 @@ export default class Trade extends Command {
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}`);
const tradeEmbed = new EmbedBuilder()
@ -133,12 +120,12 @@ export default class Trade extends Command {
.addFields([
{
name: `${user1Username} Receives`,
value: `${user2CardNumber}: ${user2CardName} x${user2Quantity}`,
value: `${user2CardNumber}: ${user2CardName}`,
inline: true,
},
{
name: `${user2Username} Receives`,
value: `${user1CardNumber}: ${user1CardName} x${user1Quantity}`,
value: `${user1CardNumber}: ${user1CardName}`,
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 { 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 CardSearchHelper from "../helpers/CardSearchHelper";
export default class View extends Command {
constructor() {
@ -9,32 +13,70 @@ export default class View extends Command {
this.CommandBuilder = new SlashCommandBuilder()
.setName("view")
.setDescription("Search for a card by its name")
.setDescription("View a specific command")
.addStringOption(x =>
x
.setName("name")
.setDescription("The card name to search for")
.setName("cardnumber")
.setDescription("The card number to view")
.setRequired(true));
}
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();
const searchResult = await CardSearchHelper.GenerateSearchQuery(name.value!.toString(), interaction.user.id, 7);
if (!searchResult) {
await interaction.editReply("No results found");
if (!cardNumber || !cardNumber.value) {
await interaction.reply("Card number is required.");
return;
}
await interaction.editReply({
embeds: [ searchResult.embed ],
components: [ searchResult.row ],
files: [ searchResult.attachment ],
});
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

@ -3,8 +3,4 @@ export default class CardConstants {
public static readonly TimerGiveAmount = 10;
public static readonly DailyCurrency = 100;
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;
}
public AddQuantity(amount: number) {
this.Quantity += amount;
}
public RemoveQuantity(amount: number) {
if (this.Quantity < amount) return;
this.Quantity -= amount;
}
public AddQuantity(amount: number) {
this.Quantity += amount;
}
public AddClaim(claim: Claim) {
this.Claims.push(claim);
}

View file

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

View file

@ -1,60 +0,0 @@
import {Column, Entity} from "typeorm";
import AppBaseEntity from "../../../contracts/AppBaseEntity";
import AppDataSource from "../../dataSources/appDataSource";
@Entity()
export default class UserEffect extends AppBaseEntity {
constructor(name: string, userId: string, unused: number, WhenExpires?: Date) {
super();
this.Name = name;
this.UserId = userId;
this.Unused = unused;
this.WhenExpires = WhenExpires;
}
@Column()
Name: string;
@Column()
UserId: string;
@Column()
Unused: number;
@Column({ nullable: true })
WhenExpires?: Date;
public AddUnused(amount: number) {
this.Unused += amount;
}
public UseEffect(whenExpires: Date): boolean {
if (this.Unused == 0) {
return false;
}
this.Unused -= 1;
this.WhenExpires = whenExpires;
return true;
}
public IsEffectActive(): boolean {
const now = new Date();
if (this.WhenExpires && now < this.WhenExpires) {
return true;
}
return false;
}
public static async FetchOneByUserIdAndName(userId: string, name: string): Promise<UserEffect | null> {
const repository = AppDataSource.getRepository(UserEffect);
const single = await repository.findOne({ where: { UserId: userId, Name: name } });
return single;
}
}

View file

@ -1,18 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
import MigrationHelper from "../../../../helpers/MigrationHelper";
export class CreateUserEffect1729962056556 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
MigrationHelper.Up("1729962056556-createUserEffect", "0.9", [
"01-table-userEffect",
], queryRunner);
}
public async down(queryRunner: QueryRunner): Promise<void> {
MigrationHelper.Down("1729962056556-createUserEffect", "0.9", [
"01-table-userEffect",
], queryRunner);
}
}

View file

@ -1,5 +1,5 @@
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 { DropResult } from "../contracts/SeriesMetadata";
import { CoreClient } from "../client/client";
@ -89,7 +89,7 @@ export default class CardDropHelperMetadata {
const hexCode = Number("0x" + drop.card.colour);
if (hexCode) {
colour = hexCode;
colour = hexCode;
} else {
AppLogger.LogWarn("CardDropHelperMetadata/GenerateDropEmbed", `Card's colour override is invalid: ${drop.card.id}, ${drop.card.colour}`);
}
@ -149,26 +149,4 @@ export default class CardDropHelperMetadata {
.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<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,49 +0,0 @@
import UserEffect from "../database/entities/app/UserEffect";
export default class EffectHelper {
public static async AddEffectToUserInventory(userId: string, name: string, quantity: number = 1) {
let effect = await UserEffect.FetchOneByUserIdAndName(userId, name);
if (!effect) {
effect = new UserEffect(name, userId, quantity);
} else {
effect.AddUnused(quantity);
}
await effect.Save(UserEffect, effect);
}
public static async UseEffect(userId: string, name: string, whenExpires: Date): Promise<boolean> {
const effect = await UserEffect.FetchOneByUserIdAndName(userId, name);
const now = new Date();
if (!effect || effect.Unused == 0) {
return false;
}
if (effect.WhenExpires && now < effect.WhenExpires) {
return false;
}
effect.UseEffect(whenExpires);
await effect.Save(UserEffect, effect);
return true;
}
public static async HasEffect(userId: string, name: string): Promise<boolean> {
const effect = await UserEffect.FetchOneByUserIdAndName(userId, name);
const now = new Date();
if (!effect || !effect.WhenExpires) {
return false;
}
if (now > effect.WhenExpires) {
return false;
}
return true;
}
}

View file

@ -3,7 +3,7 @@ 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 Jimp from "jimp";
interface CardInput {
id: string;
@ -46,7 +46,7 @@ export default class ImageHelper {
}
}
const image = await loadImage(await imageData.getBuffer("image/png"));
const image = await loadImage(await imageData.getBufferAsync("image/png"));
const x = i % gridWidth;
const y = Math.floor(i / gridWidth);

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 { CoreClient } from "../client/client";
import EmbedColours from "../constants/EmbedColours";
@ -24,8 +24,7 @@ interface InventoryPageCards {
interface ReturnedInventoryPage {
embed: EmbedBuilder,
row1: ActionRowBuilder<ButtonBuilder>,
row2: ActionRowBuilder<StringSelectMenuBuilder>,
row: ActionRowBuilder<ButtonBuilder>,
image: AttachmentBuilder,
}
@ -100,7 +99,7 @@ export default class InventoryHelper {
.setColor(EmbedColours.Ok)
.setImage("attachment://page.png");
const row1 = new ActionRowBuilder<ButtonBuilder>()
const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents(
new ButtonBuilder()
.setCustomId(`inventory ${userid} ${page - 1}`)
@ -113,23 +112,9 @@ export default class InventoryHelper {
.setStyle(ButtonStyle.Primary)
.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 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 Gdrivesync from "./commands/gdrivesync";
import Give from "./commands/give";
import Id from "./commands/id";
import Inventory from "./commands/inventory";
import Multidrop from "./commands/multidrop";
import Resync from "./commands/resync";
import Sacrifice from "./commands/sacrifice";
import Series from "./commands/series";
@ -26,15 +24,10 @@ import Droprarity from "./commands/stage/droprarity";
// Button Event Imports
import Claim from "./buttonEvents/Claim";
import InventoryButtonEvent from "./buttonEvents/Inventory";
import MultidropButtonEvent from "./buttonEvents/Multidrop";
import Reroll from "./buttonEvents/Reroll";
import SacrificeButtonEvent from "./buttonEvents/Sacrifice";
import SeriesEvent from "./buttonEvents/Series";
import TradeButtonEvent from "./buttonEvents/Trade";
import ViewButtonEvent from "./buttonEvents/View";
// String Dropdown Event Imports
import InventoryStringDropdown from "./stringDropdowns/Inventory";
export default class Registry {
public static RegisterCommands() {
@ -46,9 +39,7 @@ export default class Registry {
CoreClient.RegisterCommand("drop", new Drop());
CoreClient.RegisterCommand("gdrivesync", new Gdrivesync());
CoreClient.RegisterCommand("give", new Give());
CoreClient.RegisterCommand("id", new Id());
CoreClient.RegisterCommand("inventory", new Inventory());
CoreClient.RegisterCommand("multidrop", new Multidrop());
CoreClient.RegisterCommand("resync", new Resync());
CoreClient.RegisterCommand("sacrifice", new Sacrifice());
CoreClient.RegisterCommand("series", new Series());
@ -64,15 +55,9 @@ export default class Registry {
public static RegisterButtonEvents() {
CoreClient.RegisterButtonEvent("claim", new Claim());
CoreClient.RegisterButtonEvent("inventory", new InventoryButtonEvent());
CoreClient.RegisterButtonEvent("multidrop", new MultidropButtonEvent());
CoreClient.RegisterButtonEvent("reroll", new Reroll());
CoreClient.RegisterButtonEvent("sacrifice", new SacrificeButtonEvent());
CoreClient.RegisterButtonEvent("series", new SeriesEvent());
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>;
}

View file

@ -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);
});
});
});
});

View file

@ -1,281 +0,0 @@
import UserEffect from "../../src/database/entities/app/UserEffect";
import EffectHelper from "../../src/helpers/EffectHelper";
describe("AddEffectToUserInventory", () => {
describe("GIVEN effect is in database", () => {
const effectMock = {
AddUnused: jest.fn(),
Save: jest.fn(),
};
beforeAll(async () => {
UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(effectMock);
await EffectHelper.AddEffectToUserInventory("userId", "name", 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);
});
});
describe("GIVEN effect is not in database", () => {
beforeAll(async () => {
UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(null);
UserEffect.prototype.Save = jest.fn();
await EffectHelper.AddEffectToUserInventory("userId", "name", 1);
});
test("EXPECT effect to be saved", () => {
expect(UserEffect.prototype.Save).toHaveBeenCalledTimes(1);
expect(UserEffect.prototype.Save).toHaveBeenCalledWith(UserEffect, expect.any(UserEffect));
});
});
});
describe("UseEffect", () => {
describe("GIVEN effect is in database", () => {
describe("GIVEN now is before effect.WhenExpires", () => {
let result: boolean | undefined;
// nowMock < whenExpires
const nowMock = new Date(2024, 11, 3, 13, 30);
const whenExpires = new Date(2024, 11, 3, 14, 0);
const userEffect = {
Unused: 1,
WhenExpires: whenExpires,
};
beforeAll(async () => {
jest.setSystemTime(nowMock);
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);
});
});
});

66
tests/registry.test.ts Normal file
View file

@ -0,0 +1,66 @@
import {CoreClient} from "../src/client/client";
import Registry from "../src/registry";
import fs from "fs";
import path from "path";
describe("RegisterCommands", () => {
test("EXPECT every command in the commands folder to be registered", () => {
const registeredCommands: string[] = [];
CoreClient.RegisterCommand = jest.fn().mockImplementation((name: string) => {
registeredCommands.push(name);
});
Registry.RegisterCommands();
const commandFiles = getFilesInDirectory(path.join(process.cwd(), "src", "commands"))
.filter(x => x.endsWith(".ts"));
for (const file of commandFiles) {
expect(registeredCommands).toContain(file.split("/").pop()!.split(".")[0]);
}
expect(commandFiles.length).toBe(registeredCommands.length);
});
});
describe("RegisterButtonEvents", () => {
test("EXEPCT every button event in the button events folder to be registered", () => {
const registeredButtonEvents: string[] = [];
CoreClient.RegisterButtonEvent = jest.fn().mockImplementation((name: string) => {
registeredButtonEvents.push(name);
});
Registry.RegisterButtonEvents();
const eventFiles = getFilesInDirectory(path.join(process.cwd(), "src", "buttonEvents"))
.filter(x => x.endsWith(".ts"));
for (const file of eventFiles) {
expect(registeredButtonEvents).toContain(file.split("/").pop()!.split(".")[0].toLowerCase());
}
expect(eventFiles.length).toBe(registeredButtonEvents.length);
});
});
function getFilesInDirectory(dir: string): string[] {
let results: string[] = [];
const list = fs.readdirSync(dir);
list.forEach(file => {
file = path.join(dir, file);
const stat = fs.statSync(file);
if (stat && stat.isDirectory()) {
/* recurse into a subdirectory */
results = results.concat(getFilesInDirectory(file));
} else {
/* is a file */
results.push(file);
}
});
return results;
}

3197
yarn.lock

File diff suppressed because it is too large Load diff