diff --git a/.env.example b/.env.example index 6907f87..d9b14de 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,8 @@ LEVEL_2= LEVEL_1= # Channel IDs -BOT_LOGGING= \ No newline at end of file +BOT_LOGGING= +THM_USERS= +THM_ROOMS= +DISCORD_USERS= +BOT_COMMANDS= \ No newline at end of file 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" } } 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/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/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 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 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; 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/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/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."); - }, -}; 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."); - }, -}; 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(); -} 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 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; +} 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 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 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 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 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/types/discord-client.type.ts b/src/types/discord-client.type.ts new file mode 100644 index 0000000..a18b27c --- /dev/null +++ b/src/types/discord-client.type.ts @@ -0,0 +1,49 @@ +import { Client, Collection, GatewayIntentBits, REST } from 'discord.js'; // Ensure you import necessary components +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 commandArray: Array>; + public rest: REST; + + public handleCommands: () => Promise = async () => { }; + public handleEvents: () => Promise = async () => { }; + public handleComponents: () => Promise = async () => { }; + 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(); + this.commandArray = []; + + // Initialise REST + this.rest = new REST({ version: '10' }) + } + + // Static method to get the singleton instance + public static getInstance(): THMClient { + if (!this.instance) this.instance = new THMClient(); + return this.instance; + } +} \ 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, -}; 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