From 888fca1d0c6fb7ca119ae69750ed029dfee7dc35 Mon Sep 17 00:00:00 2001 From: muhamedfazeel Date: Sat, 13 Dec 2025 09:29:36 +0530 Subject: [PATCH 01/21] [doc] [env.example]: update example env --- .env.example | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 6907f87..6747496 100644 --- a/.env.example +++ b/.env.example @@ -18,6 +18,15 @@ ANNOUNCEMENTS_ROLE_ID= # Level Role IDs LEVEL_EVENT= LEVEL_BUG_HUNTER= +LEVEL_CONTRIBUTOR= +LEVEL_21= +LEVEL_20= +LEVEL_19= +LEVEL_18= +LEVEL_17= +LEVEL_16= +LEVEL_15= +LEVEL_14= LEVEL_13= LEVEL_12= LEVEL_11= @@ -33,4 +42,7 @@ LEVEL_2= LEVEL_1= # Channel IDs -BOT_LOGGING= \ No newline at end of file +BOT_LOGGING= +THM_USERS= +THM_ROOMS= +DISCORD_USERS= \ No newline at end of file From da1c194e80390702fd5a2c7d30e1b3e41c33ce19 Mon Sep 17 00:00:00 2001 From: muhamedfazeel Date: Sat, 13 Dec 2025 09:30:36 +0530 Subject: [PATCH 02/21] [refactor] [wip]: Migrate bot to TypeScript --- tsconfig.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tsconfig.json diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5d6874f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2023", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "module": "commonjs", /* Specify what module code is generated. */ + "rootDir": "./src", /* Specify the root folder within your source files. */ + "outDir": "./dist", /* Specify an output folder for all emitted files. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + "strict": true, /* Enable all strict type-checking options. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"] +} \ No newline at end of file From 38cf81463511f037c8aefa07632c7892befa7a04 Mon Sep 17 00:00:00 2001 From: muhamedfazeel Date: Sat, 13 Dec 2025 09:31:44 +0530 Subject: [PATCH 03/21] [feat] [discord-client]: create custom client by extending discord client --- src/types/discord-client.type.ts | 54 ++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/types/discord-client.type.ts diff --git a/src/types/discord-client.type.ts b/src/types/discord-client.type.ts new file mode 100644 index 0000000..9b7cff6 --- /dev/null +++ b/src/types/discord-client.type.ts @@ -0,0 +1,54 @@ +import { Client, Collection, GatewayIntentBits } from 'discord.js'; // Ensure you import necessary components +import { IAPIHandlers } from '../interfaces/api-handlers.interface'; +import { ICommand } from '../interfaces/command.interface'; + +export class THMClient extends Client { + public commands: Collection; + public buttons: Collection; + public dropdowns: Collection; + public modals: Collection; + public pages: Map; + + public handleCommands: () => Promise = async () => { }; + + public handleEvents: () => Promise = async () => { }; + + public handleComponents: () => Promise = async () => { }; + + public handleAPI!: IAPIHandlers; + + public paginationEmbed: (interaction: any, fields: any[], options?: any) => Promise = async () => { }; + + public checkPermissions: (interaction: any, role: any, deferred?: any) => Promise = async () => false; + + public updateStats: () => Promise = async () => { }; + + public syncRoles: () => Promise = async () => { }; + + private static instance?: THMClient; + + private constructor() { + super({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMembers, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + GatewayIntentBits.GuildModeration, + ], + }); + this.commands = new Collection(); + this.buttons = new Collection(); + this.dropdowns = new Collection(); + this.modals = new Collection(); + this.pages = new Map(); + } + + // Static method to get the singleton instance + public static getInstance(): THMClient { + if (!THMClient.instance) { + THMClient.instance = new THMClient(); + } + return THMClient.instance; + } +} \ No newline at end of file From be1050ca87ec9697ab81c096396d96f73979bddc Mon Sep 17 00:00:00 2001 From: muhamedfazeel Date: Sat, 13 Dec 2025 09:32:36 +0530 Subject: [PATCH 04/21] [feat] [command interface]: create new interface for commands --- src/interfaces/command.interface.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/interfaces/command.interface.ts diff --git a/src/interfaces/command.interface.ts b/src/interfaces/command.interface.ts new file mode 100644 index 0000000..9d8f509 --- /dev/null +++ b/src/interfaces/command.interface.ts @@ -0,0 +1,11 @@ +import { CommandInteraction, Interaction, SlashCommandBuilder, SlashCommandOptionsOnlyBuilder, SlashCommandSubcommandsOnlyBuilder } from "discord.js"; + +export interface ICommand { + category: string; + command: boolean; + scope: string; + data: SlashCommandBuilder | SlashCommandSubcommandsOnlyBuilder | SlashCommandOptionsOnlyBuilder; + execute(interaction: CommandInteraction, client: any): Promise; + // Optional autocomplete handler for slash command autocomplete interactions + autocomplete?: (interaction: Interaction, client: any) => Promise; +} From 1503cbb77ce8ed0cffa012cc91eada82469bb159 Mon Sep 17 00:00:00 2001 From: muhamedfazeel Date: Sat, 13 Dec 2025 09:33:47 +0530 Subject: [PATCH 05/21] [feat] [api interface]: create new interface for custom methods for api calls --- src/interfaces/api-handlers.interface.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/interfaces/api-handlers.interface.ts diff --git a/src/interfaces/api-handlers.interface.ts b/src/interfaces/api-handlers.interface.ts new file mode 100644 index 0000000..de41acd --- /dev/null +++ b/src/interfaces/api-handlers.interface.ts @@ -0,0 +1,12 @@ +export interface IAPIHandlers { + get_room_data: (code: string) => Promise; + get_token_data: (token: string) => Promise; + get_user_data: (username: string) => Promise; + get_leaderboard_data: (monthly?: boolean) => Promise; + get_site_statistics: () => Promise; + get_public_rooms: (filter_type?: string | null) => Promise; + get_articles: () => Promise; + get_article_by_id: (id: string) => Promise; + get_article_by_phrase: (phrase: string) => Promise; + get_ollie_picture: () => Promise; +} \ No newline at end of file From 8f577e7bdf4fc0e35c4e37a9de2694aaa22d0426 Mon Sep 17 00:00:00 2001 From: muhamedfazeel Date: Sun, 14 Dec 2025 08:18:32 +0530 Subject: [PATCH 06/21] [chore]: update dependencies and add nodemon as dev dependency --- nodemon.json | 6 + package-lock.json | 2987 +++++++++++++++++++++++++++++++++++++-------- package.json | 41 +- 3 files changed, 2502 insertions(+), 532 deletions(-) create mode 100644 nodemon.json diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 0000000..6367289 --- /dev/null +++ b/nodemon.json @@ -0,0 +1,6 @@ +{ + "exec": "ts-node src/index.ts", + "watch": ["src"], + "ext": "ts", + "ignore": ["src/**/*.test.ts", "node_modules"] + } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 166f28e..bfce2e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,698 +1,1301 @@ { - "name": "template", - "version": "1.0.0", + "name": "tryhackme-bot", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "template", - "version": "1.0.0", + "name": "tryhackme-bot", + "version": "2.0.0", "license": "ISC", "dependencies": { - "@discordjs/rest": "^2.0.1", - "ascii-table": "^0.0.9", - "axios": "^1.5.1", - "discord-api-types": "^0.37.57", - "discord.js": "^14.13.0", - "dotenv": "^16.3.1", - "mongoose": "^7.6.4", - "node-cron": "^3.0.3", - "sharp": "^0.33.0" + "@discordjs/rest": "^2.6.0", + "ascii-table3": "^1.0.1", + "axios": "^1.13.2", + "discord-api-types": "^0.38.37", + "discord.js": "^14.25.1", + "dotenv": "^17.2.3", + "mongoose": "^9.0.1", + "node-cron": "^4.2.1", + "sharp": "^0.34.5", + "winston": "^3.19.0" + }, + "devDependencies": { + "@eslint/js": "^9.19.0", + "eslint": "^9.19.0", + "nodemon": "^3.1.11", + "ts-node": "^10.9.2", + "typescript": "^5.7.3" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" } }, "node_modules/@discordjs/builders": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.7.0.tgz", - "integrity": "sha512-GDtbKMkg433cOZur8Dv6c25EHxduNIBsxeHrsRoIM8+AwmEZ8r0tEpckx/sHwTLwQPOF3e2JWloZh9ofCaMfAw==", - "dependencies": { - "@discordjs/formatters": "^0.3.3", - "@discordjs/util": "^1.0.2", - "@sapphire/shapeshift": "^3.9.3", - "discord-api-types": "0.37.61", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.1.tgz", + "integrity": "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/formatters": "^0.6.2", + "@discordjs/util": "^1.2.0", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "^0.38.33", "fast-deep-equal": "^3.1.3", - "ts-mixer": "^6.0.3", - "tslib": "^2.6.2" + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" }, "engines": { "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" } }, - "node_modules/@discordjs/builders/node_modules/discord-api-types": { - "version": "0.37.61", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.61.tgz", - "integrity": "sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==" - }, "node_modules/@discordjs/collection": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.0.0.tgz", - "integrity": "sha512-YTWIXLrf5FsrLMycpMM9Q6vnZoR/lN2AWX23/Cuo8uOOtS8eHB2dyQaaGnaF8aZPYnttf2bkLMcXn/j6JUOi3w==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", "engines": { "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" } }, "node_modules/@discordjs/formatters": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.3.3.tgz", - "integrity": "sha512-wTcI1Q5cps1eSGhl6+6AzzZkBBlVrBdc9IUhJbijRgVjCNIIIZPgqnUj3ntFODsHrdbGU8BEG9XmDQmgEEYn3w==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz", + "integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==", + "license": "Apache-2.0", "dependencies": { - "discord-api-types": "0.37.61" + "discord-api-types": "^0.38.33" }, "engines": { "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" } }, - "node_modules/@discordjs/formatters/node_modules/discord-api-types": { - "version": "0.37.61", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.61.tgz", - "integrity": "sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==" - }, "node_modules/@discordjs/rest": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.1.0.tgz", - "integrity": "sha512-5gFWFkZX2JCFSRzs8ltx8bWmyVi0wPMk6pBa9KGIQSDPMmrP+uOrZ9j9HOwvmVWGe+LmZ5Bov0jMnQd6/jVReg==", - "dependencies": { - "@discordjs/collection": "^2.0.0", - "@discordjs/util": "^1.0.2", - "@sapphire/async-queue": "^1.5.0", - "@sapphire/snowflake": "^3.5.1", - "@vladfrangu/async_event_emitter": "^2.2.2", - "discord-api-types": "0.37.61", - "magic-bytes.js": "^1.5.0", - "tslib": "^2.6.2", - "undici": "5.27.2" + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz", + "integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.1.1", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.38.16", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" }, "engines": { - "node": ">=16.11.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" } }, - "node_modules/@discordjs/rest/node_modules/discord-api-types": { - "version": "0.37.61", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.61.tgz", - "integrity": "sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==" - }, "node_modules/@discordjs/util": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.0.2.tgz", - "integrity": "sha512-IRNbimrmfb75GMNEjyznqM1tkI7HrZOf14njX7tCAAUetyZM1Pr8hX/EK2lxBCOgWDRmigbp24fD1hdMfQK5lw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz", + "integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.33" + }, "engines": { - "node": ">=16.11.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" } }, "node_modules/@discordjs/ws": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.0.2.tgz", - "integrity": "sha512-+XI82Rm2hKnFwAySXEep4A7Kfoowt6weO6381jgW+wVdTpMS/56qCvoXyFRY0slcv7c/U8My2PwIB2/wEaAh7Q==", - "dependencies": { - "@discordjs/collection": "^2.0.0", - "@discordjs/rest": "^2.1.0", - "@discordjs/util": "^1.0.2", - "@sapphire/async-queue": "^1.5.0", - "@types/ws": "^8.5.9", - "@vladfrangu/async_event_emitter": "^2.2.2", - "discord-api-types": "0.37.61", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz", + "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.5.1", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@types/ws": "^8.5.10", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "^0.38.1", "tslib": "^2.6.2", - "ws": "^8.14.2" + "ws": "^8.17.0" }, "engines": { "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" } }, - "node_modules/@discordjs/ws/node_modules/discord-api-types": { - "version": "0.37.61", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.61.tgz", - "integrity": "sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==" - }, "node_modules/@emnapi/runtime": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.44.0.tgz", - "integrity": "sha512-ZX/etZEZw8DR7zAB1eVQT40lNo0jeqpb6dCgOvctB6FIQ5PoXfMuNY8+ayQfu8tNQbAB8gQWSSJupR8NxeiZXw==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, - "node_modules/@fastify/busboy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", - "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, "engines": { - "node": ">=14" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.0.tgz", - "integrity": "sha512-070tEheekI1LJWTGPC9WlQEa5UoKTXzzlORBHMX4TbfUxMiL336YHR8vBEUNsjse0RJCX8dZ4ZXwT595aEF1ug==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", "cpu": [ "arm64" ], + "license": "Apache-2.0", "optional": true, "os": [ "darwin" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.0" + "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.0.tgz", - "integrity": "sha512-pu/nvn152F3qbPeUkr+4e9zVvEhD3jhwzF473veQfMPkOYo9aoWXSfdZH/E6F+nYC3qvFjbxbvdDbUtEbghLqw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", "cpu": [ "x64" ], + "license": "Apache-2.0", "optional": true, "os": [ "darwin" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.0" + "@img/sharp-libvips-darwin-x64": "1.2.4" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.0.tgz", - "integrity": "sha512-VzYd6OwnUR81sInf3alj1wiokY50DjsHz5bvfnsFpxs5tqQxESoHtJO6xyksDs3RIkyhMWq2FufXo6GNSU9BMw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", "cpu": [ "arm64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "darwin" ], - "engines": { - "macos": ">=11", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.0.tgz", - "integrity": "sha512-dD9OznTlHD6aovRswaPNEy8dKtSAmNo4++tO7uuR4o5VxbVAOoEQ1uSmN4iFAdQneTHws1lkTZeiXPrcCkh6IA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", "cpu": [ "x64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "darwin" ], - "engines": { - "macos": ">=10.13", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.0.tgz", - "integrity": "sha512-VwgD2eEikDJUk09Mn9Dzi1OW2OJFRQK+XlBTkUNmAWPrtj8Ly0yq05DFgu1VCMx2/DqCGQVi5A1dM9hTmxf3uw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", "cpu": [ "arm" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "glibc": ">=2.28", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.0.tgz", - "integrity": "sha512-xTYThiqEZEZc0PRU90yVtM3KE7lw1bKdnDQ9kCTHWbqWyHOe4NpPOtMGy27YnN51q0J5dqRrvicfPbALIOeAZA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", "cpu": [ "arm64" ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "glibc": ">=2.26", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.0.tgz", - "integrity": "sha512-o9E46WWBC6JsBlwU4QyU9578G77HBDT1NInd+aERfxeOPbk0qBZHgoDsQmA2v9TbqJRWzoBPx1aLOhprBMgPjw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", "cpu": [ "s390x" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "glibc": ">=2.28", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.0.tgz", - "integrity": "sha512-naldaJy4hSVhWBgEjfdBY85CAa4UO+W1nx6a1sWStHZ7EUfNiuBTTN2KUYT5dH1+p/xij1t2QSXfCiFJoC5S/Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", "cpu": [ "x64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "glibc": ">=2.26", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.0.tgz", - "integrity": "sha512-OdorplCyvmSAPsoJLldtLh3nLxRrkAAAOHsGWGDYfN0kh730gifK+UZb3dWORRa6EusNqCTjfXV4GxvgJ/nPDQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", "cpu": [ "arm64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "musl": ">=1.2.2", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.0.tgz", - "integrity": "sha512-FW8iK6rJrg+X2jKD0Ajhjv6y74lToIBEvkZhl42nZt563FfxkCYacrXZtd+q/sRQDypQLzY5WdLkVTbJoPyqNg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", "cpu": [ "x64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "musl": ">=1.2.2", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.0.tgz", - "integrity": "sha512-4horD3wMFd5a0ddbDY8/dXU9CaOgHjEHALAddXgafoR5oWq5s8X61PDgsSeh4Qupsdo6ycfPPSSNBrfVQnwwrg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", "cpu": [ "arm" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "glibc": ">=2.28", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.0" + "@img/sharp-libvips-linux-arm": "1.2.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.0.tgz", - "integrity": "sha512-dcomVSrtgF70SyOr8RCOCQ8XGVThXwe71A1d8MGA+mXEVRJ/J6/TrCbBEJh9ddcEIIsrnrkolaEvYSHqVhswQw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", "cpu": [ "arm64" ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.0" + "@img/sharp-libvips-linux-riscv64": "1.2.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.0.tgz", - "integrity": "sha512-TiVJbx38J2rNVfA309ffSOB+3/7wOsZYQEOlKqOUdWD/nqkjNGrX+YQGz7nzcf5oy2lC+d37+w183iNXRZNngQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", "cpu": [ "s390x" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "glibc": ">=2.28", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.0" + "@img/sharp-libvips-linux-s390x": "1.2.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.0.tgz", - "integrity": "sha512-PaZM4Zi7/Ek71WgTdvR+KzTZpBqrQOFcPe7/8ZoPRlTYYRe43k6TWsf4GVH6XKRLMYeSp8J89RfAhBrSP4itNA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", "cpu": [ "x64" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.0" + "@img/sharp-libvips-linux-x64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.0.tgz", - "integrity": "sha512-1QLbbN0zt+32eVrg7bb1lwtvEaZwlhEsY1OrijroMkwAqlHqFj6R33Y47s2XUv7P6Ie1PwCxK/uFnNqMnkd5kg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", "cpu": [ "arm64" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "musl": ">=1.2.2", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.0" + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.0.tgz", - "integrity": "sha512-CecqgB/CnkvCWFhmfN9ZhPGMLXaEBXl4o7WtA6U3Ztrlh/s7FUKX4vNxpMSYLIrWuuzjiaYdfU3+Tdqh1xaHfw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", "cpu": [ "x64" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "musl": ">=1.2.2", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.0" + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.0.tgz", - "integrity": "sha512-Hn4js32gUX9qkISlemZBUPuMs0k/xNJebUNl/L6djnU07B/HAA2KaxRVb3HvbU5fL242hLOcp0+tR+M8dvJUFw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", "cpu": [ "wasm32" ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^0.44.0" + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.0.tgz", - "integrity": "sha512-5HfcsCZi3l5nPRF2q3bllMVMDXBqEWI3Q8KQONfzl0TferFE5lnsIG0A1YrntMAGqvkzdW6y1Ci1A2uTvxhfzg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", "cpu": [ "ia32" ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.0.tgz", - "integrity": "sha512-i3DtP/2ce1yKFj4OzOnOYltOEL/+dp4dc4dJXJBv6god1AFTcmkaA99H/7SwOmkCOBQkbVvA3lCGm3/5nDtf9Q==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", "cpu": [ "x64" ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@mongodb-js/saslprep": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.1.tgz", - "integrity": "sha512-t7c5K033joZZMspnHg/gWPE4kandgc2OxE74aYOtGKfgB9VPuVJPix0H6fhmm2erj5PBJ21mqcx34lpIGtUCsQ==", - "optional": true, + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.0.tgz", + "integrity": "sha512-ZHzx7Z3rdlWL1mECydvpryWN/ETXJiCxdgQKTAH+djzIPe77HdnSizKBDi1TVDXZjXyOj2IqEG/vPw71ULF06w==", + "license": "MIT", "dependencies": { "sparse-bitfield": "^3.0.3" } }, "node_modules/@sapphire/async-queue": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.0.tgz", - "integrity": "sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", + "license": "MIT", "engines": { "node": ">=v14.0.0", "npm": ">=7.0.0" } }, "node_modules/@sapphire/shapeshift": { - "version": "3.9.3", - "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.9.3.tgz", - "integrity": "sha512-WzKJSwDYloSkHoBbE8rkRW8UNKJiSRJ/P8NqJ5iVq7U2Yr/kriIBx2hW+wj2Z5e5EnXL1hgYomgaFsdK6b+zqQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" }, "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" + "node": ">=v16" } }, "node_modules/@sapphire/snowflake": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.1.tgz", - "integrity": "sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA==", + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.5.tgz", + "integrity": "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==", + "license": "MIT", "engines": { "node": ">=v14.0.0", "npm": ">=7.0.0" } }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { - "version": "20.9.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz", - "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==", + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.1.tgz", + "integrity": "sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg==", + "license": "MIT", + "peer": true, "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~7.16.0" } }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", - "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" }, "node_modules/@types/whatwg-url": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", - "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", + "license": "MIT", "dependencies": { - "@types/node": "*", "@types/webidl-conversions": "*" } }, "node_modules/@types/ws": { - "version": "8.5.9", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.9.tgz", - "integrity": "sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@vladfrangu/async_event_emitter": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.2.2.tgz", - "integrity": "sha512-HIzRG7sy88UZjBJamssEczH5q7t5+axva19UbZLO6u0ySbYPrwzWiXBcC0WuHyhKKoeCyneH+FvYzKQq/zTtkQ==", + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz", + "integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==", + "license": "MIT", "engines": { "node": ">=v14.0.0", "npm": ">=7.0.0" } }, - "node_modules/ascii-table": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/ascii-table/-/ascii-table-0.0.9.tgz", - "integrity": "sha512-xpkr6sCDIYTPqzvjG8M3ncw1YOTaloWZOyrUmicoEifBEKzQzt+ooUpRpQ/AbOoJfO/p2ZKiyp79qHThzJDulQ==" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } }, - "node_modules/axios": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz", - "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==", - "dependencies": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/bson": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz", - "integrity": "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==", + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, "engines": { - "node": ">=14.20.1" + "node": ">=0.4.0" } }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, - "engines": { - "node": ">=12.5.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/ascii-table3": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ascii-table3/-/ascii-table3-1.0.1.tgz", + "integrity": "sha512-xOCMZC8S375W4JajrAxFWPyI1VddfbscW9G5zMfhCySSt2Rvi/rs21jAjopzldTPOaFrOocjyGKibQiGExmLrg==", + "license": "Apache-2.0", + "dependencies": { + "printable-characters": "^1.0.42" + }, + "engines": { + "node": ">=11.14.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bson": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-7.0.0.tgz", + "integrity": "sha512-Kwc6Wh4lQ5OmkqqKhYGKIuELXl+EPYSCObVE6bWsp1T/cGkOCBN0I8wF/T44BiuhHyNi1mmKVPXk60d41xZ7kw==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "license": "MIT", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" } }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -703,21 +1306,57 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" }, "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "license": "MIT", "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-string/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" } }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -725,10 +1364,40 @@ "node": ">= 0.8" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -744,51 +1413,78 @@ "node_modules/debug/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", "engines": { "node": ">=0.4.0" } }, "node_modules/detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", "engines": { "node": ">=8" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/discord-api-types": { - "version": "0.37.63", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.63.tgz", - "integrity": "sha512-WbEDWj/1JGCIC1oCMIC4z9XbYY8PrWpV5eqFFQymJhJlHMqgIjqoYbU812X5oj5cwbRrEh6Va4LNLumB2Nt6IQ==" + "version": "0.38.37", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.37.tgz", + "integrity": "sha512-Cv47jzY1jkGkh5sv0bfHYqGgKOWO1peOrGMkDFM4UmaGMOTgOW8QSexhvixa9sVOiz8MnVOBryWYyw/CEVhj7w==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] }, "node_modules/discord.js": { - "version": "14.14.1", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.14.1.tgz", - "integrity": "sha512-/hUVzkIerxKHyRKopJy5xejp4MYKDPTszAnpYxzVVv4qJYf+Tkt+jnT2N29PIPschicaEEpXwF2ARrTYHYwQ5w==", + "version": "14.25.1", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.25.1.tgz", + "integrity": "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g==", + "license": "Apache-2.0", "dependencies": { - "@discordjs/builders": "^1.7.0", + "@discordjs/builders": "^1.13.0", "@discordjs/collection": "1.5.3", - "@discordjs/formatters": "^0.3.3", - "@discordjs/rest": "^2.1.0", - "@discordjs/util": "^1.0.2", - "@discordjs/ws": "^1.0.2", - "@sapphire/snowflake": "3.5.1", - "@types/ws": "8.5.9", - "discord-api-types": "0.37.61", + "@discordjs/formatters": "^0.6.2", + "@discordjs/rest": "^2.6.0", + "@discordjs/util": "^1.2.0", + "@discordjs/ws": "^1.2.3", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "^0.38.33", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", - "tslib": "2.6.2", - "undici": "5.27.2", - "ws": "8.14.2" + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" }, "engines": { - "node": ">=16.11.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" } }, "node_modules/discord.js/node_modules/@discordjs/collection": { @@ -799,37 +1495,368 @@ "node": ">=16.11.0" } }, - "node_modules/discord.js/node_modules/discord-api-types": { - "version": "0.37.61", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.61.tgz", - "integrity": "sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==" + "node_modules/discord.js/node_modules/@sapphire/snowflake": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", + "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } }, "node_modules/dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" } }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -840,72 +1867,441 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { "node": ">= 6" } }, - "node_modules/ip": { + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + "node_modules/kareem": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-3.0.0.tgz", + "integrity": "sha512-RKhaOBSPN8L7y4yAgNhDT2602G5FD6QbOIISbjN9D6mjHPeqeg7K+EB5IGSU5o81/X2Gzm3ICnAvQW3x3OP8HA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } }, - "node_modules/kareem": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz", - "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==", + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, "engines": { - "node": ">=12.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" }, "node_modules/lodash.snakecase": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", "dependencies": { - "yallist": "^4.0.0" + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" }, "engines": { - "node": ">=10" + "node": ">= 12.0.0" } }, "node_modules/magic-bytes.js": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.5.0.tgz", - "integrity": "sha512-wJkXvutRbNWcc37tt5j1HyOK1nosspdh3dj6LUYYAvF6JYNqs53IfRvK9oEpcwiDA1NdoIi64yAMfdivPeVAyw==" + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz", + "integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==", + "license": "MIT" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } }, "node_modules/memory-pager": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "optional": true + "license": "MIT" }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -914,6 +2310,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -921,27 +2318,40 @@ "node": ">= 0.6" } }, - "node_modules/mongodb": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.0.tgz", - "integrity": "sha512-g+GCMHN1CoRUA+wb1Agv0TI4YTSiWr42B5ulkiAfLLHitGK1R+PkSAf3Lr5rPZwi/3F04LiaZEW0Kxro9Fi2TA==", + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", "dependencies": { - "bson": "^5.5.0", - "mongodb-connection-string-url": "^2.6.0", - "socks": "^2.7.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=14.20.1" + "node": "*" + } + }, + "node_modules/mongodb": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.0.0.tgz", + "integrity": "sha512-vG/A5cQrvGGvZm2mTnCSz1LUcbOPl83hfB6bxULKQ8oFZauyox/2xbZOoGNl+64m8VBrETkdGCDBdOsCr3F3jg==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^7.0.0", + "mongodb-connection-string-url": "^7.0.0" }, - "optionalDependencies": { - "@mongodb-js/saslprep": "^1.1.0" + "engines": { + "node": ">=20.19.0" }, "peerDependencies": { - "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.0.0", - "kerberos": "^1.0.0 || ^2.0.0", - "mongodb-client-encryption": ">=2.3.0 <3", - "snappy": "^7.2.2" + "@aws-sdk/credential-providers": "^3.806.0", + "@mongodb-js/zstd": "^7.0.0", + "gcp-metadata": "^7.0.1", + "kerberos": "^7.0.0", + "mongodb-client-encryption": ">=7.0.0 <7.1.0", + "snappy": "^7.3.2", + "socks": "^2.8.6" }, "peerDependenciesMeta": { "@aws-sdk/credential-providers": { @@ -950,6 +2360,9 @@ "@mongodb-js/zstd": { "optional": true }, + "gcp-metadata": { + "optional": true + }, "kerberos": { "optional": true }, @@ -958,33 +2371,40 @@ }, "snappy": { "optional": true + }, + "socks": { + "optional": true } } }, "node_modules/mongodb-connection-string-url": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", - "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.0.tgz", + "integrity": "sha512-irhhjRVLE20hbkRl4zpAYLnDMM+zIZnp0IDB9akAFFUZp/3XdOfwwddc7y6cNvF2WCEtfTYRwYbIfYa2kVY0og==", + "license": "Apache-2.0", "dependencies": { - "@types/whatwg-url": "^8.2.1", - "whatwg-url": "^11.0.0" + "@types/whatwg-url": "^13.0.0", + "whatwg-url": "^14.1.0" + }, + "engines": { + "node": ">=20.19.0" } }, "node_modules/mongoose": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-7.6.4.tgz", - "integrity": "sha512-kadPkS/f5iZJrrMxxOvSoOAErXmdnb28lMvHmuYgmV1ZQTpRqpp132PIPHkJMbG4OC2H0eSXYw/fNzYTH+LUcw==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.0.1.tgz", + "integrity": "sha512-aHPfQx2YX5UwAmMVud7OD4lIz9AEO4jI+oDnRh3lPZq9lrKTiHmOzszVffDMyQHXvrf4NXsJ34kpmAhyYAZGbw==", + "license": "MIT", "dependencies": { - "bson": "^5.5.0", - "kareem": "2.5.1", - "mongodb": "5.9.0", + "kareem": "3.0.0", + "mongodb": "~7.0", "mpath": "0.9.0", - "mquery": "5.0.0", + "mquery": "6.0.0", "ms": "2.1.3", - "sift": "16.0.1" + "sift": "17.1.3" }, "engines": { - "node": ">=14.20.1" + "node": ">=20.19.0" }, "funding": { "type": "opencollective", @@ -1000,14 +2420,12 @@ } }, "node_modules/mquery": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", - "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", - "dependencies": { - "debug": "4.x" - }, + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-6.0.0.tgz", + "integrity": "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw==", + "license": "MIT", "engines": { - "node": ">=14.0.0" + "node": ">=20.19.0" } }, "node_modules/ms": { @@ -1015,37 +2433,297 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-cron": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", - "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", + "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nodemon": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", + "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==", + "dev": true, + "license": "MIT", "dependencies": { - "uuid": "8.3.2" + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" }, "engines": { - "node": ">=6.0.0" + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" } }, + "node_modules/printable-characters": { + "version": "1.0.42", + "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", + "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", + "license": "Unlicense" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", "dependencies": { - "lru-cache": "^6.0.0" + "picomatch": "^2.2.1" }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -1054,157 +2732,412 @@ } }, "node_modules/sharp": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.0.tgz", - "integrity": "sha512-99DZKudjm/Rmz+M0/26t4DKpXyywAOJaayGS9boEn7FvgtG0RYBi46uPE2c+obcJRtA3AZa0QwJot63gJQ1F0Q==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.2", - "semver": "^7.5.4" + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" }, "engines": { - "libvips": ">=8.15.0", "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.0", - "@img/sharp-darwin-x64": "0.33.0", - "@img/sharp-libvips-darwin-arm64": "1.0.0", - "@img/sharp-libvips-darwin-x64": "1.0.0", - "@img/sharp-libvips-linux-arm": "1.0.0", - "@img/sharp-libvips-linux-arm64": "1.0.0", - "@img/sharp-libvips-linux-s390x": "1.0.0", - "@img/sharp-libvips-linux-x64": "1.0.0", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.0", - "@img/sharp-libvips-linuxmusl-x64": "1.0.0", - "@img/sharp-linux-arm": "0.33.0", - "@img/sharp-linux-arm64": "0.33.0", - "@img/sharp-linux-s390x": "0.33.0", - "@img/sharp-linux-x64": "0.33.0", - "@img/sharp-linuxmusl-arm64": "0.33.0", - "@img/sharp-linuxmusl-x64": "0.33.0", - "@img/sharp-wasm32": "0.33.0", - "@img/sharp-win32-ia32": "0.33.0", - "@img/sharp-win32-x64": "0.33.0" - } - }, - "node_modules/sift": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz", - "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==" - }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", "dependencies": { - "is-arrayish": "^0.3.1" + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" + "node": ">=8" } }, - "node_modules/socks": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", - "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", "dependencies": { - "ip": "^2.0.0", - "smart-buffer": "^4.2.0" + "semver": "^7.5.3" }, "engines": { - "node": ">= 10.13.0", - "npm": ">= 3.0.0" + "node": ">=10" } }, "node_modules/sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", - "optional": true, + "license": "MIT", "dependencies": { "memory-pager": "^1.0.2" } }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, "node_modules/tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", "dependencies": { - "punycode": "^2.1.1" + "punycode": "^2.3.1" }, "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" } }, "node_modules/ts-mixer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.3.tgz", - "integrity": "sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==" + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", + "license": "MIT" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, - "node_modules/undici": { - "version": "5.27.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.27.2.tgz", - "integrity": "sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==", + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", "dependencies": { - "@fastify/busboy": "^2.0.0" + "prelude-ls": "^1.2.1" }, "engines": { - "node": ">=14.0" + "node": ">= 0.8.0" } }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, "bin": { - "uuid": "dist/bin/uuid" + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" } }, "node_modules/whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", "dependencies": { - "tr46": "^3.0.0", + "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/winston": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", + "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, "node_modules/ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -1221,10 +3154,28 @@ } } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index eab974d..a80674b 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,36 @@ { - "name": "template", - "version": "1.0.0", - "description": "", - "main": "src/index.js", + "name": "tryhackme-bot", + "version": "2.0.0", + "main": "dist/index.js", "scripts": { - "start": "node ." + "start": "nodemon", + "build": "tsc", + "prod": "node dist/index.js", + "lint": "eslint", + "lint:fix": "eslint --fix", + "format": "prettier --write 'src/**/*.ts'" }, "keywords": [], "author": "", "license": "ISC", + "description": "", + "devDependencies": { + "@eslint/js": "^9.19.0", + "eslint": "^9.19.0", + "nodemon": "^3.1.11", + "ts-node": "^10.9.2", + "typescript": "^5.7.3" + }, "dependencies": { - "@discordjs/rest": "^2.0.1", - "ascii-table": "^0.0.9", - "axios": "^1.5.1", - "discord-api-types": "^0.37.57", - "discord.js": "^14.13.0", - "dotenv": "^16.3.1", - "mongoose": "^7.6.4", - "node-cron": "^3.0.3", - "sharp": "^0.33.0" + "@discordjs/rest": "^2.6.0", + "ascii-table3": "^1.0.1", + "axios": "^1.13.2", + "discord-api-types": "^0.38.37", + "discord.js": "^14.25.1", + "dotenv": "^17.2.3", + "mongoose": "^9.0.1", + "node-cron": "^4.2.1", + "sharp": "^0.34.5", + "winston": "^3.19.0" } } From d7fd73f6a510b427198a66836fff0659506e027b Mon Sep 17 00:00:00 2001 From: muhamedfazeel Date: Sun, 14 Dec 2025 08:20:45 +0530 Subject: [PATCH 07/21] [doc] [env.example]: update example env --- .env.example | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 6747496..d9b14de 100644 --- a/.env.example +++ b/.env.example @@ -45,4 +45,5 @@ LEVEL_1= BOT_LOGGING= THM_USERS= THM_ROOMS= -DISCORD_USERS= \ No newline at end of file +DISCORD_USERS= +BOT_COMMANDS= \ No newline at end of file From 7984a0d5f50621be5bd0f7134692219e5e0363c7 Mon Sep 17 00:00:00 2001 From: muhamedfazeel Date: Sun, 14 Dec 2025 08:52:16 +0530 Subject: [PATCH 08/21] [feat] [config]: added common config to read env values --- src/config/configuration.ts | 73 +++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/config/configuration.ts diff --git a/src/config/configuration.ts b/src/config/configuration.ts new file mode 100644 index 0000000..2f7da3d --- /dev/null +++ b/src/config/configuration.ts @@ -0,0 +1,73 @@ +import * as dotenv from 'dotenv'; +import { IConfig, IEnvConfig } from '../interfaces/config.interface'; + +dotenv.config(); + +const env = process.env as unknown as IEnvConfig; + +if (!env.BOT_TOKEN || !env.CLIENT_ID) { + throw new Error( + 'Missing required environment variables: BOT_TOKEN or CLIENT_ID' + ); +} + +const config: IConfig = { + bot: { + token: env.BOT_TOKEN!, + clientId: env.CLIENT_ID!, + }, + guildId: env.GUILD_ID!, + mongo: { + uri: env.MONGODB_URI!, + }, + intercom: { + token: env.INTERCOM_TOKEN, + }, + users: { + developerId: env.BOT_DEVELOPER_ID!, + }, + roles: { + owner: env.OWNER_ROLE_ID!, + admin: env.ADMIN_ROLE_ID!, + moderator: env.MODERATOR_ROLE_ID!, + subscriber: env.SUBSCRIBER_ROLE_ID!, + verified: env.VERIFIED_ROLE_ID!, + announcements: env.ANNOUNCEMENTS_ROLE_ID!, + levels: { + event: env.LEVEL_EVENT!, + bugHunter: env.LEVEL_BUG_HUNTER!, + contributor: env.LEVEL_CONTRIBUTOR!, + level21: env.LEVEL_21!, + level20: env.LEVEL_20!, + level19: env.LEVEL_19!, + level18: env.LEVEL_18!, + level17: env.LEVEL_17!, + level16: env.LEVEL_16!, + level15: env.LEVEL_15!, + level14: env.LEVEL_14!, + level13: env.LEVEL_13!, + level12: env.LEVEL_12!, + level11: env.LEVEL_11!, + level10: env.LEVEL_10!, + level9: env.LEVEL_9!, + level8: env.LEVEL_8!, + level7: env.LEVEL_7!, + level6: env.LEVEL_6!, + level5: env.LEVEL_5!, + level4: env.LEVEL_4!, + level3: env.LEVEL_3!, + level2: env.LEVEL_2!, + level1: env.LEVEL_1!, + }, + }, + channels: { + botLogging: env.BOT_LOGGING!, + communityAnnouncements: env.COMMUNITY_ANNOUNCEMENTS!, + thmUsers: env.THM_USERS, + thmRooms: env.THM_ROOMS, + discordUsers: env.DISCORD_USERS, + botCommands: env.BOT_COMMANDS, + }, +}; + +export default config; \ No newline at end of file From d3565a35448e08fbdb885f4560ec70186207e8d2 Mon Sep 17 00:00:00 2001 From: muhamedfazeel Date: Sun, 14 Dec 2025 08:53:43 +0530 Subject: [PATCH 09/21] [chore] [api handler]: remove api handler interface --- src/interfaces/api-handlers.interface.ts | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 src/interfaces/api-handlers.interface.ts diff --git a/src/interfaces/api-handlers.interface.ts b/src/interfaces/api-handlers.interface.ts deleted file mode 100644 index de41acd..0000000 --- a/src/interfaces/api-handlers.interface.ts +++ /dev/null @@ -1,12 +0,0 @@ -export interface IAPIHandlers { - get_room_data: (code: string) => Promise; - get_token_data: (token: string) => Promise; - get_user_data: (username: string) => Promise; - get_leaderboard_data: (monthly?: boolean) => Promise; - get_site_statistics: () => Promise; - get_public_rooms: (filter_type?: string | null) => Promise; - get_articles: () => Promise; - get_article_by_id: (id: string) => Promise; - get_article_by_phrase: (phrase: string) => Promise; - get_ollie_picture: () => Promise; -} \ No newline at end of file From 31b8943d37c268652e0cf2f2a0aeb53719266f20 Mon Sep 17 00:00:00 2001 From: muhamedfazeel Date: Sun, 14 Dec 2025 08:54:26 +0530 Subject: [PATCH 10/21] [feat] [interfaces]: add interface for component and config --- src/interfaces/component.interface.ts | 8 ++ src/interfaces/config.interface.ts | 132 ++++++++++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 src/interfaces/component.interface.ts create mode 100644 src/interfaces/config.interface.ts diff --git a/src/interfaces/component.interface.ts b/src/interfaces/component.interface.ts new file mode 100644 index 0000000..33d5cbc --- /dev/null +++ b/src/interfaces/component.interface.ts @@ -0,0 +1,8 @@ +import { ButtonInteraction, Client } from "discord.js"; + +export interface IComponent { + data: { + name: string; + }; + execute: (interaction: ButtonInteraction, client: Client) => Promise; +} \ No newline at end of file diff --git a/src/interfaces/config.interface.ts b/src/interfaces/config.interface.ts new file mode 100644 index 0000000..8aeb196 --- /dev/null +++ b/src/interfaces/config.interface.ts @@ -0,0 +1,132 @@ +export interface IEnvConfig { + // Discord Bot + BOT_TOKEN: string; + CLIENT_ID: string; + + // Guild + GUILD_ID: string; + + // MongoDB + MONGODB_URI: string; + + // Intercom + INTERCOM_TOKEN: string; + + // User IDs + BOT_DEVELOPER_ID: string; + + // Role IDs + VERIFIED_ROLE_ID: string; + MODERATOR_ROLE_ID: string; + ADMIN_ROLE_ID: string; + OWNER_ROLE_ID: string; + SUBSCRIBER_ROLE_ID: string; + ANNOUNCEMENTS_ROLE_ID: string; + + // Level Role IDs + LEVEL_EVENT: string; + LEVEL_BUG_HUNTER: string; + LEVEL_CONTRIBUTOR: string; + LEVEL_21: string; + LEVEL_20: string; + LEVEL_19: string; + LEVEL_18: string; + LEVEL_17: string; + LEVEL_16: string; + LEVEL_15: string; + LEVEL_14: string; + LEVEL_13: string; + LEVEL_12: string; + LEVEL_11: string; + LEVEL_10: string; + LEVEL_9: string; + LEVEL_8: string; + LEVEL_7: string; + LEVEL_6: string; + LEVEL_5: string; + LEVEL_4: string; + LEVEL_3: string; + LEVEL_2: string; + LEVEL_1: string; + + // Channel IDs + BOT_LOGGING: string; + COMMUNITY_ANNOUNCEMENTS: string; + THM_USERS: string; + THM_ROOMS: string; + DISCORD_USERS: string; + BOT_COMMANDS: string; +} + +interface IBotConfig { + token: string; // BOT_TOKEN must be defined, or we can throw an error if missing + clientId: string; // CLIENT_ID must be defined, or we can throw an error if missing +} + +interface IMongoConfig { + uri: string; // MONGODB_URI must be defined, or we can throw an error if missing +} + +interface IIntercomConfig { + token: string; // INTERCOM_TOKEN must be defined, or we can throw an error if missing +} + +interface IUsersConfig { + developerId: string; // BOT_DEVELOPER_ID must be defined, or we can throw an error if missing +} + +interface ILevelsConfig { + event: string; + bugHunter: string; + contributor: string; + level21: string; + level20: string; + level19: string; + level18: string; + level17: string; + level16: string; + level15: string; + level14: string; + level13: string; + level12: string; + level11: string; + level10: string; + level9: string; + level8: string; + level7: string; + level6: string; + level5: string; + level4: string; + level3: string; + level2: string; + level1: string; +} + +interface IRolesConfig { + verified: string; + moderator: string; + admin: string; + owner: string; + subscriber: string; + announcements: string; + levels: ILevelsConfig; +} + +interface IChannelsConfig { + botLogging: string; + communityAnnouncements: string; + thmUsers: string; + thmRooms: string; + discordUsers: string; + botCommands: string; +} + +export interface IConfig { + bot: IBotConfig; + guildId: string; + intercom: IIntercomConfig; + mongo: IMongoConfig; + users: IUsersConfig; + roles: IRolesConfig; + channels: IChannelsConfig; +} \ No newline at end of file From 9380e286724bd8beb9209396871c092e0d5b4083 Mon Sep 17 00:00:00 2001 From: muhamedfazeel Date: Sun, 14 Dec 2025 09:16:53 +0530 Subject: [PATCH 11/21] [chore] [events]: remove mongo events --- src/events/mongo/connected.js | 6 ------ src/events/mongo/connecting.js | 6 ------ src/events/mongo/disconnected.js | 6 ------ src/events/mongo/error.js | 9 --------- src/events/mongo/reconnected.js | 6 ------ 5 files changed, 33 deletions(-) delete mode 100644 src/events/mongo/connected.js delete mode 100644 src/events/mongo/connecting.js delete mode 100644 src/events/mongo/disconnected.js delete mode 100644 src/events/mongo/error.js delete mode 100644 src/events/mongo/reconnected.js diff --git a/src/events/mongo/connected.js b/src/events/mongo/connected.js deleted file mode 100644 index 2dfa589..0000000 --- a/src/events/mongo/connected.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - name: "connected", - execute() { - console.log("\x1b[32m%s\x1b[0m", "[MONGODB] Connected."); - }, -}; diff --git a/src/events/mongo/connecting.js b/src/events/mongo/connecting.js deleted file mode 100644 index 75f1f91..0000000 --- a/src/events/mongo/connecting.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - name: "connecting", - execute() { - console.log("\x1b[33m%s\x1b[0m", "[MONGODB] Connecting..."); - }, -}; diff --git a/src/events/mongo/disconnected.js b/src/events/mongo/disconnected.js deleted file mode 100644 index dea85b7..0000000 --- a/src/events/mongo/disconnected.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - name: "disconnected", - execute() { - console.log("\x1b[31m%s\x1b[0m", "[MONGODB] Disconnected."); - }, -}; diff --git a/src/events/mongo/error.js b/src/events/mongo/error.js deleted file mode 100644 index 5c69c30..0000000 --- a/src/events/mongo/error.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - name: "error", - execute(error) { - console.log( - "\x1b[31m%s\x1b[0m", - `[MONGODB] An error has occured! \n${error}` - ); - }, -}; diff --git a/src/events/mongo/reconnected.js b/src/events/mongo/reconnected.js deleted file mode 100644 index 7698cf3..0000000 --- a/src/events/mongo/reconnected.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - name: "reconnected", - execute() { - console.log("\x1b[32m%s\x1b[0m", "[MONGODB] Reconnected."); - }, -}; From 61ca5906eca24c20860187cae5f0e1f1904ea4b3 Mon Sep 17 00:00:00 2001 From: muhamedfazeel Date: Sun, 14 Dec 2025 09:18:09 +0530 Subject: [PATCH 12/21] [chore] [mongo schema]: migrate mongo schema to typescript --- .../mongo/schema/BanSchema.js => schemas/ban.schema.ts} | 4 ++-- .../schema/GiveawaySchema.js => schemas/giveaway.schema.ts} | 4 ++-- src/schemas/index.ts | 4 ++++ .../schema/ProfileSchema.js => schemas/profile.schema.ts} | 4 ++-- 4 files changed, 10 insertions(+), 6 deletions(-) rename src/{events/mongo/schema/BanSchema.js => schemas/ban.schema.ts} (64%) rename src/{events/mongo/schema/GiveawaySchema.js => schemas/giveaway.schema.ts} (78%) create mode 100644 src/schemas/index.ts rename src/{events/mongo/schema/ProfileSchema.js => schemas/profile.schema.ts} (63%) diff --git a/src/events/mongo/schema/BanSchema.js b/src/schemas/ban.schema.ts similarity index 64% rename from src/events/mongo/schema/BanSchema.js rename to src/schemas/ban.schema.ts index 3493d84..b5b6d48 100644 --- a/src/events/mongo/schema/BanSchema.js +++ b/src/schemas/ban.schema.ts @@ -1,4 +1,4 @@ -const mongoose = require("mongoose"); +import mongoose from "mongoose"; const BanSchema = new mongoose.Schema({ discordId: String, @@ -9,4 +9,4 @@ const BanSchema = new mongoose.Schema({ avatar: String, }); -module.exports = mongoose.model("Ban", BanSchema); +export const Ban = mongoose.model("Ban", BanSchema); diff --git a/src/events/mongo/schema/GiveawaySchema.js b/src/schemas/giveaway.schema.ts similarity index 78% rename from src/events/mongo/schema/GiveawaySchema.js rename to src/schemas/giveaway.schema.ts index 7598cb6..11cf194 100644 --- a/src/events/mongo/schema/GiveawaySchema.js +++ b/src/schemas/giveaway.schema.ts @@ -1,4 +1,4 @@ -const mongoose = require("mongoose"); +import mongoose from "mongoose"; const GiveawaySchema = new mongoose.Schema({ endDate: { @@ -26,4 +26,4 @@ const GiveawaySchema = new mongoose.Schema({ ], }); -module.exports = mongoose.model("Giveaway", GiveawaySchema); +export const Giveaway = mongoose.model('Giveaway', GiveawaySchema); \ No newline at end of file diff --git a/src/schemas/index.ts b/src/schemas/index.ts new file mode 100644 index 0000000..deb209b --- /dev/null +++ b/src/schemas/index.ts @@ -0,0 +1,4 @@ +// Export every schema here +export * from './ban.schema'; +export * from './giveaway.schema'; +export * from './profile.schema'; \ No newline at end of file diff --git a/src/events/mongo/schema/ProfileSchema.js b/src/schemas/profile.schema.ts similarity index 63% rename from src/events/mongo/schema/ProfileSchema.js rename to src/schemas/profile.schema.ts index 0fe49f7..e7289a3 100644 --- a/src/events/mongo/schema/ProfileSchema.js +++ b/src/schemas/profile.schema.ts @@ -1,4 +1,4 @@ -const mongoose = require("mongoose"); +import mongoose from "mongoose"; const ProfileSchema = new mongoose.Schema({ discordId: String, @@ -9,4 +9,4 @@ const ProfileSchema = new mongoose.Schema({ avatar: String, }); -module.exports = mongoose.model("Profile", ProfileSchema); +export const Profile = mongoose.model("Profile", ProfileSchema); \ No newline at end of file From 32d876412bbcd2893e707b603b31cc040ed741cf Mon Sep 17 00:00:00 2001 From: muhamedfazeel Date: Sun, 14 Dec 2025 09:18:45 +0530 Subject: [PATCH 13/21] [feat] [constants]: add constants --- src/shared/constants/api.contstants.ts | 21 +++++++++++++++++++++ src/shared/constants/commands.constants.ts | 10 ++++++++++ src/shared/constants/index.ts | 2 ++ 3 files changed, 33 insertions(+) create mode 100644 src/shared/constants/api.contstants.ts create mode 100644 src/shared/constants/commands.constants.ts create mode 100644 src/shared/constants/index.ts diff --git a/src/shared/constants/api.contstants.ts b/src/shared/constants/api.contstants.ts new file mode 100644 index 0000000..14e849d --- /dev/null +++ b/src/shared/constants/api.contstants.ts @@ -0,0 +1,21 @@ +export const API_ROOM = "https://tryhackme.com/api/room/details?codes="; + +export const API_NEW_ROOM = "http://tryhackme.com/api/new-rooms/"; + +export const API_TOKEN = "https://tryhackme.com/tokens/discord/"; + +export const API_LEADERBOARD = "https://tryhackme.com/api/leaderboards"; + +export const API_STATS = "https://tryhackme.com/api/site-stats"; + +export const API_HACKTIVITIES = "https://tryhackme.com/api/hacktivities"; + +export const API_USER = "https://tryhackme.com/api/discord/user/"; + +export const API_GET_ARTICLES = "https://api.intercom.io/articles"; + +export const API_SEARCH_ARTICLES = "https://api.intercom.io/articles/search"; + +export const API_GET_ARTICLE_ID = ""; + +export const API_OLLIE_PICTURE = "https://ollie.muirlandoracle.co.uk"; \ No newline at end of file diff --git a/src/shared/constants/commands.constants.ts b/src/shared/constants/commands.constants.ts new file mode 100644 index 0000000..dae0bd4 --- /dev/null +++ b/src/shared/constants/commands.constants.ts @@ -0,0 +1,10 @@ +export const COMMAND_SCOPE = { + GUILD: 'GUILD', + APPLICATION: 'APPLICATION', +}; + +export const COMMAND_CATEGORY = { + ADMIN: 'administrator', + MOD: 'moderator', + UTIL: 'utility' +} \ No newline at end of file diff --git a/src/shared/constants/index.ts b/src/shared/constants/index.ts new file mode 100644 index 0000000..f129138 --- /dev/null +++ b/src/shared/constants/index.ts @@ -0,0 +1,2 @@ +export * from './api.contstants'; +export * from './commands.constants'; \ No newline at end of file From 2576f1ca9a55b8fd70fa1201080c9ff37c1e57f9 Mon Sep 17 00:00:00 2001 From: muhamedfazeel Date: Sun, 14 Dec 2025 09:19:12 +0530 Subject: [PATCH 14/21] [feat] [custom logger]: implement custom logger --- src/shared/custom-logger.ts | 66 +++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/shared/custom-logger.ts diff --git a/src/shared/custom-logger.ts b/src/shared/custom-logger.ts new file mode 100644 index 0000000..91c7f87 --- /dev/null +++ b/src/shared/custom-logger.ts @@ -0,0 +1,66 @@ +import { createLogger, Logger, format, transports } from 'winston'; +import { basename } from 'node:path'; + +const { combine, timestamp, printf, label, colorize } = format; + +export class CustomLogger { + private context: string; + private logger: Logger; + + constructor(context: string = 'App') { + this.context = basename(context); + + // Console log format (with colors) + const consoleLogFormat = combine( + colorize({ all: true }), // Colorize based on log level + printf(({ timestamp, level, message, label }) => { + return `${timestamp} [${label}] [${level}]: ${message}`; + }) + ); + + this.logger = createLogger({ + format: combine( + label({ label: this.context }), // Add context to each log entry + timestamp() // Add timestamp to logs + ), + transports: [ + new transports.Console({ + format: combine( + label({ label: this.context }), // Add context to each log entry + timestamp(), + consoleLogFormat // Colorized format for console + ), + }), + ], + }); + } + + private _format(level: string, ...messages: any[]) { + const logMessage = messages + .map((message) => { + if (typeof message === 'object') { + return message.message || JSON.stringify(message, null, 2); + } else if (Array.isArray(message)) { + return JSON.stringify(message, null, 2); + } else { + return String(message); + } + }) + .join(' '); + + this.logger.log({ level, message: logMessage }); + } + + // Shortcuts for commonly used log levels + info(...messages: any[]) { + this._format('info', ...messages); + } + + warn(...messages: any[]) { + this._format('warn', ...messages); + } + + error(...messages: any[]) { + this._format('error', ...messages); + } +} \ No newline at end of file From bae52bbf928a408b555c4726f9d38a18d9a933a6 Mon Sep 17 00:00:00 2001 From: muhamedfazeel Date: Sun, 14 Dec 2025 09:20:29 +0530 Subject: [PATCH 15/21] [refactor] [utils]: migrate utils to typescript --- src/shared/utils/error.utils.ts | 30 +++++++++ src/shared/utils/giveway.utils.ts | 103 ++++++++++++++++++++++++++++++ src/shared/utils/member.utils.ts | 14 ++++ src/shared/utils/reply.utils.ts | 30 +++++++++ src/shared/utils/role.utils.ts | 82 ++++++++++++++++++++++++ src/shared/utils/timer.util.ts | 27 ++++++++ src/utils/giveawayUtils.js | 62 ------------------ src/utils/memberUtils.js | 12 ---- src/utils/roleUtils.js | 78 ---------------------- src/utils/timerUtils.js | 53 --------------- 10 files changed, 286 insertions(+), 205 deletions(-) create mode 100644 src/shared/utils/error.utils.ts create mode 100644 src/shared/utils/giveway.utils.ts create mode 100644 src/shared/utils/member.utils.ts create mode 100644 src/shared/utils/reply.utils.ts create mode 100644 src/shared/utils/role.utils.ts create mode 100644 src/shared/utils/timer.util.ts delete mode 100644 src/utils/giveawayUtils.js delete mode 100644 src/utils/memberUtils.js delete mode 100644 src/utils/roleUtils.js delete mode 100644 src/utils/timerUtils.js diff --git a/src/shared/utils/error.utils.ts b/src/shared/utils/error.utils.ts new file mode 100644 index 0000000..da73914 --- /dev/null +++ b/src/shared/utils/error.utils.ts @@ -0,0 +1,30 @@ +import { EmbedBuilder, Interaction, RepliableInteraction } from "discord.js"; +import { CustomLogger } from "../custom-logger"; +import { safeReply } from "./reply.utils"; + +const logger = new CustomLogger("ErrorHandler"); + +export async function handleCommandError( + interaction: Interaction, + error: any, + tag: string +) { + logger.error(`❌ Error in ${tag}:`, error); + + const embed = new EmbedBuilder() + .setColor("#FF0000") + .setTitle("❌ An error occurred") + .setDescription("Something went wrong while processing this interaction.") + .setTimestamp(); + + // Only repliable interactions can receive a message + if (interaction.isRepliable()) { + return safeReply(interaction as RepliableInteraction, { + embeds: [embed], + flags: 64, + }); + } + + // Non-repliable interactions (Autocomplete, Ping, etc.) + logger.warn(`Interaction (${interaction.type}) is not repliable β†’ cannot send error embed.`); +} diff --git a/src/shared/utils/giveway.utils.ts b/src/shared/utils/giveway.utils.ts new file mode 100644 index 0000000..568cd44 --- /dev/null +++ b/src/shared/utils/giveway.utils.ts @@ -0,0 +1,103 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, TextChannel } from "discord.js"; +import { Giveaway } from "../../schemas"; +import { CustomLogger } from "../custom-logger"; +import { THMClient } from "../../types/discord-client.type"; +import config from "../../config/configuration"; + +const logger = new CustomLogger(__filename); + +function selectRandomWinners(participants: any[], winnersCount: number) { + const shuffled = participants.sort(() => 0.5 - Math.random()); + // Select up to the number of winners or all participants if fewer than desired winners + return shuffled.slice(0, Math.min(winnersCount, participants.length)); +} + +export async function checkGiveaways(client: THMClient) { + try { + const now = new Date(); + const giveaways = await Giveaway.find({ + endDate: { $lte: now }, + concluded: { $ne: true }, + }); + + giveaways.forEach(async (giveaway: { _id: any; concluded: boolean; save: () => any; }) => { + await endGiveaway(client, giveaway._id, true); + logger.info(`Giveaway ended for giveaway ID: ${giveaway._id}`); + // Update the giveaway as concluded + giveaway.concluded = true; + await giveaway.save(); + }); + } catch (err) { + logger.error("Error checking giveaways:", err); + } +} + +export async function endGiveaway(client: THMClient, id: any, announce: any) { + try { + const giveaway = await Giveaway.findById(id); + if (!giveaway) { + const msg = `Giveaway with ID ${id} not found.`; + logger.error(msg); + throw new Error(msg); + } + + // Check if the giveaway has already concluded + if (giveaway.concluded) { + logger.info(`Giveaway with ID ${id} has already concluded.`); + return; + } + + const channel = await client.channels.fetch( + config.channels.communityAnnouncements + ); + if (!channel) { + const msg = `Channel with ID ${config.channels.communityAnnouncements} not found.`; + logger.error(msg); + throw new Error(msg); + } + + if (!(channel instanceof TextChannel)) { + const msg = `Channel ${config.channels.communityAnnouncements} is not a text channel and cannot contain messages.`; + logger.error(msg); + throw new Error(msg); + } + + if (!giveaway.messageId) { + logger.error(`Giveaway ${id} does not have a messageId.`); + return; + } + const message = await channel.messages.fetch(giveaway.messageId); + + const disabledButton = new ButtonBuilder() + .setCustomId("join-giveaway") + .setLabel("Join Giveaway") + .setStyle(ButtonStyle.Primary) + .setDisabled(true); + + const newRow = new ActionRowBuilder().addComponents(disabledButton); + await message.edit({ components: [newRow] }); + + // Select winners + const winners = selectRandomWinners( + giveaway.participants, + giveaway.winners + ); + let winnersMention = + winners.length > 0 + ? winners.map((winner) => `<@${winner}>`).join(", ") + : "No winners, not enough participants."; + + // Announce winners if required + if (announce) { + channel.send( + `The giveaway is over!\n\nHere are the winners:\n${winnersMention}` + ); + } + + // Update the giveaway as concluded in the database + giveaway.concluded = true; + await giveaway.save(); + } catch (error) { + logger.error("Error in giveawayEnd function:", error); + } +} \ No newline at end of file diff --git a/src/shared/utils/member.utils.ts b/src/shared/utils/member.utils.ts new file mode 100644 index 0000000..034aa71 --- /dev/null +++ b/src/shared/utils/member.utils.ts @@ -0,0 +1,14 @@ +import { Guild, GuildMember } from "discord.js"; +import { CustomLogger } from "../custom-logger"; + +const logger = new CustomLogger(__filename);; + +export async function fetchMember(guild: Guild, discordId: string): Promise { + try { + return guild.members.fetch(discordId); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error(`Error fetching member with ID ${discordId}: ${errorMessage}`); + return null; + } +} \ No newline at end of file diff --git a/src/shared/utils/reply.utils.ts b/src/shared/utils/reply.utils.ts new file mode 100644 index 0000000..0a894b1 --- /dev/null +++ b/src/shared/utils/reply.utils.ts @@ -0,0 +1,30 @@ +import { + RepliableInteraction, + InteractionReplyOptions, + MessageFlags, + InteractionEditReplyOptions +} from "discord.js"; + +export async function safeReply( + interaction: RepliableInteraction, + content: string | InteractionReplyOptions +) { + const payload: InteractionReplyOptions | InteractionEditReplyOptions = + typeof content === "string" + ? { content, flags: MessageFlags.Ephemeral } + : { ...content, flags: content.flags ?? MessageFlags.Ephemeral }; + + try { + if (!interaction.deferred && !interaction.replied) { + return interaction.reply(payload); + } else { + return interaction.editReply(payload as InteractionEditReplyOptions); + } + } catch { + // fallback + return interaction.followUp({ + ...payload, + flags: MessageFlags.Ephemeral, + }); + } +} diff --git a/src/shared/utils/role.utils.ts b/src/shared/utils/role.utils.ts new file mode 100644 index 0000000..4f34b50 --- /dev/null +++ b/src/shared/utils/role.utils.ts @@ -0,0 +1,82 @@ +import { GuildMember } from "discord.js"; +import config from "../../config/configuration"; +import { CustomLogger } from "../custom-logger"; + +const logger = new CustomLogger(__filename); +const { levels, ...roles } = config.roles; +const levelToRoleMap: Record = { + 1337: levels.event, + 999: levels.bugHunter, + 998: levels.contributor, + 997: levels.level13, + 21: levels.level21, + 20: levels.level20, + 19: levels.level19, + 18: levels.level18, + 17: levels.level17, + 16: levels.level16, + 15: levels.level15, + 14: levels.level14, + 13: levels.level13, + 12: levels.level12, + 11: levels.level11, + 10: levels.level10, + 9: levels.level9, + 8: levels.level8, + 7: levels.level7, + 6: levels.level6, + 5: levels.level5, + 4: levels.level4, + 3: levels.level3, + 2: levels.level2, + 1: levels.level1, +} + +export function assignRoles(member: GuildMember, apiData: any) { + if (apiData.level) { + const { level } = apiData; + const newRole = levelToRoleMap[level]; + + member.roles.add(roles.verified); + + if (newRole) { + + // Remove other level roles + for (const role of Object.values(levelToRoleMap)) { + if (member.roles.cache.has(role) && role !== newRole) { + member.roles.remove(role); + } + } + + // Add the new role + if (newRole) { + member.roles.add(newRole); + } + } + } + + if (apiData.subscribed == 1) { + member.roles.add(roles.subscriber); + } else { + member.roles.remove(roles.subscriber); + } +} + +export function removeRoles(member: GuildMember) { + // Remove verified role + member.roles.remove(roles.verified); + + // Remove subscriber role + member.roles.remove(roles.subscriber); + + // Remove level roles + for (const role of Object.values(levelToRoleMap)) { + if (member.roles.cache.has(role)) { + member.roles.remove(role); + } + } +} + +export function getRolesForLevel(level: number) { + return levelToRoleMap[level] || null; +} \ No newline at end of file diff --git a/src/shared/utils/timer.util.ts b/src/shared/utils/timer.util.ts new file mode 100644 index 0000000..e57b99b --- /dev/null +++ b/src/shared/utils/timer.util.ts @@ -0,0 +1,27 @@ +import { CustomLogger } from "../custom-logger"; + +const logger = new CustomLogger(__filename); + +export async function checkDate(day: number | undefined, month: number, year: number, time: number | undefined) { + try { + const adjustedMonth = month - 1; + + return new Date(year, adjustedMonth, day, time, 0); + } catch (error) { + logger.error("There was an error checking the date:", error); + return false; + } +} + +export async function getTimeoutDuration(futureDate: { getTime: () => number; }) { + try { + const now = new Date(); + + const duration = futureDate.getTime() - now.getTime(); + + return duration > 0 ? duration : 0; + } catch (err) { + logger.error("There was an error getting the timeout duration:", err); + return false; + } +} \ No newline at end of file diff --git a/src/utils/giveawayUtils.js b/src/utils/giveawayUtils.js deleted file mode 100644 index 28dac16..0000000 --- a/src/utils/giveawayUtils.js +++ /dev/null @@ -1,62 +0,0 @@ -const Giveaway = require("../events/mongo/schema/GiveawaySchema"); -const { ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js"); - -async function giveawayEnd(client, id, announce) { - try { - const giveaway = await Giveaway.findById(id); - if (!giveaway) throw new Error("Giveaway not found"); - - // Check if the giveaway has already concluded - if (giveaway.concluded) { - console.log(`Giveaway with ID ${id} has already concluded.`); - return; - } - - const channel = await client.channels.fetch( - process.env.COMMUNITY_ANNOUNCEMENTS - ); - const message = await channel.messages.fetch(giveaway.messageId); - - const disabledButton = new ButtonBuilder() - .setCustomId("join-giveaway") - .setLabel("Join Giveaway") - .setStyle(ButtonStyle.Primary) - .setDisabled(true); - - const newRow = new ActionRowBuilder().addComponents(disabledButton); - await message.edit({ components: [newRow] }); - - // Select winners - const winners = selectRandomWinners( - giveaway.participants, - giveaway.winners - ); - let winnersMention = - winners.length > 0 - ? winners.map((winner) => `<@${winner}>`).join(", ") - : "No winners, not enough participants."; - - // Announce winners if required - if (announce) { - channel.send( - `The giveaway is over!\n\nHere are the winners:\n${winnersMention}` - ); - } - - // Update the giveaway as concluded in the database - giveaway.concluded = true; - await giveaway.save(); - } catch (error) { - console.error("Error in giveawayEnd function:", error); - } -} - -function selectRandomWinners(participants, winnersCount) { - const shuffled = participants.sort(() => 0.5 - Math.random()); - // Select up to the number of winners or all participants if fewer than desired winners - return shuffled.slice(0, Math.min(winnersCount, participants.length)); -} - -module.exports = { - giveawayEnd, -}; diff --git a/src/utils/memberUtils.js b/src/utils/memberUtils.js deleted file mode 100644 index 57f7d63..0000000 --- a/src/utils/memberUtils.js +++ /dev/null @@ -1,12 +0,0 @@ -async function fetchMember(guild, userId) { - try { - return await guild.members.fetch(userId); - } catch (error) { - console.error(`Error fetching member with ID ${userId}: ${error.message}`); - return null; - } -} - -module.exports = { - fetchMember, -}; diff --git a/src/utils/roleUtils.js b/src/utils/roleUtils.js deleted file mode 100644 index 5ae529b..0000000 --- a/src/utils/roleUtils.js +++ /dev/null @@ -1,78 +0,0 @@ -const levelToRoleMap = { - 1337: process.env.LEVEL_EVENT, - 999: process.env.LEVEL_BUG_HUNTER, - 998: process.env.LEVEL_CONTRIBUTOR, - 997: process.env.LEVEL_13, - 21: process.env.LEVEL_21, - 20: process.env.LEVEL_20, - 19: process.env.LEVEL_19, - 18: process.env.LEVEL_18, - 17: process.env.LEVEL_17, - 16: process.env.LEVEL_16, - 15: process.env.LEVEL_15, - 14: process.env.LEVEL_14, - 13: process.env.LEVEL_13, - 12: process.env.LEVEL_12, - 11: process.env.LEVEL_11, - 10: process.env.LEVEL_10, - 9: process.env.LEVEL_9, - 8: process.env.LEVEL_8, - 7: process.env.LEVEL_7, - 6: process.env.LEVEL_6, - 5: process.env.LEVEL_5, - 4: process.env.LEVEL_4, - 3: process.env.LEVEL_3, - 2: process.env.LEVEL_2, - 1: process.env.LEVEL_1, -}; - -function assignRoles(member, apiData) { - if (apiData.level) { - const roleToAdd = getRolesForLevel(apiData.level); - - member.roles.add(process.env.VERIFIED_ROLE_ID); - - // Remove other level roles - for (const role of Object.values(levelToRoleMap)) { - if (member.roles.cache.has(role) && role !== roleToAdd) { - member.roles.remove(role); - } - } - - // Add the new role - if (roleToAdd) { - member.roles.add(roleToAdd); - } - } - - if (apiData.subscribed == 1) { - member.roles.add(process.env.SUBSCRIBER_ROLE_ID); - } else { - member.roles.remove(process.env.SUBSCRIBER_ROLE_ID); - } -} - -function removeRoles(member) { - // Remove verified role - member.roles.remove(process.env.VERIFIED_ROLE_ID); - - // Remove subscriber role - member.roles.remove(process.env.SUBSCRIBER_ROLE_ID); - - // Remove level roles - for (const role of Object.values(levelToRoleMap)) { - if (member.roles.cache.has(role)) { - member.roles.remove(role); - } - } -} - -function getRolesForLevel(level) { - return levelToRoleMap[level] || null; -} - -module.exports = { - assignRoles, - getRolesForLevel, - removeRoles, -}; diff --git a/src/utils/timerUtils.js b/src/utils/timerUtils.js deleted file mode 100644 index 688b3bd..0000000 --- a/src/utils/timerUtils.js +++ /dev/null @@ -1,53 +0,0 @@ -const Giveaway = require("../events/mongo/schema/GiveawaySchema"); - -const { giveawayEnd } = require("../utils/giveawayUtils"); - -async function checkDate(day, month, year, time) { - try { - const adjustedMonth = month - 1; - - return new Date(year, adjustedMonth, day, time, 0); - } catch (error) { - console.log("There was an error checking the date:", error); - return false; - } -} - -async function getTimeoutDuration(futureDate) { - try { - const now = new Date(); - - const duration = futureDate.getTime() - now.getTime(); - - return duration > 0 ? duration : 0; - } catch (err) { - console.log("There was an error getting the timeout duration:", error); - return false; - } -} - -async function checkGiveaways(client) { - try { - const now = new Date(); - const giveaways = await Giveaway.find({ - endDate: { $lte: now }, - concluded: { $ne: true }, - }); - - giveaways.forEach(async (giveaway) => { - await giveawayEnd(client, giveaway._id, true); - console.log(`Giveaway ended for giveaway ID: ${giveaway._id}`); - // Update the giveaway as concluded - giveaway.concluded = true; - await giveaway.save(); - }); - } catch (err) { - console.error("Error checking giveaways:", err); - } -} - -module.exports = { - checkDate, - getTimeoutDuration, - checkGiveaways, -}; From 070d8cbde5799bcd7363940aa1be5c06f1633a4f Mon Sep 17 00:00:00 2001 From: muhamedfazeel Date: Sun, 14 Dec 2025 09:22:01 +0530 Subject: [PATCH 16/21] [refactor] [thm client]: update custom client --- src/types/discord-client.type.ts | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/types/discord-client.type.ts b/src/types/discord-client.type.ts index 9b7cff6..a18b27c 100644 --- a/src/types/discord-client.type.ts +++ b/src/types/discord-client.type.ts @@ -1,5 +1,4 @@ -import { Client, Collection, GatewayIntentBits } from 'discord.js'; // Ensure you import necessary components -import { IAPIHandlers } from '../interfaces/api-handlers.interface'; +import { Client, Collection, GatewayIntentBits, REST } from 'discord.js'; // Ensure you import necessary components import { ICommand } from '../interfaces/command.interface'; export class THMClient extends Client { @@ -8,21 +7,15 @@ export class THMClient extends Client { public dropdowns: Collection; public modals: Collection; public pages: Map; + public commandArray: Array>; + public rest: REST; public handleCommands: () => Promise = async () => { }; - public handleEvents: () => Promise = async () => { }; - public handleComponents: () => Promise = async () => { }; - - public handleAPI!: IAPIHandlers; - public paginationEmbed: (interaction: any, fields: any[], options?: any) => Promise = async () => { }; - public checkPermissions: (interaction: any, role: any, deferred?: any) => Promise = async () => false; - public updateStats: () => Promise = async () => { }; - public syncRoles: () => Promise = async () => { }; private static instance?: THMClient; @@ -42,13 +35,15 @@ export class THMClient extends Client { this.dropdowns = new Collection(); this.modals = new Collection(); this.pages = new Map(); + this.commandArray = []; + + // Initialise REST + this.rest = new REST({ version: '10' }) } // Static method to get the singleton instance public static getInstance(): THMClient { - if (!THMClient.instance) { - THMClient.instance = new THMClient(); - } - return THMClient.instance; + if (!this.instance) this.instance = new THMClient(); + return this.instance; } } \ No newline at end of file From 900cc10414f10ff4dfcc41250e6d80d2d4d28981 Mon Sep 17 00:00:00 2001 From: muhamedfazeel Date: Sun, 14 Dec 2025 09:22:58 +0530 Subject: [PATCH 17/21] [refactor] [events]: migrate events to typescript --- src/discord/events/guild-ban-add.event.ts | 41 ++++++ src/discord/events/guild-ban-remove.event.ts | 33 +++++ .../events/interaction-create.event.ts | 121 +++++++++++++++++ src/discord/events/ratelimit.event.ts | 16 +++ src/discord/events/ready.event.ts | 58 ++++++++ src/events/client/guildBanAdd.js | 36 ----- src/events/client/guildBanRemove.js | 27 ---- src/events/client/interactionCreate.js | 128 ------------------ src/events/client/messageCreate.js | 5 - src/events/client/rateLimit.js | 10 -- src/events/client/ready.js | 32 ----- 11 files changed, 269 insertions(+), 238 deletions(-) create mode 100644 src/discord/events/guild-ban-add.event.ts create mode 100644 src/discord/events/guild-ban-remove.event.ts create mode 100644 src/discord/events/interaction-create.event.ts create mode 100644 src/discord/events/ratelimit.event.ts create mode 100644 src/discord/events/ready.event.ts delete mode 100644 src/events/client/guildBanAdd.js delete mode 100644 src/events/client/guildBanRemove.js delete mode 100644 src/events/client/interactionCreate.js delete mode 100644 src/events/client/messageCreate.js delete mode 100644 src/events/client/rateLimit.js delete mode 100644 src/events/client/ready.js diff --git a/src/discord/events/guild-ban-add.event.ts b/src/discord/events/guild-ban-add.event.ts new file mode 100644 index 0000000..7087223 --- /dev/null +++ b/src/discord/events/guild-ban-add.event.ts @@ -0,0 +1,41 @@ +import { Events, GuildBan } from "discord.js"; +import { CustomLogger } from "../../shared/custom-logger"; +import { Ban, Profile } from "../../schemas"; + +const logger = new CustomLogger(__filename); + +const GuildBanAddEvent = { + name: Events.GuildBanAdd, + label: 'On Guild Ban Add', + once: false, + async execute(ban: GuildBan) { + logger.info(`${ban.user.tag} was banned from ${ban.guild.name}`); + + try { + const profile = await Profile.findOne({ discordId: ban.user.id }); + + if (profile) { + const banData = new Ban({ + discordId: profile.discordId, + username: profile.username, + token: profile.token, + subscribed: profile.subscribed, + level: profile.level, + avatar: profile.avatar, + }); + + await banData.save(); + + logger.info( + `User data transferred to Ban collection for ${ban.user.tag}` + ); + } else { + logger.info(`User not found in Profile collection for ${ban.user.tag}`); + } + } catch (error) { + logger.error(`Error transferring user data for ${ban.user.tag}:`, error); + } + }, +} + +export default GuildBanAddEvent; \ No newline at end of file diff --git a/src/discord/events/guild-ban-remove.event.ts b/src/discord/events/guild-ban-remove.event.ts new file mode 100644 index 0000000..b18ce12 --- /dev/null +++ b/src/discord/events/guild-ban-remove.event.ts @@ -0,0 +1,33 @@ +import { Events, GuildBan } from "discord.js"; +import { CustomLogger } from "../../shared/custom-logger"; +import { Ban } from "../../schemas"; + +const logger = new CustomLogger(__filename); + +const GuildBanRemoveEvent = { + name: Events.GuildBanRemove, + label: 'On Guild Ban Remove', + once: false, + async execute(ban: GuildBan) { + logger.info(`${ban.user.tag} was unbanned from ${ban.guild.name}`); + + try { + const bannedUser = await Ban.findOne({ discordId: ban.user.id }); + + if (bannedUser) { + await Ban.deleteOne({ discordId: ban.user.id }); + + logger.info(`User removed from Ban collection: ${ban.user.tag}`); + } else { + logger.info(`User not found in Ban collection: ${ban.user.tag}`); + } + } catch (error) { + logger.error( + `Error removing user from Ban collection: ${ban.user.tag}`, + error + ); + } + }, +} + +export default GuildBanRemoveEvent; \ No newline at end of file diff --git a/src/discord/events/interaction-create.event.ts b/src/discord/events/interaction-create.event.ts new file mode 100644 index 0000000..a627d25 --- /dev/null +++ b/src/discord/events/interaction-create.event.ts @@ -0,0 +1,121 @@ +import { + Events, + Interaction, + InteractionType, +} from "discord.js"; + +import { THMClient } from "../../types/discord-client.type"; +import { CustomLogger } from "../../shared/custom-logger"; +import { handleCommandError } from "../../shared/utils/error.utils"; +import { safeReply } from "../../shared/utils/reply.utils"; + +const logger = new CustomLogger(__filename); + +export default { + name: Events.InteractionCreate, + label: "Interaction Create", + + async execute(client: THMClient, interaction: Interaction) { + const start = performance.now(); + + // Helper to safely identify interaction + const resolveInteractionName = () => { + if (interaction.isChatInputCommand()) return `Command:/${interaction.commandName}`; + if (interaction.isButton()) return `Button:${interaction.customId}`; + if (interaction.isStringSelectMenu()) return `Dropdown:${interaction.customId}`; + if (interaction.type === InteractionType.ModalSubmit) return `Modal:${interaction.customId}`; + if (interaction.isContextMenuCommand()) return `Context:${interaction.commandName}`; + if (interaction.type === InteractionType.ApplicationCommandAutocomplete) + return `Autocomplete:${interaction.commandName}`; + return "Unknown Interaction"; + }; + + try { + // Slash Commands + if (interaction.isChatInputCommand()) { + const command = client.commands.get(interaction.commandName); + + if (!command) return safeReply(interaction, "Command not found."); + + try { + await command.execute(interaction, client); + } catch (err) { + return handleCommandError(interaction, err, `Command: ${interaction.commandName}`); + } + } + + // Buttons + else if (interaction.isButton()) { + const button = client.buttons.get(interaction.customId); + + if (!button) return safeReply(interaction, "Button not found."); + + try { + await button.execute(interaction, client); + } catch (err) { + return handleCommandError(interaction, err, `Button: ${interaction.customId}`); + } + } + + // Dropdowns + else if (interaction.isStringSelectMenu()) { + const dropdown = client.dropdowns.get(interaction.customId); + + if (!dropdown) return safeReply(interaction, "Dropdown not found."); + + try { + await dropdown.execute(interaction, client); + } catch (err) { + return handleCommandError(interaction, err, `Dropdown: ${interaction.customId}`); + } + } + + // Autocomplete + else if (interaction.type === InteractionType.ApplicationCommandAutocomplete) { + const command = client.commands.get(interaction.commandName); + if (!command?.autocomplete) return; + + try { + await command.autocomplete(interaction, client); + } catch (err) { + logger.error("Autocomplete Error:", err); + } + } + + // Modals + else if (interaction.type === InteractionType.ModalSubmit) { + const modal = client.modals.get(interaction.customId); + + if (!modal) return safeReply(interaction, "Modal not found."); + + try { + await modal.execute(interaction, client); + } catch (err) { + return handleCommandError(interaction, err, `Modal: ${interaction.customId}`); + } + } + + // Context Menus + else if (interaction.isContextMenuCommand()) { + const contextCmd = client.commands.get(interaction.commandName); + + if (!contextCmd) return safeReply(interaction, "Context command not found."); + + try { + await contextCmd.execute(interaction, client); + } catch (err) { + return handleCommandError(interaction, err, `Context: ${interaction.commandName}`); + } + } + + } finally { + // ⏱ Slow execution logger + const end = performance.now(); + const duration = Math.round(end - start); + + if (duration > 2000) { + logger.warn(`⚠ Slow Interaction (${duration}ms): ${resolveInteractionName()}`); + } + } + } +}; diff --git a/src/discord/events/ratelimit.event.ts b/src/discord/events/ratelimit.event.ts new file mode 100644 index 0000000..b7c79a4 --- /dev/null +++ b/src/discord/events/ratelimit.event.ts @@ -0,0 +1,16 @@ +import { CustomLogger } from "../../shared/custom-logger"; + +const logger = new CustomLogger(__filename); + +const RateLimitEvent = { + name: "rateLimit", + label: 'Rate Limit', + once: false, + async execute(info: { route: any; limit: any; timeout: any; global: any; }) { + logger.warn(`Rate Limit Hit: Request on ${info.route} with limit ${info.limit}`); + logger.warn(`Timeout: ${info.timeout}ms`); + logger.warn(`Global: ${info.global}`); + } +} + +export default RateLimitEvent; \ No newline at end of file diff --git a/src/discord/events/ready.event.ts b/src/discord/events/ready.event.ts new file mode 100644 index 0000000..a5d39b3 --- /dev/null +++ b/src/discord/events/ready.event.ts @@ -0,0 +1,58 @@ +import { Events, Routes } from "discord.js"; +import { CustomLogger } from "../../shared/custom-logger"; +import { THMClient } from "../../types/discord-client.type"; +import { Giveaway } from "../../schemas"; +import { endGiveaway } from "../../shared/utils/giveway.utils"; +import config from "../../config/configuration"; + +const logger = new CustomLogger(__filename); + +const ReadyEvent = { + name: Events.ClientReady, + label: 'On Ready', + once: true, + async execute(client: THMClient) { + logger.info(`Ready! Logged in as ${client?.user?.tag}`); + + // Set bot's activity + client.user?.setActivity(`https://tryhackme.com/`); + + // Register Slash Commands + try { + if (!client.commandArray?.length) { + logger.warn("⚠ No slash commands found in client.commandArray."); + } else { + logger.info("πŸš€ Registering Application (/) Commands..."); + + const result = await client.rest.put( + Routes.applicationCommands(config.bot.clientId), + { body: client.commandArray } + ); + + if (Array.isArray(result)) { + logger.info(`βœ… Registered ${Array.isArray(result) ? result.length : 0} slash commands.`); + } else { + logger.warn("⚠ Unexpected response from Discord while registering commands."); + } + } + } catch (error) { + logger.error("❌ Failed to register slash commands:", error); + } + + // Initial check for giveaways that might have ended while the bot was offline + logger.info("🎁 Checking for giveaways that ended while offline..."); + const now = new Date(); + const expired = await Giveaway.find({ + endDate: { $lt: now }, + concluded: { $ne: true } + }); + + for (const giveaway of expired) { + await endGiveaway(client, giveaway._id, true); + } + + logger.info("Giveaway cleanup complete."); + } +}; + +export default ReadyEvent; \ No newline at end of file diff --git a/src/events/client/guildBanAdd.js b/src/events/client/guildBanAdd.js deleted file mode 100644 index 827d367..0000000 --- a/src/events/client/guildBanAdd.js +++ /dev/null @@ -1,36 +0,0 @@ -const Profile = require("../mongo/schema/ProfileSchema"); -const Ban = require("../mongo/schema/BanSchema"); - -module.exports = { - name: "guildBanAdd", - once: false, - - async execute(ban) { - console.log(`${ban.user.tag} was banned from ${ban.guild.name}`); - - try { - const profile = await Profile.findOne({ discordId: ban.user.id }); - - if (profile) { - const banData = new Ban({ - discordId: profile.discordId, - username: profile.username, - token: profile.token, - subscribed: profile.subscribed, - level: profile.level, - avatar: profile.avatar, - }); - - await banData.save(); - - console.log( - `User data transferred to Ban collection for ${ban.user.tag}` - ); - } else { - console.log(`User not found in Profile collection for ${ban.user.tag}`); - } - } catch (error) { - console.error(`Error transferring user data for ${ban.user.tag}:`, error); - } - }, -}; diff --git a/src/events/client/guildBanRemove.js b/src/events/client/guildBanRemove.js deleted file mode 100644 index 750c2fe..0000000 --- a/src/events/client/guildBanRemove.js +++ /dev/null @@ -1,27 +0,0 @@ -const Ban = require("../mongo/schema/BanSchema"); - -module.exports = { - name: "guildBanRemove", - once: false, - - async execute(ban) { - console.log(`${ban.user.tag} was unbanned from ${ban.guild.name}`); - - try { - const bannedUser = await Ban.findOne({ discordId: ban.user.id }); - - if (bannedUser) { - await Ban.deleteOne({ discordId: ban.user.id }); - - console.log(`User removed from Ban collection: ${ban.user.tag}`); - } else { - console.log(`User not found in Ban collection: ${ban.user.tag}`); - } - } catch (error) { - console.error( - `Error removing user from Ban collection: ${ban.user.tag}`, - error - ); - } - }, -}; diff --git a/src/events/client/interactionCreate.js b/src/events/client/interactionCreate.js deleted file mode 100644 index 3864483..0000000 --- a/src/events/client/interactionCreate.js +++ /dev/null @@ -1,128 +0,0 @@ -const { InteractionType } = require("discord.js"); - -module.exports = { - name: "interactionCreate", - - async execute(interaction, client) { - if (interaction.isChatInputCommand()) { - const { commandName } = interaction; - const command = client.commands.get(commandName); - - if (!command) { - return interaction.reply({ - content: "Command not found.", - ephemeral: true, - }); - } - - try { - await command.execute(interaction, client); - } catch (error) { - console.log(`Error executing command ${commandName}:`, error); - interaction.reply({ - content: "An error occurred while executing this command.", - ephemeral: true, - }); - } - } else if (interaction.isButton()) { - const { customId } = interaction; - const button = client.buttons.get(customId); - - if (!button) { - console.log(`No button found with ID: ${customId}`); - return interaction.reply({ - content: "Button interaction failed.", - ephemeral: true, - }); - } - - try { - await button.execute(interaction, client); - } catch (error) { - console.log(`Error executing button ${customId}:`, error); - interaction.reply({ - content: "An error occurred while handling this button.", - ephemeral: true, - }); - } - } else if (interaction.isStringSelectMenu()) { - const { customId } = interaction; - const dropdown = client.dropdowns.get(customId); - - console.log(client.dropdowns); - console.log(customId); - - if (!dropdown) { - console.log(`No dropdown found with ID: ${customId}`); - return interaction.reply({ - content: "Dropdown interaction failed.", - ephemeral: true, - }); - } - - try { - await dropdown.execute(interaction, client); - } catch (error) { - console.log(`Error executing dropdown ${customId}:`, error); - interaction.reply({ - content: "An error occurred while handling this dropdown.", - ephemeral: true, - }); - } - } else if ( - interaction.type === InteractionType.ApplicationCommandAutocomplete - ) { - const { commandName } = interaction; - const command = client.commands.get(commandName); - - if (!command) return; - - try { - await command.autocomplete(interaction, client); - } catch (err) { - console.log(err); - } - } else if (interaction.type === InteractionType.ModalSubmit) { - const { customId } = interaction; - const modal = client.modals.get(customId); - - if (!modal) { - console.log(`No modal found with ID: ${customId}`); - return interaction.reply({ - content: "Modal interaction failed.", - ephemeral: true, - }); - } - - try { - await modal.execute(interaction, client); - } catch (error) { - console.log(`Error executing modal ${customId}:`, error); - interaction.reply({ - content: "An error occurred while handling this modal.", - ephemeral: true, - }); - } - } else if (interaction.isContextMenu()) { - const { commandName } = interaction; - const contextCommand = client.commands.get(commandName); - - if (!contextCommand) { - return interaction.reply({ - content: "Context command not found.", - ephemeral: true, - }); - } - - try { - await contextCommand.execute(interaction, client); - } catch (error) { - console.log(`Error executing context command ${commandName}:`, error); - interaction.reply({ - content: "An error occurred while executing this context command.", - ephemeral: true, - }); - } - } - }, -}; diff --git a/src/events/client/messageCreate.js b/src/events/client/messageCreate.js deleted file mode 100644 index 1a7e1be..0000000 --- a/src/events/client/messageCreate.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - name: "messageCreate", - - async execute(message, client) {}, -}; diff --git a/src/events/client/rateLimit.js b/src/events/client/rateLimit.js deleted file mode 100644 index d673192..0000000 --- a/src/events/client/rateLimit.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - name: "rateLimit", - async execute(info, client) { - console.error( - `Rate Limit Hit: Request on ${info.route} with limit ${info.limit}` - ); - console.error(`Timeout: ${info.timeout}ms`); - console.error(`Global: ${info.global}`); - }, -}; diff --git a/src/events/client/ready.js b/src/events/client/ready.js deleted file mode 100644 index 8f7e000..0000000 --- a/src/events/client/ready.js +++ /dev/null @@ -1,32 +0,0 @@ -const Giveaway = require("../mongo/schema/GiveawaySchema"); -const { giveawayEnd } = require("../../utils/giveawayUtils"); - -module.exports = { - name: "ready", - once: true, - - async execute(client) { - console.log(`${client.user.username} is ready`); - - // Set bot's activity - client.user.setActivity(`https://tryhackme.com/`); - - // Initial check for giveaways that might have ended while the bot was offline - console.log("Checking for giveaways that might have ended..."); - - const now = new Date(); - const endedGiveaways = await Giveaway.find({ - endDate: { $lt: now }, - concluded: { $ne: true }, - }); - - endedGiveaways.forEach(async (giveaway) => { - console.log( - `Ending giveaway ID ${giveaway._id} which has passed its end date.` - ); - await giveawayEnd(client, giveaway._id, true); - }); - - console.log("Finished checking for giveaways."); - }, -}; From 90537308ba31011c880c0d650cf8c86702dcff92 Mon Sep 17 00:00:00 2001 From: muhamedfazeel Date: Sun, 14 Dec 2025 09:23:58 +0530 Subject: [PATCH 18/21] [refactor] [handlers]: migrate handler functions to typescript --- src/discord/handlers/api.handler.ts | 87 ++++++++++ src/discord/handlers/command.handler.ts | 46 ++++++ src/discord/handlers/components.handler.ts | 68 ++++++++ src/discord/handlers/events.handler.ts | 57 +++++++ src/discord/handlers/pagination.handler.ts | 95 +++++++++++ src/discord/handlers/permissions.handler.ts | 91 +++++++++++ src/discord/handlers/stats.handler.ts | 64 ++++++++ src/discord/handlers/sync-roles.handler.ts | 109 +++++++++++++ src/functions/handlers/apiHandler.js | 149 ------------------ src/functions/handlers/commandHandler.js | 46 ------ src/functions/handlers/componentHandler.js | 48 ------ src/functions/handlers/eventHandler.js | 63 -------- src/functions/handlers/paginationHandler.js | 82 ---------- src/functions/handlers/permissionsHandler.js | 113 ------------- src/functions/handlers/statsHandler.js | 40 ----- src/functions/handlers/verificationHandler.js | 55 ------- 16 files changed, 617 insertions(+), 596 deletions(-) create mode 100644 src/discord/handlers/api.handler.ts create mode 100644 src/discord/handlers/command.handler.ts create mode 100644 src/discord/handlers/components.handler.ts create mode 100644 src/discord/handlers/events.handler.ts create mode 100644 src/discord/handlers/pagination.handler.ts create mode 100644 src/discord/handlers/permissions.handler.ts create mode 100644 src/discord/handlers/stats.handler.ts create mode 100644 src/discord/handlers/sync-roles.handler.ts delete mode 100644 src/functions/handlers/apiHandler.js delete mode 100644 src/functions/handlers/commandHandler.js delete mode 100644 src/functions/handlers/componentHandler.js delete mode 100644 src/functions/handlers/eventHandler.js delete mode 100644 src/functions/handlers/paginationHandler.js delete mode 100644 src/functions/handlers/permissionsHandler.js delete mode 100644 src/functions/handlers/statsHandler.js delete mode 100644 src/functions/handlers/verificationHandler.js diff --git a/src/discord/handlers/api.handler.ts b/src/discord/handlers/api.handler.ts new file mode 100644 index 0000000..103e97f --- /dev/null +++ b/src/discord/handlers/api.handler.ts @@ -0,0 +1,87 @@ +import axios, { AxiosRequestConfig } from "axios"; +import * as K from "../../shared/constants"; +import { CustomLogger } from "../../shared/custom-logger"; +import config from "../../config/configuration"; + +const logger = new CustomLogger(__filename); + +// --------------------------- +// Helper: Perform GET request +// --------------------------- +async function apiGet(url: string, config?: AxiosRequestConfig): Promise { + try { + const response = await axios.get(url, config); + return response.data; + } catch (error: any) { + logger.error(`API GET Error β†’ ${url}`); + logger.error("Message:", error?.message); + if (error?.response?.data) logger.error("Response:", error.response.data); + return undefined; + } +} + +// --------------------------- +// Helper: Intercom config +// --------------------------- +function intercomConfig(): AxiosRequestConfig { + const token = config.intercom.token; + if (!token) logger.warn("INTERCOM_TOKEN is not set in environment variables."); + + return { + headers: { + Authorization: `Bearer ${token}`, + "Intercom-Version": "2.10", + }, + }; +} + +export const handleAPI = { + get_room_data: (code: string) => + apiGet(K.API_ROOM + code), + + get_token_data: (token: string) => + apiGet(K.API_TOKEN + token), + + get_user_data: (username: string) => + apiGet(K.API_USER + username), + + get_leaderboard_data: async (monthly = false) => { + const query = monthly ? "?type=monthly" : ""; + const data = await apiGet<{ ranks: any[] }>(K.API_LEADERBOARD + query); + return data?.ranks ?? undefined; + }, + + get_site_statistics: () => + apiGet(K.API_STATS), + + get_public_rooms: (filter_type: string | null = null) => { + const query = filter_type ? `?type=${filter_type}` : ""; + return apiGet(K.API_HACKTIVITIES + query); + }, + + get_articles: () => + apiGet(K.API_GET_ARTICLES, intercomConfig()), + + get_article_by_id: (id: string) => + apiGet(K.API_GET_ARTICLES + `/${id}`, intercomConfig()), + + get_article_by_phrase: async (phrase: string) => { + const url = `${K.API_SEARCH_ARTICLES}?phrase=${phrase}`; + const data = await apiGet(url, intercomConfig()); + + if (!data || data.total_count === 0) return null; + return data.data?.articles?.[0] ?? null; + }, + + get_ollie_picture: async () => { + const data = await apiGet<{ status: string; message: string }>(K.API_OLLIE_PICTURE); + + if (!data) return undefined; + if (data.status !== "success") { + logger.error("Picture retrieval failed:", data); + return null; + } + + return data.message; + }, +}; \ No newline at end of file diff --git a/src/discord/handlers/command.handler.ts b/src/discord/handlers/command.handler.ts new file mode 100644 index 0000000..fede464 --- /dev/null +++ b/src/discord/handlers/command.handler.ts @@ -0,0 +1,46 @@ +import { join } from "node:path"; +import { AsciiTable3 } from "ascii-table3"; +import { readdirSync, statSync } from "node:fs"; +import { CustomLogger } from "../../shared/custom-logger"; +import { THMClient } from "../../types/discord-client.type"; + +const logger = new CustomLogger(__filename); + +export const loadCommands = async (client: THMClient) => { + const table = new AsciiTable3().setHeading("Command", "Scope", "Status"); + const commandsRoot = join(__dirname, "../commands"); + + logger.info("Loading Application (/) commands..."); + + const folders = readdirSync(commandsRoot) + .filter(dir => statSync(join(commandsRoot, dir)).isDirectory()); + + for (const folder of folders) { + const folderPath = join(commandsRoot, folder); + const files = readdirSync(folderPath).filter(f => f.endsWith(".ts") || f.endsWith(".js")); + + for (const file of files) { + const filePath = join(folderPath, file); + + try { + const module = await import(filePath); + const command = module.default; + + if (!command?.command) continue; + if (!command.data || !command.execute) { + logger.warn(`Invalid command: ${filePath}`); + continue; + } + + client.commands.set(command.data.name, command); + client.commandArray.push(command.data.toJSON()); + + table.addRow(command.data.name, command.scope, "βœ…"); + } catch (err) { + logger.error(`Failed loading command: ${filePath}`, err); + } + } + } + + logger.info("\n" + table.toString()); +}; \ No newline at end of file diff --git a/src/discord/handlers/components.handler.ts b/src/discord/handlers/components.handler.ts new file mode 100644 index 0000000..abc0c36 --- /dev/null +++ b/src/discord/handlers/components.handler.ts @@ -0,0 +1,68 @@ +import { join } from "node:path"; +import { readdirSync } from "node:fs"; +import { AsciiTable3 } from "ascii-table3"; +import { CustomLogger } from "../../shared/custom-logger"; +import { THMClient } from "../../types/discord-client.type"; + +const logger = new CustomLogger(__filename); + +export const registerComponents = async (client: THMClient) => { + const table = new AsciiTable3().setHeading("Component", "Status", "Type"); + const basePath = join(__dirname, "../components"); + + try { + const folders = readdirSync(basePath); + + for (const category of folders) { + const categoryPath = join(basePath, category); + const files = readdirSync(categoryPath) + .filter((f) => f.endsWith(".ts") || f.endsWith(".js")); + + for (const file of files) { + const filePath = join(categoryPath, file); + + try { + const module = await import(filePath); + const component = module.default; + + if (!component || !component.data) { + logger.error(`Invalid component in ${filePath}`); + table.addRow(file, "❌", category); + continue; + } + + switch (category) { + case "buttons": + client.buttons.set(component.data.name, component); + table.addRow(file, "βœ…", "Button"); + break; + + case "dropdowns": + client.dropdowns.set(component.data.id, component); + table.addRow(file, "βœ…", "Dropdown"); + break; + + case "modals": + client.modals.set(component.data.id, component); + table.addRow(file, "βœ…", "Modal"); + break; + + default: + table.addRow(file, "⚠️", "Unknown"); + break; + } + + } catch (err) { + logger.error(`Failed to load component: ${file}`, err); + table.addRow(file, "❌", category); + } + } + } + + logger.info("\n" + table.toString()); + logger.info("All components loaded successfully."); + + } catch (err) { + logger.error("Failed to register components:", err); + } +}; diff --git a/src/discord/handlers/events.handler.ts b/src/discord/handlers/events.handler.ts new file mode 100644 index 0000000..6d24ac9 --- /dev/null +++ b/src/discord/handlers/events.handler.ts @@ -0,0 +1,57 @@ +import { join } from "node:path"; +import { readdirSync } from "node:fs"; +import { CustomLogger } from "../../shared/custom-logger"; +import { THMClient } from "../../types/discord-client.type"; +import { AsciiTable3 } from "ascii-table3"; + +const logger = new CustomLogger(__filename); + +export const registerClientEvents = async (client: THMClient) => { + const table = new AsciiTable3().setHeading("Event", "Status"); + const eventsPath = join(__dirname, "../events"); + + let eventFiles: string[] = []; + + try { + eventFiles = readdirSync(eventsPath).filter( + (file) => file.endsWith(".ts") || file.endsWith(".js") + ); + } catch (err) { + logger.error("Failed to read events directory:", err); + return; + } + + logger.info(`Found ${eventFiles.length} event files.`); + + for (const file of eventFiles) { + const filePath = join(eventsPath, file); + + try { + const eventModule = await import(filePath); + const event = eventModule.default; + + if (!event || !event.name || !event.execute) { + logger.error(`Invalid event structure in: ${file}`); + continue; + } + + const handler = (...args: unknown[]) => event.execute(client, ...args); + + if (event.once) { + client.once(event.name, handler); + } else { + client.on(event.name, handler); + } + + table.addRow(event.label ?? event.name, "βœ…"); + logger.info(`Loaded event: ${event.label ?? event.name}`); + + } catch (err) { + logger.error(`Failed to load event file "${file}"`, err); + table.addRow(file, "❌"); + } + } + + logger.info("\n" + table.toString()); + logger.info(`Successfully loaded ${eventFiles.length} events.`); +}; diff --git a/src/discord/handlers/pagination.handler.ts b/src/discord/handlers/pagination.handler.ts new file mode 100644 index 0000000..6f5e635 --- /dev/null +++ b/src/discord/handlers/pagination.handler.ts @@ -0,0 +1,95 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + EmbedBuilder, + ComponentType, + ButtonInteraction, + ChatInputCommandInteraction, + ColorResolvable, +} from "discord.js"; +import { THMClient } from "../../types/discord-client.type"; + +export default (client: THMClient): void => { + client.paginationEmbed = async ( + interaction: ChatInputCommandInteraction, + fields: { name: string; value: string }[], + options: { maxPerPage?: number; color?: string; title?: string } = {} + ) => { + const maxPerPage = options.maxPerPage ?? 5; + const color = options.color ?? "#8AC7DB"; + const title = options.title ?? ""; + + let page = 1; + const totalPages = Math.ceil(fields.length / maxPerPage); + + // Generate a unique pagination ID + const sessionId = `${interaction.id}-${Date.now()}`; + + const buildRow = (currentPage: number) => + new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`prev-${sessionId}`) + .setLabel("Previous") + .setStyle(ButtonStyle.Primary) + .setDisabled(currentPage === 1), + new ButtonBuilder() + .setCustomId(`next-${sessionId}`) + .setLabel("Next") + .setStyle(ButtonStyle.Primary) + .setDisabled(currentPage === totalPages) + ); + + const buildEmbed = (page: number) => { + const start = (page - 1) * maxPerPage; + const end = start + maxPerPage; + + return new EmbedBuilder() + .setColor(color as ColorResolvable) + .setTitle(`${title} - Page ${page}/${totalPages}`) + .addFields(fields.slice(start, end)); + }; + + await interaction.reply({ + embeds: [buildEmbed(page)], + components: [buildRow(page)], + withResponse: true, + }) + const message = await interaction.fetchReply(); + + const collector = message.createMessageComponentCollector({ + componentType: ComponentType.Button, + filter: (i) => i.user.id === interaction.user.id, + time: 30000, + }); + + collector.on("collect", async (btn: ButtonInteraction) => { + if (btn.customId === `prev-${sessionId}`) page--; + if (btn.customId === `next-${sessionId}`) page++; + + await btn.update({ + embeds: [buildEmbed(page)], + components: [buildRow(page)], + }); + }); + + collector.on("end", async () => { + await message.edit({ + components: [ + new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`prev-${sessionId}`) + .setLabel("Previous") + .setStyle(ButtonStyle.Primary) + .setDisabled(true), + new ButtonBuilder() + .setCustomId(`next-${sessionId}`) + .setLabel("Next") + .setStyle(ButtonStyle.Primary) + .setDisabled(true) + ), + ], + }); + }); + }; +}; diff --git a/src/discord/handlers/permissions.handler.ts b/src/discord/handlers/permissions.handler.ts new file mode 100644 index 0000000..139e617 --- /dev/null +++ b/src/discord/handlers/permissions.handler.ts @@ -0,0 +1,91 @@ +import { + GuildMember, + Interaction, + InteractionEditReplyOptions, + InteractionReplyOptions, + MessageFlags, + RepliableInteraction, +} from "discord.js"; +import { THMClient } from "../../types/discord-client.type"; +import config from "../../config/configuration"; +import { CustomLogger } from "../../shared/custom-logger"; + +const logger = new CustomLogger(__filename); + +type RoleName = "Verified" | "Moderator" | "Administrator" | "Owner"; + +export default (client: THMClient): void => { + + /** Helper: send ephemeral message safely */ + const safeReply = async (interaction: RepliableInteraction, content: string) => { + const reply: InteractionReplyOptions | InteractionEditReplyOptions = { content }; + + if (!interaction.replied && !interaction.deferred) { + return interaction.reply({ ...reply as InteractionReplyOptions, flags: MessageFlags.Ephemeral }); + } + + try { + return interaction.editReply({ ...reply as InteractionEditReplyOptions }); + } catch { + return interaction.followUp({ ...reply as InteractionReplyOptions }); + } + }; + + client.checkPermissions = async ( + interaction: Interaction, + requiredRole: RoleName + ): Promise => { + + // Must be in guild + if (!interaction.inGuild()) { + if (interaction.isRepliable()) { + await safeReply(interaction, "This command can only be used in a server (guild)."); + } + return false; + } + + // Ensure repliable + if (!interaction.isRepliable()) { + logger.warn("Interaction is not repliable β€” cannot send permission message."); + return false; + } + + const member = interaction.member as GuildMember | null; + + if (!member) { + await safeReply(interaction, "Unable to get your member data. Please try again."); + return false; + } + + // Developer bypass + if (member.id === config.users.developerId) return true; + + // Role checks + const roles = config.roles; + + const has = (roleId?: string) => !!roleId && member.roles.cache.has(roleId); + + const level = { + Owner: has(roles.owner), + Administrator: has(roles.admin), + Moderator: has(roles.moderator), + Verified: has(roles.verified), + }; + + // Build hierarchical structure (Owner > Admin > Mod > Verified) + const hierarchy = { + Owner: level.Owner, + Administrator: level.Owner || level.Administrator, + Moderator: level.Owner || level.Administrator || level.Moderator, + Verified: level.Owner || level.Administrator || level.Moderator || level.Verified, + }; + + // Check permission + const allowed = hierarchy[requiredRole]; + + if (allowed) return true; + + await safeReply(interaction, `You do not have the required permission: **${requiredRole}**.`); + return false; + }; +}; diff --git a/src/discord/handlers/stats.handler.ts b/src/discord/handlers/stats.handler.ts new file mode 100644 index 0000000..d629bd7 --- /dev/null +++ b/src/discord/handlers/stats.handler.ts @@ -0,0 +1,64 @@ +import { VoiceChannel, GuildBasedChannel } from "discord.js"; +import config from "../../config/configuration"; +import { CustomLogger } from "../../shared/custom-logger"; +import { THMClient } from "../../types/discord-client.type"; +import { handleAPI } from "./api.handler"; + +const logger = new CustomLogger(__filename); + +export default (client: THMClient): void => { + client.updateStats = async () => { + try { + const guild = client.guilds.cache.get(config.guildId); + if (!guild) { + logger.error(`Guild not found: ${config.guildId}`); + return; + } + + const stats = await handleAPI.get_site_statistics(); + if (!stats) { + logger.error("Failed to fetch site statistics."); + return; + } + + // Helper: Safely rename channel + const renameVoice = (channelId: string, newName: string) => { + const channel = client.channels.cache.get(channelId); + + if (!channel) { + logger.warn(`Channel not found: ${channelId}`); + return; + } + + if (!channel.isVoiceBased()) { + logger.warn(`Channel is not a voice channel: ${channelId}`); + return; + } + + (channel as VoiceChannel) + .setName(newName) + .catch((err) => logger.error(`Failed to rename ${channelId}:`, err)); + }; + + // ----------------------------- + // Update channels safely + // ----------------------------- + + if (typeof stats.totalUsers === "number") { + renameVoice(config.channels.thmUsers, `THM Users: ${stats.totalUsers}`); + } + + if (typeof stats.publicRooms === "number") { + renameVoice(config.channels.thmRooms, `Total Rooms: ${stats.publicRooms}`); + } + + renameVoice( + config.channels.discordUsers, + `Discord Users: ${guild.memberCount}` + ); + + } catch (error) { + logger.error("updateStats failed:", error); + } + }; +}; diff --git a/src/discord/handlers/sync-roles.handler.ts b/src/discord/handlers/sync-roles.handler.ts new file mode 100644 index 0000000..c5abb2d --- /dev/null +++ b/src/discord/handlers/sync-roles.handler.ts @@ -0,0 +1,109 @@ +import { Guild, GuildMember } from "discord.js"; +import config from "../../config/configuration"; +import { Profile } from "../../schemas"; +import { CustomLogger } from "../../shared/custom-logger"; +import { fetchMember } from "../../shared/utils/member.utils"; +import { THMClient } from "../../types/discord-client.type"; +import { assignRoles } from "../../shared/utils/role.utils"; +import { handleAPI } from "./api.handler"; + +const logger = new CustomLogger(__filename); + +export default (client: THMClient): void => { + + client.syncRoles = async () => { + const guild = client.guilds.cache.get(config.guildId); + if (!guild) { + logger.error(`Guild not found: ${config.guildId}`); + return; + } + + let profiles = []; + try { + profiles = await Profile.find(); + } catch (err) { + logger.error("Failed to load user profiles:", err); + return; + } + + logger.info(`Syncing roles for ${profiles.length} users...`); + + for (const profile of profiles) { + try { + // Step 1: Ensure member exists in guild + const member = await fetchMember(guild, profile.discordId); + if (!member) { + await removeProfile(profile); + continue; + } + + // Step 2: Skip if no token available + if (!profile.token) continue; + + // Step 3: Fetch data from API + const apiData = await handleAPI.get_token_data(profile.token); + if (!isValidApiResponse(apiData)) { + logger.warn(`Invalid API response for ${profile.discordId}`); + continue; + } + + // Step 4: Update profile if needed + const hasUpdated = updateProfile(profile, apiData); + if (hasUpdated) { + await profile.save(); + logger.info(`Profile updated: ${profile.discordId}`); + } + + // Step 5: Assign roles + await assignRoles(member, apiData); + + } catch (err) { + logger.error(`Error syncing profile ${profile.discordId}:`, err); + } + } + + logger.info("Role sync completed."); + + try { + await client.updateStats(); + } catch (error) { + logger.error("Failed to update stats:", error); + } + }; +}; + +// ------------------------ +// Helper: remove profile +// ------------------------ +async function removeProfile(profile: any) { + await Profile.findOneAndDelete({ discordId: profile.discordId }); + logger.info(`Removed stale profile: ${profile.discordId}`); +} + +// ------------------------ +// Helper: API validation +// ------------------------ +function isValidApiResponse(data: any): boolean { + return ( + data && + data.success === true && + typeof data.username === "string" + ); +} + +// ------------------------ +// Helper: Profile update +// ------------------------ +function updateProfile(profile: any, apiData: any) { + const { username, subscribed, usersRank, points, level, avatar } = apiData; + + const before = profile.toObject(); + + profile.username = username; + profile.subscribed = !!subscribed; + profile.level = level; + profile.avatar = avatar; + + // Check for changes AFTER modification + return profile.isModified(); +} diff --git a/src/functions/handlers/apiHandler.js b/src/functions/handlers/apiHandler.js deleted file mode 100644 index ff13358..0000000 --- a/src/functions/handlers/apiHandler.js +++ /dev/null @@ -1,149 +0,0 @@ -const axios = require("axios"); - -const API_ROOM = "https://tryhackme.com/api/room/details?codes="; -const API_NEW_ROOM = "http://tryhackme.com/api/new-rooms/"; -const API_TOKEN = "https://tryhackme.com/tokens/discord/"; -const API_LEADERBOARD = "https://tryhackme.com/api/leaderboards"; -const API_STATS = "https://tryhackme.com/api/site-stats"; -const API_HACKTIVITIES = "https://tryhackme.com/api/hacktivities"; -const API_USER = "https://tryhackme.com/api/discord/user/"; -const API_GET_ARTICLES = "https://api.intercom.io/articles"; -const API_SEARCH_ARTICLES = "https://api.intercom.io/articles/search"; -const API_GET_ARTICLE_ID = ""; - -module.exports = (client) => { - client.handleAPI = { - get_room_data: async (code) => { - try { - const response = await axios.get(API_ROOM + code); - return response.data; - } catch (error) { - console.error(error); - } - }, - - get_token_data: async (token) => { - try { - const response = await axios.get(API_TOKEN + token); - return response.data; - } catch (error) { - console.error(error); - } - }, - - get_user_data: async (username) => { - try { - const response = await axios.get(API_USER + username); - return response.data; - } catch (error) { - console.error(error); - } - }, - - get_leaderboard_data: async (monthly = false) => { - const query = monthly ? "?type=monthly" : ""; - try { - const response = await axios.get(API_LEADERBOARD + query); - return response.data["ranks"]; - } catch (error) { - console.error(error); - } - }, - - get_site_statistics: async () => { - try { - const response = await axios.get(API_STATS); - return response.data; - } catch (error) { - console.error(error); - } - }, - - get_public_rooms: async (filter_type = null) => { - const query = filter_type ? `?type={filter_type}` : ""; - try { - const response = await axios.get(API_HACKTIVITIES + query); - return response.data; - } catch (error) { - console.error(error); - } - }, - - get_articles: async () => { - try { - const config = { - headers: { - Authorization: `Bearer ${process.env.INTERCOM_TOKEN}`, - "Intercom-Version": "2.10", - }, - }; - - const response = await axios.get(API_ARTICLES, config); - - return response.data; - } catch (error) { - console.error(error); - } - }, - - get_article_by_id: async (id) => { - try { - const config = { - headers: { - Authorization: `Bearer ${process.env.INTERCOM_TOKEN}`, - "Intercom-Version": "2.10", - }, - }; - - const response = await axios.get(API_ARTICLES, config); - - return response.data; - } catch (error) { - console.error(error); - } - }, - - get_article_by_phrase: async (phrase) => { - try { - const config = { - headers: { - Authorization: `Bearer ${process.env.INTERCOM_TOKEN}`, - "Intercom-Version": "2.10", - }, - }; - - const response = await axios.get( - API_SEARCH_ARTICLES + `?phrase=${phrase}`, - config - ); - - if (response.data.total_count == 0) { - return null; - } - - const firstArticle = response.data.data.articles[0] || null; - - return firstArticle; - } catch (error) { - console.error(error); - } - }, - - get_ollie_picture: async () => { - try { - const response = await axios.get("https://ollie.muirlandoracle.co.uk"); - const data = response.data; - - if (data.status === "success") { - return data.message; - } else { - console.error("Picture retrieval failed: ", data); - return null; - } - } catch (error) { - console.error("Error fetching picture: ", error); - return null; - } - }, - }; -}; diff --git a/src/functions/handlers/commandHandler.js b/src/functions/handlers/commandHandler.js deleted file mode 100644 index cf6d6e4..0000000 --- a/src/functions/handlers/commandHandler.js +++ /dev/null @@ -1,46 +0,0 @@ -const { REST, Routes } = require("discord.js"); -const fs = require("fs"); -const ascii = require("ascii-table"); - -module.exports = (client) => { - client.handleCommands = async () => { - const table = new ascii().setHeading("Commands", "Status"); - - const commandFolders = fs.readdirSync("./src/commands"); - for (const folder of commandFolders) { - const commandFiles = fs - .readdirSync(`./src/commands/${folder}`) - .filter((file) => file.endsWith(".js")); - - for (const file of commandFiles) { - try { - const command = require(`../../commands/${folder}/${file}`); - client.commands.set(command.data.name, command); - client.commandArray.push(command.data.toJSON()); - table.addRow(file, "βœ…"); - } catch (err) { - console.error(`Error loading command ${file}:`, err); - table.addRow(file, "❌"); - } - } - } - - const rest = new REST().setToken(process.env.BOT_TOKEN); - - try { - console.log( - `Started refreshing ${client.commandArray.length} application (/) commands.` - ); - await rest.put(Routes.applicationCommands(process.env.CLIENT_ID), { - body: client.commandArray, - }); - console.log( - `Successfully reloaded ${client.commandArray.length} application (/) commands.` - ); - } catch (error) { - console.error(error); - } - - return console.log(table.toString(), "\n Commands loaded"); - }; -}; diff --git a/src/functions/handlers/componentHandler.js b/src/functions/handlers/componentHandler.js deleted file mode 100644 index 5cd19bf..0000000 --- a/src/functions/handlers/componentHandler.js +++ /dev/null @@ -1,48 +0,0 @@ -const fs = require("fs"); -const ascii = require("ascii-table"); - -module.exports = (client) => { - client.handleComponents = async () => { - const table = new ascii().setHeading("Component", "Status", "Type"); - - const componentFolders = fs.readdirSync("./src/components"); - for (const folder of componentFolders) { - const componentFiles = fs - .readdirSync(`./src/components/${folder}`) - .filter((file) => file.endsWith(".js")); - - switch (folder) { - case "buttons": - for (const file of componentFiles) { - try { - const button = require(`../../components/${folder}/${file}`); - client.buttons.set(button.data.name, button); - table.addRow(file, "βœ…", "Button"); - } catch (err) { - console.error(`Error loading button component ${file}:`, err); - table.addRow(file, "❌", "Button"); - } - } - break; - - case "dropdowns": - for (const file of componentFiles) { - try { - const dropdown = require(`../../components/${folder}/${file}`); - client.dropdowns.set(dropdown.data.id, dropdown); - table.addRow(file, "βœ…", "Dropdown"); - } catch (err) { - console.error(`Error loading button component ${file}:`, err); - table.addRow(file, "❌", "Dropdown"); - } - } - break; - - default: - break; - } - } - - return console.log(table.toString(), "\n Components loaded"); - }; -}; diff --git a/src/functions/handlers/eventHandler.js b/src/functions/handlers/eventHandler.js deleted file mode 100644 index 3ab4677..0000000 --- a/src/functions/handlers/eventHandler.js +++ /dev/null @@ -1,63 +0,0 @@ -const fs = require("fs"); -const ascii = require("ascii-table"); -const mongoose = require("mongoose"); - -module.exports = (client) => { - client.handleEvents = async () => { - const eventFolders = fs.readdirSync("./src/events"); - - const table = new ascii().setHeading("Event", "Status", "Type"); - - for (const folder of eventFolders) { - const eventFiles = fs - .readdirSync(`./src/events/${folder}`) - .filter((file) => file.endsWith(".js")); - - switch (folder) { - case "client": - for (const file of eventFiles) { - try { - const event = require(`../../events/${folder}/${file}`); - if (event.once) - client.once(event.name, (...args) => - event.execute(...args, client) - ); - else - client.on(event.name, (...args) => - event.execute(...args, client) - ); - table.addRow(file, "βœ…", "Client"); - } catch (err) { - console.error(`Error loading event ${file}:`, err); - table.addRow(file, "❌", "Client"); - } - } - break; - - case "mongo": - for (const file of eventFiles) { - try { - const event = require(`../../events/${folder}/${file}`); - if (event.once) - mongoose.connection.once(event.name, (...args) => - event.execute(...args, client) - ); - else - mongoose.connection.on(event.name, (...args) => - event.execute(...args, client) - ); - table.addRow(file, "βœ…", "MongoDB"); - } catch (err) { - console.error(`Error loading Mongo event ${file}:`, err); - table.addRow(file, "❌", "MongoDB"); - } - } - break; - - default: - break; - } - } - return console.log(table.toString(), "\n Events loaded"); - }; -}; diff --git a/src/functions/handlers/paginationHandler.js b/src/functions/handlers/paginationHandler.js deleted file mode 100644 index 8390c17..0000000 --- a/src/functions/handlers/paginationHandler.js +++ /dev/null @@ -1,82 +0,0 @@ -const { - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, - EmbedBuilder, - ComponentType, -} = require("discord.js"); - -module.exports = (client) => { - client.paginationEmbed = async (interaction, fields, options = {}) => { - const maxPerPage = options.maxPerPage || 5; - const color = options.color || "#8AC7DB"; - const title = options.title || ""; - - let page = 1; - const totalPages = Math.ceil(fields.length / maxPerPage); - - const row = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId("previous") - .setLabel("Previous") - .setStyle(ButtonStyle.Primary) - .setDisabled(true), - new ButtonBuilder() - .setCustomId("next") - .setLabel("Next") - .setStyle(ButtonStyle.Primary) - .setDisabled(totalPages <= 1) - ); - - const embedMessage = async (page) => { - const start = (page - 1) * maxPerPage; - const end = start + maxPerPage; - const paginatedFields = fields.slice(start, end); - - const embed = new EmbedBuilder() - .setColor(color) - .setTitle(`${title} - Page ${page}/${totalPages}`) - .addFields(paginatedFields); - return embed; - }; - - reply = await interaction.reply({ - embeds: [await embedMessage(page)], - components: [row], - }); - - client.pages = client.pages || new Map(); - - client.pages.set(interaction.id, { - page, - fields, - totalPages, - originalEmbed: await embedMessage(page), - }); - - const filter = (i) => i.user.id === interaction.user.id; - - const collector = reply.createMessageComponentCollector({ - componentType: ComponentType.Button, - filter, - time: 30000, - }); - - collector.on("end", () => { - const row = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId("previous") - .setLabel("Previous") - .setStyle(ButtonStyle.Primary) - .setDisabled(true), - new ButtonBuilder() - .setCustomId("next") - .setLabel("Next") - .setStyle(ButtonStyle.Primary) - .setDisabled(true) - ); - - reply.edit({ components: [row] }); - }); - }; -}; diff --git a/src/functions/handlers/permissionsHandler.js b/src/functions/handlers/permissionsHandler.js deleted file mode 100644 index c2fc2d6..0000000 --- a/src/functions/handlers/permissionsHandler.js +++ /dev/null @@ -1,113 +0,0 @@ -module.exports = (client) => { - client.checkPermissions = async (interaction, role, deffered) => { - const member = interaction.member; - let deferred; - - if (deferred === true) { - deferred = true; - } else { - deferred = false; - } - - if (!member) { - await interaction.reply({ - content: `There was an issue getting your user. Please try again.`, - empheral: true, - }); - return false; - } - - if (member.id === process.env.BOT_DEVELOPER_ID) return true; - - const verified = member.roles.cache.has(process.env.VERIFIED_ROLE_ID); - const moderator = member.roles.cache.has(process.env.MODERATOR_ROLE_ID); - const admin = member.roles.cache.has(process.env.ADMIN_ROLE_ID); - const owner = member.roles.cache.has(process.env.OWNER_ROLE_ID); - - switch (role) { - case "Verified": - if (verified || moderator || admin || owner) { - return true; - } else { - if (deferred == false) { - await interaction.reply({ - content: "You are not Verified.", - ephemeral: true, - }); - return false; - } else { - await interaction.editReply({ - content: "You are not Verified.", - ephemeral: true, - }); - return false; - } - } - - case "Moderator": - if (moderator || admin || owner) { - return true; - } else { - if (deferred == false) { - await interaction.reply({ - content: "You are not a Moderator.", - ephemeral: true, - }); - return false; - } else { - await interaction.editReply({ - content: "You are not an Moderator.", - ephemeral: true, - }); - return false; - } - } - - case "Administrator": - if (admin || owner) { - return true; - } else { - if (deferred == false) { - await interaction.reply({ - content: "You are not an Administrator.", - ephemeral: true, - }); - return false; - } else { - await interaction.editReply({ - content: "You are not an Administrator.", - ephemeral: true, - }); - return false; - } - } - - case "Owner": - if (owner) { - return true; - } else { - if (deferred == false) { - await interaction.reply({ - content: "You are not an Owner.", - ephemeral: true, - }); - return false; - } else { - await interaction.editReply({ - content: "You are not an Owner.", - ephemeral: true, - }); - return false; - } - } - - default: - console.error(`Invalid role specified in checkPermissions: ${role}`); - await interaction.reply({ - content: "An error occurred while checking permissions.", - ephemeral: true, - }); - return false; - } - }; -}; diff --git a/src/functions/handlers/statsHandler.js b/src/functions/handlers/statsHandler.js deleted file mode 100644 index f71eda5..0000000 --- a/src/functions/handlers/statsHandler.js +++ /dev/null @@ -1,40 +0,0 @@ -const UserProfile = require("../../events/mongo/schema/ProfileSchema"); - -module.exports = (client) => { - client.updateStats = async () => { - console.log("Stats"); - const guild = client.guilds.cache.get(process.env.GUILD_ID); - - const statsApiData = await client.handleAPI.get_site_statistics(); - console.log(statsApiData); - console.log(guild.memberCount); - - try { - if (statsApiData.totalUsers !== undefined) { - client.channels.cache - .get(process.env.THM_USERS) - .setName(`THM Users: ${statsApiData.totalUsers}`); - } - } catch (err) { - console.log("There was an issue with updating the total users."); - } - - try { - client.channels.cache - .get(process.env.THM_ROOMS) - .setName(`Total Rooms: ${statsApiData.publicRooms}`); - } catch (err) { - console.log("There was an issue with updating the public rooms."); - } - - try { - client.channels.cache - .get(process.env.DISCORD_USERS) - .setName(`Discord Users: ${guild.memberCount}`); - } catch (err) { - console.log("There was an issue with updating the discord users."); - } - - console.log("Successfully updated the statistics"); - }; -}; diff --git a/src/functions/handlers/verificationHandler.js b/src/functions/handlers/verificationHandler.js deleted file mode 100644 index 511a12e..0000000 --- a/src/functions/handlers/verificationHandler.js +++ /dev/null @@ -1,55 +0,0 @@ -const UserProfile = require("../../events/mongo/schema/ProfileSchema"); -const { fetchMember } = require("../../utils/memberUtils"); -const { assignRoles } = require("../../utils/roleUtils"); - -module.exports = (client) => { - client.roleSync = async () => { - const guild = client.guilds.cache.get(process.env.GUILD_ID); - const userProfiles = await UserProfile.find(); - - for (const profile of userProfiles) { - const member = await fetchMember(guild, profile.discordId); - - if (!member) { - await UserProfile.findOneAndDelete({ discordId: profile.discordId }); - console.log(`${profile.discordId} has been removed from the Database.`); - continue; - } - - const userApiData = await client.handleAPI.get_token_data(profile.token); - - if (isValidApiData(userApiData)) { - const hasUpdated = updateProfileFromApiData(profile, userApiData); - if (hasUpdated) { - assignRoles(member, userApiData); - await profile.save().catch(console.error); - console.log(`${profile.discordId} has been updated!`); - } else { - console.log(`${profile.discordId} is up-to-date!`); - } - } else { - console.error( - "Received invalid data from the API for user:", - profile.discordId - ); - } - } - console.log("Role sync finished!"); - - client.updateStats(client); - }; -}; - -function isValidApiData(data) { - return data && data.success; -} - -function updateProfileFromApiData(profile, apiData) { - const { username, subscribed, usersRank, points, level, avatar } = apiData; - profile.username = username; - profile.subscribed = subscribed === 1; - profile.level = level; - profile.avatar = avatar; - - return profile.isModified(); -} From b635a82329cb44f4c56997f0ed5b1153f0b1fa54 Mon Sep 17 00:00:00 2001 From: muhamedfazeel Date: Sun, 14 Dec 2025 09:24:36 +0530 Subject: [PATCH 19/21] [refactor] [component]: migrate button component to typescript --- src/components/buttons/join-giveaway.js | 37 ---------------- .../buttons/join-giveaway.component.ts | 42 +++++++++++++++++++ 2 files changed, 42 insertions(+), 37 deletions(-) delete mode 100644 src/components/buttons/join-giveaway.js create mode 100644 src/discord/components/buttons/join-giveaway.component.ts diff --git a/src/components/buttons/join-giveaway.js b/src/components/buttons/join-giveaway.js deleted file mode 100644 index af14fe2..0000000 --- a/src/components/buttons/join-giveaway.js +++ /dev/null @@ -1,37 +0,0 @@ -const Giveaway = require("../../events/mongo/schema/GiveawaySchema"); - -module.exports = { - data: { - name: "join-giveaway", - }, - async execute(interaction, client) { - const userId = interaction.user.id; - const messageId = interaction.message.id; - - const giveaway = await Giveaway.findOne({ messageId: messageId }); - - if (!giveaway) { - await interaction.reply({ - content: "This giveaway does not exist or has ended.", - ephemeral: true, - }); - return; - } - - if (giveaway.participants.includes(userId)) { - await interaction.reply({ - content: "You have already joined this giveaway!", - ephemeral: true, - }); - return; - } - - giveaway.participants.push(userId); - await giveaway.save(); - - await interaction.reply({ - content: "You have successfully joined the giveaway!", - ephemeral: true, - }); - }, -}; diff --git a/src/discord/components/buttons/join-giveaway.component.ts b/src/discord/components/buttons/join-giveaway.component.ts new file mode 100644 index 0000000..e07adcb --- /dev/null +++ b/src/discord/components/buttons/join-giveaway.component.ts @@ -0,0 +1,42 @@ +import { ButtonInteraction, MessageFlags } from "discord.js"; +import { IComponent } from "../../../interfaces/component.interface"; +import { Giveaway } from "../../../schemas/giveaway.schema"; + + +const component: IComponent = { + data: { + name: "join-giveaway", + }, + async execute(interaction: ButtonInteraction) { + const userId: string = interaction.user.id; + const messageId: string = interaction.message.id; + + const giveaway = await Giveaway.findOne({ messageId: messageId }); + + if (!giveaway) { + await interaction.reply({ + content: "This giveaway does not exist or has ended.", + flags: [MessageFlags.Ephemeral], + }); + return; + } + + if (giveaway.participants.includes(userId)) { + await interaction.reply({ + content: "You have already joined this giveaway!", + flags: [MessageFlags.Ephemeral], + }); + return; + } + + giveaway.participants.push(userId); + await giveaway.save(); + + await interaction.reply({ + content: "You have successfully joined the giveaway!", + flags: [MessageFlags.Ephemeral], + }); + }, +}; + +export default component; From b2408100caf53d5e62fef8ff814bfb7b0c7630f0 Mon Sep 17 00:00:00 2001 From: muhamedfazeel Date: Sun, 14 Dec 2025 09:25:24 +0530 Subject: [PATCH 20/21] [refactor] [commands]: migrate commands to typescript --- src/commands/administrator/giveaway.js | 286 ------------------ src/commands/administrator/newroom.js | 69 ----- src/commands/moderator/lookup.js | 108 ------- src/commands/moderator/rule.js | 111 ------- src/commands/moderator/unlink.js | 132 -------- src/commands/utility/docs.js | 65 ---- src/commands/utility/help.js | 24 -- src/commands/utility/notifyme.js | 50 --- src/commands/utility/ollie.js | 30 -- src/commands/utility/ping.js | 30 -- src/commands/utility/rank.js | 86 ------ src/commands/utility/socials.js | 160 ---------- src/commands/utility/verify.js | 81 ----- .../administrator/giveaway.command.ts | 286 ++++++++++++++++++ .../commands/administrator/newroom.command.ts | 75 +++++ .../commands/moderator/lookup.command.ts | 185 +++++++++++ .../commands/moderator/rule.command.ts | 118 ++++++++ .../commands/moderator/unlink.command.ts | 138 +++++++++ src/discord/commands/utility/docs.command.ts | 79 +++++ .../commands/utility/health.command.ts | 92 ++++++ src/discord/commands/utility/help.command.ts | 31 ++ .../commands/utility/notify-me.command.ts | 75 +++++ src/discord/commands/utility/ollie.command.ts | 39 +++ src/discord/commands/utility/ping.command.ts | 42 +++ src/discord/commands/utility/rank.command.ts | 97 ++++++ .../commands/utility/socials.command.ts | 165 ++++++++++ .../commands/utility/verify.command.ts | 102 +++++++ 27 files changed, 1524 insertions(+), 1232 deletions(-) delete mode 100644 src/commands/administrator/giveaway.js delete mode 100644 src/commands/administrator/newroom.js delete mode 100644 src/commands/moderator/lookup.js delete mode 100644 src/commands/moderator/rule.js delete mode 100644 src/commands/moderator/unlink.js delete mode 100644 src/commands/utility/docs.js delete mode 100644 src/commands/utility/help.js delete mode 100644 src/commands/utility/notifyme.js delete mode 100644 src/commands/utility/ollie.js delete mode 100644 src/commands/utility/ping.js delete mode 100644 src/commands/utility/rank.js delete mode 100644 src/commands/utility/socials.js delete mode 100644 src/commands/utility/verify.js create mode 100644 src/discord/commands/administrator/giveaway.command.ts create mode 100644 src/discord/commands/administrator/newroom.command.ts create mode 100644 src/discord/commands/moderator/lookup.command.ts create mode 100644 src/discord/commands/moderator/rule.command.ts create mode 100644 src/discord/commands/moderator/unlink.command.ts create mode 100644 src/discord/commands/utility/docs.command.ts create mode 100644 src/discord/commands/utility/health.command.ts create mode 100644 src/discord/commands/utility/help.command.ts create mode 100644 src/discord/commands/utility/notify-me.command.ts create mode 100644 src/discord/commands/utility/ollie.command.ts create mode 100644 src/discord/commands/utility/ping.command.ts create mode 100644 src/discord/commands/utility/rank.command.ts create mode 100644 src/discord/commands/utility/socials.command.ts create mode 100644 src/discord/commands/utility/verify.command.ts diff --git a/src/commands/administrator/giveaway.js b/src/commands/administrator/giveaway.js deleted file mode 100644 index 333b7b4..0000000 --- a/src/commands/administrator/giveaway.js +++ /dev/null @@ -1,286 +0,0 @@ -const { - SlashCommandBuilder, - PermissionFlagsBits, - EmbedBuilder, - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, -} = require("discord.js"); - -const { checkDate } = require("../../utils/timerUtils.js"); - -const Giveaway = require("../../events/mongo/schema/GiveawaySchema"); - -const { giveawayEnd } = require("../../utils/giveawayUtils"); - -module.exports = { - data: new SlashCommandBuilder() - .setName("giveaway") - .setDescription("Returns the API and Client latency") - .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages) - .addSubcommand((subcommand) => - subcommand - .setName("create") - .setDescription("Start a giveaway") - .addIntegerOption((option) => - option - .setName("day") - .setDescription("Day of the month, 1-31") - .setRequired(true) - ) - .addIntegerOption((option) => - option - .setName("month") - .setDescription("Month of the year, 1-12") - .setRequired(true) - ) - .addIntegerOption((option) => - option - .setName("year") - .setDescription("Year must start with 20, 20XX") - .setRequired(true) - ) - .addIntegerOption((option) => - option - .setName("time") - .setDescription("24-hour time format, 0-23") - .setRequired(true) - ) - .addIntegerOption((option) => - option - .setName("winners") - .setDescription("Amount of winners to be chosen.") - .setRequired(true) - ) - ) - .addSubcommand((subcommand) => - subcommand - .setName("stop") - .setDescription("Reroll the winners.") - .addStringOption((option) => - option - .setName("stop-id") - .setDescription("id of the giveaway you want to stop") - .setRequired(true) - ) - .addBooleanOption((option) => - option - .setName("announce") - .setDescription( - "Whether you want it posted in #community-announcements" - ) - .setRequired(true) - ) - ) - .addSubcommand((subcommand) => - subcommand - .setName("stats") - .setDescription("Reroll the winners.") - .addStringOption((option) => - option - .setName("stats-id") - .setDescription("id of the giveaway you want to view") - .setRequired(true) - ) - ) - .addSubcommand((subcommand) => - subcommand - .setName("view") - .setDescription("View the current active giveaway details.") - ) - .addSubcommand((subcommand) => - subcommand - .setName("reroll") - .setDescription("Reroll the winners.") - .addStringOption((option) => - option - .setName("id") - .setDescription("id of the giveaway you want to view") - .setRequired(true) - ) - .addIntegerOption((option) => - option - .setName("amount") - .setDescription("Amount of winners to be rerolled.") - .setRequired(true) - ) - ), - async execute(interaction, client) { - const hasPermission = await client.checkPermissions( - interaction, - "Administrator" - ); - if (!hasPermission) return; - - const subcommand = interaction.options.getSubcommand(); - - switch (subcommand) { - case "create": - const day = interaction.options.getInteger("day"); - const month = interaction.options.getInteger("month"); - const year = interaction.options.getInteger("year"); - const time = interaction.options.getInteger("time"); - const winners = interaction.options.getInteger("winners"); - const description = `Click below to join the giveaway! <@&${process.env.ANNOUNCEMENTS_ROLE_ID}>`; - - const endDate = await checkDate(day, month, year, time); - if (!endDate || endDate < new Date()) { - await interaction.reply("Invalid date provided."); - return; - } - - const button = new ButtonBuilder() - .setCustomId("join-giveaway") - .setLabel("Join Giveaway") - .setStyle(ButtonStyle.Primary); - - const announcementChannel = client.guilds.cache - .get(process.env.GUILD_ID) - .channels.cache.get(process.env.COMMUNITY_ANNOUNCEMENTS); - - const giveawayMessage = await announcementChannel - .send({ - content: description, - components: [new ActionRowBuilder().addComponents(button)], - }) - .catch((err) => { - console.error("[GIVEAWAY] Error sending giveaway message:", err); - return null; - }); - - if (!giveawayMessage) { - await interaction.reply("Failed to send giveaway message."); - return; - } - - await Giveaway.create({ - endDate, - winners, - participants: [], - messageId: giveawayMessage.id, - concluded: false, - }); - - await interaction.reply( - `Giveaway created and will end on ${endDate.toLocaleString()}.` - ); - break; - - case "stop": - const stopId = interaction.options.getString("stop-id"); - const announce = interaction.options.getBoolean("announce"); - - await giveawayEnd(client, stopId, announce); - console.log(`Giveaway ended for giveaway ID: ${stopId}`); - - await interaction.reply({ - content: `Successfully ended giveaway: ${stopId}`, - }); - - break; - - case "view": - const now = new Date(); - const activeGiveaways = await Giveaway.find({ - endDate: { $gte: now }, - concluded: { $ne: true }, - }).sort({ endDate: 1 }); - - const giveawayFields = activeGiveaways.map((g, index) => ({ - name: `Giveaway #${index + 1}`, - value: `Ends on: ${g.endDate.toLocaleString()}\nID: ${g._id}`, - })); - - if (giveawayFields.length === 0) { - await interaction.reply("No active giveaways found."); - break; - } - - await client.paginationEmbed(interaction, giveawayFields, { - title: "Active Giveaways", - color: "#8AC7DB", - maxPerPage: 5, - }); - break; - - case "stats": - const statsId = interaction.options.getString("stats-id"); - let giveawayStats; - - try { - giveawayStats = await Giveaway.findById(statsId); - if (!giveawayStats) { - await interaction.reply(`No giveaway found with ID: ${statsId}`); - break; - } - - const statsEmbed = new EmbedBuilder() - .setColor("#8AC7DB") - .setTitle(`Giveaway Stats - ID: ${statsId}`) - .addFields( - { - name: "End Date", - value: giveawayStats.endDate.toLocaleString(), - inline: true, - }, - { - name: "Total Winners", - value: giveawayStats.winners.toString(), - inline: true, - }, - { - name: "Message ID", - value: giveawayStats.messageId || "Not Available", - inline: true, - }, - { - name: "Total Participants", - value: giveawayStats.participants.length.toString(), - inline: true, - } - ); - - await interaction.reply({ embeds: [statsEmbed] }); - } catch (err) { - await interaction.reply({ - content: `An error occurred while retrieving the giveaway: ${err.message}. Please check the ID and try again.`, - }); - break; - } - break; - - case "reroll": - const rerollId = interaction.options.getString("id"); - const amountToReroll = interaction.options.getInteger("amount"); - - const giveawayToReroll = await Giveaway.findById(rerollId); - - if (!giveawayToReroll) { - await interaction.reply(`No giveaway found with ID: ${rerollId}`); - break; - } - - if (amountToReroll <= 0 || amountToReroll > giveawayToReroll.winners) { - await interaction.reply("Invalid amount to reroll."); - break; - } - - const shuffledParticipants = [...giveawayToReroll.participants].sort( - () => 0.5 - Math.random() - ); - const newWinners = shuffledParticipants.slice(0, amountToReroll); - - var newWinnersMessage = `New winners for giveaway ID: ${rerollId}\n\n`; - for (const winner of newWinners) { - newWinnersMessage += `<@${winner}>\n`; - } - - await interaction.reply(newWinnersMessage); - - break; - - default: - break; - } - }, -}; diff --git a/src/commands/administrator/newroom.js b/src/commands/administrator/newroom.js deleted file mode 100644 index 14fcf34..0000000 --- a/src/commands/administrator/newroom.js +++ /dev/null @@ -1,69 +0,0 @@ -const { - SlashCommandBuilder, - EmbedBuilder, - PermissionFlagsBits, -} = require("discord.js"); - -const sharp = require("sharp"); - -module.exports = { - data: new SlashCommandBuilder() - .setName("newroom") - .setDescription("Returns the API and Client latency") - .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages) - .addStringOption((option) => - option - .setName("code") - .setDescription("the room that you want to announce") - .setRequired(true) - ), - - async execute(interaction, client) { - const hasPermission = await client.checkPermissions( - interaction, - "Administrator" - ); - if (!hasPermission) return; - - interaction.reply({ content: "This command is currently disabled" }); - return; - await interaction.deferReply({ - ephemeral: false, - }); - - const code = interaction.options.getString("code"); - let svg; - - room = await client.handleAPI.get_room_data(code); - - console.log(room); - - if (isSvgFile(room.image)) { - sharp(room.data[code].image) - .png() - .toFile("recent_room_image.png", (err, info) => { - if (err) throw err; - console.log("Image converted", info); - }); - - svg = true; - } else { - svg = false; - } - - const pingEmbed = new EmbedBuilder() - .setTitle("Pong!") - .setImage(svg ? room.data[code].image : room.data[code].image) - .setTimestamp(Date.now()) - .setFields({ - name: "API Latency", - value: `ms`, - }); - - await interaction.editReply({ embeds: [pingEmbed] }); - }, -}; - -function isSvgFile(filePath) { - return path.extname(filePath).toLowerCase() === ".svg"; -} diff --git a/src/commands/moderator/lookup.js b/src/commands/moderator/lookup.js deleted file mode 100644 index 7da336f..0000000 --- a/src/commands/moderator/lookup.js +++ /dev/null @@ -1,108 +0,0 @@ -const { - SlashCommandBuilder, - EmbedBuilder, - PermissionFlagsBits, -} = require("discord.js"); - -const UserProfile = require("../../events/mongo/schema/ProfileSchema"); - -const axios = require("axios"); - -module.exports = { - data: new SlashCommandBuilder() - .setName("lookup") - .setDescription("Looks up a linked account by token or user.") - .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages) - .addSubcommand((subcommand) => - subcommand - .setName("token") - .setDescription("Looks up by TryHackMe token.") - .addStringOption((option) => - option - .setName("token") - .setDescription("The TryHackMe token to lookup.") - .setRequired(true) - ) - ) - .addSubcommand((subcommand) => - subcommand - .setName("user") - .setDescription("Looks up by Discord user.") - .addUserOption((option) => - option - .setName("user") - .setDescription("The Discord user to lookup.") - .setRequired(true) - ) - ), - - async execute(interaction, client) { - const subcommand = interaction.options.getSubcommand(); - let userProfile; - notFoundMessage = "No Discord account linked with this TryHackMe token."; - - const hasPermission = await client.checkPermissions( - interaction, - "Moderator" - ); - if (!hasPermission) return; - - if (subcommand === "token") { - const token = interaction.options.getString("token"); - userProfile = await UserProfile.findOne({ token }).exec(); - } else if (subcommand === "user") { - const user = interaction.options.getUser("user"); - userProfile = await UserProfile.findOne({ discordId: user.id }).exec(); - } - - if (!userProfile) { - return await interaction.reply({ - content: notFoundMessage, - ephemeral: true, - }); - } - - const deleted = await axios - .get(`https://tryhackme.com/p/${userProfile.username}`) - .then((res) => - res.request.path == "/r/not-found" ? "Deleted" : "Active" - ) - .catch((err) => console.log(err)); - - const embed = new EmbedBuilder() - .setColor("#5865F2") - .setAuthor({ - name: `TryHackMe`, - iconURL: `https://tryhackme.com/img/favicon.png`, - }) - .setThumbnail(userProfile.avatar) - .setTimestamp() - .addFields([ - { - name: "Discord Mention", - value: `<@${userProfile.discordId}>`, - inline: true, - }, - { name: "Discord ID", value: userProfile.discordId, inline: true }, - { name: "THM Token", value: userProfile.token, inline: true }, - { - name: "THM Username", - value: `[${userProfile.username}]()`, - inline: true, - }, - { name: "THM Level", value: userProfile.level, inline: true }, - { - name: "Subscription Status", - value: `${userProfile.subscribed}`, - inline: true, - }, - { - name: "Account Status", - value: `${deleted}`, - inline: true, - }, - ]); - - await interaction.reply({ embeds: [embed], ephemeral: false }); - }, -}; diff --git a/src/commands/moderator/rule.js b/src/commands/moderator/rule.js deleted file mode 100644 index 7d6b6c8..0000000 --- a/src/commands/moderator/rule.js +++ /dev/null @@ -1,111 +0,0 @@ -const { - SlashCommandBuilder, - EmbedBuilder, - PermissionFlagsBits, -} = require("discord.js"); - -module.exports = { - data: new SlashCommandBuilder() - .setName("rule") - .setDescription("Quote a rule from the Discord server") - .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages) - .addIntegerOption((option) => - option - .setName("number") - .setDescription("The rule to be quoted.") - .setRequired(true) - ) - .addUserOption((option) => - option - .setName("mention") - .setDescription("Optionally mention a user with the response.") - .setRequired(false) - ), - async execute(interaction, client) { - const number = interaction.options.getInteger("number"); - const mention = interaction.options.getUser("mention"); - - const hasPermission = await client.checkPermissions( - interaction, - "Moderator" - ); - if (!hasPermission) return; - - switch (number) { - case 1: - text = `No Abusive Language`; - description = - "Harassment, bullying, discrimination, or abusive language of any kind is not tolerated. We're here to grow together, share insights, and celebrate wins. No one should ever feel unsafe or threatened.\nKeep your language β€œsafe for work.” If you're not okay with your employer seeing it, don't write it. And who knows, you may meet your future employer here someday!"; - break; - - case 2: - text = "Keep Discussions Relevant"; - description = "Please keep discussion relevant to the channel topic."; - break; - - case 3: - text = "No Advertising"; - description = - "No excessive self-promotion. While you're welcome to post your write-ups, walkthroughs, and streams of TryHackMe content, spamming of your own channels isn't tolerated. "; - break; - - case 4: - text = "No Illegal or Harmful Activity"; - description = - "We do not teach unethical hackers. Please don't discuss illegal or unethical topics. Please don't post any intentionally harmful commands or distribute malware."; - break; - - case 5: - text = "No Cheating"; - description = - "Cheating of any form is not allowed. This is not limited to asking for help with assessed schoolwork or exams."; - break; - - case 6: - text = "No Spamming"; - description = - "If your question hasn’t been answered right away, don’t spam. Massive walls of text or many individual messages count as spam. This includes posting the same message across multiple channels."; - break; - - case 7: - text = "Use English"; - description = - "Please keep all communication in English. This also means no encrypted posting."; - break; - - case 8: - text = "No DMs Without Consent"; - description = - "Always ask permission before sending a DM or friend request to another user. "; - break; - - case 9: - text = "Follow Server Staff Direction"; - description = - "Our team are here to protect the interests of the community. Follow their direction at all times."; - break; - - default: - text = "Rule Not Found"; - description = "Please ensure you are selecting one listed from #rules"; - break; - } - - const embed = new EmbedBuilder() - .setColor("#5865F2") - .setTitle(`<#${process.env.RULES}>`) - .addFields({ - name: `Rule ${number} - ${text}`, - value: description, - }) - .setFooter({ - text: `TryHackMe Rules`, - iconURL: "https://assets.tryhackme.com/img/favicon.png", - }) - .setTimestamp(); - - const messageContent = mention ? `${mention}` : ""; - - await interaction.reply({ embeds: [embed], content: messageContent }); - }, -}; diff --git a/src/commands/moderator/unlink.js b/src/commands/moderator/unlink.js deleted file mode 100644 index fb01107..0000000 --- a/src/commands/moderator/unlink.js +++ /dev/null @@ -1,132 +0,0 @@ -const { - SlashCommandBuilder, - EmbedBuilder, - PermissionFlagsBits, -} = require("discord.js"); - -const UserProfile = require("../../events/mongo/schema/ProfileSchema"); -const { fetchMember } = require("../../utils/memberUtils"); -const { removeRoles } = require("../../utils/roleUtils"); - -module.exports = { - data: new SlashCommandBuilder() - .setName("unlink") - .setDescription("Detaches a Discord account from a TryHackMe token.") - .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages) - .addSubcommand((subcommand) => - subcommand - .setName("user") - .setDescription("Unlink a user via their Discord username") - .addUserOption((option) => - option - .setName("user") - .setDescription("The user to be unlinked.") - .setRequired(true) - ) - ) - .addSubcommand((subcommand) => - subcommand - .setName("token") - .setDescription("Unlink a user via their TryHackMe Discord token.") - .addStringOption((option) => - option - .setName("token") - .setDescription("Token attached to their account.") - .setRequired(true) - ) - ), - - async execute(interaction, client) { - const subcommand = interaction.options.getSubcommand(); - - const token = interaction.options.getString("token"); - const user = interaction.options.getUser("user"); - - let userProfile; - - const hasPermission = await client.checkPermissions( - interaction, - "Moderator" - ); - if (!hasPermission) return; - - // Fetch user profile from database - - switch (subcommand) { - case "token": - userProfile = await UserProfile.findOne({ token }); - - // Check if the user profile exists and the token matches - if (!userProfile || userProfile.token !== token) { - return interaction.reply({ - content: - "No linked account found with this token, or token does not match.\nEnsure that there are no spaces before or after the token.", - ephemeral: true, - }); - } - - // Remove the user's data from the database - await UserProfile.deleteOne({ token }); - break; - - case "user": - userProfile = await UserProfile.findOne({ discordId: user.id }); - - // Check if the user profile exists - if (!userProfile) { - return interaction.reply({ - content: "No linked account found with this user.", - ephemeral: true, - }); - } - - // Remove the user's data from the database - await UserProfile.deleteOne({ discordId: user.id }); - break; - - default: - break; - } - - // Remove roles from the user if they have them - const guild = client.guilds.cache.get(process.env.GUILD_ID); - const member = await fetchMember(guild, userProfile.discordId); - - if (member) { - removeRoles(member); - } - - // Send a confirmation message with the user mention - await interaction.reply({ - content: `<@${userProfile.discordId}> has been successfully unlinked.`, - ephemeral: true, - }); - - const unlinkEmbed = new EmbedBuilder() - .setAuthor({ - name: `${interaction.user.tag}`, - iconURL: interaction.user.displayAvatarURL(), - }) - .setColor("#FFA500") - .setTitle(`Account Unlink`) - .setFields([ - { name: "User", value: `<@${userProfile.discordId}>`, inline: true }, - { name: "Token", value: userProfile.token, inline: true }, - ]) - .setTimestamp(); - - // Send the embed to the logging channel - const loggingChannel = client.guilds.cache - .get(process.env.GUILD_ID) - .channels.cache.get(process.env.BOT_LOGGING); - if (loggingChannel) { - loggingChannel - .send({ embeds: [unlinkEmbed] }) - .catch((err) => - console.log( - "[UNLINK] Error with sending the embed to the logging channel." - ) - ); - } - }, -}; diff --git a/src/commands/utility/docs.js b/src/commands/utility/docs.js deleted file mode 100644 index ec12690..0000000 --- a/src/commands/utility/docs.js +++ /dev/null @@ -1,65 +0,0 @@ -const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); - -module.exports = { - data: new SlashCommandBuilder() - .setName("docs") - .setDescription("Search for a document on our knowledge base") - .addStringOption((option) => - option - .setName("search") - .setDescription("find an article by its title") - .setRequired(true) - ) - .addUserOption((option) => - option - .setName("mention") - .setDescription("Optionally mention a user with the response.") - .setRequired(false) - ), - - async execute(interaction, client) { - await interaction.deferReply({}); - - const user = interaction.options.getUser("mention"); - const search = interaction.options.getString("search"); - - if (search && search.length > 100) { - return await interaction.editReply({ - content: "Your input exceeds the character limit. Please try again.", - ephemeral: true, - }); - } - - let article; - try { - article = await client.handleAPI.get_article_by_phrase(search); - } catch (error) { - console.error("Error fetching article:", error); - return await interaction.editReply({ - content: - "An error occurred while fetching the article. Please try again later.", - ephemeral: true, - }); - } - - if (article === null) { - return await interaction.editReply({ - content: "I could not find an article, please try again.", - ephemeral: true, - }); - } - - const embed = new EmbedBuilder() - .setTitle(article.title) - .setThumbnail(client.user.displayAvatarURL()) - .setTimestamp(Date.now()) - .addFields({ - name: "Article Link", - value: `${article.url}`, - }); - - const messageContent = user ? `${user}` : ""; - - await interaction.editReply({ content: messageContent, embeds: [embed] }); - }, -}; diff --git a/src/commands/utility/help.js b/src/commands/utility/help.js deleted file mode 100644 index 9e41391..0000000 --- a/src/commands/utility/help.js +++ /dev/null @@ -1,24 +0,0 @@ -const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); - -module.exports = { - data: new SlashCommandBuilder() - .setName("help") - .setDescription("Information on how to use the bot!"), - - async execute(interaction, client) { - const message = await interaction.deferReply({ - fetchReply: true, - }); - - const pingEmbed = new EmbedBuilder() - .setTitle("Information") - .setThumbnail(client.user.displayAvatarURL()) - .setTimestamp(Date.now()) - .setFields({ - name: "Discord Slash Commands", - value: `https://support.discord.com/hc/en-us/articles/1500000368501-Slash-Commands-FAQ`, - }); - - await interaction.editReply({ embeds: [pingEmbed] }); - }, -}; diff --git a/src/commands/utility/notifyme.js b/src/commands/utility/notifyme.js deleted file mode 100644 index 20e977a..0000000 --- a/src/commands/utility/notifyme.js +++ /dev/null @@ -1,50 +0,0 @@ -const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); - -module.exports = { - data: new SlashCommandBuilder() - .setName("notifyme") - .setDescription("Toggle announcements notifications.") - .setDMPermission(false), - - async execute(interaction, client) { - try { - let content; - - const roleID = process.env.ANNOUNCEMENTS_ROLE_ID; - if (!roleID) { - console.log( - "ANNOUNCEMENTS_ROLE_ID is not set in environment variables." - ); - return interaction.reply({ - content: "Role ID is not configured.", - ephemeral: true, - }); - } - - const member = await interaction.guild.members.fetch(interaction.user.id); - if (!member) { - return interaction.reply({ - content: - "Member not found in the guild. Please try again in 15 minutes.", - ephemeral: true, - }); - } - - if (member.roles.cache.has(roleID)) { - await member.roles.remove(roleID); - content = "You will no longer receive announcements notifications."; - } else { - await member.roles.add(roleID); - content = "You will now receive announcements notifications."; - } - - await interaction.reply({ content: content, ephemeral: true }); - } catch (error) { - console.error("Error in notifyme command:", error); - await interaction.reply({ - content: "An error occurred while processing your request.", - ephemeral: true, - }); - } - }, -}; diff --git a/src/commands/utility/ollie.js b/src/commands/utility/ollie.js deleted file mode 100644 index 681d641..0000000 --- a/src/commands/utility/ollie.js +++ /dev/null @@ -1,30 +0,0 @@ -const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); - -module.exports = { - data: new SlashCommandBuilder() - .setName("ollie") - .setDescription("Retrieves a special picture"), - - async execute(interaction, client) { - await interaction.deferReply(); - - const pictureUrl = await client.handleAPI.get_ollie_picture(); - if (!pictureUrl) { - return interaction.editReply("Failed to retrieve the picture."); - } - - const ollieEmbed = new EmbedBuilder() - .setTitle("Ollie") - .setColor("#5865F2") - .setImage(pictureUrl) - .setAuthor({ - name: client.user.username, - iconURL: client.user.displayAvatarURL(), - }) - .setFooter({ - text: "Ollie Unix Montgomery - Rest in Peace, 5th of January, 2023", - }); - - await interaction.editReply({ embeds: [ollieEmbed] }); - }, -}; diff --git a/src/commands/utility/ping.js b/src/commands/utility/ping.js deleted file mode 100644 index 179054f..0000000 --- a/src/commands/utility/ping.js +++ /dev/null @@ -1,30 +0,0 @@ -const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); - -module.exports = { - data: new SlashCommandBuilder() - .setName("ping") - .setDescription("Returns the API and Client latency"), - - async execute(interaction, client) { - const message = await interaction.deferReply({ - fetchReply: true, - }); - - const pingEmbed = new EmbedBuilder() - .setTitle("Pong!") - .setThumbnail(client.user.displayAvatarURL()) - .setTimestamp(Date.now()) - .setFields( - { - name: "API Latency", - value: `${client.ws.ping}ms`, - }, - { - name: "Client Ping", - value: `${message.createdTimestamp - interaction.createdTimestamp}ms`, - } - ); - - await interaction.editReply({ embeds: [pingEmbed] }); - }, -}; diff --git a/src/commands/utility/rank.js b/src/commands/utility/rank.js deleted file mode 100644 index 8fefdeb..0000000 --- a/src/commands/utility/rank.js +++ /dev/null @@ -1,86 +0,0 @@ -const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); -const UserProfile = require("../../events/mongo/schema/ProfileSchema"); - -module.exports = { - data: new SlashCommandBuilder() - .setName("rank") - .setDescription("Get the TryHackMe rank of a user.") - .addStringOption((option) => - option - .setName("username") - .setDescription("The username to retrieve the rank for.") - .setRequired(true) - ), - - async execute(interaction, client) { - const username = interaction.options.getString("username"); - - if ( - interaction.channel && - interaction.channel.id === process.env.BOT_COMMANDS - ) { - await interaction.deferReply({ ephemeral: false }); - } else { - await interaction.deferReply({ ephemeral: true }); - } - - if (username && username.length > 30) { - return await interaction.editReply({ - content: - "Your input exceeds the username character limit. Please try again.", - ephemeral: true, - }); - } - - try { - const userProfile = await client.handleAPI.get_user_data(username); - if ( - !userProfile || - userProfile.userRank == 0 || - userProfile.userRank == undefined || - userProfile.points === "undefined" || - userProfile.points === undefined - ) { - return interaction.editReply({ - content: "User not found.", - ephemeral: true, - }); - } - - const tryhackme_url = "https://tryhackme.com/p/"; - const rankEmbed = new EmbedBuilder() - .setColor("#5865F2") - .setTitle(userProfile.username || username) - .addFields( - { - name: "Leaderboard Position", - value: `${userProfile.rank || userProfile.userRank}`, - }, - { - name: "Username", - value: `[${userProfile.username || username}](${tryhackme_url}${ - userProfile.username || username - })`, - inline: true, - }, - { name: "Points", value: String(userProfile.points), inline: true }, - { - name: "Subscribed?", - value: userProfile.subscribed ? "Yes" : "No", - inline: true, - } - ) - .setThumbnail(userProfile.avatar); - - await interaction.editReply({ - embeds: [rankEmbed], - }); - } catch (error) { - console.error("Error in rank command:", error); - await interaction.editReply({ - content: "An error occurred while processing your request.", - ephemeral: true, - }); - } - }, -}; diff --git a/src/commands/utility/socials.js b/src/commands/utility/socials.js deleted file mode 100644 index 9b9f1b0..0000000 --- a/src/commands/utility/socials.js +++ /dev/null @@ -1,160 +0,0 @@ -const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); - -module.exports = { - data: new SlashCommandBuilder() - .setName("socials") - .setDescription("Get the latest information for TryHackMe's social media!") - .addSubcommand((subcommand) => - subcommand - .setName("github") - .setDescription("Get the bot's GitHub link.") - .addUserOption((option) => - option - .setName("mention") - .setDescription("Optionally mention a user with the response.") - .setRequired(false) - ) - ) - .addSubcommand((subcommand) => - subcommand - .setName("twitter") - .setDescription("Get TryHackMe's official Twitter!") - .addUserOption((option) => - option - .setName("mention") - .setDescription("Optionally mention a user with the response.") - .setRequired(false) - ) - ) - .addSubcommand((subcommand) => - subcommand - .setName("reddit") - .setDescription("Get the link to our amazing subreddit!") - .addUserOption((option) => - option - .setName("mention") - .setDescription("Optionally mention a user with the response.") - .setRequired(false) - ) - ) - .addSubcommand((subcommand) => - subcommand - .setName("website") - .setDescription("You should know our website by now!") - .addUserOption((option) => - option - .setName("mention") - .setDescription("Optionally mention a user with the response.") - .setRequired(false) - ) - ) - .addSubcommand((subcommand) => - subcommand - .setName("discord") - .setDescription("Looking to invite people to the Discord server?") - .addUserOption((option) => - option - .setName("mention") - .setDescription("Optionally mention a user with the response.") - .setRequired(false) - ) - ) - .addSubcommand((subcommand) => - subcommand - .setName("blog") - .setDescription("Look at the awesome resources written by TryHackMe!") - .addUserOption((option) => - option - .setName("mention") - .setDescription("Optionally mention a user with the response.") - .setRequired(false) - ) - ) - .addSubcommand((subcommand) => - subcommand - .setName("shop") - .setDescription("Get some awesome swag to show off to your friends.") - .addUserOption((option) => - option - .setName("mention") - .setDescription("Optionally mention a user with the response.") - .setRequired(false) - ) - ) - .addSubcommand((subcommand) => - subcommand - .setName("email") - .setDescription("TryHackMe's support email address.") - .addUserOption((option) => - option - .setName("mention") - .setDescription("Optionally mention a user with the response.") - .setRequired(false) - ) - ), - - async execute(interaction) { - const subcommand = interaction.options.getSubcommand(); - const user = interaction.options.getUser("mention"); - - let text; - let description; - - switch (subcommand) { - case "github": - text = "https://github.com/JabbaSec/tryhackme"; - description = "Get the bot's GitHub link."; - break; - case "twitter": - text = "https://twitter.com/realtryhackme/"; - description = "Get TryHackMe's official Twitter!"; - break; - case "reddit": - text = "https://www.reddit.com/r/tryhackme/"; - description = "Get the link to our amazing subreddit!"; - break; - case "website": - text = "https://tryhackme.com/"; - description = "You should know our website by now!"; - break; - case "discord": - text = "https://discord.com/invite/tryhackme"; - description = "Looking to invite people to the Discord server?"; - break; - case "blog": - text = "https://tryhackme.com/r/resources/blog"; - description = "Look at the awesome resources written by TryHackMe!"; - break; - case "shop": - text = "https://store.tryhackme.com/"; - description = "Get some awesome swag to show off to your friends."; - break; - case "email": - text = "support@tryhackme.com"; - description = "TryHackMe's support email address."; - break; - } - - const embed = new EmbedBuilder() - .setColor("#5865F2") - .setTitle( - `TryHackMe's ${ - subcommand.charAt(0).toUpperCase() + subcommand.slice(1) - }` - ) - .setDescription(description) - .addFields({ - name: "\u200B", - value: text, - }) - .setFooter({ - text: "TryHackMe Socials", - iconURL: "https://assets.tryhackme.com/img/favicon.png", - }) - .setTimestamp(); - - const messageContent = user ? `${user}` : ""; - - await interaction.reply({ content: messageContent, embeds: [embed] }); - }, -}; diff --git a/src/commands/utility/verify.js b/src/commands/utility/verify.js deleted file mode 100644 index e3d376e..0000000 --- a/src/commands/utility/verify.js +++ /dev/null @@ -1,81 +0,0 @@ -const { SlashCommandBuilder } = require("discord.js"); -const UserProfile = require("../../events/mongo/schema/ProfileSchema"); -const { fetchMember } = require("../../utils/memberUtils"); -const { assignRoles } = require("../../utils/roleUtils"); - -module.exports = { - data: new SlashCommandBuilder() - .setName("verify") - .setDescription("Syncs your TryHackMe site account to Discord") - .addStringOption((option) => - option - .setName("token") - .setDescription( - "Use your Discord token from https://tryhackme.com/profile" - ) - .setRequired(true) - ), - - async execute(interaction, client) { - await interaction.deferReply({ ephemeral: true }); - - const token = interaction.options.getString("token"); - const guild = client.guilds.cache.get(process.env.GUILD_ID); - const discordId = interaction.user.id; - - let userDiscordProfile = await UserProfile.findOne({ discordId }); - if (userDiscordProfile && userDiscordProfile.token !== token) { - return interaction.editReply({ - content: - "Your Discord account is already linked with a token. If you wish to update your token, please contact a moderator.", - }); - } - - let tokenProfile = await UserProfile.findOne({ token }); - if (tokenProfile && tokenProfile.discordId !== discordId) { - return interaction.editReply({ - content: "This token is already in use by another account.", - }); - } - - const userApiData = await client.handleAPI - .get_token_data(token) - .catch((error) => { - console.error("Received invalid data from the API:", error); - return interaction.editReply({ - content: - "Failed to verify your account. Please ensure your token is correct.", - }); - }); - - if (!userApiData || !userApiData.success) { - return interaction.editReply({ - content: - "Failed to verify your account. Please ensure your token is correct.", - }); - } - - let userProfile = userDiscordProfile || new UserProfile({ discordId }); - userProfile = updateUserProfile(userProfile, userApiData, token); - await userProfile.save().catch(console.error); - - const member = await fetchMember(guild, discordId); - if (member) { - assignRoles(member, userProfile); - } - - await interaction.editReply({ - content: "Your account has been updated and verified!", - }); - }, -}; - -function updateUserProfile(userProfile, userApiData, token) { - userProfile.username = userApiData.username; - userProfile.token = token; - userProfile.subscribed = userApiData.subscribed === 1; - userProfile.level = userApiData.level; - userProfile.avatar = userApiData.avatar; - - return userProfile; -} diff --git a/src/discord/commands/administrator/giveaway.command.ts b/src/discord/commands/administrator/giveaway.command.ts new file mode 100644 index 0000000..dd597c6 --- /dev/null +++ b/src/discord/commands/administrator/giveaway.command.ts @@ -0,0 +1,286 @@ +import { ActionRowBuilder, PermissionFlagsBits, SlashCommandBuilder, ChatInputCommandInteraction, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js"; +import { COMMAND_CATEGORY, COMMAND_SCOPE } from "../../../shared/constants"; +import { Giveaway } from "../../../schemas"; +import { checkDate } from "../../../shared/utils/timer.util"; +import { endGiveaway } from "../../../shared/utils/giveway.utils"; +import { CustomLogger } from "../../../shared/custom-logger"; +import { ICommand } from "../../../interfaces/command.interface"; + +const logger = new CustomLogger(__filename); + +const GiveawayCommand: ICommand = { + category: COMMAND_CATEGORY.ADMIN, + command: true, + scope: COMMAND_SCOPE.APPLICATION, + data: new SlashCommandBuilder() + .setName('giveaway') + .setDescription("Returns the API and Client latency") + .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages) + .addSubcommand((subcommand) => + subcommand + .setName("create") + .setDescription("Start a giveaway") + .addIntegerOption((option) => + option + .setName("day") + .setDescription("Day of the month, 1-31") + .setRequired(true) + ) + .addIntegerOption((option) => + option + .setName("month") + .setDescription("Month of the year, 1-12") + .setRequired(true) + ) + .addIntegerOption((option) => + option + .setName("year") + .setDescription("Year must start with 20, 20XX") + .setRequired(true) + ) + .addIntegerOption((option) => + option + .setName("time") + .setDescription("24-hour time format, 0-23") + .setRequired(true) + ) + .addIntegerOption((option) => + option + .setName("winners") + .setDescription("Amount of winners to be chosen.") + .setRequired(true) + ) + ) + .addSubcommand((subcommand) => + subcommand + .setName("stop") + .setDescription("Reroll the winners.") + .addStringOption((option) => + option + .setName("stop-id") + .setDescription("id of the giveaway you want to stop") + .setRequired(true) + ) + .addBooleanOption((option) => + option + .setName("announce") + .setDescription( + "Whether you want it posted in #community-announcements" + ) + .setRequired(true) + ) + ) + .addSubcommand((subcommand) => + subcommand + .setName("stats") + .setDescription("Reroll the winners.") + .addStringOption((option) => + option + .setName("stats-id") + .setDescription("id of the giveaway you want to view") + .setRequired(true) + ) + ) + .addSubcommand((subcommand) => + subcommand + .setName("view") + .setDescription("View the current active giveaway details.") + ) + .addSubcommand((subcommand) => + subcommand + .setName("reroll") + .setDescription("Reroll the winners.") + .addStringOption((option) => + option + .setName("id") + .setDescription("id of the giveaway you want to view") + .setRequired(true) + ) + .addIntegerOption((option) => + option + .setName("amount") + .setDescription("Amount of winners to be rerolled.") + .setRequired(true) + ) + ), + execute: async (interaction: ChatInputCommandInteraction, client: any) => { + const hasPermission = await client.checkPermissions( + interaction, + "Administrator" + ); + if (!hasPermission) return; + + const subcommand = interaction.options.getSubcommand(); + + switch (subcommand) { + case "create": + const day: number = interaction.options.getInteger("day") as number; + const month: number = interaction.options.getInteger("month") as number; + const year: number = interaction.options.getInteger("year") as number; + const time: number = interaction.options.getInteger("time") as number; + const winners: number = interaction.options.getInteger("winners") as number; + const description = `Click below to join the giveaway! <@&${process.env.ANNOUNCEMENTS_ROLE_ID}>`; + + const endDate = await checkDate(day, month, year, time); + if (!endDate || endDate < new Date()) { + await interaction.reply("Invalid date provided."); + return; + } + + const button = new ButtonBuilder() + .setCustomId("join-giveaway") + .setLabel("Join Giveaway") + .setStyle(ButtonStyle.Primary); + + const announcementChannel = client.guilds.cache + .get(process.env.GUILD_ID) + .channels.cache.get(process.env.COMMUNITY_ANNOUNCEMENTS); + + const giveawayMessage = await announcementChannel + .send({ + content: description, + components: [new ActionRowBuilder().addComponents(button)], + }) + .catch((err: any) => { + logger.error("[GIVEAWAY] Error sending giveaway message:", err); + return null; + }); + + if (!giveawayMessage) { + await interaction.reply("Failed to send giveaway message."); + return; + } + + await Giveaway.create({ + endDate, + winners, + participants: [], + messageId: giveawayMessage.id, + concluded: false, + }); + + await interaction.reply( + `Giveaway created and will end on ${endDate.toLocaleString()}.` + ); + break; + + case "stop": + const stopId = interaction.options.getString("stop-id"); + const announce = interaction.options.getBoolean("announce"); + + await endGiveaway(client, stopId, announce); + logger.info(`Giveaway ended for giveaway ID: ${stopId}`); + + await interaction.reply({ + content: `Successfully ended giveaway: ${stopId}`, + }); + + break; + + case "view": + const now = new Date(); + const activeGiveaways = await Giveaway.find({ + endDate: { $gte: now }, + concluded: { $ne: true }, + }).sort({ endDate: 1 }); + + const giveawayFields = activeGiveaways.map((g, index) => ({ + name: `Giveaway #${index + 1}`, + value: `Ends on: ${g.endDate.toLocaleString()}\nID: ${g._id}`, + })); + + if (giveawayFields.length === 0) { + await interaction.reply("No active giveaways found."); + break; + } + + await client.paginationEmbed(interaction, giveawayFields, { + title: "Active Giveaways", + color: "#8AC7DB", + maxPerPage: 5, + }); + break; + + case "stats": + const statsId = interaction.options.getString("stats-id"); + let giveawayStats; + + try { + giveawayStats = await Giveaway.findById(statsId); + if (!giveawayStats) { + await interaction.reply(`No giveaway found with ID: ${statsId}`); + break; + } + + const statsEmbed = new EmbedBuilder() + .setColor("#8AC7DB") + .setTitle(`Giveaway Stats - ID: ${statsId}`) + .addFields( + { + name: "End Date", + value: giveawayStats.endDate.toLocaleString(), + inline: true, + }, + { + name: "Total Winners", + value: giveawayStats.winners.toString(), + inline: true, + }, + { + name: "Message ID", + value: giveawayStats.messageId || "Not Available", + inline: true, + }, + { + name: "Total Participants", + value: giveawayStats.participants.length.toString(), + inline: true, + } + ); + + await interaction.reply({ embeds: [statsEmbed] }); + } catch (err: any) { + await interaction.reply({ + content: `An error occurred while retrieving the giveaway: ${err?.message}. Please check the ID and try again.`, + }); + break; + } + break; + + case "reroll": + const rerollId = interaction.options.getString("id"); + const amountToReroll = interaction.options.getInteger("amount") as number; + + const giveawayToReroll = await Giveaway.findById(rerollId); + + if (!giveawayToReroll) { + await interaction.reply(`No giveaway found with ID: ${rerollId}`); + break; + } + + if (amountToReroll <= 0 || amountToReroll > giveawayToReroll.winners) { + await interaction.reply("Invalid amount to reroll."); + break; + } + + const shuffledParticipants = [...giveawayToReroll.participants].sort( + () => 0.5 - Math.random() + ); + const newWinners = shuffledParticipants.slice(0, amountToReroll); + + var newWinnersMessage = `New winners for giveaway ID: ${rerollId}\n\n`; + for (const winner of newWinners) { + newWinnersMessage += `<@${winner}>\n`; + } + + await interaction.reply(newWinnersMessage); + + break; + + default: + break; + } + } +} + +export default GiveawayCommand; \ No newline at end of file diff --git a/src/discord/commands/administrator/newroom.command.ts b/src/discord/commands/administrator/newroom.command.ts new file mode 100644 index 0000000..70581ad --- /dev/null +++ b/src/discord/commands/administrator/newroom.command.ts @@ -0,0 +1,75 @@ +import { ChatInputCommandInteraction, EmbedBuilder, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } from "discord.js"; +import { COMMAND_CATEGORY, COMMAND_SCOPE } from "../../../shared/constants"; +import { CustomLogger } from "../../../shared/custom-logger"; +import { extname } from 'node:path'; +import { ICommand } from "../../../interfaces/command.interface"; +import { handleAPI } from "../../handlers/api.handler"; +import sharp from "sharp"; + +const logger = new CustomLogger(__filename); + +const NewRoomCommand: ICommand = { + category: COMMAND_CATEGORY.ADMIN, + // Disabling command + command: false, + scope: COMMAND_SCOPE.APPLICATION, + data: new SlashCommandBuilder() + .setName('newroom') + .setDescription("Returns the API and Client latency") + .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages) + .addStringOption((option) => + option + .setName("code") + .setDescription("the room that you want to announce") + .setRequired(true) + ), + execute: async (interaction: ChatInputCommandInteraction, client: any) => { + const hasPermission = await client.checkPermissions( + interaction, + "Administrator" + ); + if (!hasPermission) return; + + await interaction.deferReply({ + flags: [MessageFlags.Ephemeral] + }); + + const code: string = interaction.options.getString("code") as string; + let svg; + + const room = await handleAPI.get_room_data(code); + + logger.info(room); + + if (isSvgFile(room.image)) { + sharp(room.data[code].image) + .png() + .toFile("recent_room_image.png", (err, info) => { + if (err) throw err; + logger.info("Image converted", info); + }); + + svg = true; + } else { + svg = false; + } + + const pingEmbed = new EmbedBuilder() + .setTitle("Pong!") + .setImage(svg ? room.data[code].image : room.data[code].image) + .setTimestamp(Date.now()) + .setFields({ + name: "API Latency", + value: `ms`, + }); + + await interaction.editReply({ embeds: [pingEmbed] }); + }, +}; + +function isSvgFile(filePath: any) { + return extname(filePath).toLowerCase() === ".svg"; +} + + +export default NewRoomCommand; \ No newline at end of file diff --git a/src/discord/commands/moderator/lookup.command.ts b/src/discord/commands/moderator/lookup.command.ts new file mode 100644 index 0000000..826c70e --- /dev/null +++ b/src/discord/commands/moderator/lookup.command.ts @@ -0,0 +1,185 @@ +import { + ChatInputCommandInteraction, + PermissionFlagsBits, + SlashCommandBuilder, + EmbedBuilder, + MessageFlags +} from "discord.js"; + +import { ICommand } from "../../../interfaces/command.interface"; +import { COMMAND_CATEGORY, COMMAND_SCOPE } from "../../../shared/constants"; +import { Profile } from "../../../schemas"; +import axios from "axios"; +import { CustomLogger } from "../../../shared/custom-logger"; +import { handleCommandError } from "../../../shared/utils/error.utils"; + +const logger = new CustomLogger(__filename); + +/** + * THM User Status Fetcher + * Returns: "Active" | "Deleted" | "Unknown" + */ +async function fetchTHMAccountStatus(username: string): Promise { + try { + const res = await axios.get(`https://tryhackme.com/p/${username}`); + return res.request.path === "/r/not-found" ? "Deleted" : "Active"; + } catch (err) { + logger.error("THM Status Check Error:", err); + return "Unknown"; + } +} + +/** + * Sanitizes strings for safe markdown rendering (fixes Unknown Integration) + */ +function sanitizeMarkdown(input: string): string { + if (!input) return "Unknown"; + return input.replace(/[[\]()<>{}`]/g, ""); +} + +/** + * Safely formats embed values + */ +function safeValue(val: any): string { + if (val === null || val === undefined || val === "") return "N/A"; + return String(val); +} + +const LookupCommand: ICommand = { + category: COMMAND_CATEGORY.MOD, + command: true, + scope: COMMAND_SCOPE.APPLICATION, + + data: new SlashCommandBuilder() + .setName("lookup") + .setDescription("Looks up a linked account by token or user.") + .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages) + .addSubcommand((sub) => + sub + .setName("token") + .setDescription("Look up by TryHackMe token.") + .addStringOption((opt) => + opt.setName("token") + .setDescription("TryHackMe token") + .setRequired(true) + ) + ) + .addSubcommand((sub) => + sub + .setName("user") + .setDescription("Look up by Discord user.") + .addUserOption((opt) => + opt.setName("user") + .setDescription("Discord user") + .setRequired(true) + ) + ), + + async execute(interaction: ChatInputCommandInteraction, client) { + try { + // Permission check + const allowed = await client.checkPermissions(interaction, "Moderator"); + if (!allowed) { + await interaction.reply({ content: 'You are not allowed to use this command', flags: [MessageFlags.Ephemeral] }) + return; + }; + + // Acknowledge + await interaction.deferReply({ ephemeral: true }); + + // INPUT HANDLING + const sub = interaction.options.getSubcommand(); + let profile = null; + + if (sub === "token") { + const token = interaction.options.getString("token", true); + profile = await Profile.findOne({ token }); + } else { + const user = interaction.options.getUser("user", true); + profile = await Profile.findOne({ discordId: user.id }); + } + + // πŸ” PROFILE VALIDATION + if (!profile) { + await interaction.editReply({ + content: "No linked TryHackMe account found for this input." + }); + + return; + } + + if (!profile.username || !profile.token) { + await interaction.editReply({ + content: "Profile exists but is missing required fields. (Corrupted / Incomplete)" + }); + return; + } + + // ACCOUNT STATUS CHECK + const accountStatus = await fetchTHMAccountStatus(profile.username); + + // Sanitize username for markdown + const safeUsername = sanitizeMarkdown(profile.username); + + // FALLBACK AVATAR + const avatar = profile.avatar || + "https://tryhackme.com/img/favicon.png"; + + // BUILD EMBED + const embed = new EmbedBuilder() + .setColor(0x5865F2) + .setAuthor({ + name: "TryHackMe", + iconURL: "https://tryhackme.com/img/favicon.png", + }) + .setThumbnail(avatar) + .setTimestamp() + .addFields( + { + name: "Discord", + value: `<@${profile.discordId}>`, + inline: true, + }, + { + name: "Discord ID", + value: safeValue(profile.discordId), + inline: true, + }, + { + name: "THM Token", + value: safeValue(profile.token), + inline: true, + }, + { + name: "THM Username", + value: `[${safeUsername}](https://tryhackme.com/p/${safeUsername})`, + inline: true, + }, + { + name: "THM Level", + value: safeValue(profile.level), + inline: true, + }, + { + name: "Subscribed", + value: safeValue(profile.subscribed), + inline: true, + }, + { + name: "Account Status", + value: accountStatus, + inline: true, + } + ); + + await interaction.editReply({ embeds: [embed] }); + return; + + } catch (err) { + await handleCommandError(interaction, err, "LookupCommand"); + return; + } + }, +}; + +export default LookupCommand; diff --git a/src/discord/commands/moderator/rule.command.ts b/src/discord/commands/moderator/rule.command.ts new file mode 100644 index 0000000..57ac5c9 --- /dev/null +++ b/src/discord/commands/moderator/rule.command.ts @@ -0,0 +1,118 @@ +import { ChatInputCommandInteraction, EmbedBuilder, PermissionFlagsBits, SlashCommandBuilder } from "discord.js"; +import { ICommand } from "../../../interfaces/command.interface"; +import { COMMAND_CATEGORY, COMMAND_SCOPE } from "../../../shared/constants"; +import { THMClient } from "../../../types/discord-client.type"; + +const RuleCommand: ICommand = { + category: COMMAND_CATEGORY.MOD, + command: true, + scope: COMMAND_SCOPE.APPLICATION, + data: new SlashCommandBuilder() + .setName("rule") + .setDescription("Quote a rule from the Discord server") + .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages) + .addIntegerOption((option) => + option + .setName("number") + .setDescription("The rule to be quoted.") + .setRequired(true) + ) + .addUserOption((option) => + option + .setName("mention") + .setDescription("Optionally mention a user with the response.") + .setRequired(false) + ), + execute: async (interaction: ChatInputCommandInteraction, client: THMClient) => { + const number = interaction.options.getInteger("number"); + const mention = interaction.options.getUser("mention"); + + const hasPermission = await client.checkPermissions( + interaction, + "Moderator" + ); + if (!hasPermission) return; + + let text = ""; + let description = ""; + + switch (number) { + case 1: + text = `No Abusive Language`; + description = + "Harassment, bullying, discrimination, or abusive language of any kind is not tolerated. We're here to grow together, share insights, and celebrate wins. No one should ever feel unsafe or threatened.\nKeep your language β€œsafe for work.” If you're not okay with your employer seeing it, don't write it. And who knows, you may meet your future employer here someday!"; + break; + + case 2: + text = "Keep Discussions Relevant"; + description = "Please keep discussion relevant to the channel topic."; + break; + + case 3: + text = "No Advertising"; + description = + "No excessive self-promotion. While you're welcome to post your write-ups, walkthroughs, and streams of TryHackMe content, spamming of your own channels isn't tolerated. "; + break; + + case 4: + text = "No Illegal or Harmful Activity"; + description = + "We do not teach unethical hackers. Please don't discuss illegal or unethical topics. Please don't post any intentionally harmful commands or distribute malware."; + break; + + case 5: + text = "No Cheating"; + description = + "Cheating of any form is not allowed. This is not limited to asking for help with assessed schoolwork or exams."; + break; + + case 6: + text = "No Spamming"; + description = + "If your question hasn’t been answered right away, don’t spam. Massive walls of text or many individual messages count as spam. This includes posting the same message across multiple channels."; + break; + + case 7: + text = "Use English"; + description = + "Please keep all communication in English. This also means no encrypted posting."; + break; + + case 8: + text = "No DMs Without Consent"; + description = + "Always ask permission before sending a DM or friend request to another user. "; + break; + + case 9: + text = "Follow Server Staff Direction"; + description = + "Our team are here to protect the interests of the community. Follow their direction at all times."; + break; + + default: + text = "Rule Not Found"; + description = "Please ensure you are selecting one listed from #rules"; + break; + } + + const embed = new EmbedBuilder() + .setColor("#5865F2") + .setTitle(`<#${process.env.RULES}>`) + .addFields({ + name: `Rule ${number} - ${text}`, + value: description, + }) + .setFooter({ + text: `TryHackMe Rules`, + iconURL: "https://assets.tryhackme.com/img/favicon.png", + }) + .setTimestamp(); + + const messageContent = mention ? `${mention}` : ""; + + await interaction.reply({ embeds: [embed], content: messageContent }); + } +} + +export default RuleCommand; \ No newline at end of file diff --git a/src/discord/commands/moderator/unlink.command.ts b/src/discord/commands/moderator/unlink.command.ts new file mode 100644 index 0000000..974e2d5 --- /dev/null +++ b/src/discord/commands/moderator/unlink.command.ts @@ -0,0 +1,138 @@ +import { ChatInputCommandInteraction, EmbedBuilder, Guild, PermissionFlagsBits, SlashCommandBuilder } from "discord.js"; +import { ICommand } from "../../../interfaces/command.interface"; +import { COMMAND_CATEGORY, COMMAND_SCOPE } from "../../../shared/constants"; +import { THMClient } from "../../../types/discord-client.type"; +import { Profile } from "../../../schemas"; +import config from "../../../config/configuration"; +import { fetchMember } from "../../../shared/utils/member.utils"; +import { removeRoles } from "../../../shared/utils/role.utils"; +import { CustomLogger } from "../../../shared/custom-logger"; + +const logger = new CustomLogger(__filename); + +const UnlinkCommand: ICommand = { + category: COMMAND_CATEGORY.MOD, + command: true, + scope: COMMAND_SCOPE.APPLICATION, + data: new SlashCommandBuilder() + .setName("unlink") + .setDescription("Detaches a Discord account from a TryHackMe token.") + .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages) + .addSubcommand((subcommand) => + subcommand + .setName("user") + .setDescription("Unlink a user via their Discord username") + .addUserOption((option) => + option + .setName("user") + .setDescription("The user to be unlinked.") + .setRequired(true) + ) + ) + .addSubcommand((subcommand) => + subcommand + .setName("token") + .setDescription("Unlink a user via their TryHackMe Discord token.") + .addStringOption((option) => + option + .setName("token") + .setDescription("Token attached to their account.") + .setRequired(true) + ) + ), + execute: async (interaction: ChatInputCommandInteraction, client: THMClient) => { + const subcommand = interaction.options.getSubcommand(); + const user = interaction.options.getUser("user"); + const token = interaction.options.getString("token"); + + const hasPermission = await client.checkPermissions( + interaction, + "Moderator" + ); + if (!hasPermission) return; + + let userProfile: any; + switch (subcommand) { + case "token": + userProfile = await Profile.findOne({ token }); + + // Check if the user profile exists and the token matches + if (!userProfile || userProfile.token !== token) { + await interaction.reply({ + content: + "No linked account found with this token, or token does not match.\nEnsure that there are no spaces before or after the token.", + ephemeral: true, + }); + return; + } + + // Remove the user's data from the database + await Profile.deleteOne({ token }); + break; + + case "user": + userProfile = await Profile.findOne({ discordId: user?.id }); + + // Check if the user profile exists + if (!userProfile) { + await interaction.reply({ + content: "No linked account found with this user.", + ephemeral: true, + }); + return; + } + + // Remove the user's data from the database + await Profile.deleteOne({ discordId: user?.id }); + break; + + default: + break; + } + + // Remove roles from the user if they have them + const guild = client.guilds.cache.get(config.guildId); + if (userProfile?.discordId) { + const member = await fetchMember(guild as Guild, userProfile.discordId); + + if (member) { + removeRoles(member); + } + } + + // Send a confirmation message with the user mention + await interaction.reply({ + content: `<@${userProfile?.discordId}> has been successfully unlinked.`, + ephemeral: true, + }); + + const unlinkEmbed = new EmbedBuilder() + .setAuthor({ + name: `${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }) + .setColor("#FFA500") + .setTitle(`Account Unlink`) + .setFields([ + { name: "User", value: `<@${userProfile?.discordId}>`, inline: true }, + { name: "Token", value: userProfile?.token, inline: true }, + ]) + .setTimestamp(); + + // Send the embed to the logging channel + const loggingChannel = client.guilds.cache + .get(config.guildId) + ?.channels.cache.get(config.channels.botLogging); + if (loggingChannel && loggingChannel.isTextBased()) { + loggingChannel + .send({ embeds: [unlinkEmbed] }) + .catch((err) => + logger.error( + "[UNLINK] Error with sending the embed to the logging channel." + ) + ); + } + }, +} + +export default UnlinkCommand; \ No newline at end of file diff --git a/src/discord/commands/utility/docs.command.ts b/src/discord/commands/utility/docs.command.ts new file mode 100644 index 0000000..043d72a --- /dev/null +++ b/src/discord/commands/utility/docs.command.ts @@ -0,0 +1,79 @@ +import { ChatInputCommandInteraction, EmbedBuilder, SlashCommandBuilder } from "discord.js"; +import { ICommand } from "../../../interfaces/command.interface"; +import { COMMAND_CATEGORY, COMMAND_SCOPE } from "../../../shared/constants"; +import { THMClient } from "../../../types/discord-client.type"; +import { CustomLogger } from "../../../shared/custom-logger"; +import { handleAPI } from "../../handlers/api.handler"; + +const logger = new CustomLogger(__filename); + +const DocsCommand: ICommand = { + category: COMMAND_CATEGORY.UTIL, + scope: COMMAND_SCOPE.APPLICATION, + command: true, + data: new SlashCommandBuilder() + .setName("docs") + .setDescription("Search for a document on our knowledge base") + .addStringOption((option) => + option + .setName("search") + .setDescription("find an article by its title") + .setRequired(true) + ) + .addUserOption((option) => + option + .setName("mention") + .setDescription("Optionally mention a user with the response.") + .setRequired(false) + ), + execute: async (interaction: ChatInputCommandInteraction, client: THMClient) => { + await interaction.deferReply({}); + + const user = interaction.options.getUser("mention"); + const search = interaction.options.getString("search"); + + if (search && search.length > 100) { + await interaction.editReply({ + content: "Your input exceeds the character limit. Please try again.", + }); + + return; + } + + let article; + try { + article = await handleAPI.get_article_by_phrase(search as string); + } catch (error) { + logger.error("Error fetching article:", error); + await interaction.editReply({ + content: + "An error occurred while fetching the article. Please try again later." + }); + + return; + } + + if (article === null) { + await interaction.editReply({ + content: "I could not find an article, please try again." + }); + + return; + } + + const embed = new EmbedBuilder() + .setTitle(article.title) + .setThumbnail(client.user?.displayAvatarURL() as string) + .setTimestamp(Date.now()) + .addFields({ + name: "Article Link", + value: `${article.url}`, + }); + + const messageContent = user ? `${user}` : ""; + + await interaction.editReply({ content: messageContent, embeds: [embed] }); + } +} + +export default DocsCommand; \ No newline at end of file diff --git a/src/discord/commands/utility/health.command.ts b/src/discord/commands/utility/health.command.ts new file mode 100644 index 0000000..6fbf6e9 --- /dev/null +++ b/src/discord/commands/utility/health.command.ts @@ -0,0 +1,92 @@ +import { + ChatInputCommandInteraction, + SlashCommandBuilder, + EmbedBuilder +} from "discord.js"; +import { ICommand } from "../../../interfaces/command.interface"; +import os from "node:os"; +import mongoose from "mongoose"; +import { COMMAND_CATEGORY, COMMAND_SCOPE } from "../../../shared/constants"; + +function formatBytes(bytes: number) { + return `${(bytes / 1024 / 1024).toFixed(2)} MB`; +} + +const HealthCommand: ICommand = { + category: COMMAND_CATEGORY.UTIL, + scope: COMMAND_SCOPE.APPLICATION, + command: true, + data: new SlashCommandBuilder() + .setName("health") + .setDescription("Displays health and performance statistics for the bot"), + + async execute(interaction: ChatInputCommandInteraction, client) { + await interaction.deferReply(); + + // CPU Load + const cpus = os.cpus(); + const cpuLoad = + cpus + .map((cpu) => cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.irq) + .reduce((a, b) => a + b, 0) / + cpus.length; + + // Memory + const memoryUsage = process.memoryUsage(); + + // Uptime + const uptime = process.uptime(); + const uptimeFormatted = `${Math.floor(uptime / 3600)}h ${Math.floor( + (uptime % 3600) / 60 + )}m`; + + // DB + const dbState = mongoose.connection.readyState; + const dbStatus = ["Disconnected", "Connected", "Connecting", "Disconnecting"][dbState]; + + const embed = new EmbedBuilder() + .setTitle("πŸ“Š Bot Health Dashboard") + .setColor("#34eb95") + .setFields( + { + name: "🧠 Memory Usage", + value: ` + **RSS:** ${formatBytes(memoryUsage.rss)} + **Heap Used:** ${formatBytes(memoryUsage.heapUsed)} + **Heap Total:** ${formatBytes(memoryUsage.heapTotal)} + `, + inline: true, + }, + { + name: "βš™ CPU Load", + value: `${cpuLoad.toFixed(2)} ms`, + inline: true, + }, + { + name: "πŸ“‘ WebSocket Ping", + value: `${client.ws.ping}ms`, + inline: true, + }, + { + name: "🏒 Guild Members", + value: `${interaction.guild?.memberCount ?? "N/A"}`, + inline: true, + }, + { + name: "πŸ—„ Database", + value: `${dbStatus}`, + inline: true, + }, + { + name: "⏱ Bot Uptime", + value: uptimeFormatted, + inline: true, + } + ) + .setTimestamp(); + + await interaction.editReply({ embeds: [embed] }); + } +}; + +export default HealthCommand; diff --git a/src/discord/commands/utility/help.command.ts b/src/discord/commands/utility/help.command.ts new file mode 100644 index 0000000..9fa00d8 --- /dev/null +++ b/src/discord/commands/utility/help.command.ts @@ -0,0 +1,31 @@ +import { CommandInteraction, EmbedBuilder, SlashCommandBuilder } from "discord.js"; +import { ICommand } from "../../../interfaces/command.interface"; +import { COMMAND_CATEGORY, COMMAND_SCOPE } from "../../../shared/constants"; +import { THMClient } from "../../../types/discord-client.type"; + +const HelpCommand: ICommand = { + category: COMMAND_CATEGORY.UTIL, + scope: COMMAND_SCOPE.APPLICATION, + command: true, + data: new SlashCommandBuilder() + .setName("help") + .setDescription("Information on how to use the bot!"), + execute: async (interaction: CommandInteraction, client: THMClient) => { + await interaction.deferReply({ + withResponse: true + }); + + const pingEmbed = new EmbedBuilder() + .setTitle("Information") + .setThumbnail(client.user?.displayAvatarURL() as string) + .setTimestamp(Date.now()) + .setFields({ + name: "Discord Slash Commands", + value: `https://support.discord.com/hc/en-us/articles/1500000368501-Slash-Commands-FAQ`, + }); + + await interaction.editReply({ embeds: [pingEmbed] }); + } +} + +export default HelpCommand; \ No newline at end of file diff --git a/src/discord/commands/utility/notify-me.command.ts b/src/discord/commands/utility/notify-me.command.ts new file mode 100644 index 0000000..75e2a2c --- /dev/null +++ b/src/discord/commands/utility/notify-me.command.ts @@ -0,0 +1,75 @@ +import { CommandInteraction, SlashCommandBuilder } from "discord.js"; +import { ICommand } from "../../../interfaces/command.interface"; +import { COMMAND_CATEGORY, COMMAND_SCOPE } from "../../../shared/constants"; +import { THMClient } from "../../../types/discord-client.type"; +import config from "../../../config/configuration"; +import { CustomLogger } from "../../../shared/custom-logger"; + +const logger = new CustomLogger(__filename); + +const NotifyMeCommand: ICommand = { + category: COMMAND_CATEGORY.UTIL, + scope: COMMAND_SCOPE.APPLICATION, + command: true, + + data: new SlashCommandBuilder() + .setName("notifyme") + .setDescription("Toggle announcements notifications."), + + execute: async (interaction: CommandInteraction, client: THMClient): Promise => { + try { + const roleID = config.roles.announcements; + + if (!roleID) { + logger.info("ANNOUNCEMENTS_ROLE_ID is not set."); + await interaction.reply({ + content: "Announcements role is not configured.", + ephemeral: true, + }); + return; + } + + const member = await interaction.guild?.members.fetch(interaction.user.id); + + if (!member) { + await interaction.reply({ + content: "Member could not be found. Try again shortly.", + ephemeral: true, + }); + return; + } + + let content: string; + + if (member.roles.cache.has(roleID)) { + await member.roles.remove(roleID); + content = "You will no longer receive announcements."; + } else { + await member.roles.add(roleID); + content = "You will now receive announcements."; + } + + await interaction.reply({ + content, + ephemeral: true, + }); + } catch (error) { + logger.error("Error in notifyme command:", error); + + // Handle case where reply already exists + if (interaction.replied || interaction.deferred) { + await interaction.followUp({ + content: "An error occurred while processing your request.", + ephemeral: true, + }); + } else { + await interaction.reply({ + content: "An error occurred while processing your request.", + ephemeral: true, + }); + } + } + } +}; + +export default NotifyMeCommand; diff --git a/src/discord/commands/utility/ollie.command.ts b/src/discord/commands/utility/ollie.command.ts new file mode 100644 index 0000000..f363b50 --- /dev/null +++ b/src/discord/commands/utility/ollie.command.ts @@ -0,0 +1,39 @@ +import { ChatInputCommandInteraction, EmbedBuilder, SlashCommandBuilder } from "discord.js"; +import { ICommand } from "../../../interfaces/command.interface"; +import { COMMAND_CATEGORY, COMMAND_SCOPE } from "../../../shared/constants"; +import { THMClient } from "../../../types/discord-client.type"; +import { handleAPI } from "../../handlers/api.handler"; + +const OllieCommand: ICommand = { + category: COMMAND_CATEGORY.UTIL, + scope: COMMAND_SCOPE.APPLICATION, + command: true, + data: new SlashCommandBuilder() + .setName("ollie") + .setDescription("Retrieves a special picture"), + execute: async (interaction: ChatInputCommandInteraction, client: THMClient) => { + await interaction.deferReply(); + + const pictureUrl = await handleAPI.get_ollie_picture(); + if (!pictureUrl) { + await interaction.editReply("Failed to retrieve the picture."); + return; + } + + const ollieEmbed = new EmbedBuilder() + .setTitle("Ollie") + .setColor("#5865F2") + .setImage(pictureUrl) + .setAuthor({ + name: client?.user?.username as string, + iconURL: client?.user?.displayAvatarURL(), + }) + .setFooter({ + text: "Ollie Unix Montgomery - Rest in Peace, 5th of January, 2023", + }); + + await interaction.editReply({ embeds: [ollieEmbed] }); + } +} + +export default OllieCommand; \ No newline at end of file diff --git a/src/discord/commands/utility/ping.command.ts b/src/discord/commands/utility/ping.command.ts new file mode 100644 index 0000000..69564c5 --- /dev/null +++ b/src/discord/commands/utility/ping.command.ts @@ -0,0 +1,42 @@ +import { EmbedBuilder, SlashCommandBuilder, CommandInteraction } from "discord.js"; +import { ICommand } from "../../../interfaces/command.interface"; +import { COMMAND_CATEGORY, COMMAND_SCOPE } from "../../../shared/constants"; + +const PingCommand: ICommand = { + category: COMMAND_CATEGORY.UTIL, + scope: COMMAND_SCOPE.APPLICATION, + command: true, + + data: new SlashCommandBuilder() + .setName("ping") + .setDescription("Returns the API and Client latency"), + + async execute(interaction: CommandInteraction, client) { + const start = Date.now(); + + await interaction.deferReply(); + + const end = Date.now(); + const latency = end - start; + const pingEmbed = new EmbedBuilder() + .setTitle("Pong!") + .setThumbnail(client.user.displayAvatarURL()) + .setTimestamp(Date.now()) + .addFields( + { + name: "API Latency", + value: `${client.ws.ping}ms`, + inline: true, + }, + { + name: "Client Latency", + value: `${latency}ms`, + inline: true, + } + ); + + await interaction.editReply({ embeds: [pingEmbed] }); + }, +}; + +export default PingCommand; diff --git a/src/discord/commands/utility/rank.command.ts b/src/discord/commands/utility/rank.command.ts new file mode 100644 index 0000000..bb5bc8b --- /dev/null +++ b/src/discord/commands/utility/rank.command.ts @@ -0,0 +1,97 @@ +import { ChatInputCommandInteraction, EmbedBuilder, MessageFlags, SlashCommandBuilder } from "discord.js"; +import { ICommand } from "../../../interfaces/command.interface"; +import { COMMAND_CATEGORY, COMMAND_SCOPE } from "../../../shared/constants"; +import { THMClient } from "../../../types/discord-client.type"; +import config from "../../../config/configuration"; +import { CustomLogger } from "../../../shared/custom-logger"; +import { handleAPI } from "../../handlers/api.handler"; + +const logger = new CustomLogger(__filename); + +const RankCommand: ICommand = { + category: COMMAND_CATEGORY.UTIL, + scope: COMMAND_SCOPE.APPLICATION, + command: true, + data: new SlashCommandBuilder() + .setName("rank") + .setDescription("Get the TryHackMe rank of a user.") + .addStringOption((option) => + option + .setName("username") + .setDescription("The username to retrieve the rank for.") + .setRequired(true) + ), + execute: async (interaction: ChatInputCommandInteraction, client: THMClient) => { + const username: string = interaction.options.getString("username") as string; + + if ( + interaction.channel && + interaction.channel.id === config.channels.botCommands + ) { + await interaction.deferReply(); + } else { + await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); + } + + if (username && username.length > 30) { + await interaction.editReply({ + content: + "Your input exceeds the username character limit. Please try again.", + }); + + return; + } + + try { + const userProfile = await handleAPI.get_user_data(username); + if ( + !userProfile || + userProfile.userRank == 0 || + userProfile.userRank == undefined || + userProfile.points === "undefined" || + userProfile.points === undefined + ) { + await interaction.editReply({ + content: "User not found.", + }); + + return; + } + + const tryhackme_url = "https://tryhackme.com/p/"; + const rankEmbed = new EmbedBuilder() + .setColor("#5865F2") + .setTitle(userProfile.username || username) + .addFields( + { + name: "Leaderboard Position", + value: `${userProfile.rank || userProfile.userRank}`, + }, + { + name: "Username", + value: `[${userProfile.username || username}](${tryhackme_url}${userProfile.username || username + })`, + inline: true, + }, + { name: "Points", value: String(userProfile.points), inline: true }, + { + name: "Subscribed?", + value: userProfile.subscribed ? "Yes" : "No", + inline: true, + } + ) + .setThumbnail(userProfile.avatar); + + await interaction.editReply({ + embeds: [rankEmbed], + }); + } catch (error) { + logger.error("Error in rank command:", error); + await interaction.editReply({ + content: "An error occurred while processing your request.", + }); + } + } +} + +export default RankCommand; \ No newline at end of file diff --git a/src/discord/commands/utility/socials.command.ts b/src/discord/commands/utility/socials.command.ts new file mode 100644 index 0000000..491a854 --- /dev/null +++ b/src/discord/commands/utility/socials.command.ts @@ -0,0 +1,165 @@ +import { ChatInputCommandInteraction, EmbedBuilder, SlashCommandBuilder } from "discord.js"; +import { ICommand } from "../../../interfaces/command.interface"; +import { COMMAND_CATEGORY, COMMAND_SCOPE } from "../../../shared/constants"; + +const SocialsCommand: ICommand = { + category: COMMAND_CATEGORY.UTIL, + scope: COMMAND_SCOPE.APPLICATION, + command: true, + data: new SlashCommandBuilder() + .setName("socials") + .setDescription("Get the latest information for TryHackMe's social media!") + .addSubcommand((subcommand) => + subcommand + .setName("github") + .setDescription("Get the bot's GitHub link.") + .addUserOption((option) => + option + .setName("mention") + .setDescription("Optionally mention a user with the response.") + .setRequired(false) + ) + ) + .addSubcommand((subcommand) => + subcommand + .setName("twitter") + .setDescription("Get TryHackMe's official Twitter!") + .addUserOption((option) => + option + .setName("mention") + .setDescription("Optionally mention a user with the response.") + .setRequired(false) + ) + ) + .addSubcommand((subcommand) => + subcommand + .setName("reddit") + .setDescription("Get the link to our amazing subreddit!") + .addUserOption((option) => + option + .setName("mention") + .setDescription("Optionally mention a user with the response.") + .setRequired(false) + ) + ) + .addSubcommand((subcommand) => + subcommand + .setName("website") + .setDescription("You should know our website by now!") + .addUserOption((option) => + option + .setName("mention") + .setDescription("Optionally mention a user with the response.") + .setRequired(false) + ) + ) + .addSubcommand((subcommand) => + subcommand + .setName("discord") + .setDescription("Looking to invite people to the Discord server?") + .addUserOption((option) => + option + .setName("mention") + .setDescription("Optionally mention a user with the response.") + .setRequired(false) + ) + ) + .addSubcommand((subcommand) => + subcommand + .setName("blog") + .setDescription("Look at the awesome resources written by TryHackMe!") + .addUserOption((option) => + option + .setName("mention") + .setDescription("Optionally mention a user with the response.") + .setRequired(false) + ) + ) + .addSubcommand((subcommand) => + subcommand + .setName("shop") + .setDescription("Get some awesome swag to show off to your friends.") + .addUserOption((option) => + option + .setName("mention") + .setDescription("Optionally mention a user with the response.") + .setRequired(false) + ) + ) + .addSubcommand((subcommand) => + subcommand + .setName("email") + .setDescription("TryHackMe's support email address.") + .addUserOption((option) => + option + .setName("mention") + .setDescription("Optionally mention a user with the response.") + .setRequired(false) + ) + ), + execute: async (interaction: ChatInputCommandInteraction) => { + const subcommand = interaction.options.getSubcommand(); + const user = interaction.options.getUser("mention"); + + let text: string = ''; + let description: string = ''; + + switch (subcommand) { + case "github": + text = "https://github.com/JabbaSec/tryhackme"; + description = "Get the bot's GitHub link."; + break; + case "twitter": + text = "https://twitter.com/realtryhackme/"; + description = "Get TryHackMe's official Twitter!"; + break; + case "reddit": + text = "https://www.reddit.com/r/tryhackme/"; + description = "Get the link to our amazing subreddit!"; + break; + case "website": + text = "https://tryhackme.com/"; + description = "You should know our website by now!"; + break; + case "discord": + text = "https://discord.com/invite/tryhackme"; + description = "Looking to invite people to the Discord server?"; + break; + case "blog": + text = "https://tryhackme.com/r/resources/blog"; + description = "Look at the awesome resources written by TryHackMe!"; + break; + case "shop": + text = "https://store.tryhackme.com/"; + description = "Get some awesome swag to show off to your friends."; + break; + case "email": + text = "support@tryhackme.com"; + description = "TryHackMe's support email address."; + break; + } + + const embed = new EmbedBuilder() + .setColor("#5865F2") + .setTitle( + `TryHackMe's ${subcommand.charAt(0).toUpperCase() + subcommand.slice(1) + }` + ) + .setDescription(description) + .addFields({ + name: "\u200B" as string, + value: text, + }) + .setFooter({ + text: "TryHackMe Socials", + iconURL: "https://assets.tryhackme.com/img/favicon.png", + }) + .setTimestamp(); + + const messageContent = user ? `${user}` : ""; + + await interaction.reply({ content: messageContent, embeds: [embed] }); + } +} + +export default SocialsCommand; \ No newline at end of file diff --git a/src/discord/commands/utility/verify.command.ts b/src/discord/commands/utility/verify.command.ts new file mode 100644 index 0000000..183c90c --- /dev/null +++ b/src/discord/commands/utility/verify.command.ts @@ -0,0 +1,102 @@ +import { ChatInputCommandInteraction, Guild, SlashCommandBuilder } from "discord.js"; +import { ICommand } from "../../../interfaces/command.interface"; +import { COMMAND_CATEGORY, COMMAND_SCOPE } from "../../../shared/constants"; +import { THMClient } from "../../../types/discord-client.type"; +import config from "../../../config/configuration"; +import { Profile } from "../../../schemas"; +import { fetchMember } from "../../../shared/utils/member.utils"; +import { assignRoles } from "../../../shared/utils/role.utils"; +import { CustomLogger } from "../../../shared/custom-logger"; +import { handleAPI } from "../../handlers/api.handler"; + +const logger = new CustomLogger(__filename); + +const VerifyCommand: ICommand = { + category: COMMAND_CATEGORY.UTIL, + scope: COMMAND_SCOPE.APPLICATION, + command: true, + data: new SlashCommandBuilder() + .setName("verify") + .setDescription("Syncs your TryHackMe site account to Discord") + .addStringOption((option) => + option + .setName("token") + .setDescription( + "Use your Discord token from https://tryhackme.com/profile" + ) + .setRequired(true) + ), + execute: async (interaction: ChatInputCommandInteraction, client: THMClient) => { + await interaction.deferReply({ ephemeral: true }); + + const token: string = interaction.options.getString("token") as string; + const guild: Guild = client.guilds.cache.get(config.guildId) as Guild; + const discordId = interaction.user.id; + + let userDiscordProfile = await Profile.findOne({ discordId }); + if (userDiscordProfile && userDiscordProfile.token !== token) { + await interaction.editReply({ + content: + "Your Discord account is already linked with a token. If you wish to update your token, please contact a moderator.", + }); + + return; + } + + let tokenProfile = await Profile.findOne({ token }); + if (tokenProfile && tokenProfile.discordId !== discordId) { + await interaction.editReply({ + content: "This token is already in use by another account.", + }); + + return + } + + const userApiData = await handleAPI + .get_token_data(token) + .catch(async (error) => { + logger.error("Received invalid data from the API:", error); + await interaction.editReply({ + content: + "Failed to verify your account. Please ensure your token is correct.", + }); + + return; + }); + + if (!userApiData || !userApiData.success) { + await interaction.editReply({ + content: + "Failed to verify your account. Please ensure your token is correct.", + }); + + return; + } + + let userProfile = userDiscordProfile || new Profile({ discordId }); + userProfile = updateUserProfile(userProfile, userApiData, token); + await userProfile.save().catch(logger.error); + + const member = await fetchMember(guild, discordId); + if (member) { + assignRoles(member, userProfile); + } + + await interaction.editReply({ + content: "Your account has been updated and verified!", + }); + }, +}; + +function updateUserProfile(userProfile: any, userApiData: any, token: string) { + userProfile.username = userApiData.username; + userProfile.token = token; + userProfile.subscribed = userApiData.subscribed === 1; + userProfile.level = userApiData.level; + userProfile.avatar = userApiData.avatar; + + return userProfile; +} + + +export default VerifyCommand; \ No newline at end of file From e1da3a9f5d01267d9d777d25548bd931979b027f Mon Sep 17 00:00:00 2001 From: muhamedfazeel Date: Sun, 14 Dec 2025 09:25:41 +0530 Subject: [PATCH 21/21] [refactor] [index]: migrate index file to typescript --- src/index.js | 63 ---------------------------------------------------- src/index.ts | 55 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 63 deletions(-) delete mode 100644 src/index.js create mode 100644 src/index.ts diff --git a/src/index.js b/src/index.js deleted file mode 100644 index eaf37ee..0000000 --- a/src/index.js +++ /dev/null @@ -1,63 +0,0 @@ -require("dotenv").config(); - -const { Client, Collection, GatewayIntentBits } = require("discord.js"); - -const fs = require("fs"); -const mongoose = require("mongoose"); -const cron = require("node-cron"); - -const { checkGiveaways } = require("../src/utils/timerUtils"); - -const client = new Client({ - intents: [ - GatewayIntentBits.GuildMembers, - GatewayIntentBits.GuildModeration, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.Guilds, - GatewayIntentBits.MessageContent, - ], -}); - -client.commands = new Collection(); -client.buttons = new Collection(); -client.dropdowns = new Collection(); - -client.commandArray = []; -client.timerArray = []; - -const functionFolders = fs.readdirSync(`./src/functions`); -for (const folder of functionFolders) { - const functionFiles = fs - .readdirSync(`./src/functions/${folder}`) - .filter((file) => file.endsWith(".js")); - - for (const file of functionFiles) - require(`./functions/${folder}/${file}`)(client); -} - -(async () => { - mongoose.connect(`${process.env.MONGODB_URI}`).catch(console.error); -})(); - -client - .login(process.env.BOT_TOKEN) - .then(() => { - try { - client.handleEvents(); - client.handleCommands(); - client.handleComponents(); - client.roleSync(client); - - cron.schedule("* * * * *", async () => { - await checkGiveaways(client); - }); - - // Sync roles every 24 hours - setInterval(() => { - client.roleSync(client); - }, 86400000); // 86,400,000 - } catch (err) { - console.error(err); - } - }) - .catch((err) => console.log(err)); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..f437e0c --- /dev/null +++ b/src/index.ts @@ -0,0 +1,55 @@ +import mongoose from "mongoose"; +import config from "./config/configuration"; +import { CustomLogger } from "./shared/custom-logger"; +import { THMClient } from "./types/discord-client.type"; +import { loadCommands } from "./discord/handlers/command.handler"; +import { registerClientEvents } from "./discord/handlers/events.handler"; +import { registerComponents } from "./discord/handlers/components.handler"; + +const logger = new CustomLogger(__filename); +const client = THMClient.getInstance(); + +(async () => { + try { + // Load commands + logger.info("πŸ“₯ Loading Commands..."); + await loadCommands(client); + + // Connect MongoDB + logger.info("Connecting to MongoDB..."); + await mongoose.connect(config.mongo.uri); + logger.info("βœ… MongoDB connection successful."); + + // Register events + logger.info("⬆ Registering events..."); + await registerClientEvents(client); + + // Register components + logger.info("πŸ“¦ Registering components..."); + await registerComponents(client); + + // Set REST token before login + client.rest.setToken(config.bot.token); + + logger.info("πŸ”Œ Logging into Discord..."); + await client.login(config.bot.token); + + } catch (error) { + logger.error("❌ Startup failure:", error); + process.exit(1); + } +})(); + +// ------------------------------------ +// GLOBAL ERROR HANDLING +// ------------------------------------ +client.on("error", (err) => logger.error(`Client Error: ${err.message}`)); +client.on("warn", (warn) => logger.warn(`Warning: ${warn}`)); + +process.on("unhandledRejection", (reason) => + logger.error("🚨 Unhandled Promise Rejection:", reason) +); + +process.on("uncaughtException", (error) => + logger.error("πŸ”₯ Uncaught Exception:", error) +); \ No newline at end of file