diff --git a/.gitignore b/.gitignore index c910527..4bc3ac9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -data +data/* +!data/mail node_modules .venv .DS_Store diff --git a/bun.lock b/bun.lock index fc444f6..4899d2d 100644 --- a/bun.lock +++ b/bun.lock @@ -6,26 +6,26 @@ "dependencies": { "@elysiajs/cors": "^1.3.3", "@elysiajs/swagger": "1.3.0", - "@sentry/bun": "^9.29.0", - "axios": "^1.9.0", + "@sentry/bun": "^9.31.0", + "axios": "^1.10.0", "chalk": "^5.4.1", "change-case": "^5.4.4", - "croner": "^9.0.0", - "discord.js": "^14.19.3", + "croner": "^9.1.0", + "discord.js": "^14.20.0", "dotenv": "^16.5.0", - "elysia": "1.3.4", + "elysia": "1.3.5", "elysia-ip": "^1.0.10", "jsonwebtoken": "^9.0.2", "minimist": "^1.2.8", "moment": "^2.30.1", - "mongoose": "^8.15.2", + "mongoose": "^8.16.0", "ms": "^2.1.3", "nodemailer": "^7.0.3", "sharp": "^0.34.2", }, "devDependencies": { - "@types/bun": "^1.2.16", - "@types/jsonwebtoken": "^9.0.9", + "@types/bun": "^1.2.17", + "@types/jsonwebtoken": "^9.0.10", "@types/minimist": "^1.2.5", "@types/nodemailer": "^6.4.17", }, @@ -38,11 +38,11 @@ "@discordjs/formatters": ["@discordjs/formatters@0.6.1", "", { "dependencies": { "discord-api-types": "^0.38.1" } }, "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg=="], - "@discordjs/rest": ["@discordjs/rest@2.5.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.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.1" } }, "sha512-PWhchxTzpn9EV3vvPRpwS0EE2rNYB9pvzDU/eLLW3mByJl0ZHZjHI2/wA8EbH2gRMQV7nu+0FoDF84oiPl8VAQ=="], + "@discordjs/rest": ["@discordjs/rest@2.5.1", "", { "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.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-Tg9840IneBcbrAjcGaQzHUJWFNq1MMWZjTdjJ0WS/89IffaNKc++iOvffucPxQTF/gviO9+9r8kEPea1X5J2Dw=="], "@discordjs/util": ["@discordjs/util@1.1.1", "", {}, "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g=="], - "@discordjs/ws": ["@discordjs/ws@1.2.2", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.0", "@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.17.0" } }, "sha512-dyfq7yn0wO0IYeYOs3z79I6/HumhmKISzFL0Z+007zQJMtAFGtt3AEoq1nuLXtcunUE5YYYQqgKvybXukAK8/w=="], + "@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "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.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="], "@elysiajs/cors": ["@elysiajs/cors@1.3.3", "", { "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-mYIU6PyMM6xIJuj7d27Vt0/wuzVKIEnFPjcvlkyd7t/m9xspAG37cwNjFxVOnyvY43oOd2I/oW2DB85utXpA2Q=="], @@ -158,7 +158,7 @@ "@opentelemetry/sql-common": ["@opentelemetry/sql-common@0.40.1", "", { "dependencies": { "@opentelemetry/core": "^1.1.0" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0" } }, "sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg=="], - "@prisma/instrumentation": ["@prisma/instrumentation@6.8.2", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0" }, "peerDependencies": { "@opentelemetry/api": "^1.8" } }, "sha512-5NCTbZjw7a+WIZ/ey6G8SY+YKcyM2zBF0hOT1muvqC9TbVtTCr5Qv3RL/2iNDOzLUHEvo4I1uEfioyfuNOGK8Q=="], + "@prisma/instrumentation": ["@prisma/instrumentation@6.9.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0" }, "peerDependencies": { "@opentelemetry/api": "^1.8" } }, "sha512-HFfr89v7WEbygdTzh1t171SUMYlkFRTXf48QthDc1cKduEsGIsOdt1QhOlpF7VK+yMg9EXHaXQo5Z8lQ7WtEYA=="], "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="], @@ -172,13 +172,13 @@ "@scalar/types": ["@scalar/types@0.0.12", "", { "dependencies": { "@scalar/openapi-types": "0.1.1", "@unhead/schema": "^1.9.5" } }, "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ=="], - "@sentry/bun": ["@sentry/bun@9.29.0", "", { "dependencies": { "@sentry/core": "9.29.0", "@sentry/node": "9.29.0", "@sentry/opentelemetry": "9.29.0" } }, "sha512-85wFqI0oEOZ/73KcTTmd4wMkKCyaxMLMWCiDJVrXxGbuC37pevQqZmIEgNABfs6NZLc2bHSxOzKlWzgJs30Pig=="], + "@sentry/bun": ["@sentry/bun@9.31.0", "", { "dependencies": { "@sentry/core": "9.31.0", "@sentry/node": "9.31.0" } }, "sha512-3HDw93Io+u6uJpOYKpejdsXKRL/dGiPXzglzH7jpuQXnYn7Ztdm8VbEAPQhyctKrGvc+waeqa2mWxbGXUVDE+Q=="], - "@sentry/core": ["@sentry/core@9.29.0", "", {}, "sha512-wDyNe45PM+RCGtUn1tK7LzJ08ksv8i8KRUHrst7lsinEfRm83YH+wbWrPmwkVNEngUZvYkHwGLbNXM7xgFUuDQ=="], + "@sentry/core": ["@sentry/core@9.31.0", "", {}, "sha512-6JeoPGvBgT9m2YFIf2CrW+KrrOYzUqb9+Xwr/Dw25kPjVKy+WJjWqK8DKCNLgkBA22OCmSOmHuRwFR0YxGVdZQ=="], - "@sentry/node": ["@sentry/node@9.29.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1", "@opentelemetry/core": "^1.30.1", "@opentelemetry/instrumentation": "^0.57.2", "@opentelemetry/instrumentation-amqplib": "^0.46.1", "@opentelemetry/instrumentation-connect": "0.43.1", "@opentelemetry/instrumentation-dataloader": "0.16.1", "@opentelemetry/instrumentation-express": "0.47.1", "@opentelemetry/instrumentation-fs": "0.19.1", "@opentelemetry/instrumentation-generic-pool": "0.43.1", "@opentelemetry/instrumentation-graphql": "0.47.1", "@opentelemetry/instrumentation-hapi": "0.45.2", "@opentelemetry/instrumentation-http": "0.57.2", "@opentelemetry/instrumentation-ioredis": "0.47.1", "@opentelemetry/instrumentation-kafkajs": "0.7.1", "@opentelemetry/instrumentation-knex": "0.44.1", "@opentelemetry/instrumentation-koa": "0.47.1", "@opentelemetry/instrumentation-lru-memoizer": "0.44.1", "@opentelemetry/instrumentation-mongodb": "0.52.0", "@opentelemetry/instrumentation-mongoose": "0.46.1", "@opentelemetry/instrumentation-mysql": "0.45.1", "@opentelemetry/instrumentation-mysql2": "0.45.2", "@opentelemetry/instrumentation-pg": "0.51.1", "@opentelemetry/instrumentation-redis-4": "0.46.1", "@opentelemetry/instrumentation-tedious": "0.18.1", "@opentelemetry/instrumentation-undici": "0.10.1", "@opentelemetry/resources": "^1.30.1", "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.34.0", "@prisma/instrumentation": "6.8.2", "@sentry/core": "9.29.0", "@sentry/opentelemetry": "9.29.0", "import-in-the-middle": "^1.13.1", "minimatch": "^9.0.0" } }, "sha512-oABipgC/fClRuvyMeK43rigv9F+OAaoR84UaMKB7aPXN6iz634wBRVsaoZAwiR3xLL+R7MafEPPA/s9XqlG7ag=="], + "@sentry/node": ["@sentry/node@9.31.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1", "@opentelemetry/core": "^1.30.1", "@opentelemetry/instrumentation": "^0.57.2", "@opentelemetry/instrumentation-amqplib": "^0.46.1", "@opentelemetry/instrumentation-connect": "0.43.1", "@opentelemetry/instrumentation-dataloader": "0.16.1", "@opentelemetry/instrumentation-express": "0.47.1", "@opentelemetry/instrumentation-fs": "0.19.1", "@opentelemetry/instrumentation-generic-pool": "0.43.1", "@opentelemetry/instrumentation-graphql": "0.47.1", "@opentelemetry/instrumentation-hapi": "0.45.2", "@opentelemetry/instrumentation-http": "0.57.2", "@opentelemetry/instrumentation-ioredis": "0.47.1", "@opentelemetry/instrumentation-kafkajs": "0.7.1", "@opentelemetry/instrumentation-knex": "0.44.1", "@opentelemetry/instrumentation-koa": "0.47.1", "@opentelemetry/instrumentation-lru-memoizer": "0.44.1", "@opentelemetry/instrumentation-mongodb": "0.52.0", "@opentelemetry/instrumentation-mongoose": "0.46.1", "@opentelemetry/instrumentation-mysql": "0.45.1", "@opentelemetry/instrumentation-mysql2": "0.45.2", "@opentelemetry/instrumentation-pg": "0.51.1", "@opentelemetry/instrumentation-redis-4": "0.46.1", "@opentelemetry/instrumentation-tedious": "0.18.1", "@opentelemetry/instrumentation-undici": "0.10.1", "@opentelemetry/resources": "^1.30.1", "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.34.0", "@prisma/instrumentation": "6.9.0", "@sentry/core": "9.31.0", "@sentry/opentelemetry": "9.31.0", "import-in-the-middle": "^1.13.1", "minimatch": "^9.0.0" } }, "sha512-PXB0mg/VauM/vVI0/kgtuVszQDSCwNndx0V9f2hRE3IvxRiMpds0wG+KBU1217Oe16/g5x/yASusOoTJ/UWwzQ=="], - "@sentry/opentelemetry": ["@sentry/opentelemetry@9.29.0", "", { "dependencies": { "@sentry/core": "9.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", "@opentelemetry/core": "^1.30.1 || ^2.0.0", "@opentelemetry/instrumentation": "^0.57.1 || ^0.200.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", "@opentelemetry/semantic-conventions": "^1.34.0" } }, "sha512-QTUmre8i5+832RjzQW+g8IQ3UmBe5fbQXGbCF5hQ0UNuHle9r3Z8UZcIff5W8tm5AXMxPqvptTnDEZUUXHgBiA=="], + "@sentry/opentelemetry": ["@sentry/opentelemetry@9.31.0", "", { "dependencies": { "@sentry/core": "9.31.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", "@opentelemetry/core": "^1.30.1 || ^2.0.0", "@opentelemetry/instrumentation": "^0.57.1 || ^0.200.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", "@opentelemetry/semantic-conventions": "^1.34.0" } }, "sha512-2makhSOGku8gGhjpH1spF2sLAiWESlSoxNV86XnlcnfQiM9ukE4d5TXUFrrKECRtI+BBCE0WorUlOPbTnnamZA=="], "@sinclair/typebox": ["@sinclair/typebox@0.34.33", "", {}, "sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g=="], @@ -186,11 +186,11 @@ "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], - "@types/bun": ["@types/bun@1.2.16", "", { "dependencies": { "bun-types": "1.2.16" } }, "sha512-1aCZJ/6nSiViw339RsaNhkNoEloLaPzZhxMOYEa7OzRzO41IGg5n/7I43/ZIAW/c+Q6cT12Vf7fOZOoVIzb5BQ=="], + "@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="], "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], - "@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.9", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ=="], + "@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.10", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA=="], "@types/minimist": ["@types/minimist@1.2.5", "", {}, "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag=="], @@ -226,17 +226,17 @@ "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], - "axios": ["axios@1.9.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg=="], + "axios": ["axios@1.10.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - "bson": ["bson@6.10.3", "", {}, "sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ=="], + "bson": ["bson@6.10.4", "", {}, "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng=="], "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], - "bun-types": ["bun-types@1.2.16", "", { "dependencies": { "@types/node": "*" } }, "sha512-ciXLrHV4PXax9vHvUrkvun9VPVGOVwbbbBF/Ev1cXz12lyEZMoJpIJABOfPcN9gDJRaiKF9MVbSygLg4NXu3/A=="], + "bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], @@ -258,7 +258,7 @@ "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], - "croner": ["croner@9.0.0", "", {}, "sha512-onMB0OkDjkXunhdW9htFjEhqrD54+M94i6ackoUkjHKbRnXdyEyKRelp4nJ1kAz32+s27jP1FsebpJCVl0BsvA=="], + "croner": ["croner@9.1.0", "", {}, "sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g=="], "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], @@ -268,7 +268,7 @@ "discord-api-types": ["discord-api-types@0.38.3", "", {}, "sha512-vijevLh06Gtmex6BQzc9jRrGce6La0qnsF4bKwKM2L1ou0/sbJIOAkg7wz6YLLaodnUwQLljIhtrGxnkMjc1Ew=="], - "discord.js": ["discord.js@14.19.3", "", { "dependencies": { "@discordjs/builders": "^1.11.2", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.1", "@discordjs/rest": "^2.5.0", "@discordjs/util": "^1.1.1", "@discordjs/ws": "^1.2.2", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.1", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.1" } }, "sha512-lncTRk0k+8Q5D3nThnODBR8fR8x2fM798o8Vsr40Krx0DjPwpZCuxxTcFMrXMQVOqM1QB9wqWgaXPg3TbmlHqA=="], + "discord.js": ["discord.js@14.20.0", "", { "dependencies": { "@discordjs/builders": "^1.11.2", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.1", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.1", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.1", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-5fRTptK2vpuz+bTuAEUQLSo/3AgCSLHl6Mm9+/ofb+8cbbnjWllhtaqRBq7XcpzlBnfNEugKv8HvCwcOtIHpCg=="], "dotenv": ["dotenv@16.5.0", "", {}, "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg=="], @@ -276,7 +276,7 @@ "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], - "elysia": ["elysia@1.3.4", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.1.2", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": "^0.34.33", "openapi-types": "^12.1.3" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-kAfM3Zwovy3z255IZgTKVxBw91HbgKhYl3TqrGRdZqqr+Fd+4eKOfvxgaKij22+MZLczPzIHtscAmvfpI3+q/A=="], + "elysia": ["elysia@1.3.5", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.1.2", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": "^0.34.33", "openapi-types": "^12.1.3" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-XVIKXlKFwUT7Sta8GY+wO5reD9I0rqAEtaz1Z71UgJb61csYt8Q3W9al8rtL5RgumuRR8e3DNdzlUN9GkC4KDw=="], "elysia-ip": ["elysia-ip@1.0.10", "", { "peerDependencies": { "elysia": ">= 1.0.9" } }, "sha512-xmCxPOl4266sq6CLk5d82P3BZOatG9z0gMP473cYEnORssuopbEI8GAwpOhiaz69X76AOrkYgvCdLkqMJC49dQ=="], @@ -372,11 +372,11 @@ "moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="], - "mongodb": ["mongodb@6.16.0", "", { "dependencies": { "@mongodb-js/saslprep": "^1.1.9", "bson": "^6.10.3", "mongodb-connection-string-url": "^3.0.0" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.188.0", "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0 <7", "snappy": "^7.2.2", "socks": "^2.7.1" }, "optionalPeers": ["@aws-sdk/credential-providers", "@mongodb-js/zstd", "gcp-metadata", "kerberos", "mongodb-client-encryption", "snappy", "socks"] }, "sha512-D1PNcdT0y4Grhou5Zi/qgipZOYeWrhLEpk33n3nm6LGtz61jvO88WlrWCK/bigMjpnOdAUKKQwsGIl0NtWMyYw=="], + "mongodb": ["mongodb@6.17.0", "", { "dependencies": { "@mongodb-js/saslprep": "^1.1.9", "bson": "^6.10.4", "mongodb-connection-string-url": "^3.0.0" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.188.0", "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0 <7", "snappy": "^7.2.2", "socks": "^2.7.1" }, "optionalPeers": ["@aws-sdk/credential-providers", "@mongodb-js/zstd", "gcp-metadata", "kerberos", "mongodb-client-encryption", "snappy", "socks"] }, "sha512-neerUzg/8U26cgruLysKEjJvoNSXhyID3RvzvdcpsIi2COYM3FS3o9nlH7fxFtefTb942dX3W9i37oPfCVj4wA=="], "mongodb-connection-string-url": ["mongodb-connection-string-url@3.0.2", "", { "dependencies": { "@types/whatwg-url": "^11.0.2", "whatwg-url": "^14.1.0 || ^13.0.0" } }, "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA=="], - "mongoose": ["mongoose@8.15.2", "", { "dependencies": { "bson": "^6.10.3", "kareem": "2.6.3", "mongodb": "~6.16.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", "sift": "17.1.3" } }, "sha512-GLwghI2dS/n5BTBljspF4+FsCEBeHgnMQyX8GloYkLkl+MKljKkjcP9DhLr47Yod2RO1RCr4vZ3evUZAyuoILw=="], + "mongoose": ["mongoose@8.16.0", "", { "dependencies": { "bson": "^6.10.4", "kareem": "2.6.3", "mongodb": "~6.17.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", "sift": "17.1.3" } }, "sha512-gLuAZsbwY0PHjrvfuXvUkUq9tXjyAjN3ioXph5Y6Seu7/Uo8xJaM+rrMbL/x34K4T3UTgtXRyfoq1YU16qKyIw=="], "mpath": ["mpath@0.9.0", "", {}, "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew=="], @@ -450,7 +450,7 @@ "uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="], - "undici": ["undici@6.21.1", "", {}, "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ=="], + "undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], diff --git a/src/mail/banned.html b/data/mail/banned.html similarity index 100% rename from src/mail/banned.html rename to data/mail/banned.html diff --git a/src/mail/icon_changed.html b/data/mail/icon_changed.html similarity index 100% rename from src/mail/icon_changed.html rename to data/mail/icon_changed.html diff --git a/src/mail/icon_cleared.html b/data/mail/icon_cleared.html similarity index 100% rename from src/mail/icon_cleared.html rename to data/mail/icon_cleared.html diff --git a/src/mail/position_changed.html b/data/mail/position_changed.html similarity index 100% rename from src/mail/position_changed.html rename to data/mail/position_changed.html diff --git a/src/mail/tag_changed.html b/data/mail/tag_changed.html similarity index 100% rename from src/mail/tag_changed.html rename to data/mail/tag_changed.html diff --git a/src/mail/tag_cleared.html b/data/mail/tag_cleared.html similarity index 100% rename from src/mail/tag_cleared.html rename to data/mail/tag_cleared.html diff --git a/src/mail/unbanned.html b/data/mail/unbanned.html similarity index 100% rename from src/mail/unbanned.html rename to data/mail/unbanned.html diff --git a/src/mail/verification.html b/data/mail/verification.html similarity index 100% rename from src/mail/verification.html rename to data/mail/verification.html diff --git a/src/mail/verified.html b/data/mail/verified.html similarity index 100% rename from src/mail/verified.html rename to data/mail/verified.html diff --git a/locales/en_us.json b/locales/en_us.json index 104e32f..6309c58 100644 --- a/locales/en_us.json +++ b/locales/en_us.json @@ -80,7 +80,7 @@ "alreadyReported": "You've already reported this player's tag!", "invalidReason": "You have to provide a valid reason!", "validation": "Keep the \"reason\" field between and characters.", - "success": "The player was successfully reported!", + "success": "You have successfully created the report with the ID #!", "immune": "This player cannot be reported!", "delete": { "not_found": "The report was not found!", diff --git a/package.json b/package.json index 52c92ae..efd5872 100644 --- a/package.json +++ b/package.json @@ -12,26 +12,26 @@ "dependencies": { "@elysiajs/cors": "^1.3.3", "@elysiajs/swagger": "1.3.0", - "@sentry/bun": "^9.29.0", - "axios": "^1.9.0", + "@sentry/bun": "^9.31.0", + "axios": "^1.10.0", "chalk": "^5.4.1", "change-case": "^5.4.4", - "croner": "^9.0.0", - "discord.js": "^14.19.3", + "croner": "^9.1.0", + "discord.js": "^14.20.0", "dotenv": "^16.5.0", - "elysia": "1.3.4", + "elysia": "1.3.5", "elysia-ip": "^1.0.10", "jsonwebtoken": "^9.0.2", "minimist": "^1.2.8", "moment": "^2.30.1", - "mongoose": "^8.15.2", + "mongoose": "^8.16.0", "ms": "^2.1.3", "nodemailer": "^7.0.3", "sharp": "^0.34.2" }, "devDependencies": { - "@types/bun": "^1.2.16", - "@types/jsonwebtoken": "^9.0.9", + "@types/bun": "^1.2.17", + "@types/jsonwebtoken": "^9.0.10", "@types/minimist": "^1.2.5", "@types/nodemailer": "^6.4.17" } diff --git a/src/auth/AuthProvider.ts b/src/auth/AuthProvider.ts index e2b74c7..e0111c4 100644 --- a/src/auth/AuthProvider.ts +++ b/src/auth/AuthProvider.ts @@ -1,12 +1,12 @@ import { existsSync, readdirSync } from "fs"; import { join } from "path"; import Logger from "../libs/Logger"; -import players, { Player } from "../database/schemas/players"; +import { Player, PlayerDocument } from "../database/schemas/Player"; import { stripUUID } from "../libs/game-profiles"; export type SessionData = { uuid: string | null, - player: Player | null, + player: PlayerDocument | null, self: boolean } @@ -22,7 +22,7 @@ export default abstract class AuthProvider { const tokenUUID = await this.getUUID(token); if(uuid) uuid = stripUUID(uuid); if(!tokenUUID) return { uuid: null, player: null, self: false }; - const data = await players.findOne({ uuid: tokenUUID }); + const data = await Player.findOne({ uuid: tokenUUID }); if(!data) return { uuid: tokenUUID, player: null, self: tokenUUID == uuid }; return { uuid: tokenUUID, diff --git a/src/auth/providers/ApiKeyProvider.ts b/src/auth/providers/ApiKeyProvider.ts index 165ba89..620b1e1 100644 --- a/src/auth/providers/ApiKeyProvider.ts +++ b/src/auth/providers/ApiKeyProvider.ts @@ -1,4 +1,4 @@ -import players from "../../database/schemas/players"; +import { Player } from "../../database/schemas/Player"; import AuthProvider from "../AuthProvider"; export default class ApiKeyProvider extends AuthProvider { @@ -8,7 +8,7 @@ export default class ApiKeyProvider extends AuthProvider { async getUUID(token: string): Promise { token = AuthProvider.trimTokenType(token); - const player = await players.findOne({ 'api_keys.key': token }); + const player = await Player.findOne({ 'api_keys.key': token }); if(!player) return null; const usedKey = player.api_keys.find(key => key.key === token); if(usedKey) { diff --git a/src/bot/bot.ts b/src/bot/bot.ts index aa330fa..a1314a9 100644 --- a/src/bot/bot.ts +++ b/src/bot/bot.ts @@ -38,7 +38,7 @@ export const buttons = new Collection(); export const menus = new Collection(); export const modals = new Collection(); -(async () => { +export async function registerFeatures() { const eventDir = join(__dirname, 'events'); const commandDir = join(__dirname, 'commands'); const buttonDir = join(__dirname, 'buttons'); @@ -73,7 +73,7 @@ export const modals = new Collection(); modals.set(modal.id, modal); }); -})(); +}; export const spawn = () => client.login(config.discordBot.token); export const destroy = () => client.destroy(); diff --git a/src/bot/buttons/Actions.ts b/src/bot/buttons/Actions.ts index de33435..c1dd3f9 100644 --- a/src/bot/buttons/Actions.ts +++ b/src/bot/buttons/Actions.ts @@ -1,7 +1,7 @@ import { ButtonInteraction, Message, GuildMember, ButtonStyle, ButtonBuilder, ActionRowBuilder, EmbedBuilder, MessageFlags } from "discord.js"; import Button from "../structs/Button"; import { colors } from "../bot"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { Permission } from "../../types/Permission"; import { stripUUID, uuidRegex } from "../../libs/game-profiles"; import { config } from "../../libs/config"; @@ -19,7 +19,7 @@ export default class ActionsButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { if(!player.canManagePlayers()) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ You\'re not allowed to perform this action!')], flags: [MessageFlags.Ephemeral] }); const uuid = interaction.customId.split('_')[1] || message.embeds[0]?.author?.name || message.embeds[0]?.fields[0]?.value.match(uuidRegex)?.[0]; if(!uuid) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/AddRole.ts b/src/bot/buttons/AddRole.ts index 227dd8f..8a0a7dd 100644 --- a/src/bot/buttons/AddRole.ts +++ b/src/bot/buttons/AddRole.ts @@ -1,10 +1,10 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, ActionRowBuilder, StringSelectMenuBuilder, MessageFlags } from "discord.js"; import Button from "../structs/Button"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { capitalCase, snakeCase } from "change-case"; import { Permission } from "../../types/Permission"; -import { getCachedRoles } from "../../database/schemas/roles"; +import { getCachedRoles } from "../../database/schemas/Role"; export default class AddRoleButton extends Button { constructor() { @@ -14,7 +14,7 @@ export default class AddRoleButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/Ban.ts b/src/bot/buttons/Ban.ts index adf498d..d966b46 100644 --- a/src/bot/buttons/Ban.ts +++ b/src/bot/buttons/Ban.ts @@ -1,7 +1,7 @@ import { ButtonInteraction, Message, GuildMember, ModalBuilder, ActionRowBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; import Button from "../structs/Button"; import { Permission } from "../../types/Permission"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; export default class BanButton extends Button { constructor() { @@ -11,7 +11,7 @@ export default class BanButton extends Button { }); } - public trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + public trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const modal = new ModalBuilder() .setTitle('Ban player') .setCustomId(`ban_${interaction.customId.split('_')[1]}`) diff --git a/src/bot/buttons/BanHistory.ts b/src/bot/buttons/BanHistory.ts index d1c147c..fff4eda 100644 --- a/src/bot/buttons/BanHistory.ts +++ b/src/bot/buttons/BanHistory.ts @@ -1,7 +1,7 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, MessageFlags, ActionRowBuilder, StringSelectMenuBuilder } from "discord.js"; import Button from "../structs/Button"; import { colors } from "../bot"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { GameProfile } from "../../libs/game-profiles"; import { Permission } from "../../types/Permission"; @@ -13,7 +13,7 @@ export default class BanHistoryButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.editReply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')] }); diff --git a/src/bot/buttons/ClearIconTexture.ts b/src/bot/buttons/ClearIconTexture.ts index cff90f9..d00b817 100644 --- a/src/bot/buttons/ClearIconTexture.ts +++ b/src/bot/buttons/ClearIconTexture.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import Button from "../structs/Button"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; import { sendIconClearEmail } from "../../libs/mailer"; @@ -15,7 +15,7 @@ export default class ClearIconTextureButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); if(!target.icon.hash) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription(`❌ This player does not have a custom icon!`)], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/ClearTag.ts b/src/bot/buttons/ClearTag.ts index a49b5c3..67c5703 100644 --- a/src/bot/buttons/ClearTag.ts +++ b/src/bot/buttons/ClearTag.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import Button from "../structs/Button"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; import { sendTagClearEmail } from "../../libs/mailer"; @@ -15,7 +15,7 @@ export default class ClearTagButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); if(!target.tag) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ This player does not have a tag!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/Clears.ts b/src/bot/buttons/Clears.ts index b4ba947..8047f3a 100644 --- a/src/bot/buttons/Clears.ts +++ b/src/bot/buttons/Clears.ts @@ -1,12 +1,12 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import Button from "../structs/Button"; import { colors } from "../bot"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { GameProfile } from "../../libs/game-profiles"; import { Permission } from "../../types/Permission"; import { formatTimestamp } from "../../libs/discord-notifier"; import { stripColors } from "../../libs/chat-color"; -import { getCustomIconUrl } from "../../routes/players/[uuid]/icon"; +import { getCustomIconUrl } from "../../routes/players/[uuid]/icons"; export default class ClearsButton extends Button { constructor() { @@ -16,7 +16,7 @@ export default class ClearsButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.editReply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')] }); diff --git a/src/bot/buttons/CreateApiKey.ts b/src/bot/buttons/CreateApiKey.ts index 268611b..ae83104 100644 --- a/src/bot/buttons/CreateApiKey.ts +++ b/src/bot/buttons/CreateApiKey.ts @@ -1,7 +1,7 @@ import { ButtonInteraction, Message, GuildMember, ActionRowBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; import Button from "../structs/Button"; import { Permission } from "../../types/Permission"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; export default class CreateApiKeyButton extends Button { constructor() { @@ -11,7 +11,7 @@ export default class CreateApiKeyButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const modal = new ModalBuilder() .setTitle('Create API key') .setCustomId(`createApiKey_${interaction.customId.split('_')[1]}`) diff --git a/src/bot/buttons/CreateGiftCode.ts b/src/bot/buttons/CreateGiftCode.ts index 8e6367c..ab2c315 100644 --- a/src/bot/buttons/CreateGiftCode.ts +++ b/src/bot/buttons/CreateGiftCode.ts @@ -1,9 +1,9 @@ import { ButtonInteraction, Message, GuildMember, User, EmbedBuilder, ActionRowBuilder, MessageFlags, StringSelectMenuBuilder } from "discord.js"; import Button from "../structs/Button"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { Permission } from "../../types/Permission"; -import { getCachedRoles } from "../../database/schemas/roles"; +import { getCachedRoles } from "../../database/schemas/Role"; import { capitalCase } from "change-case"; export default class CreateGiftCodeButton extends Button { @@ -14,7 +14,7 @@ export default class CreateGiftCodeButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const embed = new EmbedBuilder() .setColor(colors.gray) .setTitle('Create gift code') diff --git a/src/bot/buttons/CreateNote.ts b/src/bot/buttons/CreateNote.ts index c93dfcf..624c938 100644 --- a/src/bot/buttons/CreateNote.ts +++ b/src/bot/buttons/CreateNote.ts @@ -2,7 +2,7 @@ import { ButtonInteraction, Message, GuildMember, ModalBuilder, ActionRowBuilder import Button from "../structs/Button"; import { config } from "../../libs/config"; import { Permission } from "../../types/Permission"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; export default class CreateNoteButton extends Button { constructor() { @@ -12,7 +12,7 @@ export default class CreateNoteButton extends Button { }); } - public trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + public trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const modal = new ModalBuilder() .setTitle('Create note') .setCustomId(`createNote_${interaction.customId.split('_')[1]}`) diff --git a/src/bot/buttons/CreateRole.ts b/src/bot/buttons/CreateRole.ts index 20eb069..442565a 100644 --- a/src/bot/buttons/CreateRole.ts +++ b/src/bot/buttons/CreateRole.ts @@ -2,7 +2,7 @@ import { ButtonInteraction, Message, GuildMember, ModalBuilder, ActionRowBuilder import Button from "../structs/Button"; import { config } from "../../libs/config"; import { Permission } from "../../types/Permission"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; export default class CreateRoleButton extends Button { constructor() { @@ -12,7 +12,7 @@ export default class CreateRoleButton extends Button { }); } - public trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + public trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const modal = new ModalBuilder() .setTitle('Create role') .setCustomId('createRole') diff --git a/src/bot/buttons/DeleteApiKey.ts b/src/bot/buttons/DeleteApiKey.ts index 6ba5b14..e886330 100644 --- a/src/bot/buttons/DeleteApiKey.ts +++ b/src/bot/buttons/DeleteApiKey.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, ActionRowBuilder, MessageFlags, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from "discord.js"; import Button from "../structs/Button"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { Permission } from "../../types/Permission"; @@ -12,7 +12,7 @@ export default class DeleteApiKeyButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/DeleteGiftCode.ts b/src/bot/buttons/DeleteGiftCode.ts index a3cb07d..01ec2f0 100644 --- a/src/bot/buttons/DeleteGiftCode.ts +++ b/src/bot/buttons/DeleteGiftCode.ts @@ -1,9 +1,9 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, ActionRowBuilder, MessageFlags, StringSelectMenuBuilder } from "discord.js"; import Button from "../structs/Button"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { Permission } from "../../types/Permission"; -import codeSchema from "../../database/schemas/gift-codes"; +import { GiftCode } from "../../database/schemas/GiftCode"; export default class DeleteGiftCodeButton extends Button { constructor() { @@ -13,13 +13,13 @@ export default class DeleteGiftCodeButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const embed = new EmbedBuilder() .setColor(colors.gray) .setTitle('Delete gift code') .setDescription('Here you can select a gift code to be deleted.'); - const codes = await codeSchema.find(); + const codes = await GiftCode.find(); const codeMap = codes.filter((code) => code.isValid()).slice(0, 25).map((code) => ({ value: code.code, label: code.name, diff --git a/src/bot/buttons/DeleteNote.ts b/src/bot/buttons/DeleteNote.ts index eeb7db9..c53ac92 100644 --- a/src/bot/buttons/DeleteNote.ts +++ b/src/bot/buttons/DeleteNote.ts @@ -1,7 +1,7 @@ import { ButtonInteraction, Message, GuildMember, ActionRowBuilder, EmbedBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, MessageFlags } from "discord.js"; import Button from "../structs/Button"; import { colors } from "../bot"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { GameProfile } from "../../libs/game-profiles"; import { Permission } from "../../types/Permission"; @@ -13,7 +13,7 @@ export default class DeleteNoteButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.editReply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')] }); diff --git a/src/bot/buttons/DeleteReport.ts b/src/bot/buttons/DeleteReport.ts index 8180638..6e06ed9 100644 --- a/src/bot/buttons/DeleteReport.ts +++ b/src/bot/buttons/DeleteReport.ts @@ -1,7 +1,7 @@ import { ButtonInteraction, Message, GuildMember, ActionRowBuilder, EmbedBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, MessageFlags } from "discord.js"; import Button from "../structs/Button"; import { colors } from "../bot"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { GameProfile } from "../../libs/game-profiles"; import { Permission } from "../../types/Permission"; @@ -13,7 +13,7 @@ export default class DeleteReportButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.editReply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')] }); diff --git a/src/bot/buttons/DeleteRole.ts b/src/bot/buttons/DeleteRole.ts index 238543f..8ce5c46 100644 --- a/src/bot/buttons/DeleteRole.ts +++ b/src/bot/buttons/DeleteRole.ts @@ -1,11 +1,10 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import Button from "../structs/Button"; import { Permission } from "../../types/Permission"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; -import { updateRoleCache } from "../../database/schemas/roles"; +import { Role, updateRoleCache } from "../../database/schemas/Role"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; -import roles from "../../database/schemas/roles"; export default class DeleteRole extends Button { constructor() { @@ -15,15 +14,15 @@ export default class DeleteRole extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { - const role = await roles.findOne({ name: interaction.customId.split('_')[1] }); + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { + const role = await Role.findOne({ name: interaction.customId.split('_')[1] }); if(!role) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Role not found!')], flags: [MessageFlags.Ephemeral] }); sendModLogMessage({ logType: ModLogType.DeleteRole, staff: await player.getGameProfile(), discord: true, - role: role.name + role: role }); await role.deleteOne(); diff --git a/src/bot/buttons/EditRole.ts b/src/bot/buttons/EditRole.ts index 6e92f56..78bd9c2 100644 --- a/src/bot/buttons/EditRole.ts +++ b/src/bot/buttons/EditRole.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, ActionRowBuilder, StringSelectMenuBuilder, MessageFlags } from "discord.js"; import Button from "../structs/Button"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { capitalCase, snakeCase } from "change-case"; import { Permission } from "../../types/Permission"; @@ -13,7 +13,7 @@ export default class EditRoleButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/EditRoleExpiration.ts b/src/bot/buttons/EditRoleExpiration.ts index 0835f1d..082196e 100644 --- a/src/bot/buttons/EditRoleExpiration.ts +++ b/src/bot/buttons/EditRoleExpiration.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, ActionRowBuilder, MessageFlags, ModalBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; import Button from "../structs/Button"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { Permission } from "../../types/Permission"; @@ -12,7 +12,7 @@ export default class EditRoleExpirationButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/EditRoleNote.ts b/src/bot/buttons/EditRoleNote.ts index 9f41d2e..2043d15 100644 --- a/src/bot/buttons/EditRoleNote.ts +++ b/src/bot/buttons/EditRoleNote.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, ActionRowBuilder, MessageFlags, ModalBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; import Button from "../structs/Button"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { Permission } from "../../types/Permission"; @@ -12,7 +12,7 @@ export default class EditRoleNoteButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/FinishActions.ts b/src/bot/buttons/FinishActions.ts index de07ea3..83b2964 100644 --- a/src/bot/buttons/FinishActions.ts +++ b/src/bot/buttons/FinishActions.ts @@ -1,7 +1,7 @@ import { ButtonInteraction, Message, GuildMember, ActionRowBuilder, EmbedBuilder, ButtonBuilder, MessageFlags } from "discord.js"; import Button from "../structs/Button"; import { colors } from "../bot"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; import { stripUUID } from "../../libs/game-profiles"; export default class FinishActionsButton extends Button { @@ -12,7 +12,7 @@ export default class FinishActionsButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { if(!player.canManagePlayers()) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ You\'re not allowed to perform this action!')], flags: [MessageFlags.Ephemeral] }); await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/ManageAccount.ts b/src/bot/buttons/ManageAccount.ts index ef0fb89..8871640 100644 --- a/src/bot/buttons/ManageAccount.ts +++ b/src/bot/buttons/ManageAccount.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } from "discord.js"; import Button from "../structs/Button"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { Permission } from "../../types/Permission"; @@ -12,7 +12,7 @@ export default class ManageAccountButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/ManageApiKeys.ts b/src/bot/buttons/ManageApiKeys.ts index a10951d..bfd4fe0 100644 --- a/src/bot/buttons/ManageApiKeys.ts +++ b/src/bot/buttons/ManageApiKeys.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } from "discord.js"; import Button from "../structs/Button"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { Permission } from "../../types/Permission"; @@ -12,7 +12,7 @@ export default class ManageApiKeysButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/ManageBans.ts b/src/bot/buttons/ManageBans.ts index 5b8cd64..c106c01 100644 --- a/src/bot/buttons/ManageBans.ts +++ b/src/bot/buttons/ManageBans.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } from "discord.js"; import Button from "../structs/Button"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { Permission } from "../../types/Permission"; import { GameProfile } from "../../libs/game-profiles"; @@ -14,7 +14,7 @@ export default class ManageBansButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/ManageConnections.ts b/src/bot/buttons/ManageConnections.ts index eab8616..9b385ef 100644 --- a/src/bot/buttons/ManageConnections.ts +++ b/src/bot/buttons/ManageConnections.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } from "discord.js"; import Button from "../structs/Button"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { Permission } from "../../types/Permission"; @@ -12,7 +12,7 @@ export default class ManageConnectionsButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/ManageIcon.ts b/src/bot/buttons/ManageIcon.ts index e59716a..81d322a 100644 --- a/src/bot/buttons/ManageIcon.ts +++ b/src/bot/buttons/ManageIcon.ts @@ -1,9 +1,8 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } from "discord.js"; import Button from "../structs/Button"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; -import { snakeCase } from "change-case"; -import { getCustomIconUrl } from "../../routes/players/[uuid]/icon"; +import { getCustomIconUrl } from "../../routes/players/[uuid]/icons"; import { Permission } from "../../types/Permission"; import { GlobalIcon } from "../../types/GlobalIcon"; import { config } from "../../libs/config"; @@ -16,7 +15,7 @@ export default class ManageIconButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); @@ -24,9 +23,8 @@ export default class ManageIconButton extends Button { .setTitle('Manage icon') .setDescription('Here you can edit the player\'s icon type and texture.'); - const icon = snakeCase(target.icon.name); - if(icon != snakeCase(GlobalIcon[GlobalIcon.None])) { - if(icon == snakeCase(GlobalIcon[GlobalIcon.Custom])) { + if(target.icon.name != GlobalIcon.None) { + if(target.icon.name == GlobalIcon.Custom) { if(!!target.icon.hash) { embed.setThumbnail(getCustomIconUrl(target.uuid, target.icon.hash)); } diff --git a/src/bot/buttons/ManagePermissions.ts b/src/bot/buttons/ManagePermissions.ts index ae67548..9c693c1 100644 --- a/src/bot/buttons/ManagePermissions.ts +++ b/src/bot/buttons/ManagePermissions.ts @@ -1,9 +1,9 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, StringSelectMenuBuilder, ActionRowBuilder, MessageFlags } from "discord.js"; import Button from "../structs/Button"; import { Permission, permissions } from "../../types/Permission"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; import { colors, images } from "../bot"; -import { getCachedRoles } from "../../database/schemas/roles"; +import { getCachedRoles } from "../../database/schemas/Role"; import { capitalCase } from "change-case"; export default class ManagePermissionsButton extends Button { @@ -14,7 +14,7 @@ export default class ManagePermissionsButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const role = getCachedRoles().find((role) => role.name == interaction.customId.split('_')[1]); if(!role) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Role not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/ManageRoles.ts b/src/bot/buttons/ManageRoles.ts index 198fb80..edf9e5e 100644 --- a/src/bot/buttons/ManageRoles.ts +++ b/src/bot/buttons/ManageRoles.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, ActionRowBuilder, MessageFlags, ButtonBuilder, ButtonStyle } from "discord.js"; import Button from "../structs/Button"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { Permission } from "../../types/Permission"; @@ -12,7 +12,7 @@ export default class ManageRolesButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/ManageTag.ts b/src/bot/buttons/ManageTag.ts index 0017d43..36bd304 100644 --- a/src/bot/buttons/ManageTag.ts +++ b/src/bot/buttons/ManageTag.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } from "discord.js"; import Button from "../structs/Button"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { Permission } from "../../types/Permission"; @@ -12,7 +12,7 @@ export default class ManageTagButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/ManageWatchlist.ts b/src/bot/buttons/ManageWatchlist.ts index 669079a..da8eab2 100644 --- a/src/bot/buttons/ManageWatchlist.ts +++ b/src/bot/buttons/ManageWatchlist.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } from "discord.js"; import Button from "../structs/Button"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { Permission } from "../../types/Permission"; @@ -12,7 +12,7 @@ export default class ManageWatchlistButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/ModerateAccount.ts b/src/bot/buttons/ModerateAccount.ts index 8cbeeb0..c22fb8c 100644 --- a/src/bot/buttons/ModerateAccount.ts +++ b/src/bot/buttons/ModerateAccount.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } from "discord.js"; import Button from "../structs/Button"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { Permission } from "../../types/Permission"; @@ -12,7 +12,7 @@ export default class ModerateAccountButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/Notes.ts b/src/bot/buttons/Notes.ts index 5b242b9..7069bbb 100644 --- a/src/bot/buttons/Notes.ts +++ b/src/bot/buttons/Notes.ts @@ -1,7 +1,7 @@ import { ButtonInteraction, Message, GuildMember, ButtonBuilder, ActionRowBuilder, EmbedBuilder, ButtonStyle, MessageFlags } from "discord.js"; import Button from "../structs/Button"; import { colors } from "../bot"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { GameProfile } from "../../libs/game-profiles"; import { Permission } from "../../types/Permission"; import { formatTimestamp } from "../../libs/discord-notifier"; @@ -14,7 +14,7 @@ export default class NotesButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.editReply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')] }); diff --git a/src/bot/buttons/OverwriteDiscord.ts b/src/bot/buttons/OverwriteDiscord.ts index 6ee96f5..85d36ea 100644 --- a/src/bot/buttons/OverwriteDiscord.ts +++ b/src/bot/buttons/OverwriteDiscord.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, Message, GuildMember, ModalBuilder, ActionRowBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; import Button from "../structs/Button"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; import { Permission } from "../../types/Permission"; export default class OverwriteDiscordButton extends Button { @@ -11,7 +11,7 @@ export default class OverwriteDiscordButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const modal = new ModalBuilder() .setTitle('Overwrite Discord ID') .setCustomId(`overwriteDiscord_${interaction.customId.split('_')[1]}`) diff --git a/src/bot/buttons/OverwriteEmail.ts b/src/bot/buttons/OverwriteEmail.ts index e40b679..8285848 100644 --- a/src/bot/buttons/OverwriteEmail.ts +++ b/src/bot/buttons/OverwriteEmail.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, Message, GuildMember, ModalBuilder, ActionRowBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; import Button from "../structs/Button"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; import { Permission } from "../../types/Permission"; export default class OverwriteEmailButton extends Button { @@ -11,7 +11,7 @@ export default class OverwriteEmailButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const modal = new ModalBuilder() .setTitle('Overwrite email address') .setCustomId(`overwriteEmail_${interaction.customId.split('_')[1]}`) diff --git a/src/bot/buttons/Referrals.ts b/src/bot/buttons/Referrals.ts index 34296a9..45e28c3 100644 --- a/src/bot/buttons/Referrals.ts +++ b/src/bot/buttons/Referrals.ts @@ -1,7 +1,7 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import Button from "../structs/Button"; import { colors } from "../bot"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { GameProfile } from "../../libs/game-profiles"; import { formatTimestamp } from "../../libs/discord-notifier"; @@ -12,7 +12,7 @@ export default class ReferralsButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.editReply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')] }); diff --git a/src/bot/buttons/RegenerateApiKey.ts b/src/bot/buttons/RegenerateApiKey.ts index e1e4613..0d0b273 100644 --- a/src/bot/buttons/RegenerateApiKey.ts +++ b/src/bot/buttons/RegenerateApiKey.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, ActionRowBuilder, MessageFlags, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from "discord.js"; import Button from "../structs/Button"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { Permission } from "../../types/Permission"; @@ -12,7 +12,7 @@ export default class RegenerateApiKeyButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/RemoveRole.ts b/src/bot/buttons/RemoveRole.ts index 2f6b3cc..87bc1a9 100644 --- a/src/bot/buttons/RemoveRole.ts +++ b/src/bot/buttons/RemoveRole.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, ActionRowBuilder, StringSelectMenuBuilder, MessageFlags } from "discord.js"; import Button from "../structs/Button"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { capitalCase, snakeCase } from "change-case"; import { Permission } from "../../types/Permission"; @@ -13,7 +13,7 @@ export default class RemoveRoleButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/RenameRole.ts b/src/bot/buttons/RenameRole.ts index dd9df8b..6144446 100644 --- a/src/bot/buttons/RenameRole.ts +++ b/src/bot/buttons/RenameRole.ts @@ -1,10 +1,10 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, ModalBuilder, ActionRowBuilder, TextInputBuilder, TextInputStyle, MessageFlags } from "discord.js"; import Button from "../structs/Button"; import { colors } from "../bot"; -import { getCachedRoles } from "../../database/schemas/roles"; +import { getCachedRoles } from "../../database/schemas/Role"; import { config } from "../../libs/config"; import { Permission } from "../../types/Permission"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; export default class RenameRoleButton extends Button { constructor() { @@ -14,7 +14,7 @@ export default class RenameRoleButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const role = getCachedRoles().find((role) => role.name == interaction.customId.split('_')[1]); if(!role) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Role not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/Reports.ts b/src/bot/buttons/Reports.ts index e8d2922..6ccd61b 100644 --- a/src/bot/buttons/Reports.ts +++ b/src/bot/buttons/Reports.ts @@ -1,7 +1,7 @@ import { ButtonInteraction, Message, GuildMember, ButtonBuilder, ActionRowBuilder, EmbedBuilder, ButtonStyle, MessageFlags } from "discord.js"; import Button from "../structs/Button"; import { colors } from "../bot"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { GameProfile } from "../../libs/game-profiles"; import { Permission } from "../../types/Permission"; import { formatTimestamp } from "../../libs/discord-notifier"; @@ -14,7 +14,7 @@ export default class ReportsButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.editReply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')] }); diff --git a/src/bot/buttons/ResetDiscordLinkingCode.ts b/src/bot/buttons/ResetDiscordLinkingCode.ts index 2b96a68..17e0b30 100644 --- a/src/bot/buttons/ResetDiscordLinkingCode.ts +++ b/src/bot/buttons/ResetDiscordLinkingCode.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import Button from "../structs/Button"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; import { Permission } from "../../types/Permission"; @@ -13,7 +13,7 @@ export default class ResetDiscordLinkingCodeButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); if(!target.connections.discord.code) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ This player does not have a linking code!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/ResetEmailLinkingCode.ts b/src/bot/buttons/ResetEmailLinkingCode.ts index 5dffdf3..0f3b101 100644 --- a/src/bot/buttons/ResetEmailLinkingCode.ts +++ b/src/bot/buttons/ResetEmailLinkingCode.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import Button from "../structs/Button"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; import { Permission } from "../../types/Permission"; @@ -13,7 +13,7 @@ export default class ResetEmailLinkingCodeButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); if(!target.connections.email.code) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ This player does not have a linking code!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/SetIconType.ts b/src/bot/buttons/SetIconType.ts index fe9ff6a..41e19cf 100644 --- a/src/bot/buttons/SetIconType.ts +++ b/src/bot/buttons/SetIconType.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, ActionRowBuilder, MessageFlags } from "discord.js"; import Button from "../structs/Button"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { capitalCase, snakeCase } from "change-case"; import { Permission } from "../../types/Permission"; @@ -14,7 +14,7 @@ export default class SetIconTypeButton extends Button { }) } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); @@ -22,8 +22,6 @@ export default class SetIconTypeButton extends Button { .setTitle('Set icon type') .setDescription(`The player's current icon type is \`${capitalCase(target.icon.name)}\`.`); - const playerIcon = snakeCase(target.icon.name); - const menu = new StringSelectMenuBuilder() .setCustomId(`setIconType_${target.uuid}`) .setPlaceholder('Please select an icon.') @@ -31,9 +29,9 @@ export default class SetIconTypeButton extends Button { .setMaxValues(1) .setOptions(icons.map((icon) => new StringSelectMenuOptionBuilder() - .setLabel(capitalCase(GlobalIcon[icon])) - .setDefault(snakeCase(GlobalIcon[icon]) == playerIcon) - .setValue(GlobalIcon[icon]) + .setLabel(icon) + .setDefault(icon == target.icon.name) + .setValue(icon) ).slice(0, 25)); interaction.reply({ embeds: [embed], components: [new ActionRowBuilder().addComponents(menu)], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/SetPosition.ts b/src/bot/buttons/SetPosition.ts index 2cf4966..25d4827 100644 --- a/src/bot/buttons/SetPosition.ts +++ b/src/bot/buttons/SetPosition.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, ActionRowBuilder, MessageFlags } from "discord.js"; import Button from "../structs/Button"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { capitalCase, snakeCase } from "change-case"; import { Permission } from "../../types/Permission"; @@ -14,7 +14,7 @@ export default class SetPositionButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); @@ -22,8 +22,6 @@ export default class SetPositionButton extends Button { .setTitle('Set position') .setDescription(`The player's current position is \`${capitalCase(target.position)}\`.`); - const playerPosition = snakeCase(target.position); - const menu = new StringSelectMenuBuilder() .setCustomId(`setPosition_${target.uuid}`) .setPlaceholder('Please select a position.') @@ -31,9 +29,9 @@ export default class SetPositionButton extends Button { .setMaxValues(1) .setOptions(positions.map((position) => new StringSelectMenuOptionBuilder() - .setLabel(capitalCase(GlobalPosition[position])) - .setDefault(snakeCase(GlobalPosition[position]) == playerPosition) - .setValue(GlobalPosition[position]) + .setLabel(capitalCase(position)) + .setDefault(position == target.position) + .setValue(position) )); interaction.reply({ embeds: [embed], components: [new ActionRowBuilder().addComponents(menu)], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/SetSku.ts b/src/bot/buttons/SetSku.ts index ac5458f..d383c7f 100644 --- a/src/bot/buttons/SetSku.ts +++ b/src/bot/buttons/SetSku.ts @@ -1,8 +1,8 @@ import { ButtonInteraction, Message, GuildMember, User, EmbedBuilder, ActionRowBuilder, MessageFlags, StringSelectMenuBuilder } from "discord.js"; import Button from "../structs/Button"; import { client, colors, images } from "../bot"; -import { getCachedRoles } from "../../database/schemas/roles"; -import { Player } from "../../database/schemas/players"; +import { getCachedRoles } from "../../database/schemas/Role"; +import { PlayerDocument } from "../../database/schemas/Player"; import { Permission } from "../../types/Permission"; export default class SetSkuButton extends Button { @@ -13,7 +13,7 @@ export default class SetSkuButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const role = getCachedRoles().find((role) => role.name == interaction.customId.split('_')[1]); if(!role) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Role not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/SetTag.ts b/src/bot/buttons/SetTag.ts index 52f2ad2..70d7dc1 100644 --- a/src/bot/buttons/SetTag.ts +++ b/src/bot/buttons/SetTag.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, ActionRowBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, MessageFlags } from "discord.js"; import Button from "../structs/Button"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { Permission } from "../../types/Permission"; @@ -12,7 +12,7 @@ export default class SetTagButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/TagHistory.ts b/src/bot/buttons/TagHistory.ts index a893bd9..d3a9d17 100644 --- a/src/bot/buttons/TagHistory.ts +++ b/src/bot/buttons/TagHistory.ts @@ -1,7 +1,7 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import Button from "../structs/Button"; import { colors } from "../bot"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { Permission } from "../../types/Permission"; import { stripColors } from "../../libs/chat-color"; import { config } from "../../libs/config"; @@ -14,7 +14,7 @@ export default class TagHistoryButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.editReply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')] }); diff --git a/src/bot/buttons/ToggleIcon.ts b/src/bot/buttons/ToggleIcon.ts index d1e5346..66f13a1 100644 --- a/src/bot/buttons/ToggleIcon.ts +++ b/src/bot/buttons/ToggleIcon.ts @@ -1,10 +1,10 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import Button from "../structs/Button"; import { Permission } from "../../types/Permission"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; -import roles, { updateRoleCache } from "../../database/schemas/roles"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; +import { Role, updateRoleCache } from "../../database/schemas/Role"; export default class ToggleIconButton extends Button { constructor() { @@ -14,8 +14,8 @@ export default class ToggleIconButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { - const role = await roles.findOne({ name: interaction.customId.split('_')[1] }); + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { + const role = await Role.findOne({ name: interaction.customId.split('_')[1] }); if(!role) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Role not found!')], flags: [MessageFlags.Ephemeral] }); role.hasIcon = !role.hasIcon; diff --git a/src/bot/buttons/Unban.ts b/src/bot/buttons/Unban.ts index 8ab3212..3358930 100644 --- a/src/bot/buttons/Unban.ts +++ b/src/bot/buttons/Unban.ts @@ -1,7 +1,7 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import Button from "../structs/Button"; import { colors } from "../bot"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; import { sendUnbanEmail } from "../../libs/mailer"; import { getI18nFunctionByLanguage } from "../../middleware/fetch-i18n"; @@ -15,7 +15,7 @@ export default class UnbanButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); if(!target.isBanned()) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ This player is not banned!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/UnlinkDiscord.ts b/src/bot/buttons/UnlinkDiscord.ts index 1e09a0b..b1082ec 100644 --- a/src/bot/buttons/UnlinkDiscord.ts +++ b/src/bot/buttons/UnlinkDiscord.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import Button from "../structs/Button"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; import { Permission } from "../../types/Permission"; @@ -14,7 +14,7 @@ export default class UnlinkDiscordButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); if(!target.connections.discord.id) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ This player does not have their discord account linked!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/UnlinkEmail.ts b/src/bot/buttons/UnlinkEmail.ts index 22b0b0c..d1cdc75 100644 --- a/src/bot/buttons/UnlinkEmail.ts +++ b/src/bot/buttons/UnlinkEmail.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import Button from "../structs/Button"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { ModLogType, sendEmailLinkMessage, sendModLogMessage } from "../../libs/discord-notifier"; import { Permission } from "../../types/Permission"; @@ -13,7 +13,7 @@ export default class UnlinkEmailButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); if(!target.connections.email.address) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ This player does not have their email address linked!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/Unwatch.ts b/src/bot/buttons/Unwatch.ts index 06343c6..2fed5a5 100644 --- a/src/bot/buttons/Unwatch.ts +++ b/src/bot/buttons/Unwatch.ts @@ -1,7 +1,7 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import Button from "../structs/Button"; import { colors } from "../bot"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; import { Permission } from "../../types/Permission"; @@ -13,7 +13,7 @@ export default class UnwatchButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); if(!target.watchlist) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ This player is not on the watchlist!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/buttons/Watch.ts b/src/bot/buttons/Watch.ts index 3541b93..5de480e 100644 --- a/src/bot/buttons/Watch.ts +++ b/src/bot/buttons/Watch.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, Message, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import Button from "../structs/Button"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; import { Permission } from "../../types/Permission"; @@ -13,7 +13,7 @@ export default class WatchButton extends Button { }); } - async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player) { + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); if(target.watchlist) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ This player is already on the watchlist!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/commands/CustomIcon.ts b/src/bot/commands/CustomIcon.ts index dd17d16..8e8db92 100644 --- a/src/bot/commands/CustomIcon.ts +++ b/src/bot/commands/CustomIcon.ts @@ -1,6 +1,6 @@ import { ApplicationCommandOptionType, CommandInteraction, EmbedBuilder, GuildMember, MessageFlags } from "discord.js"; import Command, { CommandOptions } from "../structs/Command"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { join } from 'path'; import axios from "axios"; @@ -8,7 +8,6 @@ import { config } from "../../libs/config"; import { Permission } from "../../types/Permission"; import { GlobalIcon } from "../../types/GlobalIcon"; import { sendCustomIconUploadMessage } from "../../libs/discord-notifier"; -import { snakeCase } from "change-case"; import { generateSecureCode } from "../../libs/crypto"; export default class CustomIconCommand extends Command { @@ -54,13 +53,13 @@ export default class CustomIconCommand extends Command { }); } - async execute(interaction: CommandInteraction, options: CommandOptions, member: GuildMember, player: Player) { + async execute(interaction: CommandInteraction, options: CommandOptions, member: GuildMember, player: PlayerDocument) { await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); const sub = options.getSubcommand(); if(sub == 'toggle') { const shouldEnable = options.getBoolean('enable', true); - player.icon.name = snakeCase(GlobalIcon[shouldEnable ? GlobalIcon.Custom : GlobalIcon.None]); + player.icon.name = shouldEnable ? GlobalIcon.Custom : GlobalIcon.None; await player.save(); interaction.editReply({ embeds: [new EmbedBuilder().setColor(colors.success).setDescription(`✅ Your custom icon has been ${shouldEnable ? 'enabled' : 'disabled'}!`)] }); @@ -74,7 +73,7 @@ export default class CustomIconCommand extends Command { const request = await axios.get(file.url, { responseType: 'arraybuffer' }).catch(() => null); if(!request) return interaction.editReply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ The upload failed, please try again!')] }); - player.icon.name = snakeCase(GlobalIcon[GlobalIcon.Custom]); + player.icon.name = GlobalIcon.Custom; player.icon.hash = generateSecureCode(32); await player.save(); await Bun.write(Bun.file(join('data', 'icons', player.uuid, `${player.icon.hash}.png`)), request.data, { createPath: true }); @@ -86,7 +85,7 @@ export default class CustomIconCommand extends Command { interaction.editReply({ embeds: [new EmbedBuilder().setColor(colors.success).setDescription('✅ Your custom icon was successfully uploaded!\nYou may need to clear your cache ingame for the icon to be shown.').setThumbnail(`attachment://${file.name}`)], files: [file] }); } else if(sub == 'unset') { - player.icon.name = snakeCase(GlobalIcon[GlobalIcon.None]); + player.icon.name = GlobalIcon.None; player.icon.hash = null; await player.save(); diff --git a/src/bot/commands/GiftCodes.ts b/src/bot/commands/GiftCodes.ts index 4e1e70f..04cff89 100644 --- a/src/bot/commands/GiftCodes.ts +++ b/src/bot/commands/GiftCodes.ts @@ -1,10 +1,10 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, CommandInteractionOptionResolver, ContainerBuilder, EmbedBuilder, GuildMember, MediaGalleryBuilder, MessageFlags, SectionBuilder, SeparatorSpacingSize, TextDisplayBuilder, ThumbnailBuilder } from "discord.js"; import Command from "../structs/Command"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; import { images } from "../bot"; import { Permission } from "../../types/Permission"; -import giftCodes, { GiftCode } from "../../database/schemas/gift-codes"; import { formatTimestamp } from "../../libs/discord-notifier"; +import { GiftCode, GiftCodeDocument } from "../../database/schemas/GiftCode"; export default class GiftCodesCommand extends Command { constructor() { @@ -15,10 +15,10 @@ export default class GiftCodesCommand extends Command { }); } - async execute(interaction: CommandInteraction, options: CommandInteractionOptionResolver, member: GuildMember, player: Player) { + async execute(interaction: CommandInteraction, options: CommandInteractionOptionResolver, member: GuildMember, player: PlayerDocument) { const limit = 30; - const codes = await giftCodes.find(); - const stringifyCode = (code: GiftCode) => `↝ \`${code.name}\` [||**${code.code}**||] - \`${code.uses.length}/${code.max_uses}\` Uses${code.expires_at ? ` (Expires ${formatTimestamp(code.expires_at, 'R')})` : ''}`; + const codes = await GiftCode.find(); + const stringifyCode = (code: GiftCodeDocument) => `↝ \`${code.name}\` [||**${code.code}**||] - \`${code.uses.length}/${code.max_uses}\` Uses${code.expires_at ? ` (Expires ${formatTimestamp(code.expires_at, 'R')})` : ''}`; const maps = { active: codes.filter((code) => code.isValid()), inactive: codes.filter((code) => !code.isValid()) diff --git a/src/bot/commands/Link.ts b/src/bot/commands/Link.ts index 6b31636..a63bcb4 100644 --- a/src/bot/commands/Link.ts +++ b/src/bot/commands/Link.ts @@ -1,6 +1,6 @@ import { ApplicationCommandOptionType, CommandInteraction, EmbedBuilder, GuildMember, MessageFlags, User } from "discord.js"; import Command, { CommandOptions } from "../structs/Command"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { config } from "../../libs/config"; import { onDiscordLink } from "../../libs/events"; @@ -22,7 +22,7 @@ export default class LinkCommand extends Command { }); } - async execute(interaction: CommandInteraction, options: CommandOptions, member: GuildMember, player: Player | null) { + async execute(interaction: CommandInteraction, options: CommandOptions, member: GuildMember, player: PlayerDocument | null) { await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); if(!config.discordBot.notifications.accountConnections.enabled) return interaction.editReply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Account linking is deactivated!')] }); const code = options.getString('code', true); diff --git a/src/bot/commands/PlayerInfo.ts b/src/bot/commands/PlayerInfo.ts index 57502e6..0a6b3f6 100644 --- a/src/bot/commands/PlayerInfo.ts +++ b/src/bot/commands/PlayerInfo.ts @@ -1,6 +1,6 @@ import { ActionRowBuilder, ApplicationCommandOptionType, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder, GuildMember, MessageFlags } from "discord.js"; import Command, { CommandOptions } from "../structs/Command"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import * as bot from "../bot"; import { translateToAnsi } from "../../libs/chat-color"; import { GameProfile, stripUUID } from "../../libs/game-profiles"; @@ -24,7 +24,7 @@ export default class PlayerInfoCommand extends Command { }); } - async execute(interaction: CommandInteraction, options: CommandOptions, member: GuildMember, player: Player | null) { + async execute(interaction: CommandInteraction, options: CommandOptions, member: GuildMember, player: PlayerDocument | null) { const resolvable = options.getString('player', true); const or: any[] = [{ uuid: stripUUID(resolvable) }, { uuid: (await GameProfile.getProfileByUsername(resolvable))?.uuid }]; diff --git a/src/bot/commands/Redeem.ts b/src/bot/commands/Redeem.ts index 58ee168..85e4b72 100644 --- a/src/bot/commands/Redeem.ts +++ b/src/bot/commands/Redeem.ts @@ -1,8 +1,8 @@ import { ApplicationCommandOptionType, CommandInteraction, EmbedBuilder, GuildMember, MessageFlags } from "discord.js"; import Command, { CommandOptions } from "../structs/Command"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; import { colors, images } from "../bot"; -import giftCodes from "../../database/schemas/gift-codes"; +import giftCodes from "../../database/schemas/GiftCode"; import { formatTimestamp, sendGiftCodeRedeemMessage } from "../../libs/discord-notifier"; import { capitalCase } from "change-case"; @@ -23,7 +23,7 @@ export default class RedeemCommand extends Command { }); } - async execute(interaction: CommandInteraction, options: CommandOptions, member: GuildMember, player: Player) { + async execute(interaction: CommandInteraction, options: CommandOptions, member: GuildMember, player: PlayerDocument) { const code = await giftCodes.findOne({ code: options.getString('code', true) }); if(!code || !code.isValid()) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Code not found!')], flags: [MessageFlags.Ephemeral] }); if(code.uses.includes(player.uuid)) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ You already redeemed this code!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/commands/Roles.ts b/src/bot/commands/Roles.ts index 20029ea..77dc728 100644 --- a/src/bot/commands/Roles.ts +++ b/src/bot/commands/Roles.ts @@ -2,9 +2,9 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, Embed import Command, { CommandOptions } from "../structs/Command"; import { config } from "../../libs/config"; import { colors, images } from "../bot"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; import { Permission } from "../../types/Permission"; -import { getCachedRoles } from "../../database/schemas/roles"; +import { getCachedRoles } from "../../database/schemas/Role"; import { capitalCase } from "change-case"; export default class RolesCommand extends Command { @@ -16,7 +16,7 @@ export default class RolesCommand extends Command { }); } - async execute(interaction: CommandInteraction, options: CommandOptions, member: GuildMember, player: Player) { + async execute(interaction: CommandInteraction, options: CommandOptions, member: GuildMember, player: PlayerDocument) { await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); const roles = getCachedRoles(); diff --git a/src/bot/commands/Unlink.ts b/src/bot/commands/Unlink.ts index de93c2e..e0f19b2 100644 --- a/src/bot/commands/Unlink.ts +++ b/src/bot/commands/Unlink.ts @@ -1,6 +1,6 @@ import { CommandInteraction, EmbedBuilder, GuildMember, MessageFlags, User } from "discord.js"; import Command, { CommandOptions } from "../structs/Command"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { onDiscordUnlink } from "../../libs/events"; @@ -14,7 +14,7 @@ export default class UnlinkCommand extends Command { }); } - async execute(interaction: CommandInteraction, options: CommandOptions, member: GuildMember, player: Player) { + async execute(interaction: CommandInteraction, options: CommandOptions, member: GuildMember, player: PlayerDocument) { await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); await onDiscordUnlink(await player.getGameProfile(), player.connections.discord.id!); diff --git a/src/bot/events/EntitlementCreate.ts b/src/bot/events/EntitlementCreate.ts index 8b0121a..8b09936 100644 --- a/src/bot/events/EntitlementCreate.ts +++ b/src/bot/events/EntitlementCreate.ts @@ -1,10 +1,10 @@ import { Entitlement } from "discord.js"; import Event from "../structs/Event"; import { fetchSku } from "../bot"; -import players from "../../database/schemas/players"; +import players from "../../database/schemas/Player"; import { sendEntitlementMessage } from "../../libs/discord-notifier"; import { config } from "../../libs/config"; -import { getCachedRoles } from "../../database/schemas/roles"; +import { getCachedRoles } from "../../database/schemas/Role"; const roleReason = (entitlement: string) => `Discord entitlement: ${entitlement}`; diff --git a/src/bot/events/EntitlementDelete.ts b/src/bot/events/EntitlementDelete.ts index 277c15b..0f3de70 100644 --- a/src/bot/events/EntitlementDelete.ts +++ b/src/bot/events/EntitlementDelete.ts @@ -1,6 +1,6 @@ import { Entitlement } from "discord.js"; import Event from "../structs/Event"; -import players from "../../database/schemas/players"; +import players from "../../database/schemas/Player"; import { sendEntitlementMessage } from "../../libs/discord-notifier"; import entitlements from "../../database/schemas/entitlement"; import { config } from "../../libs/config"; diff --git a/src/bot/events/EntitlementUpdate.ts b/src/bot/events/EntitlementUpdate.ts index 4719154..869cee7 100644 --- a/src/bot/events/EntitlementUpdate.ts +++ b/src/bot/events/EntitlementUpdate.ts @@ -2,7 +2,7 @@ import { Entitlement } from "discord.js"; import Event from "../structs/Event"; import entitlement from "../../database/schemas/entitlement"; import { sendEntitlementMessage } from "../../libs/discord-notifier"; -import players from "../../database/schemas/players"; +import players from "../../database/schemas/Player"; import { config } from "../../libs/config"; import { fetchSku } from "../bot"; diff --git a/src/bot/events/GuildMemberAdd.ts b/src/bot/events/GuildMemberAdd.ts index b6ec619..15f8cda 100644 --- a/src/bot/events/GuildMemberAdd.ts +++ b/src/bot/events/GuildMemberAdd.ts @@ -1,6 +1,6 @@ import { GuildMember } from "discord.js"; import Event from "../structs/Event"; -import players from "../../database/schemas/players"; +import players from "../../database/schemas/Player"; import { config } from "../../libs/config"; export default class GuildMemberAddEvent extends Event { diff --git a/src/bot/events/GuildMemberUpdate.ts b/src/bot/events/GuildMemberUpdate.ts index 6f2f3e6..58fa0a2 100644 --- a/src/bot/events/GuildMemberUpdate.ts +++ b/src/bot/events/GuildMemberUpdate.ts @@ -1,6 +1,6 @@ import { GuildMember } from "discord.js"; import Event from "../structs/Event"; -import players from "../../database/schemas/players"; +import players from "../../database/schemas/Player"; import { config } from "../../libs/config"; export default class InteractionCreateEvent extends Event { diff --git a/src/bot/events/InteractionCreate.ts b/src/bot/events/InteractionCreate.ts index a11439c..909fc0e 100644 --- a/src/bot/events/InteractionCreate.ts +++ b/src/bot/events/InteractionCreate.ts @@ -2,7 +2,7 @@ import { CommandInteractionOptionResolver, EmbedBuilder, GuildMember, Interactio import Event from "../structs/Event"; import * as bot from "../bot"; import { captureException } from "@sentry/bun"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { config } from "../../libs/config"; import Interaction from "../structs/Interaction"; @@ -92,7 +92,7 @@ function getErrorReplyOptions(error: string): InteractionReplyOptions { return { embeds: [new EmbedBuilder().setColor(bot.colors.error).setDescription(error)], flags: [MessageFlags.Ephemeral] }; } -function getInteractionError(interaction: Interaction, player: Player | null): string | null { +function getInteractionError(interaction: Interaction, player: PlayerDocument | null): string | null { if(interaction.requireDiscordLink) { if(!config.discordBot.notifications.accountConnections.enabled) return '❌ Account linking is deactivated!'; if(!player) return '❌ You need to link your Minecraft account with `/link`!'; diff --git a/src/bot/events/Ready.ts b/src/bot/events/Ready.ts index 768669d..1a5bda9 100644 --- a/src/bot/events/Ready.ts +++ b/src/bot/events/Ready.ts @@ -1,7 +1,7 @@ import { ActivityType, REST, Routes } from "discord.js"; import Logger from "../../libs/Logger"; import Event from "../structs/Event"; -import players from "../../database/schemas/players"; +import players from "../../database/schemas/Player"; import * as bot from "../bot"; import { captureException } from "@sentry/bun"; diff --git a/src/bot/menus/AddRole.ts b/src/bot/menus/AddRole.ts index 6b3e2ff..5b87825 100644 --- a/src/bot/menus/AddRole.ts +++ b/src/bot/menus/AddRole.ts @@ -1,6 +1,6 @@ import { StringSelectMenuInteraction, Message, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import SelectMenu from "../structs/SelectMenu"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; import { Permission } from "../../types/Permission"; @@ -13,7 +13,7 @@ export default class AddRoleMenu extends SelectMenu { }); } - async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: Player) { + async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/menus/BanHistory.ts b/src/bot/menus/BanHistory.ts index bec84ba..1206dc5 100644 --- a/src/bot/menus/BanHistory.ts +++ b/src/bot/menus/BanHistory.ts @@ -1,6 +1,6 @@ import { StringSelectMenuInteraction, Message, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import SelectMenu from "../structs/SelectMenu"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { formatTimestamp } from "../../libs/discord-notifier"; import { Permission } from "../../types/Permission"; @@ -14,7 +14,7 @@ export default class BanHistoryMenu extends SelectMenu { }); } - async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: Player) { + async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: PlayerDocument) { if(values.length == 0) return interaction.deferUpdate(); const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); diff --git a/src/bot/menus/CreateGiftCode.ts b/src/bot/menus/CreateGiftCode.ts index d41d867..6d82803 100644 --- a/src/bot/menus/CreateGiftCode.ts +++ b/src/bot/menus/CreateGiftCode.ts @@ -1,6 +1,6 @@ import { StringSelectMenuInteraction, Message, GuildMember, ModalBuilder, ActionRowBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; import SelectMenu from "../structs/SelectMenu"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; import { Permission } from "../../types/Permission"; export default class CreateGiftCodeMenu extends SelectMenu { @@ -11,7 +11,7 @@ export default class CreateGiftCodeMenu extends SelectMenu { }); } - async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: Player) { + async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: PlayerDocument) { if(values.length == 0) return interaction.deferUpdate(); const modal = new ModalBuilder() diff --git a/src/bot/menus/DeleteApiKey.ts b/src/bot/menus/DeleteApiKey.ts index 2ecd0b7..94924c0 100644 --- a/src/bot/menus/DeleteApiKey.ts +++ b/src/bot/menus/DeleteApiKey.ts @@ -1,6 +1,6 @@ import { StringSelectMenuInteraction, Message, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import SelectMenu from "../structs/SelectMenu"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; import { Permission } from "../../types/Permission"; @@ -13,7 +13,7 @@ export default class DeleteApiKeyMenu extends SelectMenu { }); } - async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: Player) { + async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: PlayerDocument) { if(values.length == 0) return interaction.deferUpdate(); const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); diff --git a/src/bot/menus/DeleteGiftCode.ts b/src/bot/menus/DeleteGiftCode.ts index e2430c8..1bf1796 100644 --- a/src/bot/menus/DeleteGiftCode.ts +++ b/src/bot/menus/DeleteGiftCode.ts @@ -1,10 +1,10 @@ import { StringSelectMenuInteraction, Message, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import SelectMenu from "../structs/SelectMenu"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; import { Permission } from "../../types/Permission"; -import codeSchema from "../../database/schemas/gift-codes"; +import { GiftCode } from "../../database/schemas/GiftCode"; export default class DeleteGiftCodeMenu extends SelectMenu { constructor() { @@ -14,10 +14,10 @@ export default class DeleteGiftCodeMenu extends SelectMenu { }); } - async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: Player) { + async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: PlayerDocument) { if(values.length == 0) return interaction.deferUpdate(); - const code = await codeSchema.findOne({ code: values[0] }); + const code = await GiftCode.findOne({ code: values[0] }); if(!code) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Code not found!')], flags: [MessageFlags.Ephemeral] }); await code.deleteOne(); diff --git a/src/bot/menus/DeleteNote.ts b/src/bot/menus/DeleteNote.ts index a9535e4..8928f55 100644 --- a/src/bot/menus/DeleteNote.ts +++ b/src/bot/menus/DeleteNote.ts @@ -1,6 +1,6 @@ import { StringSelectMenuInteraction, Message, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import SelectMenu from "../structs/SelectMenu"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; import { Permission } from "../../types/Permission"; @@ -13,7 +13,7 @@ export default class DeleteNoteMenu extends SelectMenu { }); } - async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: Player) { + async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/menus/DeleteReport.ts b/src/bot/menus/DeleteReport.ts index 1535513..1c471dd 100644 --- a/src/bot/menus/DeleteReport.ts +++ b/src/bot/menus/DeleteReport.ts @@ -1,6 +1,6 @@ import { StringSelectMenuInteraction, Message, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import SelectMenu from "../structs/SelectMenu"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; import { Permission } from "../../types/Permission"; @@ -13,7 +13,7 @@ export default class DeleteReportMenu extends SelectMenu { }); } - async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: Player) { + async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/menus/EditRole.ts b/src/bot/menus/EditRole.ts index 2ea7770..08b27f4 100644 --- a/src/bot/menus/EditRole.ts +++ b/src/bot/menus/EditRole.ts @@ -1,6 +1,6 @@ import { StringSelectMenuInteraction, Message, GuildMember, EmbedBuilder, MessageFlags, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; import SelectMenu from "../structs/SelectMenu"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { formatTimestamp } from "../../libs/discord-notifier"; import { Permission } from "../../types/Permission"; @@ -13,7 +13,7 @@ export default class EditRoleMenu extends SelectMenu { }); } - async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: Player) { + async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: PlayerDocument) { if(values.length == 0) return interaction.deferUpdate(); const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/menus/ManagePermissions.ts b/src/bot/menus/ManagePermissions.ts index eed0641..f25b19f 100644 --- a/src/bot/menus/ManagePermissions.ts +++ b/src/bot/menus/ManagePermissions.ts @@ -1,6 +1,6 @@ import { StringSelectMenuInteraction, Message, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import SelectMenu from "../structs/SelectMenu"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { Permission } from "../../types/Permission"; @@ -12,7 +12,7 @@ export default class ManagePermissionsMenu extends SelectMenu { }); } - async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: Player) { + async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: PlayerDocument) { return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Permission management is disabled!')], flags: [MessageFlags.Ephemeral] }); } } \ No newline at end of file diff --git a/src/bot/menus/ManageRole.ts b/src/bot/menus/ManageRole.ts index 5f5a68e..13d925b 100644 --- a/src/bot/menus/ManageRole.ts +++ b/src/bot/menus/ManageRole.ts @@ -1,9 +1,9 @@ import { StringSelectMenuInteraction, Message, GuildMember, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags, parseEmoji } from "discord.js"; import SelectMenu from "../structs/SelectMenu"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; import { colors, images } from "../bot"; import { Permission, permissions as allPermissions } from "../../types/Permission"; -import { getCachedRoles } from "../../database/schemas/roles"; +import { getCachedRoles } from "../../database/schemas/Role"; import { capitalCase, pascalCase } from "change-case"; import { config } from "../../libs/config"; @@ -15,7 +15,7 @@ export default class ManageRoleMenu extends SelectMenu { }); } - async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: Player) { + async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: PlayerDocument) { if(values.length == 0) return interaction.deferUpdate(); const role = getCachedRoles().find((role) => role.name == values[0]); diff --git a/src/bot/menus/RegenerateApiKey.ts b/src/bot/menus/RegenerateApiKey.ts index 026f6b6..793ca38 100644 --- a/src/bot/menus/RegenerateApiKey.ts +++ b/src/bot/menus/RegenerateApiKey.ts @@ -1,6 +1,6 @@ import { StringSelectMenuInteraction, Message, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import SelectMenu from "../structs/SelectMenu"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; import { Permission } from "../../types/Permission"; @@ -14,7 +14,7 @@ export default class RegenerateApiKeyMenu extends SelectMenu { }); } - async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: Player) { + async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: PlayerDocument) { if(values.length == 0) return interaction.deferUpdate(); const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); diff --git a/src/bot/menus/RemoveRole.ts b/src/bot/menus/RemoveRole.ts index 68afe0d..6594ac3 100644 --- a/src/bot/menus/RemoveRole.ts +++ b/src/bot/menus/RemoveRole.ts @@ -1,6 +1,6 @@ import { StringSelectMenuInteraction, Message, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import SelectMenu from "../structs/SelectMenu"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; import { Permission } from "../../types/Permission"; @@ -13,7 +13,7 @@ export default class RemoveRoleMenu extends SelectMenu { }); } - async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: Player) { + async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/menus/SetIconType.ts b/src/bot/menus/SetIconType.ts index 708ba6f..26d4bc9 100644 --- a/src/bot/menus/SetIconType.ts +++ b/src/bot/menus/SetIconType.ts @@ -1,6 +1,6 @@ import { StringSelectMenuInteraction, Message, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import SelectMenu from "../structs/SelectMenu"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; import { sendIconTypeChangeEmail } from "../../libs/mailer"; @@ -15,7 +15,7 @@ export default class SetIconTypeMenu extends SelectMenu { }); } - async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: Player) { + async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); if(target.isBanned()) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ This player is already banned!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/menus/SetPosition.ts b/src/bot/menus/SetPosition.ts index 03856e7..4bc2b4a 100644 --- a/src/bot/menus/SetPosition.ts +++ b/src/bot/menus/SetPosition.ts @@ -1,6 +1,6 @@ import { StringSelectMenuInteraction, Message, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import SelectMenu from "../structs/SelectMenu"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; import { sendPositionChangeEmail } from "../../libs/mailer"; @@ -15,7 +15,7 @@ export default class SetPositionMenu extends SelectMenu { }); } - async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: Player) { + async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); if(target.isBanned()) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ This player is already banned!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/menus/SetSku.ts b/src/bot/menus/SetSku.ts index 90dca3e..f2511ff 100644 --- a/src/bot/menus/SetSku.ts +++ b/src/bot/menus/SetSku.ts @@ -1,10 +1,10 @@ import { StringSelectMenuInteraction, Message, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import SelectMenu from "../structs/SelectMenu"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { Permission } from "../../types/Permission"; -import roles, { updateRoleCache } from "../../database/schemas/roles"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; +import { Role, updateRoleCache } from "../../database/schemas/Role"; export default class SetSkuMenu extends SelectMenu { constructor() { @@ -14,10 +14,10 @@ export default class SetSkuMenu extends SelectMenu { }); } - async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: Player) { + async selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: PlayerDocument) { if(values.length == 0) return interaction.deferUpdate(); - const role = await roles.findOne({ name: interaction.customId.split('_')[1] }); + const role = await Role.findOne({ name: interaction.customId.split('_')[1] }); if(!role) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Role not found!')], flags: [MessageFlags.Ephemeral] }); const sku = values[0] || null; diff --git a/src/bot/modals/Ban.ts b/src/bot/modals/Ban.ts index df9dd46..c8f27ff 100644 --- a/src/bot/modals/Ban.ts +++ b/src/bot/modals/Ban.ts @@ -1,6 +1,6 @@ import { ModalSubmitInteraction, Message, ModalSubmitFields, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import Modal from "../structs/Modal"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; import { sendBanEmail } from "../../libs/mailer"; @@ -16,7 +16,7 @@ export default class BanModal extends Modal { }); } - async submit(interaction: ModalSubmitInteraction, message: Message, fields: ModalSubmitFields, member: GuildMember, player: Player) { + async submit(interaction: ModalSubmitInteraction, message: Message, fields: ModalSubmitFields, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); if(target.isBanned()) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ This player is already banned!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/modals/CreateApiKey.ts b/src/bot/modals/CreateApiKey.ts index 7514015..32253c6 100644 --- a/src/bot/modals/CreateApiKey.ts +++ b/src/bot/modals/CreateApiKey.ts @@ -1,5 +1,5 @@ import { Message, GuildMember, EmbedBuilder, ModalSubmitInteraction, ModalSubmitFields, MessageFlags } from "discord.js"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import Modal from "../structs/Modal"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; @@ -14,7 +14,7 @@ export default class CreateApiKeyModal extends Modal { }); } - async submit(interaction: ModalSubmitInteraction, message: Message, fields: ModalSubmitFields, member: GuildMember, player: Player) { + async submit(interaction: ModalSubmitInteraction, message: Message, fields: ModalSubmitFields, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); const name = snakeCase(fields.getTextInputValue('name').trim()); diff --git a/src/bot/modals/CreateGiftCode.ts b/src/bot/modals/CreateGiftCode.ts index 034a01e..13dd9b6 100644 --- a/src/bot/modals/CreateGiftCode.ts +++ b/src/bot/modals/CreateGiftCode.ts @@ -1,10 +1,10 @@ import { ModalSubmitInteraction, Message, ModalSubmitFields, GuildMember, EmbedBuilder, MessageFlags } from "discord.js"; import Modal from "../structs/Modal"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import { Permission } from "../../types/Permission"; import ms, { StringValue } from "ms"; -import { createGiftCode } from "../../database/schemas/gift-codes"; +import { createGiftCode, GiftType } from "../../database/schemas/GiftCode"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; export default class CreateGiftCodeModal extends Modal { @@ -15,7 +15,7 @@ export default class CreateGiftCodeModal extends Modal { }); } - async submit(interaction: ModalSubmitInteraction, message: Message, fields: ModalSubmitFields, member: GuildMember, player: Player) { + async submit(interaction: ModalSubmitInteraction, message: Message, fields: ModalSubmitFields, member: GuildMember, player: PlayerDocument) { const name = fields.getTextInputValue('name'); const code = fields.getTextInputValue('code'); const role = interaction.customId.split('_')[1]; @@ -36,7 +36,7 @@ export default class CreateGiftCodeModal extends Modal { code: code?.trim() || undefined, maxUses, gift: { - type: 'role', + type: GiftType.Role, value: role, duration: giftExpiresAt }, diff --git a/src/bot/modals/CreateNote.ts b/src/bot/modals/CreateNote.ts index f943802..553ba08 100644 --- a/src/bot/modals/CreateNote.ts +++ b/src/bot/modals/CreateNote.ts @@ -1,5 +1,5 @@ import { Message, GuildMember, EmbedBuilder, ModalSubmitInteraction, ModalSubmitFields, MessageFlags } from "discord.js"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import Modal from "../structs/Modal"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; @@ -13,7 +13,7 @@ export default class CreateNoteModal extends Modal { }); } - async submit(interaction: ModalSubmitInteraction, message: Message, fields: ModalSubmitFields, member: GuildMember, player: Player) { + async submit(interaction: ModalSubmitInteraction, message: Message, fields: ModalSubmitFields, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); const note = fields.getTextInputValue('note'); diff --git a/src/bot/modals/CreateRole.ts b/src/bot/modals/CreateRole.ts index 18df861..4679729 100644 --- a/src/bot/modals/CreateRole.ts +++ b/src/bot/modals/CreateRole.ts @@ -1,11 +1,11 @@ import { Message, GuildMember, EmbedBuilder, ModalSubmitInteraction, ModalSubmitFields, MessageFlags } from "discord.js"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import Modal from "../structs/Modal"; import { Permission } from "../../types/Permission"; -import roles, { getNextPosition, updateRoleCache } from "../../database/schemas/roles"; import { snakeCase } from "change-case"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; +import { getNextPosition, Role, updateRoleCache } from "../../database/schemas/Role"; export default class CreateRoleModal extends Modal { constructor() { @@ -15,12 +15,12 @@ export default class CreateRoleModal extends Modal { }); } - async submit(interaction: ModalSubmitInteraction, message: Message, fields: ModalSubmitFields, member: GuildMember, player: Player) { + async submit(interaction: ModalSubmitInteraction, message: Message, fields: ModalSubmitFields, member: GuildMember, player: PlayerDocument) { const name = snakeCase(fields.getTextInputValue('name').trim()); - if(await roles.exists({ name })) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription(`❌ The role \`${name}\` already exists!`)], flags: [MessageFlags.Ephemeral] }); + if(await Role.exists({ name })) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription(`❌ The role \`${name}\` already exists!`)], flags: [MessageFlags.Ephemeral] }); - await roles.insertMany([{ + await Role.insertMany([{ name, position: await getNextPosition(), hasIcon: false, diff --git a/src/bot/modals/EditRoleExpiration.ts b/src/bot/modals/EditRoleExpiration.ts index 231c79b..cb72bec 100644 --- a/src/bot/modals/EditRoleExpiration.ts +++ b/src/bot/modals/EditRoleExpiration.ts @@ -1,5 +1,5 @@ import { Message, GuildMember, EmbedBuilder, ModalSubmitInteraction, ModalSubmitFields, MessageFlags } from "discord.js"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import Modal from "../structs/Modal"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; @@ -14,7 +14,7 @@ export default class EditRoleExpirationModal extends Modal { }); } - async submit(interaction: ModalSubmitInteraction, message: Message, fields: ModalSubmitFields, member: GuildMember, player: Player) { + async submit(interaction: ModalSubmitInteraction, message: Message, fields: ModalSubmitFields, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/modals/EditRoleNote.ts b/src/bot/modals/EditRoleNote.ts index 7c628b0..7534ea2 100644 --- a/src/bot/modals/EditRoleNote.ts +++ b/src/bot/modals/EditRoleNote.ts @@ -1,5 +1,5 @@ import { Message, GuildMember, EmbedBuilder, ModalSubmitInteraction, ModalSubmitFields, MessageFlags } from "discord.js"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import Modal from "../structs/Modal"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; @@ -13,7 +13,7 @@ export default class EditRoleNoteModal extends Modal { }); } - async submit(interaction: ModalSubmitInteraction, message: Message, fields: ModalSubmitFields, member: GuildMember, player: Player) { + async submit(interaction: ModalSubmitInteraction, message: Message, fields: ModalSubmitFields, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/modals/OverwriteDiscord.ts b/src/bot/modals/OverwriteDiscord.ts index 3d19bcf..eabd3d7 100644 --- a/src/bot/modals/OverwriteDiscord.ts +++ b/src/bot/modals/OverwriteDiscord.ts @@ -1,5 +1,5 @@ import { Message, GuildMember, EmbedBuilder, ModalSubmitInteraction, ModalSubmitFields, MessageFlags } from "discord.js"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import Modal from "../structs/Modal"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; @@ -13,7 +13,7 @@ export default class OverwriteDiscordModal extends Modal { }); } - async submit(interaction: ModalSubmitInteraction, message: Message, fields: ModalSubmitFields, member: GuildMember, player: Player) { + async submit(interaction: ModalSubmitInteraction, message: Message, fields: ModalSubmitFields, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/modals/OverwriteEmail.ts b/src/bot/modals/OverwriteEmail.ts index c151af3..a0679c5 100644 --- a/src/bot/modals/OverwriteEmail.ts +++ b/src/bot/modals/OverwriteEmail.ts @@ -1,5 +1,5 @@ import { Message, GuildMember, EmbedBuilder, ModalSubmitInteraction, ModalSubmitFields, MessageFlags } from "discord.js"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import Modal from "../structs/Modal"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; @@ -13,7 +13,7 @@ export default class OverwriteEmailModal extends Modal { }); } - async submit(interaction: ModalSubmitInteraction, message: Message, fields: ModalSubmitFields, member: GuildMember, player: Player) { + async submit(interaction: ModalSubmitInteraction, message: Message, fields: ModalSubmitFields, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); diff --git a/src/bot/modals/RenameRole.ts b/src/bot/modals/RenameRole.ts index c335ffc..885e40d 100644 --- a/src/bot/modals/RenameRole.ts +++ b/src/bot/modals/RenameRole.ts @@ -1,12 +1,11 @@ import { Message, GuildMember, EmbedBuilder, ModalSubmitInteraction, ModalSubmitFields } from "discord.js"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import Modal from "../structs/Modal"; import { Permission } from "../../types/Permission"; -import { getCachedRoles } from "../../database/schemas/roles"; import { snakeCase } from "change-case"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; -import roleModel from "../../database/schemas/roles"; +import { getCachedRoles, Role } from "../../database/schemas/Role"; export default class RenameRoleModal extends Modal { constructor() { @@ -16,10 +15,10 @@ export default class RenameRoleModal extends Modal { }); } - async submit(interaction: ModalSubmitInteraction, message: Message, fields: ModalSubmitFields, member: GuildMember, player: Player) { + async submit(interaction: ModalSubmitInteraction, message: Message, fields: ModalSubmitFields, member: GuildMember, player: PlayerDocument) { const name = snakeCase(fields.getTextInputValue('name').trim()); const roles = getCachedRoles(); - const role = await roleModel.findOne({ name: interaction.customId.split('_')[1] }); + const role = await Role.findOne({ name: interaction.customId.split('_')[1] }); if(!role) return interaction.editReply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Role not found!')] }); if(roles.some((role) => role.name == name)) return interaction.editReply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription(`❌ The role \`${name}\` already exists!`)] }); diff --git a/src/bot/modals/SetTag.ts b/src/bot/modals/SetTag.ts index c0a4502..03a703d 100644 --- a/src/bot/modals/SetTag.ts +++ b/src/bot/modals/SetTag.ts @@ -1,5 +1,5 @@ import { Message, GuildMember, EmbedBuilder, ModalSubmitInteraction, ModalSubmitFields, MessageFlags } from "discord.js"; -import players, { Player } from "../../database/schemas/players"; +import players, { PlayerDocument } from "../../database/schemas/Player"; import { colors } from "../bot"; import Modal from "../structs/Modal"; import { ModLogType, sendModLogMessage } from "../../libs/discord-notifier"; @@ -15,7 +15,7 @@ export default class SetTagModal extends Modal { }); } - async submit(interaction: ModalSubmitInteraction, message: Message, fields: ModalSubmitFields, member: GuildMember, player: Player) { + async submit(interaction: ModalSubmitInteraction, message: Message, fields: ModalSubmitFields, member: GuildMember, player: PlayerDocument) { const target = await players.findOne({ uuid: interaction.customId.split('_')[1] }); if(!target) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription('❌ Player not found!')], flags: [MessageFlags.Ephemeral] }); const tag = fields.getTextInputValue('tag').trim(); diff --git a/src/bot/structs/Button.ts b/src/bot/structs/Button.ts index 8433a0b..b7a94a3 100644 --- a/src/bot/structs/Button.ts +++ b/src/bot/structs/Button.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, GuildMember, Message } from "discord.js"; import Interaction, { InteractionOptions } from "./Interaction"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; export default abstract class Button extends Interaction { public id: string; @@ -10,5 +10,5 @@ export default abstract class Button extends Interaction { this.id = id; } - public abstract trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: Player | null): any; + public abstract trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, player: PlayerDocument | null): any; } \ No newline at end of file diff --git a/src/bot/structs/Command.ts b/src/bot/structs/Command.ts index fdae727..a075151 100644 --- a/src/bot/structs/Command.ts +++ b/src/bot/structs/Command.ts @@ -1,5 +1,5 @@ import { AutocompleteInteraction, CacheType, CommandInteraction, CommandInteractionOptionResolver, GuildMember } from "discord.js"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; import Interaction, { InteractionOptions } from "./Interaction"; export type CommandOptions = Omit, "getMessage" | "getFocused">; @@ -17,6 +17,6 @@ export default abstract class Command extends Interaction { this.options = options; } - public abstract execute(interaction: CommandInteraction, options: CommandOptions, member: GuildMember, player: Player | null): any; - public autocomplete(interaction: AutocompleteInteraction, options: AutocompleteOptions, member: GuildMember, player: Player | null): any {}; + public abstract execute(interaction: CommandInteraction, options: CommandOptions, member: GuildMember, player: PlayerDocument | null): any; + public autocomplete(interaction: AutocompleteInteraction, options: AutocompleteOptions, member: GuildMember, player: PlayerDocument | null): any {}; } \ No newline at end of file diff --git a/src/bot/structs/Modal.ts b/src/bot/structs/Modal.ts index 05b7df8..7008039 100644 --- a/src/bot/structs/Modal.ts +++ b/src/bot/structs/Modal.ts @@ -1,6 +1,6 @@ import { GuildMember, Message, ModalSubmitFields, ModalSubmitInteraction } from "discord.js"; import Interaction, { InteractionOptions } from "./Interaction"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; export default abstract class Modale extends Interaction { public id: string; @@ -10,5 +10,5 @@ export default abstract class Modale extends Interaction { this.id = id; } - public abstract submit(interaction: ModalSubmitInteraction, message: Message, fields: ModalSubmitFields, member: GuildMember, player: Player | null): any; + public abstract submit(interaction: ModalSubmitInteraction, message: Message, fields: ModalSubmitFields, member: GuildMember, player: PlayerDocument | null): any; } \ No newline at end of file diff --git a/src/bot/structs/SelectMenu.ts b/src/bot/structs/SelectMenu.ts index 34e42c4..d94670c 100644 --- a/src/bot/structs/SelectMenu.ts +++ b/src/bot/structs/SelectMenu.ts @@ -1,6 +1,6 @@ import { GuildMember, Message, StringSelectMenuInteraction } from "discord.js"; import Interaction, { InteractionOptions } from "./Interaction"; -import { Player } from "../../database/schemas/players"; +import { PlayerDocument } from "../../database/schemas/Player"; export default abstract class SelectMenu extends Interaction { public id: string; @@ -10,5 +10,5 @@ export default abstract class SelectMenu extends Interaction { this.id = id; } - public abstract selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: Player | null): any; + public abstract selection(interaction: StringSelectMenuInteraction, message: Message, values: string[], member: GuildMember, player: PlayerDocument | null): any; } \ No newline at end of file diff --git a/src/database/mongo.ts b/src/database/mongo.ts index 140d03b..7622c0a 100644 --- a/src/database/mongo.ts +++ b/src/database/mongo.ts @@ -1,6 +1,6 @@ import mongoose from "mongoose"; import Logger from "../libs/Logger"; -import { destroy, spawn } from "../bot/bot"; +import { destroy, registerFeatures, spawn } from "../bot/bot"; import { config } from "../libs/config"; let registered = false; @@ -13,6 +13,7 @@ export async function connect(connectionString: string) { function registerEventHandler(connectionString: string) { if(registered) return; + if(config.discordBot.enabled) registerFeatures(); mongoose.connection.on('connected', () => { Logger.info('Connected to database!'); diff --git a/src/database/schemas/Application.ts b/src/database/schemas/Application.ts new file mode 100644 index 0000000..35e3d9a --- /dev/null +++ b/src/database/schemas/Application.ts @@ -0,0 +1,127 @@ +import { HydratedDocument, model, Schema } from "mongoose"; + +export enum ApplicationState { + Pending = 'pending', + Approved = 'approved', + Rejected = 'rejected', + Withdrawn = 'withdrawn' +} + +export enum ApplicationType { + Moderation = 'moderation', + Translation = 'translation', + Development = 'development', + Partnership = 'partnership' +} + +interface IApplication { + /** + * Unique identifier for the application + */ + id: string; + /** + * The UUID of the applicant + */ + applicant: string; + /** + * The application type + */ + type: ApplicationType; + /** + * The application status + */ + status: ApplicationState; + /** + * The answers provided by the applicant + */ + answers: [{ + /** + * The question being answered + */ + question: string; + /** + * The answer provided by the applicant + */ + answer: string; + }]; + /** + * The review details for the application + */ + review: { + /** + * The staff member who reviewed the application + */ + reviewer: string | null; + /** + * The timestamp of the review + */ + timestamp: Date | null; + }; + /** + * The timestamp when the application was submitted + */ + submitted_at: Date; + + /** + * Adds a review to the application + * @param reviewer The uuid of the reviewer + * @param status The new status of the application + */ + updateStatus(reviewer: string, status: ApplicationState): void; +} + +const ApplicationSchema = new Schema({ + id: { + type: String, + required: true + }, + applicant: { + type: String, + required: true + }, + type: { + type: String, + enum: ApplicationType, + required: true + }, + status: { + type: String, + enum: ApplicationState, + required: true + }, + answers: [{ + question: { + type: String, + required: true + }, + answer: { + type: String, + required: true + } + }], + review: { + reviewer: { + type: String, + default: null + }, + timestamp: { + type: Date, + default: null + } + }, + submitted_at: { + type: Date, + required: true + } +}, { + methods: { + updateStatus(reviewer: string, status: ApplicationState): void { + this.status = status; + this.review.reviewer = status !== ApplicationState.Pending ? reviewer : null; + this.review.timestamp = status !== ApplicationState.Pending ? new Date() : null; + } + } +}); + +export const Application = model('Application', ApplicationSchema); +export type ApplicationDocument = HydratedDocument; \ No newline at end of file diff --git a/src/database/schemas/GiftCode.ts b/src/database/schemas/GiftCode.ts new file mode 100644 index 0000000..61c8de1 --- /dev/null +++ b/src/database/schemas/GiftCode.ts @@ -0,0 +1,179 @@ +import { HydratedDocument, Schema, model } from "mongoose"; +import { GameProfile, stripUUID } from "../../libs/game-profiles"; +import { generateSecureCode } from "../../libs/crypto"; + +export enum GiftType { + Role = 'role' +} + +interface IGiftCode { + /** + * Unique identifier for the gift code + */ + id: string; + /** + * Name of the gift code + */ + name: string; + /** + * The actual code that users will enter to redeem the gift + */ + code: string; + /** + * List of UUIDs that have used this gift code + */ + uses: string[]; + /** + * Maximum number of times this code can be used + */ + max_uses: number; + /** + * The gift that this code provides + */ + gift: { + /** + * Type of gift being provided + */ + type: GiftType, + /** + * Value of the gift, e.g., role id + */ + value: string, + /** + * Duration for which the gift is valid, in milliseconds + * If null, the gift is permanent + */ + duration: number | null + }; + /** + * UUID of the user who created this gift code + */ + created_by: string; + /** + * Timestamp when the gift code was created + */ + created_at: Date; + /** + * Optional expiration date for the gift code + * If null, the code does not expire + */ + expires_at: Date | null; + + /** + * Get the GameProfile of the creator of this gift code + * @return {Promise} The GameProfile of the creator + */ + getCreatorProfile(): Promise; + + /** + * Check if the gift code is still valid + * @return {boolean} True if the code can still be used, false otherwise + */ + isValid(): boolean; + + /** + * Calculate how many uses are left for this gift code + * @return {number} The number of uses left + */ + usesLeft(): number; +} + +const GiftCodeSchema = new Schema({ + id: { + type: String, + required: true, + unique: true, + default: generateSecureCode + }, + name: { + type: String, + required: true + }, + code: { + type: String, + required: true + }, + uses: { + type: [String], + required: true, + default: [] + }, + max_uses: { + type: Number, + required: true + }, + gift: { + type: { + type: String, + required: true, + enum: Object.values(GiftType), + default: GiftType.Role + }, + value: { + type: String, + required: true + }, + duration: { + type: Number, + default: null + } + }, + created_by: { + type: String, + required: true + }, + created_at: { + type: Date, + required: true, + default: Date.now + }, + expires_at: { + type: Date, + required: true, + default: null + } +}, { + methods: { + getCreatorProfile(): Promise { + return GameProfile.getProfileByUUID(this.created_by); + }, + + isValid(): boolean { + return this.uses.length < this.max_uses && (!this.expires_at || this.expires_at > new Date()); + }, + + usesLeft(): number { + return this.max_uses - this.uses.length; + } + } +}); + +export async function createGiftCode({ + name, + code = generateSecureCode(12), + maxUses, + gift, + expiresAt, + createdBy +} : { + name: string, + code?: string, + maxUses: number, + gift: IGiftCode['gift'], + expiresAt?: Date | null, + createdBy: string +}): Promise { + return await GiftCode.insertOne({ + name, + code, + uses: [], + max_uses: maxUses, + gift: gift, + created_by: stripUUID(createdBy), + created_at: new Date(), + expires_at: expiresAt || null + }); +} + +export const GiftCode = model('GiftCode', GiftCodeSchema); +export type GiftCodeDocument = HydratedDocument; \ No newline at end of file diff --git a/src/database/schemas/Metric.ts b/src/database/schemas/Metric.ts new file mode 100644 index 0000000..1773fe2 --- /dev/null +++ b/src/database/schemas/Metric.ts @@ -0,0 +1,100 @@ +import { HydratedDocument, Schema, model } from "mongoose"; +import { GlobalPosition } from "../../types/GlobalPosition"; +import { GlobalIcon } from "../../types/GlobalIcon"; + +const requiredNumber = { + type: Number, + required: true +} + +interface IMetrics { + /** + * Total number of registered players + */ + players: number, + /** + * Total number of tags + */ + tags: number, + /** + * Total number of staff members with admin permissions + */ + admins: number, + /** + * Total number of banned players + */ + bans: number, + /** + * Total number of downloads from modding platforms + */ + downloads: { + /** + * Total downloads from FlintMC + */ + flintmc: number, + /** + * Total downloads from Modrinth + */ + modrinth: number + }, + /** + * Total number of ratings on modding platforms + */ + ratings: { + /** + * Total ratings on FlintMC + */ + flintmc: number + }, + /** + * Total number of requests made to the API in the last 24 hours + */ + daily_requests: number, + /** + * Most used tag positions + * Key: Position, Value: Number of tags in that position + * @see GlobalPosition + */ + positions: Record, + /** + * Most used icons + * Key: Icon, Value: Number of tags using that icon + * @see GlobalIcon + */ + icons: Record, + /** + * Timestamp when the metrics were created + */ + created_at: Date +} + +const MetricSchema = new Schema({ + players: requiredNumber, + tags: requiredNumber, + admins: requiredNumber, + bans: requiredNumber, + downloads: { + flintmc: requiredNumber, + modrinth: requiredNumber + }, + ratings: { + flintmc: requiredNumber + }, + daily_requests: requiredNumber, + positions: { + type: Object, + required: true + }, + icons: { + type: Object, + required: true + }, + created_at: { + type: Date, + required: true, + default: Date.now + } +}); + +export const Metric = model('Metric', MetricSchema); +export type MetricDocument = HydratedDocument; \ No newline at end of file diff --git a/src/database/schemas/Player.ts b/src/database/schemas/Player.ts new file mode 100644 index 0000000..0c62d66 --- /dev/null +++ b/src/database/schemas/Player.ts @@ -0,0 +1,1305 @@ +import { HydratedDocument, Schema, model } from "mongoose"; +import { Permission } from "../../types/Permission"; +import { getCachedRoles, RoleDocument } from "./Role"; +import { GlobalIcon, icons } from "../../types/GlobalIcon"; +import { GameProfile, stripUUID } from "../../libs/game-profiles"; +import { isConnected } from "../mongo"; +import { generateSecureCode } from "../../libs/crypto"; +import { Report, ReportDocument } from "./Report"; +import { GlobalPosition, positions } from "../../types/GlobalPosition"; +import { config } from "../../libs/config"; +import { WatchlistAlert, WatchlistAlertDocument } from "./WatchlistAlert"; +import { stripColors } from "../../libs/chat-color"; +import Logger from "../../libs/Logger"; + +const { watchlist } = config.validation.tag; + +export interface PlayerRole { + /** + * The role document containing the role information + * @see RoleDocument + */ + role: RoleDocument; + /** + * The reason for assigning the role to the player + * Can be null if no reason was provided + */ + reason: string | null; + /** + * Whether the role is set to auto-remove after expiration + */ + autoRemove: boolean; + /** + * Whether the role icon should be hidden + */ + visible: boolean; + /** + * The date when the role was added to the player + */ + addedAt: Date; + /** + * The expiration date of the role, if applicable + * If null, the role does not expire + */ + expiresAt: Date | null; +} + +export interface HistoryEntry { + /** + * The tag content or icon hash that was set + */ + content: string; + /** + * The timestamp when the tag or icon was set + */ + timestamp: Date; +} + +export interface ApiKey { + /** + * Unique identifier for the API key + */ + id: string; + /** + * Name of the API key, used for identification + */ + name: string; + /** + * The actual key string used for authentication + */ + key: string; + /** + * The date when the API key was created + */ + created_at: Date; + /** + * The date when the API key was last used + * Can be null if it has never been used + */ + last_used: Date | null; +} + +export interface PlayerNote { + /** + * Unique identifier for the note + */ + id: string; + /** + * The content of the note + */ + content: string; + /** + * The UUID of the player who created the note + */ + author: string; + /** + * The date when the note was created + */ + created_at: Date; +} + +export interface DataClear { + /** + * The current value of the cleared data, such as tag or icon hash + */ + current_value: string; + /** + * The reason for clearing the data + */ + reason: string; + /** + * The type of data that was cleared, either 'tag' or 'icon' + */ + type: 'tag' | 'icon'; + /** + * The timestamp when the data was cleared + */ + staff: string; + /** + * The date when the data was cleared + */ + cleared_at: Date; +} + +export enum AccountLockType { + ChangeTag = 'change_tag', + ChangePosition = 'change_position', + ChangeIcon = 'change_icon', + UploadCustomIcon = 'upload_custom_icon', + ReportPlayers = 'report_players', + SendApplication = 'send_application' +} + +export interface AccountLock { + /** + * Unique identifier for the account lock + */ + id: string; + /** + * The type of lock applied to the account + * This determines what actions are restricted + * @see AccountLockType + */ + type: AccountLockType; + /** + * The reason for applying the lock + */ + reason: string; + /** + * The UUID of the staff member who applied the lock + */ + staff: string; + /** + * The date when the lock was applied + */ + locked_at: Date; + /** + * The date when the lock expires, if applicable + * If null, the lock does not expire + */ + expires_at: Date | null; +} + +export enum WatchlistReasonType { + MatchedWord = 'matched_word', + SuspiciousActivity = 'suspicious_activity', + PreviousBan = 'previous_ban' +} + +export interface WatchlistReason { + /** + * The type of reason for the watchlist entry + */ + type: WatchlistReasonType; + /** + * Additional details about the reason, if applicable + */ + details: string | null; +} + +export interface WatchlistPeriod { + /** + * Unique identifier for the watchlist period + */ + id: string; + /** + * The reason for the watchlist entry + */ + reason: WatchlistReason; + /** + * The UUID of the staff member who added the player to the watchlist + */ + staff: string; + /** + * The date when the player was added to the watchlist + */ + watched_at: Date; + /** + * The date when the watchlist entry expires, if applicable + * If null, the watchlist entry does not expire + */ + expires_at: Date | null; +} + +export interface PlayerBan { + /** + * Unique identifier for the ban + */ + id: string; + /** + * The reason for the ban + */ + reason: string; + /** + * The UUID of the staff member who issued the ban + */ + staff: string; + /** + * The date when the player was banned + */ + banned_at: Date; + /** + * The date when the ban expires, if applicable + * If null, the ban does not expire + */ + expires_at: Date | null; + /** + * Information about the appeal status of the ban + */ + appeal: { + /** + * Whether the ban is appealable + */ + appealable: boolean; + /** + * Whether the player has appealed the ban + */ + appealed: boolean; + /** + * The reason for the appeal, if applicable + * Can be null if no appeal was made + */ + reason: string | null; + /** + * The date when the appeal was made, if applicable + * Can be null if no appeal was made + */ + appealed_at: Date | null; + }; +} + +interface IPlayer { + /** + * The UUID of the player + */ + uuid: string; + email: { + address: string | null; + last_changed_at: Date | null; + verification_code: string | null; + verification_expires_at: Date | null; + verified: boolean; + }; + /** + * The preferred language of the player, defaults to 'en_us' + */ + preferred_language: string; + /** + * The tag of the player, can be null if not set + */ + tag: string | null; + /** + * The position of the tag + */ + position: GlobalPosition; + /** + * The icon of the player, containing the name and possibly a hash for custom icons + */ + icon: { + /** + * The name of the icon, can be a global icon or a custom one + */ + type: GlobalIcon; + /** + * The hash of the custom icon, if applicable + */ + hash: string | null; + }; + /** + * The history of the player's tag changes, containing content and timestamp + */ + tag_history: HistoryEntry[]; + /** + * The history of the player's icon changes, containing hash and timestamp + * This is used to track custom icon changes + */ + custom_icon_history: HistoryEntry[]; + /** + * The referral information of the player + */ + referrals: { + /** + * All referrals made by the player, containing UUID and timestamp + */ + total: { + uuid: string; + referred_at: number; + }[]; + /** + * The current month referrals count + */ + current_month: number; + }; + /** + * The roles assigned to the player, containing role information + */ + roles: { + id: string; + auto_remove: boolean; + reason: string | null; + visible: boolean; + added_at: Date; + expires_at: Date | null; + }[]; + /** + * The API keys associated with the player, containing key information + */ + api_keys: ApiKey[]; + /** + * The staff notes created on the player, containing note information + */ + notes: PlayerNote[]; + /** + * The data clears performed on the player, containing clear information + */ + clears: DataClear[]; + /** + * The locks applied to the player, containing lock information + */ + locks: AccountLock[]; + /** + * The watchlist periods of the player, containing watchlist information + */ + watchlist_periods: WatchlistPeriod[]; + /** + * The bans applied to the player, containing ban information + */ + bans: PlayerBan[]; + /** + * Connections to external services, such as Discord or Twitch + */ + connections: { + /** + * Discord connection information, containing ID and tokens + * Can be null if the player is not connected to Discord + */ + discord: { // TODO: Implement routes + /** + * The Discord user ID of the player + */ + id: string; + /** + * The Discord OAuth2 code for authentication + */ + access_token: string; + /** + * The expiration date of the Discord access token + */ + access_expiration: Date; + /** + * The refresh token for the Discord OAuth2 authentication + */ + refresh_token: string; + } | null; + // TODO: Add twitch connection + }; + + //* Miscellaneous + + /** + * Get the player's GameProfile + * @return {Promise} A promise that resolves to the GameProfile of the player + */ + getGameProfile(): Promise; + + /** + * Change the player's tag + * @param newTag The new tag to set, can be null to clear the tag + */ + changeTag(newTag: string | null): void; + + //* API Keys + + /** + * + * @param name The name of the API key to create + * @returns The created API key object + */ + createApiKey(name: string): ApiKey; + + /** + * + * @param id The ID of the API key to retrieve + * @returns The API key object if found, otherwise null + */ + getApiKey(id: string): ApiKey | null; + + /** + * + * @param id The ID of the API key to delete + * @return True if the API key was deleted, otherwise false + */ + deleteApiKey(id: string): boolean; + + //* Reports + + /** + * + * @param reporter The UUID of the player who is reporting + * @param reason The reason for the report + * @returns A ReportDocument object representing the created report + */ + createReport(reporter: string, reason: string): Promise; + + //* Referrals + + /** + * Add a referral to the player + * @param uuid The UUID of the player being referred + */ + addReferral(uuid: string): void; + + /** + * Get the referrer of the player + * @returns {Promise} A PlayerDocument representing the referrer, or null if not found + */ + getReferrer(): Promise; + + /** + * Check if the player has a referrer + * @returns {Promise} True if the player has a referrer, otherwise false + */ + hasReferrer(): Promise; + + //* Roles + + /** + * Get all roles assigned to the player + * @returns {PlayerRole[]} An array of PlayerRole objects representing all roles assigned to + */ + getAllRoles(): PlayerRole[]; + + /** + * Get all active roles assigned to the player + * @returns {PlayerRole[]} An array of PlayerRole objects representing active roles + */ + getActiveRoles(): PlayerRole[]; + + /** + * Get a specific role by its ID + * @param role The ID of the role to retrieve + * @param active Whether to return only active roles (default: true) + * @returns {PlayerRole | null} The PlayerRole object if found, otherwise null + */ + getRole(id: string, active?: boolean): PlayerRole | null; + + /** + * Add a role to the player + * `info.expiresAt` will override `info.duration` if both are provided. + * @param info The information about the role to add + * @param info.id The id of the role to add + * @param info.reason The reason for adding the role + * @param info.autoRemove Whether the role should be removed when a subscription expires + * @param info.visible Whether the role icon should be visible (default: true) + * @param info.expiresAt The expiration date of the role, if applicable (default: null) + * @param info.duration The duration in milliseconds for which the role is valid, if applicable (default: null) + * @return {{ success: boolean, expiresAt: Date | null }} An object indicating success and the expiration date of the role + */ + addRole(info: { id: string, reason: string, autoRemove: boolean, visible?: boolean, expiresAt?: Date | null, duration?: number | null }): { success: boolean, expiresAt: Date | null }; + + /** + * Remove a role from the player + * @param id The ID of the role to remove + * @returns {boolean} True if the role was removed, otherwise false + */ + removeRole(id: string): boolean; + + /** + * Check if the player has a specific permission + * @param permission The permission to check + * @returns {boolean} True if the player has the permission, otherwise false + */ + hasPermission(permission: Permission): boolean; + + //* Notes + + /** + * Create a note for the player + * @param content The content of the note + * @param author The UUID of the author of the note + */ + createNote({ content, author }: { content: string, author: string }): PlayerNote; + + /** + * Check if a note with the given ID exists + * @param id The ID of the note to check + * @returns {boolean} True if the note exists, otherwise false + */ + existsNote(id: string): boolean; + + /** + * Delete a note by its ID + * @param id The ID of the note to delete + * @returns {boolean} True if the note was deleted, otherwise false + */ + deleteNote(id: string): boolean; + + //* Punishments + + /** + * Clear the player's tag + * @param reason The reason for clearing the tag + * @param staff The UUID of the staff member performing the action + */ + clearTag(reason: string, staff: string): void; + + /** + * Clear the player's icon texture + * @param reason The reason for clearing the icon texture + * @param staff The UUID of the staff member performing the action + */ + clearIconTexture(reason: string, staff: string): void; + + //* Locks + + /** + * Check if the player has a specific lock + * @param type The type of the lock to check + * @returns {boolean} True if the player has the lock, otherwise false + */ + hasLock(type: AccountLockType): boolean; + + /** + * Create a lock for the player + * @param data The data for the lock + * @param data.type The type of the lock + * @param data.reason The reason for the lock + * @param data.staff The UUID of the staff member creating the lock + * @param data.expiresAt The expiration date of the lock, if applicable (default: null) + * @returns {AccountLock | null} The created AccountLock object, or null if not successful + */ + createLock(data: { type: AccountLockType, reason: string, staff: string, expiresAt?: Date | null }): AccountLock | null; + + //* Watchlist + + /** + * Check if the player is currently on the watchlist + * @return {boolean} True if the player is on the watchlist, otherwise false + */ + isWatched(): boolean; + + /** + * Get the current watchlist period of the player, if any + * @returns {WatchlistPeriod | null} The current WatchlistPeriod object, or null if not on the watchlist + */ + getWatchlistPeriod(): WatchlistPeriod | null; + + /** + * Start a watchlist period for the player + * @param data The data for the watchlist period + * @param data.reason The reason for adding the player to the watchlist + * @param data.staff The UUID of the staff member adding the player to the watchlist + * @param data.expiresAt The expiration date of the watchlist period, if applicable (default: null) + * @returns {WatchlistPeriod | null} The created WatchlistPeriod object, or null if not successful + */ + startWatching(data: { reason: WatchlistReason, staff: string, expiresAt?: Date | null }): WatchlistPeriod | null; + + /** + * Send a watchlist alert for the player + * @param initial Whether this is the initial alert for the player (default: false) + * @returns {Promise} A promise that resolves to the created WatchlistAlertDocument + */ + sendWatchlistAlert(initial?: boolean): Promise; + + /** + * Stop watching the player, removing them from the watchlist + * @returns {boolean} True if the player was removed from the watchlist, otherwise false + */ + stopWatching(): boolean; + + //* Bans + + /** + * Check if the player is currently banned + * @return {boolean} True if the player is banned, otherwise false + */ + isBanned(): boolean; + + /** + * Get the current ban of the player, if any + * @returns {PlayerBan | null} The current PlayerBan object, or null if not banned + */ + getBan(): PlayerBan | null; + + /** + * Ban the player with the given reason and staff information + * @param data The data for the ban + * @param data.reason The reason for the ban + * @param data.staff The UUID of the staff member issuing the ban + * @param data.appealable Whether the ban is appealable (default: true) + * @param data.expiresAt The expiration date of the ban, if applicable (default: null) + */ + banPlayer(data: { reason: string, staff: string, appealable?: boolean, expiresAt?: Date | null }): PlayerBan | null; + + /** + * Unban the player, removing the current ban + * @returns {boolean} True if the player was unbanned, otherwise false + */ + unban(): boolean; +} + +const PlayerSchema = new Schema({ + uuid: { + type: String, + required: true, + unique: true + }, + email: { + address: { + type: String, + default: null + }, + last_changed_at: { + type: Date, + default: null + }, + verification_code: { + type: String, + default: null + }, + verification_expires_at: { + type: Date, + default: null + }, + verified: { + type: Boolean, + required: true, + default: false + } + }, + tag: { + type: String, + default: null + }, + position: { + type: String, + required: true, + enum: positions, + default: GlobalPosition.Above + }, + icon: { + type: new Schema({ + type: { + type: String, + required: true, + enum: icons, + default: GlobalIcon.None + }, + hash: { + type: String, + default: null + } + }, { _id: false }), + required: true, + default: { + type: GlobalIcon.None, + hash: null + } + }, + preferred_language: { + type: String, + required: true, + default: 'en_us' + }, + tag_history: { + type: [{ + content: { + type: String, + required: true + }, + timestamp: { + type: Date, + required: true, + default: Date.now + } + }], + required: true, + default: [] + }, + custom_icon_history: { + type: [{ + content: { + type: String, + required: true + }, + timestamp: { + type: Date, + required: true, + default: Date.now + } + }], + required: true, + default: [] + }, + referrals: { + total: { + type: [{ + uuid: { + type: String, + required: true + }, + referred_at: { + type: Date, + required: true, + default: Date.now + } + }], + required: true, + default: [] + }, + current_month: { + type: Number, + required: true, + default: 0 + } + }, + roles: { + type: [{ + id: { + type: String, + required: true + }, + auto_remove: { + type: Boolean, + required: true, + default: false + }, + reason: { + type: String, + default: null + }, + visible: { + type: Boolean, + required: true, + default: true + }, + added_at: { + type: Date, + required: true, + default: Date.now + }, + expires_at: { + type: Date, + default: null + } + }], + required: true, + default: [] + }, + api_keys: { + type: [{ + id: { + type: String, + required: true, + default: generateSecureCode + }, + name: { + type: String, + required: true + }, + key: { + type: String, + required: true + }, + created_at: { + type: Date, + required: true, + default: Date.now + }, + last_used: { + type: Date, + default: null + } + }], + required: true, + default: [] + }, + notes: { + type: [{ + id: { + type: String, + required: true, + default: generateSecureCode + }, + content: { + type: String, + required: true + }, + }], + required: true, + default: [] + }, + clears: { + type: [{ + current_value: { + type: String, + required: true + }, + reason: { + type: String, + required: true + }, + type: { + type: String, + enum: ['tag', 'icon'], + required: true + }, + staff: { + type: String, + required: true + }, + cleared_at: { + type: Date, + required: true, + default: Date.now + } + }], + required: true, + default: [] + }, + locks: { + type: [{ + id: { + type: String, + required: true + }, + type: { + type: String, + enum: Object.values(AccountLockType), + required: true + }, + reason: { + type: String, + required: true + }, + staff: { + type: String, + required: true + }, + locked_at: { + type: Date, + required: true, + default: Date.now + }, + expires_at: { + type: Date, + default: null + } + }], + required: true, + default: [] + }, + watchlist_periods: { + type: [{ + id: { + type: String, + required: true + }, + reason: { + type: String, + required: true + }, + staff: { + type: String, + required: true + }, + watched_at: { + type: Date, + required: true, + default: Date.now + }, + expires_at: { + type: Date, + default: null + } + }], + required: true, + default: [] + }, + bans: { + type: [{ + id: { + type: String, + required: true, + default: generateSecureCode + }, + reason: { + type: String, + required: true + }, + staff: { + type: String, + required: true + }, + banned_at: { + type: Date, + required: true, + default: Date.now + }, + expires_at: { + type: Date, + default: null + }, + appeal: { + appealable: { + type: Boolean, + required: true, + default: true + }, + appealed: { + type: Boolean, + required: true, + default: false + }, + reason: { + type: String, + default: null + }, + appealed_at: { + type: Date, + default: null + } + } + }], + required: true, + default: [] + }, + connections: { + discord: { + type: { + id: { + type: String, + required: true, + default: generateSecureCode + }, + access_token: { + type: String, + required: true + }, + access_expiration: { + type: Date, + required: true + }, + refresh_token: { + type: String, + required: true + } + }, + default: null + } + } +}, { + methods: { + getGameProfile(): Promise { + return GameProfile.getProfileByUUID(this.uuid); + }, + + changeTag(newTag: string | null): void { + if(this.tag === newTag) return; + + this.tag = newTag; + if(!newTag) return; + + this.tag_history.push({ + content: newTag, + timestamp: new Date() + }); + const isWatched = this.isWatched(); + const watchlistedWord = watchlist.find((word) => stripColors(newTag).toLowerCase().includes(word)); + + if(isWatched || watchlistedWord) { + if(!isWatched) { + Logger.warn(`Now watching ${this.uuid} for matching "${watchlistedWord}" in "${newTag}".`); + this.startWatching({ reason: { type: WatchlistReasonType.MatchedWord, details: watchlistedWord! }, staff: '25944a62fdd646b7baec9a0d2aad77e1' }); + } + this.sendWatchlistAlert(isWatched); + } + }, + + createApiKey(name: string): ApiKey { + const key = { + id: generateSecureCode(), + name, + key: `sk_${generateSecureCode(32)}`, + created_at: new Date(), + last_used: null + } + this.api_keys.push(key); + return key; + }, + + getApiKey(id: string): ApiKey | null { + const key = this.api_keys.find((key) => key.id == id); + if(!key) return null; + return key; + }, + + deleteApiKey(id: string): boolean { + const index = this.api_keys.findIndex((key) => key.id == id); + if(index === -1) return false; + this.api_keys.splice(index, 1); + return true; + }, + + createReport(reporter: string, reason: string): Promise { + return Report.insertOne({ + reported_uuid: this.uuid, + reporter_uuid: reporter, + reason, + actions: [], + context: { + tag: this.tag!, + position: this.position, + icon: { + type: this.icon.type, + hash: this.icon.hash || null + } + }, + created_at: new Date(), + last_updated: new Date() + }); + }, + + getReferrer(): Promise { + return Player.findOne({ 'referrals.total.uuid': this.uuid }); + }, + + addReferral(uuid: string): void { + this.referrals.total.push({ uuid, referred_at: Date.now() }); + this.referrals.current_month++; + }, + + async hasReferrer(): Promise { + const exists = await Player.exists({ 'referrals.total.uuid': this.uuid }); + return !!exists; + }, + + getAllRoles(): PlayerRole[] { + const roles = getCachedRoles(); + return this.roles.filter(({ id }) => { + return roles.some((role) => role.id === id); + }).map((playerRole) => { + const role = roles.find((role) => role.id === playerRole.id)!; + + return { + role, + autoRemove: playerRole.auto_remove, + reason: playerRole.reason, + visible: playerRole.visible, + addedAt: playerRole.added_at, + expiresAt: playerRole.expires_at + } + }); + }, + + getActiveRoles(): PlayerRole[] { + return this.getAllRoles().filter((role) => role.expiresAt == null || role.expiresAt.getTime() > Date.now()); + }, + + getRole(id: string, active: boolean = true): PlayerRole | null { + const roles = active ? this.getActiveRoles() : this.getAllRoles(); + return roles.find((role) => role.role.id === id) || null; + }, + + addRole({ id, reason, autoRemove, visible = true, expiresAt, duration }: { id: string, reason: string, autoRemove: boolean, visible?: boolean, expiresAt?: Date | null, duration?: number | null }): { success: boolean, expiresAt: Date | null } { + const roles = getCachedRoles(); + if(!roles.some((role) => role.id === id)) return { success: false, expiresAt: null }; + + const playerRole = this.roles.find((role) => role.id === id); + if(playerRole) { + if(!playerRole.expires_at) return { success: false, expiresAt: null }; + if(playerRole.expires_at.getTime() > Date.now()) { + playerRole.reason += ` | ${reason}`; + playerRole.auto_remove = autoRemove; + playerRole.expires_at = expiresAt ? expiresAt : duration ? new Date(playerRole.expires_at.getTime() + duration) : null; + return { success: true, expiresAt: playerRole.expires_at }; + } else { + playerRole.reason = reason; + playerRole.auto_remove = autoRemove; + playerRole.added_at = new Date(); + playerRole.expires_at = expiresAt ? expiresAt : duration ? new Date(Date.now() + duration) : null; + return { success: true, expiresAt: playerRole.expires_at }; + } + } else { + const role = { + id, + reason, + auto_remove: autoRemove, + visible, + added_at: new Date(), + expires_at: expiresAt ? expiresAt : duration ? new Date(Date.now() + duration) : null + }; + this.roles.push(role); + return { success: true, expiresAt: role.expires_at }; + } + }, + + removeRole(id: string): boolean { + const role = this.roles.find((role) => role.id === id); + if(!role) return false; + role.expires_at = new Date(); + return true; + }, + + hasPermission(permission: Permission): boolean { + return this.getActiveRoles().some((role) => role.role.hasPermission(permission)); + }, + + createNote({ content, author }: { content: string, author: string }): PlayerNote { + const note = { + id: generateSecureCode(), + content, + author, + created_at: new Date() + }; + + this.notes.push(note); + return note; + }, + + existsNote(id: string): boolean { + return this.notes.some((note) => note.id == id); + }, + + deleteNote(id: string): boolean { + const index = this.notes.findIndex((note) => note.id == id); + if(index === -1) return false; + this.notes.splice(index, 1); + return true; + }, + + clearTag(reason: string, staff: string): void { + this.clears.push({ + current_value: this.tag!, + reason, + type: 'tag', + staff, + cleared_at: new Date() + }); + this.tag = null; + }, + + clearIconTexture(reason: string, staff: string): void { + this.clears.push({ + current_value: this.icon.hash!, + reason, + type: 'icon', + staff, + cleared_at: new Date() + }); + this.icon.type = GlobalIcon.None; + this.icon.hash = null; + }, + + hasLock(type: AccountLockType): boolean { + const lock = this.locks.find((lock) => lock.type === type); + return !!lock && (!lock.expires_at || lock.expires_at.getTime() > Date.now()); + }, + + createLock(data: { type: AccountLockType, reason: string, staff: string, expiresAt?: Date | null }): AccountLock | null { + if(this.hasLock(data.type)) return null; + const lock = { + id: generateSecureCode(), + type: data.type, + reason: data.reason, + staff: data.staff, + locked_at: new Date(), + expires_at: data.expiresAt || null + }; + this.locks.push(lock); + return lock; + }, + + isWatched(): boolean { + const period = this.watchlist_periods.at(-1); + return !!period && (!period.expires_at || period.expires_at.getTime() > Date.now()); + }, + + getWatchlistPeriod(): WatchlistPeriod | null { + if(!this.isWatched()) return null; + return this.watchlist_periods.at(-1)!; + }, + + startWatching({ reason: { type, details }, staff, expiresAt = null }: { reason: WatchlistReason, staff: string, expiresAt?: Date | null }): WatchlistPeriod | null { + if(this.isWatched() && !this.stopWatching()) return null; + const period = { + id: generateSecureCode(), + reason: { + type: type, + details: details?.trim() || null + }, + staff, + watched_at: new Date(), + expires_at: expiresAt + }; + this.watchlist_periods.push(period); + return period; + }, + + sendWatchlistAlert(initial: boolean = false): Promise { + const period = this.getWatchlistPeriod(); + if(!period) return Promise.reject(new Error('Player is not on the watchlist')); + + // TODO: Add discord notification + + return WatchlistAlert.insertOne({ + player_uuid: this.uuid, + new: initial, + period: period.id, + context: { + tag: this.tag || '', + position: this.position, + icon: { + type: this.icon.type, + hash: this.icon.hash + } + } + }); + }, + + stopWatching(): boolean { + const period = this.getWatchlistPeriod(); + if(!period) return false; + period.expires_at = new Date(); + return true; + }, + + isBanned(): boolean { + const ban = this.bans.at(-1); + return !!ban && (!ban.expires_at || ban.expires_at.getTime() > Date.now()); + }, + + getBan(): PlayerBan | null { + if(!this.isBanned()) return null; + return this.bans.at(-1)!; + }, + + banPlayer({ reason, staff, appealable = true, expiresAt }: { reason: string, staff: string, appealable?: boolean, expiresAt?: Date | null }): PlayerBan | null { + if(this.isBanned()) return null; + + const ban = { + id: generateSecureCode(), + reason: reason.trim(), + staff, + banned_at: new Date(), + expires_at: expiresAt || null, + appeal: { + appealable, + appealed: false, + reason: null, + appealed_at: null + } + }; + this.bans.push(ban); + + return ban; + }, + + unban() { + const ban = this.getBan(); + if(!ban) return false; + ban.expires_at = new Date(); + return true; + }, + } +}); + +export async function getOrCreatePlayer(uuid: string): Promise { + uuid = stripUUID(uuid); + const player = await Player.findOne({ uuid }); + if(player) return player; + return await Player.create({ uuid }); +} + +export async function resetMonthlyReferrals() { + if(!isConnected()) return; + const data = await Player.find({ 'referrals.current_month': { $gt: 0 } }); + + for(const player of data) { + player.referrals.current_month = 0; + player.save(); + } +} + +export const Player = model('Player', PlayerSchema); +export type PlayerDocument = HydratedDocument; \ No newline at end of file diff --git a/src/database/schemas/Report.ts b/src/database/schemas/Report.ts new file mode 100644 index 0000000..55d07ca --- /dev/null +++ b/src/database/schemas/Report.ts @@ -0,0 +1,230 @@ +import { HydratedDocument, model, Schema } from "mongoose"; +import { GlobalIcon, icons } from "../../types/GlobalIcon"; +import { GlobalPosition, positions } from "../../types/GlobalPosition"; +import { GameProfile } from "../../libs/game-profiles"; +import { generateSecureCode } from "../../libs/crypto"; + +export enum PunishmentActionType { + Banned = 'banned', + Watched = 'watched', + Locked = 'locked', + Dismissed = 'dismissed' +} + +export interface PunishmentAction { + /** + * UUID of the user who took the action + */ + user: string; + /** + * Type of action taken on the punishment + * @see PunishmentActionType + */ + type: PunishmentActionType; + /** + * Additional comment provided by the user who took the action + */ + comment: string | null; + /** + * Timestamp of when the action was taken + */ + added_at: Date; +} + +export interface PlayerContext { + /** + * The current tag of the player + */ + tag: string; + /** + * The current position of the player in the global ranking + */ + position: GlobalPosition; + /** + * The current icon of the player + */ + icon: { + /** + * The type of the icon + */ + type: GlobalIcon; + /** + * The custom icon hash, if applicable + */ + hash: string | null; + }; +} + +interface IReport { + /** + * Unique identifier for the report + */ + id: string; + /** + * UUID of the reported user + */ + reported_uuid: string; + /** + * UUID of the user who reported + */ + reporter_uuid: string; + /** + * Reason for the report + */ + reason: string; + /** + * Actions taken on the report + * @see PunishmentAction + */ + actions: PunishmentAction[]; + /** + * Contextual information about the report + */ + context: PlayerContext; + /** + * Timestamp of when the report was created + */ + created_at: Date; + /** + * Timestamp of when the report was last updated + */ + last_updated: Date; + + /** + * Indicates if the report has been resolved + * @return {boolean} True if the report has actions, false otherwise + */ + isResolved(): boolean; + + /** + * + * @param data - The data for the action to be performed + * @param data.user - UUID of the user performing the action + * @param data.type - Type of action to be performed + * @param data.comment - Optional comment for the action + */ + performAction(data: { user: string, type: PunishmentActionType, comment?: string | null }): PunishmentAction; + + /** + * Fetches the GameProfile of the reporter + * @returns {Promise} The GameProfile of the reporter + */ + getReportedProfile(): Promise; + + /** + * Fetches the GameProfile of the reported user + * @returns {Promise} The GameProfile of the reported user + */ + getReporterProfile(): Promise; +} + +export const PunishmentActionSchema = new Schema({ + user: { + type: String, + required: true + }, + type: { + type: String, + enum: Object.values(PunishmentActionType), + required: true, + }, + comment: { + type: String, + default: null + }, + added_at: { + type: Date, + required: true, + default: Date.now + }, +}, { _id: false }); + +export const ContextSchema = new Schema({ + tag: { type: String, required: true }, + position: { + type: String, + enum: positions, + required: true, + default: GlobalPosition.Above, + }, + icon: { + type: { + type: String, + enum: icons, + required: true, + default: GlobalIcon.None, + }, + hash: { + type: String, + default: null + }, + }, +}, { _id: false }); + +const ReportSchema = new Schema({ + id: { + type: String, + required: true, + unique: true, + default: generateSecureCode + }, + reported_uuid: { + type: String, + required: true + }, + reporter_uuid: { + type: String, + required: true + }, + reason: { + type: String, + required: true + }, + actions: { + type: [PunishmentActionSchema], + required: true, + default: [] + }, + context: { + type: ContextSchema, + required: true + }, + created_at: { + type: Date, + required: true, + default: Date.now + }, + last_updated: { + type: Date, + required: true, + default: Date.now + }, +}, { + methods: { + isResolved(): boolean { + return this.actions.length > 0 + }, + + performAction({ user, type, comment }: { user: string, type: PunishmentActionType, comment?: string | null }): PunishmentAction { + const action: PunishmentAction = { + user, + type, + comment: comment || null, + added_at: new Date(), + }; + this.actions.push(action); + return action; + }, + + getReportedProfile(): Promise { + return GameProfile.getProfileByUUID(this.reported_uuid); + }, + + getReporterProfile(): Promise { + return GameProfile.getProfileByUUID(this.reporter_uuid); + } + } +}); + +export const Report = model('Report', ReportSchema); +export type ReportDocument = HydratedDocument; \ No newline at end of file diff --git a/src/database/schemas/roles.ts b/src/database/schemas/Role.ts similarity index 61% rename from src/database/schemas/roles.ts rename to src/database/schemas/Role.ts index 4f99558..ea18299 100644 --- a/src/database/schemas/roles.ts +++ b/src/database/schemas/Role.ts @@ -3,32 +3,73 @@ import { config } from "../../libs/config"; import { Permission, permissions } from "../../types/Permission"; import { isConnected } from "../mongo"; import Logger from "../../libs/Logger"; -import playerSchema from "./players"; import { fetchGuild } from "../../bot/bot"; -import players from "./players"; +import { generateSecureCode } from "../../libs/crypto"; +import { Player } from "./Player"; + +const cachedRoles: RoleDocument[] = []; interface IRole { - id: string, - name: string, - position: number, - color?: string | null, - hasIcon: boolean, - sku?: string | null, - permissions: number, - getPermissions(): Permission[], - hasPermission(permission: Permission, ignoreAdmin?: boolean): boolean, - getSyncedRoles(): string[], - rename(name: string): Promise + /** + * Unique identifier for the role + */ + id: string; + /** + * Name of the role + */ + name: string; + /** + * Position of the role in the list + */ + position: number; + /** + * Color of the role in hex format (e.g., 'FF0000' for red) + * Can be null if no color is set + */ + color: string | null; + /** + * Whether the role has an icon + */ + hasIcon: boolean; + /** + * SKU of the role, used for Discord integration + * Can be null if not applicable + * @deprecated + */ + sku: string | null; + /** + * Bitwise representation of permissions assigned to the role + * @see Permission + */ + permissions: number; + + /** + * Retrieves the permissions of the role as an array of Permission enums + * @returns {Permission[]} Array of permissions + */ + getPermissions(): Permission[]; + + /** + * Checks if the role has a specific permission + * @param {Permission} permission - The permission to check + * @param {boolean} [ignoreAdmin=false] - Whether to ignore the Administrator permission + * @returns {boolean} True if the role has the permission, false otherwise + */ + hasPermission(permission: Permission, ignoreAdmin?: boolean): boolean; + + /** + * Retrieves the list of Discord role IDs that are synced with this role + * @returns {string[]} Array of Discord role IDs + */ + getSyncedRoles(): string[]; } -export type Role = HydratedDocument; -const cachedRoles: Role[] = []; - -const schema = new Schema({ +const RoleSchema = new Schema({ id: { type: String, required: true, - unique: true + unique: true, + default: generateSecureCode }, name: { type: String, @@ -40,20 +81,21 @@ const schema = new Schema({ }, color: { type: String, - required: false, default: null }, hasIcon: { type: Boolean, - required: true + required: true, + default: false }, sku: { type: String, - required: false + default: null }, permissions: { type: Number, - required: true + required: true, + default: 0 } }, { methods: { @@ -67,21 +109,11 @@ const schema = new Schema({ getSyncedRoles(): string[] { return config.discordBot.syncedRoles.getRoles(this.name); - }, - - async rename(name: string): Promise { - const oldName = this.name; - this.name = name; - await this.save(); - await players.updateMany({ 'roles.name': oldName }, { $set: { 'roles.$.name': name } }); - updateRoleCache(); } } }); -const Roles = model('roles', schema); - -export function getCachedRoles(): Role[] { +export function getCachedRoles(): RoleDocument[] { return cachedRoles; } @@ -99,9 +131,9 @@ const defaultRoles = [ export async function updateRoleCache(): Promise { if(!isConnected()) return; cachedRoles.length = 0; - let roles = await Roles.find(); + let roles = await Role.find(); if(roles.length == 0) { - cachedRoles.push(...await Roles.insertMany(defaultRoles)); + cachedRoles.push(...await Role.insertMany(defaultRoles)); } for(const role of roles) { @@ -113,19 +145,19 @@ export async function updateRoleCache(): Promise { export async function getNextPosition(): Promise { if(!isConnected()) return -1; - const roles = await Roles.find(); + const roles = await Role.find(); roles.sort((a, b) => a.position - b.position); return roles[roles.length - 1].position + 1; } -export async function synchronizeRoles() { +export async function synchronizeDiscordRoles() { if(!isConnected()) return; - const players = await playerSchema.find({ 'connections.discord.id': { $exists: true } }); + const players = await Player.find({ 'connections.discord.id': { $exists: true } }); const guild = await fetchGuild(); const roles = getCachedRoles(); for(const player of players) { - const member = await guild.members.fetch(player.connections.discord.id!).catch(() => null); + const member = await guild.members.fetch(player.connections.discord!.id).catch(() => null); if(!member || !member.id) continue; const playerRoles = player.getActiveRoles(); @@ -151,4 +183,5 @@ export async function synchronizeRoles() { } } -export default Roles; \ No newline at end of file +export const Role = model('Role', RoleSchema); +export type RoleDocument = HydratedDocument; \ No newline at end of file diff --git a/src/database/schemas/staff-categories.ts b/src/database/schemas/StaffCategory.ts similarity index 51% rename from src/database/schemas/staff-categories.ts rename to src/database/schemas/StaffCategory.ts index ff9f797..571d55c 100644 --- a/src/database/schemas/staff-categories.ts +++ b/src/database/schemas/StaffCategory.ts @@ -1,19 +1,28 @@ import { HydratedDocument, model, Schema } from "mongoose"; import { isConnected } from "../mongo"; +import { generateSecureCode } from "../../libs/crypto"; interface IStaffCategory { + /** + * Unique identifier for the staff category + */ id: string; + /** + * Name of the staff category + */ name: string; + /** + * Position of the staff category in the list + */ position: number; } -export type StaffCategory = HydratedDocument; - -const schema = new Schema({ +const StaffCategorySchema = new Schema({ id: { type: String, required: true, - unique: true + unique: true, + default: generateSecureCode }, name: { type: String, @@ -25,13 +34,12 @@ const schema = new Schema({ } }); -const staffCategories = model('staff-categories', schema); - export async function getNextPosition(): Promise { if(!isConnected()) return -1; - const roles = await staffCategories.find(); + const roles = await StaffCategory.find(); roles.sort((a, b) => a.position - b.position); return roles[roles.length - 1].position + 1; } -export default staffCategories; \ No newline at end of file +export const StaffCategory = model('StaffCategory', StaffCategorySchema);; +export type StaffCategoryDocument = HydratedDocument; \ No newline at end of file diff --git a/src/database/schemas/StaffMember.ts b/src/database/schemas/StaffMember.ts new file mode 100644 index 0000000..63f843d --- /dev/null +++ b/src/database/schemas/StaffMember.ts @@ -0,0 +1,79 @@ +import { HydratedDocument, model, Schema } from "mongoose"; +import { GameProfile } from "../../libs/game-profiles"; +import { Player, PlayerDocument } from "./Player"; +import { StaffCategory, StaffCategoryDocument } from "./StaffCategory"; + +interface IStaffMember { + /** + * Unique identifier for the staff member + */ + uuid: string; + /** + * Optional description of the staff member + */ + description?: string | null; + /** + * Category id of the staff member + */ + category: string; + /** + * Date when the staff member joined + */ + joined_at: Date; + + /** + * Retrieves the GameProfile of the staff member + * @return {Promise} A promise that resolves to the GameProfile of the staff member + */ + getGameProfile(): Promise; + + /** + * Retrieves the Player document associated with the staff member + * @return {Promise} A promise that resolves to the Player document of the staff member, or null if not found + */ + getPlayer(): Promise; + + /** + * Retrieves the StaffCategory document associated with the staff member + * @return {Promise} A promise that resolves to the StaffCategory document of the staff member, or null if not found + */ + getCategory(): Promise; +} + +const StaffMemberSchema = new Schema({ + uuid: { + type: String, + required: true, + unique: true + }, + description: { + type: String, + default: null + }, + category: { + type: String, + required: true + }, + joined_at: { + type: Date, + required: true, + default: Date.now + } +}, { + methods: { + getGameProfile(): Promise { + return GameProfile.getProfileByUUID(this.uuid); + }, + + getPlayer(): Promise { + return Player.findOne({ uuid: this.uuid }); + }, + + getCategory(): Promise { + return StaffCategory.findOne({ id: this.category }); + } + } +}); + +export const StaffMember = model('StaffMember', StaffMemberSchema); +export type StaffMemberDocument = HydratedDocument; \ No newline at end of file diff --git a/src/database/schemas/WatchlistAlert.ts b/src/database/schemas/WatchlistAlert.ts new file mode 100644 index 0000000..6b94024 --- /dev/null +++ b/src/database/schemas/WatchlistAlert.ts @@ -0,0 +1,84 @@ +import { HydratedDocument, model, Schema } from "mongoose"; +import { ContextSchema, PlayerContext, PunishmentAction, PunishmentActionSchema } from "./Report"; +import { generateSecureCode } from "../../libs/crypto"; +import { WatchlistPeriod } from "./Player"; + +interface IWatchlistAlert { + /** + * Unique identifier for the watchlist alert + */ + id: string; + /** + * UUID of the player who is being watched + */ + player_uuid: string; + period: string; + /** + * Contextual information about the player at the time of the alert + * @see PlayerContext + */ + context: PlayerContext; + /** + * If the alert has put the player on the watchlist + */ + new: boolean; + /** + * Actions taken on the watchlist alert + * @see PunishmentAction + */ + actions: PunishmentAction[]; + /** + * Timestamp of when the alert was created + */ + created_at: Date; + + /** + * Get the watchlist period for the alert + * @returns The watchlist period for the alert + */ + getWatchlistPeriod(): Promise; +} + +const WatchlistAlertSchema = new Schema({ + id: { + type: String, + required: true, + unique: true, + default: generateSecureCode + }, + player_uuid: { + type: String, + required: true + }, + period: { + type: String, + required: true + }, + context: { + type: ContextSchema, + required: true + }, + new: { + type: Boolean, + required: true + }, + actions: { + type: [PunishmentActionSchema], + required: true, + default: [] + }, + created_at: { + type: Date, + required: true, + default: Date.now + } +}, { + methods: { + getWatchlistPeriod(): Promise { + return WatchlistAlert.findOne({ id: this.period }); + } + } +}); + +export const WatchlistAlert = model('WatchlistAlert', WatchlistAlertSchema); +export type WatchlistAlertDocument = HydratedDocument; \ No newline at end of file diff --git a/src/database/schemas/gift-codes.ts b/src/database/schemas/gift-codes.ts deleted file mode 100644 index a09c143..0000000 --- a/src/database/schemas/gift-codes.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { HydratedDocument, Schema, model as createModel } from "mongoose"; -import { GameProfile, stripUUID } from "../../libs/game-profiles"; -import { generateSecureCode } from "../../libs/crypto"; - -export interface IGiftCode { - id: string; - name: string; - code: string; - uses: string[]; - max_uses: number; - gift: { - type: 'role', - value: string, - duration?: number | null - }; - created_by: string; - created_at: Date; - expires_at?: Date | null; - getCreatorProfile(): Promise; - isValid(): boolean; - usesLeft(): number; -} -export type GiftCode = HydratedDocument; - -const schema = new Schema({ - id: { - type: String, - required: true, - unique: true - }, - name: { - type: String, - required: true - }, - code: { - type: String, - required: true - }, - uses: { - type: [String], - required: true - }, - max_uses: { - type: Number, - required: true - }, - gift: { - type: { - type: String, - required: true - }, - value: { - type: String, - required: true - }, - duration: { - type: Number, - required: false - } - }, - created_by: { - type: String, - required: true - }, - created_at: { - type: Date, - required: true - }, - expires_at: { - type: Date, - required: false - } -}, { - methods: { - getCreatorProfile(): Promise { - return GameProfile.getProfileByUUID(this.created_by); - }, - - isValid(): boolean { - return this.uses.length < this.max_uses && (!this.expires_at || this.expires_at > new Date()); - }, - - usesLeft(): number { - return this.max_uses - this.uses.length; - } - } -}); - -const model = createModel('gift-codes', schema); - -export async function createGiftCode({ - name, - code = generateSecureCode(12), - maxUses, - gift, - expiresAt, - createdBy -} : { - name: string, - code?: string, - maxUses: number, - gift: IGiftCode['gift'], - expiresAt?: Date | null, - createdBy: string -}): Promise { - return await model.insertOne({ - id: generateSecureCode(), - name, - code, - uses: [], - max_uses: maxUses, - gift: gift, - created_by: stripUUID(createdBy), - created_at: new Date(), - expires_at: expiresAt || null - }); -} - -export default model; \ No newline at end of file diff --git a/src/database/schemas/metrics.ts b/src/database/schemas/metrics.ts deleted file mode 100644 index 8df7ba04..0000000 --- a/src/database/schemas/metrics.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Schema, model } from "mongoose"; - -const requiredNumber = { - type: Number, - required: true -} - -interface IMetrics { - players: number, - tags: number, - admins: number, - bans: number, - downloads: { - flintmc: number, - modrinth: number - }, - ratings: { - flintmc: number - }, - dailyRequests: number, - positions: Record, - icons: Record, - createdAt: Date -} - -const schema = new Schema({ - players: requiredNumber, - tags: requiredNumber, - admins: requiredNumber, - bans: requiredNumber, - downloads: { - flintmc: requiredNumber, - modrinth: requiredNumber - }, - ratings: { - flintmc: requiredNumber - }, - dailyRequests: requiredNumber, - positions: { - type: Object, - required: true - }, - icons: { - type: Object, - required: true - } -}, { - timestamps: true -}); - -export default model('metrics', schema); \ No newline at end of file diff --git a/src/database/schemas/players.ts b/src/database/schemas/players.ts deleted file mode 100644 index 24eec48..0000000 --- a/src/database/schemas/players.ts +++ /dev/null @@ -1,594 +0,0 @@ -import { HydratedDocument, Schema, model } from "mongoose"; -import { snakeCase } from "change-case"; -import { Permission } from "../../types/Permission"; -import { getCachedRoles, Role } from "./roles"; -import { GlobalIcon } from "../../types/GlobalIcon"; -import { GameProfile, stripUUID } from "../../libs/game-profiles"; -import { isConnected } from "../mongo"; -import { generateSecureCode } from "../../libs/crypto"; - -export type PlayerRole = { - role: Role, - added_at: Date, - autoRemove: boolean, - expiresAt?: Date | null, - reason?: string | null -} - -export type ApiKey = { - id: string, - name: string, - key: string, - created_at: Date, - last_used?: Date | null -} - -interface IPlayer { - uuid: string, - tag?: string | null, - position: string, - icon: { - name: string, - hash?: string | null - }, - last_language: string, - history: string[], - watchlist: boolean, - referrals: { - has_referred: boolean, - total: { - uuid: string, - timestamp: number - }[], - current_month: number - }, - reports: { id: string, by: string, reported_tag: string, reason: string, created_at: Date }[], - hide_role_icon: boolean, - roles: { - name: string, - added_at: Date, - auto_remove: boolean, - expires_at?: Date | null, - reason?: string | null - }[], - api_keys: ApiKey[], - notes: { id: string, text: string, author: string, createdAt: Date }[], - bans: { - appeal: { - appealable: boolean, - appealed: boolean, - reason?: string | null, - appealed_at?: Date | null - }, - banned_at: Date, - expires_at: Date | null, - id: string, - reason: string, - staff: string - }[], - clears: { currentData: string, type: 'tag' | 'icon', staff: string, timestamp: number }[], - connections: { - discord: { id?: string | null, code?: string | null }, - email: { address?: string | null, code?: string | null } - }, - - //* API Keys - - /** - * - * @param name The name of the API key to create - * @returns The created API key object - */ - createApiKey(name: string): ApiKey; - - /** - * - * @param id The ID of the API key to retrieve - * @returns The API key object if found, otherwise null - */ - getApiKey(id: string): ApiKey | null; - - /** - * - * @param id The ID of the API key to delete - * @return True if the API key was deleted, otherwise false - */ - deleteApiKey(id: string): boolean; - - getGameProfile(): Promise, - getReferrer(): Promise - addReferral(uuid: string): void, - isEmailVerified(): boolean, - getAllRoles(): PlayerRole[], - getActiveRoles(): PlayerRole[], - getRole(role: string): PlayerRole | null, - addRole(info: { name: string, reason: string, autoRemove: boolean, expiresAt?: Date | null, duration?: number | null }): { success: boolean, expiresAt: Date | null }, - setRoleExpiration(name: string, expiration: Date | null): boolean, - setRoleNote(name: string, note: string | null): boolean, - removeRole(role: string): boolean, - hasPermission(permission: Permission): boolean, - canManagePlayers(): boolean, - isBanned(): boolean, - banPlayer({ reason, staff, appealable, expiresAt }: { reason: string, staff: string, appealable?: boolean, expiresAt?: Date | null }): void, - unban(): void, - clearTag(staff: string): void, - clearIconTexture(staff: string): void, - createNote({ text, author }: { text: string, author: string }): void, - existsNote(id: string): boolean, - deleteNote(id: string): void, - createReport({ by, reported_tag, reason }: { by: string, reported_tag: string, reason: string }): void, - deleteReport(id: string): void -} - -export type Player = HydratedDocument; - -const schema = new Schema({ - uuid: { - type: String, - required: true, - unique: true - }, - tag: { - type: String, - default: null - }, - position: { - type: String, - required: true, - default: 'above' - }, - icon: { - name: { - type: String, - required: true, - default: 'none' - }, - hash: String - }, - last_language: { - type: String, - required: true, - default: 'en_us' - }, - history: { - type: [String], - required: true, - default: [] - }, - watchlist: { - type: Boolean, - required: true, - default: false - }, - referrals: { - has_referred: { - type: Boolean, - required: true, - default: false - }, - total: { - type: [{ - uuid: { - type: String, - required: true - }, - timestamp: { - type: Number, - required: true - } - }], - required: true, - default: [] - }, - current_month: { - type: Number, - required: true, - default: 0 - } - }, - reports: { - type: [{ - id: { - type: String, - required: true - }, - by: { - type: String, - required: true - }, - reported_tag: { - type: String, - required: true - }, - reason: { - type: String, - required: true - }, - created_at: { - type: Date, - required: true - } - }], - required: true, - default: [] - }, - hide_role_icon: { - type: Boolean, - required: true, - default: false - }, - roles: { - type: [{ - name: { - type: String, - required: true - }, - added_at: { - type: Date, - required: true - }, - auto_remove: { - type: Boolean, - required: true - }, - expires_at: Date, - reason: String - }], - required: true, - default: [] - }, - api_keys: { - type: [{ - id: { - type: String, - required: true - }, - name: { - type: String, - required: true - }, - key: { - type: String, - required: true - }, - created_at: { - type: Date, - required: true - }, - last_used: Date - }], - required: true, - default: [] - }, - notes: [{ - id: { - type: String, - required: true - }, - text: { - type: String, - required: true - }, - author: { - type: String, - required: true - }, - createdAt: { - type: Date, - required: true - } - }], - bans: [{ - appeal: { - appealable: { - type: Boolean, - required: true, - default: true - }, - appealed: { - type: Boolean, - required: true, - default: false - }, - reason: { - type: String, - required: false - }, - appealed_at: { - type: Date, - required: false - } - }, - banned_at: { - type: Date, - required: true, - default: Date.now - }, - expires_at: { - type: Date, - required: false - }, - id: { - type: String, - required: true - }, - reason: { - type: String, - required: true - }, - staff: { - type: String, - required: true - } - }], - clears: [{ - currentData: { - type: String, - required: true - }, - type: { - type: String, - enum: ['tag', 'icon'], - required: true - }, - staff: { - type: String, - required: true - }, - timestamp: { - type: Number, - required: true - } - }], - connections: { - discord: { - id: String, - code: String - }, - email: { - address: String, - code: String - } - } -}, { - methods: { - createApiKey(name: string): ApiKey { - const key = { - id: generateSecureCode(), - name, - key: `sk_${generateSecureCode(32)}`, - created_at: new Date(), - last_used: null - } - this.api_keys.push(key); - return key; - }, - - getApiKey(id: string): ApiKey | null { - const key = this.api_keys.find((key) => key.id == id); - if(!key) return null; - return key; - }, - - deleteApiKey(id: string): boolean { - const index = this.api_keys.findIndex((key) => key.id == id); - if(index === -1) return false; - this.api_keys.splice(index, 1); - return true; - }, - - async getGameProfile(): Promise { - return await GameProfile.getProfileByUUID(this.uuid); - }, - - async getReferrer(): Promise { - if(!this.referrals.has_referred) return null; - const referrer = await this.model('players').findOne({ 'referrals.total.uuid': this.uuid }); - return referrer as Player | null; - }, - - addReferral(uuid: string) { - this.referrals.total.push({ uuid, timestamp: Date.now() }); - this.referrals.current_month++; - }, - - isEmailVerified() { - return this.connections.email.address && !this.connections.email.code; - }, - - getAllRoles(): PlayerRole[] { - return getCachedRoles().filter(({ name: role }) => { - role = snakeCase(role); - const playerRole = this.roles.find((playerRole) => snakeCase(playerRole.name) == role); - return playerRole && (!playerRole.expires_at || playerRole.expires_at.getTime() > Date.now()); - }).map((role) => { - const name = snakeCase(role.name); - const playerRole = this.roles.find((playerRole) => snakeCase(playerRole.name) == name)!; - - return { - role, - added_at: playerRole.added_at, - autoRemove: playerRole.auto_remove, - expires_at: playerRole.expires_at, - reason: playerRole.reason - } - }); - }, - - getActiveRoles(): PlayerRole[] { - return this.getAllRoles().filter((role) => role.expiresAt == null || role.expiresAt.getTime() > Date.now()); - }, - - getRole(role: string): PlayerRole | null { - role = snakeCase(role); - return this.getActiveRoles().find((playerRole) => playerRole.role.name == role) || null; - }, - - addRole({ name, reason, autoRemove, expiresAt, duration }: { name: string, reason: string, autoRemove: boolean, expiresAt?: Date | null, duration?: number | null }): { success: boolean, expiresAt: Date | null } { - name = snakeCase(name); - const role = this.roles.find((playerRole) => playerRole.name == name); - if(role) { - if(!role.expires_at) return { success: false, expiresAt: null }; - if(role.expires_at.getTime() > Date.now()) { - role.reason += ` | ${reason}`; - role.auto_remove = autoRemove; - role.expires_at = expiresAt ? expiresAt : duration ? new Date(role.expires_at.getTime() + duration) : null; - return { success: true, expiresAt: role.expires_at }; - } else { - role.reason = reason; - role.auto_remove = autoRemove; - role.expires_at = expiresAt ? expiresAt : duration ? new Date(Date.now() + duration) : null; - return { success: true, expiresAt: role.expires_at }; - } - } else { - const role = { - name, - reason, - added_at: new Date(), - auto_remove: autoRemove, - expires_at: expiresAt ? expiresAt : duration ? new Date(Date.now() + duration) : null - }; - this.roles.push(role); - return { success: true, expiresAt: role.expires_at }; - } - }, - - setRoleExpiration(name: string, expiration: Date | null): boolean { - const role = this.roles.find((role) => role.name == name); - if(!role) return false; - role.expires_at = expiration; - return true; - }, - - setRoleNote(name: string, note: string): boolean { - const role = this.roles.find((role) => role.name == name); - if(!role) return false; - role.reason = note; - return true; - }, - - removeRole(role: string): boolean { - role = snakeCase(role); - const playerRole = this.roles.find((playerRole) => playerRole.name == role); - if(!playerRole) return false; - playerRole.expires_at = new Date(); - return true; - }, - - hasPermission(permission: Permission): boolean { - return this.getActiveRoles().some((role) => role.role.hasPermission(permission)); - }, - - canManagePlayers(): boolean { - return [ - Permission.ViewBans, - Permission.ViewApiKeys, - Permission.ViewConnections, - Permission.ViewNotes, - Permission.ViewReports, - Permission.ViewRoles, - Permission.ManagePlayerTags, - Permission.ManageWatchlistEntries - ].some((permission) => this.hasPermission(permission)); - }, - - isBanned(): boolean { - const ban = this.bans.at(0); - return !!ban && (!ban.expires_at || ban.expires_at.getTime() > Date.now()); - }, - - banPlayer({ reason, staff, appealable = true, expiresAt }: { reason: string, staff: string, appealable?: boolean, expiresAt?: Date | null }) { - this.bans.unshift({ - appeal: { - appealable, - appealed: false, - reason: null, - appealed_at: null - }, - banned_at: new Date(), - expires_at: expiresAt || null, - id: generateSecureCode(), - reason, - staff - }); - }, - - unban() { - const ban = this.bans.at(0); - if(ban) ban.expires_at = new Date(); - }, - - clearTag(staff: string) { - this.clears.push({ - currentData: this.tag || '--', - type: 'tag', - staff, - timestamp: new Date().getTime() - }) - this.tag = null; - }, - - clearIconTexture(staff: string) { - this.clears.push({ - currentData: this.icon.hash || '--', - type: 'icon', - staff, - timestamp: new Date().getTime() - }) - this.icon.name = snakeCase(GlobalIcon[GlobalIcon.None]); - this.icon.hash = null; - }, - - createNote({ text, author }: { text: string, author: string }) { - this.notes.push({ - id: generateSecureCode(), - text, - author, - createdAt: new Date() - }); - }, - - existsNote(id: string) { - this.notes.some((note) => note.id == id); - }, - - deleteNote(id: string) { - this.notes = this.notes.filter((note) => note.id != id); - }, - - createReport({ by, reported_tag, reason }: { by: string, reported_tag: string, reason: string }) { - this.reports.push({ - id: generateSecureCode(), - by, - reported_tag, - reason, - created_at: new Date() - }) - }, - - deleteReport(id: string) { - this.reports = this.reports.filter((report) => report.id != id); - } - } -}); - -const players = model('players', schema); - -export async function getOrCreatePlayer(uuid: string): Promise { - uuid = stripUUID(uuid); - const player = await players.findOne({ uuid }); - if(player) return player; - return await players.create({ uuid }); -} - -export async function resetMonthlyReferrals() { - if(!isConnected()) return; - const data = await players.find({ 'referrals.current_month': { $gt: 0 } }); - - for(const player of data) { - player.referrals.current_month = 0; - player.save(); - } -} - -export default players; \ No newline at end of file diff --git a/src/database/schemas/staff-members.ts b/src/database/schemas/staff-members.ts deleted file mode 100644 index 60fb1ba..0000000 --- a/src/database/schemas/staff-members.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { HydratedDocument, model, Schema } from "mongoose"; - -interface IStaffMember { - uuid: string; - description?: string | null; - category: string; - joined_at: Date; -} - -export type StaffMember = HydratedDocument; - -const schema = new Schema({ - uuid: { - type: String, - required: true, - unique: true - }, - description: String, - category: { - type: String, - required: true - }, - joined_at: { - type: Date, - required: true, - default: Date.now - } -}); - -const staffMembers = model('staff-members', schema); - -export default staffMembers; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 6cb38cf..b5be81e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,6 @@ import { swagger } from "@elysiajs/swagger"; import Logger from "./libs/Logger"; import { connect as connectDatabase } from "./database/mongo"; import { getRouter } from "./libs/route-loader"; -import { version } from "../package.json"; import access from "./middleware/access-log"; import checkDatabase from "./middleware/database-checker"; import Ratelimiter from "./libs/Ratelimiter"; @@ -23,6 +22,7 @@ import { join } from "path"; import ip from "./middleware/ip"; import { captureException } from "@sentry/bun"; import { generateSecureCode, validateKeypair } from "./libs/crypto"; +import { DocumentationCategory } from "./types/DocumentationCategory"; if(config.mongodb.trim().length == 0) { Logger.error('Database connection string is empty!'); @@ -54,7 +54,7 @@ const elysia = new Elysia() ], documentation: { info: { - version, + version: config.version, title: 'GlobalTags API', description: 'This is the official GlobalTags API documentation containing detailed descriptions about the API endpoints and their usage.', license: { @@ -68,13 +68,17 @@ const elysia = new Elysia() } }, tags: [ - { name: 'API', description: 'Get info about the API' }, - { name: 'Interactions', description: 'Interact with other players' }, - { name: 'Settings', description: 'Modify the settings of your GlobalTag' }, - { name: 'Roles', description: 'Holds role management routes' }, - { name: 'Gift codes', description: 'Holds gift code actions' }, - { name: 'Admin', description: 'Admininstrative actions' }, - { name: 'Connections', description: 'Manage account connections' } + { name: DocumentationCategory.Api, description: 'Get info about the API' }, + { name: DocumentationCategory.ApiKeys, description: 'API Key management' }, + { name: DocumentationCategory.Bans, description: 'Ban management' }, + { name: DocumentationCategory.GiftCodes, description: 'Gift code management' }, + { name: DocumentationCategory.Notes, description: 'Staff note management' }, + { name: DocumentationCategory.Partners, description: 'Partner management' }, + { name: DocumentationCategory.Referrals, description: 'Referral management' }, + { name: DocumentationCategory.Reports, description: 'Report management' }, + { name: DocumentationCategory.Roles, description: 'Route management' }, + { name: DocumentationCategory.Staff, description: 'Staff member management' }, + { name: DocumentationCategory.Tags, description: 'Tag management' }, ] } })) @@ -118,6 +122,9 @@ const elysia = new Elysia() } else if(code == 'NOT_FOUND') { set.status = 404; return { error: i18n('$.error.notFound') }; + } else if(code == 'PARSE') { + set.status = 422; + return { error: i18n('$.error.invalid_body') }; } else { set.status = 500; captureException(error); diff --git a/src/libs/chat-color.ts b/src/libs/chat-color.ts index e74d448..ea2e1ca 100644 --- a/src/libs/chat-color.ts +++ b/src/libs/chat-color.ts @@ -7,7 +7,7 @@ export function stripColors(text: string): string { return text.replaceAll(colorCodes, '').replaceAll(hexColorCodes, ''); } -export function translateToAnsi(text: string): string { +export function translateToAnsi(text: string): string { // TODO: Support hex colors return text .replaceAll(/(&|§)0/gi, '') .replaceAll(/(&|§)7/gi, '') diff --git a/src/libs/config.ts b/src/libs/config.ts index 5f0b329..0374cb4 100644 --- a/src/libs/config.ts +++ b/src/libs/config.ts @@ -1,5 +1,6 @@ import { config as loadEnv } from "dotenv"; import { constantCase, snakeCase } from "change-case"; +import * as pkg from "../../package.json"; function getEnvNumber(path: string | undefined, defaultValue: number) { const number = Number(path); @@ -15,6 +16,7 @@ loadEnv(); loadEnv({ path: `./.env.${process.env.NODE_ENV || 'dev'}`, override: true }); export let config = { + version: pkg.version, port: getEnvNumber(process.env.GT_PORT, 5500), strictAuth: getEnvBoolean(process.env.GT_STRICT_AUTH, true), logLevel: process.env.GT_LOG_LEVEL || 'Info', diff --git a/src/libs/cron-jobs.ts b/src/libs/cron-jobs.ts index f1e3bf0..0eb0f1e 100644 --- a/src/libs/cron-jobs.ts +++ b/src/libs/cron-jobs.ts @@ -1,11 +1,11 @@ import { checkExpiredEntitlements } from "./entitlement-expiry"; import { saveMetrics } from "./metrics"; import Logger from "./Logger"; -import playerSchema from "../database/schemas/players"; import { config } from "./config"; -import { synchronizeRoles, updateRoleCache } from "../database/schemas/roles"; +import { synchronizeDiscordRoles, updateRoleCache } from "../database/schemas/Role"; import { isConnected } from "../database/mongo"; import { Cron } from "croner"; +import { Player } from "../database/schemas/Player"; const tz = 'Europe/Berlin'; @@ -31,7 +31,7 @@ export function startMetrics() { export function startReferralReset() { new Cron('0 0 1 * *', async () => { if(!isConnected()) return; - const data = await playerSchema.find({ 'referrals.current_month': { $gt: 0 } }); + const data = await Player.find({ 'referrals.current_month': { $gt: 0 } }); for(const player of data) { player.referrals.current_month = 0; @@ -52,9 +52,9 @@ export function startRoleCacheJob() { } export function startRoleSynchronization() { - if(!config.discordBot.syncedRoles.enabled) return; + if(!config.discordBot.enabled || !config.discordBot.syncedRoles.enabled) return; Logger.info('Role syncronization initialized.'); - const job = new Cron('*/10 * * * *', synchronizeRoles, { + const job = new Cron('*/10 * * * *', synchronizeDiscordRoles, { name: 'Role Synchronization', timezone: tz }); diff --git a/src/libs/crypto.ts b/src/libs/crypto.ts index c7356c1..baea082 100644 --- a/src/libs/crypto.ts +++ b/src/libs/crypto.ts @@ -1,11 +1,9 @@ import crypto, { randomBytes } from "crypto"; import Logger from "./Logger"; - -const publicKeyFile = Bun.file('./data/certificate/pubkey.pem'); -const privateKeyFile = Bun.file('./data/certificate/privkey.pem'); +import { CertificateFiles } from "./data-accessor"; export async function validateKeypair() { - if(await publicKeyFile.exists() && await privateKeyFile.exists()) return; + if(await CertificateFiles.publicKeyFile.exists() && await CertificateFiles.privateKeyFile.exists()) return; Logger.info('Generating new RSA keypair for JWT signing...'); await generateKeypair(); } @@ -25,8 +23,8 @@ async function generateKeypair() { } }); - Bun.write(publicKeyFile, publicKey); - Bun.write(privateKeyFile, privateKey); + Bun.write(CertificateFiles.publicKeyFile, publicKey); + Bun.write(CertificateFiles.privateKeyFile, privateKey); } /** diff --git a/src/libs/data-accessor.ts b/src/libs/data-accessor.ts new file mode 100644 index 0000000..7fffd4e --- /dev/null +++ b/src/libs/data-accessor.ts @@ -0,0 +1,17 @@ +import { join } from 'path'; +import { MailTemplate } from '../types/MailTemplate'; + +const dataPath = (...paths: string[]) => join(__dirname, '..', '..', 'data', ...paths); + +export namespace CertificateFiles { + export const publicKeyFile = Bun.file(dataPath('certificate', 'pubkey.pem')); + export const privateKeyFile = Bun.file(dataPath('certificate', 'privkey.pem')); +} + +export const customIconFile = (uuid: string, hash: string) => { + return Bun.file(dataPath('icons', uuid, `${hash.trim()}.png`)); +} + +export const mailTemplateFile = (template: MailTemplate) => { + return Bun.file(dataPath('mail', `${template}.html`)); +} \ No newline at end of file diff --git a/src/libs/discord-notifier.ts b/src/libs/discord-notifier.ts index ee88579..10cbfeb 100644 --- a/src/libs/discord-notifier.ts +++ b/src/libs/discord-notifier.ts @@ -1,14 +1,15 @@ import * as bot from "../bot/bot"; import { ActionRowBuilder, APIMessageTopLevelComponent, ButtonBuilder, ButtonStyle, ContainerBuilder, EmbedBuilder, JSONEncodable, MessageCreateOptions, MessageFlags, SectionBuilder, TextDisplayBuilder, ThumbnailBuilder, TopLevelComponentData } from "discord.js"; import { GameProfile } from "./game-profiles"; -import { getCustomIconUrl } from "../routes/players/[uuid]/icon"; +import { getCustomIconUrl } from "../routes/players/[uuid]/icons"; import { capitalCase, pascalCase, sentenceCase } from "change-case"; import { config } from "./config"; import { stripColors, translateToAnsi } from "./chat-color"; -import { GiftCode } from "../database/schemas/gift-codes"; import Logger from "./Logger"; -import { ApiKey } from "../database/schemas/players"; -import { Role } from "../database/schemas/roles"; +import { ApiKey } from "../database/schemas/Player"; +import { RoleDocument } from "../database/schemas/Role"; +import { ReportDocument } from "../database/schemas/Report"; +import { GiftCodeDocument } from "../database/schemas/GiftCode"; export enum ModLogType { ChangeTag, @@ -70,7 +71,7 @@ type ModLogData = { appealable: boolean } | { logType: ModLogType.AddRole | ModLogType.RemoveRole | ModLogType.CreateRole | ModLogType.DeleteRole, - role: Role + role: RoleDocument } | { logType: ModLogType.EditRoleNote, role: string, @@ -97,10 +98,10 @@ type ModLogData = { key: ApiKey } | { logType: ModLogType.CreateGiftCode, - code: GiftCode + code: GiftCodeDocument } | { logType: ModLogType.DeleteGiftCode, - code: GiftCode + code: GiftCodeDocument } | { logType: ModLogType.CreateNote | ModLogType.DeleteNote, note: string @@ -137,11 +138,10 @@ export function formatTimestamp(date: Date, style: 't' | 'T' | 'd' | 'D' | 'f' | return ``; } -export function sendReportMessage({ player, reporter, tag, reason } : { +export function sendReportMessage({ player, reporter, report } : { player: GameProfile, reporter: GameProfile, - tag: string, - reason: string + report: ReportDocument }) { if(!config.discordBot.notifications.reports.enabled) return; @@ -156,20 +156,20 @@ export function sendReportMessage({ player, reporter, tag, reason } : { name: 'Reported player', value: player.getFormattedHyperlink() }, - { - name: 'Reported Tag', - value: `\`\`\`ansi\n${translateToAnsi(tag)}\`\`\`` - }, { name: 'Reporter', value: reporter.getFormattedHyperlink() }, + { + name: 'Tag', + value: `\`\`\`ansi\n${translateToAnsi(report.context.tag)}\`\`\`` + }, { name: 'Reason', - value: `\`\`\`${reason}\`\`\`` + value: `\`\`\`${report.reason}\`\`\`` } ]), - targetUUID: player.uuid!! + targetUUID: player.uuid! }); } @@ -325,7 +325,7 @@ export function sendCustomIconUploadMessage(player: GameProfile, hash: string) { }) } -export function sendGiftCodeRedeemMessage(player: GameProfile, code: GiftCode, expiresAt?: Date | null) { +export function sendGiftCodeRedeemMessage(player: GameProfile, code: GiftCodeDocument, expiresAt?: Date | null) { if(!config.discordBot.notifications.giftCodes.enabled) return; const embed = new EmbedBuilder() diff --git a/src/libs/entitlement-expiry.ts b/src/libs/entitlement-expiry.ts index 7811236..aaabb30 100644 --- a/src/libs/entitlement-expiry.ts +++ b/src/libs/entitlement-expiry.ts @@ -1,15 +1,15 @@ import entitlement from "../database/schemas/entitlement"; -import players from "../database/schemas/players"; import { fetchSku } from "../bot/bot"; import { isConnected } from "../database/mongo"; import { sendEntitlementMessage } from "./discord-notifier"; +import { Player } from "../database/schemas/Player"; export async function checkExpiredEntitlements() { if(!isConnected()) return; const entitlements = await entitlement.find({ done: false, expires_at: { $lt: new Date() } }); if(!entitlements) return; for (const entitlement of entitlements) { - const player = await players.findOne({ 'connections.discord.id': entitlement.user_id }); + const player = await Player.findOne({ 'connections.discord.id': entitlement.user_id }); const sku = await fetchSku(entitlement.sku_id); if(!sku) continue; diff --git a/src/libs/events.ts b/src/libs/events.ts index d6856d0..4771ce0 100644 --- a/src/libs/events.ts +++ b/src/libs/events.ts @@ -1,11 +1,12 @@ import { client, fetchGuild } from "../bot/bot"; -import players from "../database/schemas/players"; -import { getCachedRoles, synchronizeRoles } from "../database/schemas/roles"; +import { Player } from "../database/schemas/Player"; +import { getCachedRoles, synchronizeDiscordRoles } from "../database/schemas/Role"; import { config } from "./config"; import { sendDiscordLinkMessage } from "./discord-notifier"; import { GameProfile } from "./game-profiles"; import Logger from "./Logger"; +// TODO: Find a better solution for this export async function onDiscordLink(player: GameProfile, userId: string) { sendDiscordLinkMessage( player, @@ -17,7 +18,7 @@ export async function onDiscordLink(player: GameProfile, userId: string) { const member = await guild?.members.fetch(userId).catch(() => null); if(member) member.roles.add(config.discordBot.notifications.accountConnections.role); - const playerData = await players.findOne({ 'connections.discord.id': userId }); + const playerData = await Player.findOne({ 'connections.discord.id': userId }); if(playerData) { let save = false; const entitlements = (await client.application!.entitlements.fetch({ user: userId })).filter(e => e.isActive()); @@ -25,7 +26,7 @@ export async function onDiscordLink(player: GameProfile, userId: string) { const role = getCachedRoles().find((role) => role.sku == entitlement.skuId); if(role) { if(playerData.addRole({ - name: role.name, + id: role.id, autoRemove: true, expiresAt: entitlement.endsAt, reason: `Discord entitlement: ${entitlement.id}` @@ -33,11 +34,11 @@ export async function onDiscordLink(player: GameProfile, userId: string) { } } if(member?.premiumSince) { - const role = config.discordBot.boosterRole; - if(role.trim().length > 0 && playerData.addRole({ name: role, reason: 'Server boost', autoRemove: true }).success) save = true; + const boosterRole = getCachedRoles().find((role) => role.sku === config.discordBot.boosterRole); + if(boosterRole && playerData.addRole({ id: boosterRole.id, reason: 'Server boost', autoRemove: true }).success) save = true; } if(save) await playerData.save(); - synchronizeRoles(); + synchronizeDiscordRoles(); } } @@ -55,13 +56,13 @@ export function onDiscordUnlink(player: GameProfile, userId: string): Promise 0) { - const role = playerData.getRole(boosterRole); - if(role && role.autoRemove && playerData.removeRole(boosterRole)) playerData.save(); + const boosterRole = getCachedRoles().find((role) => role.sku == config.discordBot.boosterRole); + if(boosterRole) { + const role = playerData.getRole(boosterRole.id); + if(role && role.autoRemove && playerData.removeRole(boosterRole.id)) playerData.save(); } } for(const role of playerData.getActiveRoles()) { diff --git a/src/libs/i18n.ts b/src/libs/i18n.ts index bd929d6..d52b4ca 100644 --- a/src/libs/i18n.ts +++ b/src/libs/i18n.ts @@ -1,7 +1,6 @@ import { existsSync, readdirSync } from "fs"; import { join } from "path"; import Logger from "./Logger"; -import players from "../database/schemas/players"; import { captureException } from "@sentry/bun"; export type Language = Map; @@ -54,13 +53,4 @@ export function translate(path: string, language: Language): string { } if(language.has(path)) return language.get(path)!; return getLanguage().get(path) || path; -} - -export async function saveLastLanguage(uuid: string, language: string) { - const player = await players.findOne({ uuid }); - if(!player) return; - if(player.last_language != language && isValidLanguage(language)) { - player.last_language = language; - await player.save(); - } } \ No newline at end of file diff --git a/src/libs/mailer.ts b/src/libs/mailer.ts index dfe1423..0bee3e3 100644 --- a/src/libs/mailer.ts +++ b/src/libs/mailer.ts @@ -1,18 +1,19 @@ import { TransportOptions, createTransport } from "nodemailer"; import { config } from "./config"; -import { join } from "path"; import Logger from "./Logger"; import { capitalCase } from "change-case"; import { I18nFunction } from "./i18n"; import moment from "moment"; import { stripColors } from "./chat-color"; +import { MailTemplate } from "../types/MailTemplate"; +import { mailTemplateFile } from "./data-accessor"; const { mailer } = config; type MailOptions = { recipient: string, subject: string, - template: string, + template: MailTemplate, variables?: string[][] } @@ -40,8 +41,8 @@ export async function verify() { export async function sendEmail({ recipient, subject, template, variables = [] }: MailOptions) { if(!mailer.enabled) return; - const file = Bun.file(join(__dirname, '..', 'mail', `${template}.html`)); - if(!file.exists()) throw new Error('Template does not exist!'); + const file = mailTemplateFile(template); + if(!(await file.exists())) throw new Error('Template does not exist!'); let message = await file.text(); for(const variable of variables) { message = message.replaceAll(`[${variable[0]}]`, variable[1].trim()); @@ -67,7 +68,7 @@ export function sendBanEmail({ address, reason, duration, appealable, i18n }: { sendEmail({ recipient: address, subject: i18n('$.email.banned.subject'), - template: 'banned', + template: MailTemplate.Banned, variables: [ ['title', i18n('$.email.banned.title')], ['greeting', i18n('$.email.greeting')], @@ -86,7 +87,7 @@ export function sendUnbanEmail(address: string, i18n: I18nFunction) { sendEmail({ recipient: address, subject: i18n('$.email.unbanned.subject'), - template: 'unbanned', + template: MailTemplate.Unbanned, variables: [ ['title', i18n('$.email.unbanned.title')], ['greeting', i18n('$.email.greeting')], @@ -101,7 +102,7 @@ export function sendTagClearEmail(address: string, tag: string, i18n: I18nFuncti sendEmail({ recipient: address, subject: i18n('$.email.tagCleared.subject'), - template: 'tag_cleared', + template: MailTemplate.TagCleared, variables: [ ['title', i18n('$.email.tagCleared.title')], ['greeting', i18n('$.email.greeting')], @@ -117,7 +118,7 @@ export function sendTagChangeEmail(address: string, oldTag: string, newTag: stri sendEmail({ recipient: address, subject: i18n('$.email.tagChanged.subject'), - template: 'tag_changed', + template: MailTemplate.TagChanged, variables: [ ['title', i18n('$.email.tagChanged.title')], ['greeting', i18n('$.email.greeting')], @@ -136,7 +137,7 @@ export function sendPositionChangeEmail(address: string, oldPosition: string, ne sendEmail({ recipient: address, subject: i18n('$.email.positionChanged.subject'), - template: 'position_changed', + template: MailTemplate.PositionChanged, variables: [ ['title', i18n('$.email.positionChanged.title')], ['greeting', i18n('$.email.greeting')], @@ -155,7 +156,7 @@ export function sendIconTypeChangeEmail(address: string, oldIcon: string, newIco sendEmail({ recipient: address, subject: i18n('$.email.iconChanged.subject'), - template: 'icon_changed', + template: MailTemplate.IconChanged, variables: [ ['title', i18n('$.email.iconChanged.title')], ['greeting', i18n('$.email.greeting')], @@ -174,7 +175,7 @@ export function sendIconClearEmail(address: string, i18n: I18nFunction) { sendEmail({ recipient: address, subject: i18n('$.email.iconCleared.subject'), - template: 'icon_cleared', + template: MailTemplate.IconCleared, variables: [ ['title', i18n('$.email.iconCleared.title')], ['greeting', i18n('$.email.greeting')], diff --git a/src/libs/metrics.ts b/src/libs/metrics.ts index 49be9bd..07d62b0 100644 --- a/src/libs/metrics.ts +++ b/src/libs/metrics.ts @@ -1,15 +1,15 @@ -import metrics from "../database/schemas/metrics"; -import players from "../database/schemas/players"; import Logger from "./Logger"; import axios from "axios"; import { fetchGuild } from "../bot/bot"; import { args } from ".."; import { config } from "./config"; -import { getCachedRoles } from "../database/schemas/roles"; -import { GlobalIcon, icons as iconList } from "../types/GlobalIcon"; +import { getCachedRoles } from "../database/schemas/Role"; +import { icons as iconList } from "../types/GlobalIcon"; import { snakeCase } from "change-case"; -import { GlobalPosition, positions as positionList } from "../types/GlobalPosition"; +import { positions as positionList } from "../types/GlobalPosition"; import { captureException } from "@sentry/bun"; +import { Metric } from "../database/schemas/Metric"; +import { Player } from "../database/schemas/Player"; let requests: number; @@ -57,30 +57,28 @@ type Addon = { export async function saveMetrics() { if(config.discordBot.syncedRoles.enabled) await (await fetchGuild())?.members.fetch(); - const users = await players.find(); - const tags = users.filter((user) => user.tag != null).length; - const staff = users.filter((user) => { + const players = await Player.find(); + const tags = players.filter((player) => player.tag != null).length; + const admins = players.filter((player) => { const adminRole = getCachedRoles().find((role) => role.name == config.metrics.adminRole); - return !!adminRole && user.getActiveRoles().some((role) => role.role.name == adminRole.name); + return !!adminRole && player.getActiveRoles().some((role) => role.role.name == adminRole.name); }).length; - const bans = users.filter((user) => user.isBanned()).length; + const bans = players.filter((player) => player.isBanned()).length; const positions = positionList.reduce((object: any, position) => { - const name = snakeCase(GlobalPosition[position]); - object[name] = users.filter((user) => name == snakeCase(user.position)).length; + object[position] = players.filter((player) => position == snakeCase(player.position)).length; return object; }, {}); const icons = iconList.reduce((object: any, icon) => { - const name = snakeCase(GlobalIcon[icon]); - object[name] = users.filter((user) => name == snakeCase(user.icon.name)).length; + object[icon] = players.filter((user) => icon == snakeCase(user.icon.type)).length; return object; }, {}); const addon = await fetchAddon('globaltags'); const mod = await fetchMod('globaltags'); - metrics.insertMany({ - players: users.length, + Metric.insertOne({ + players: players.length, tags, - admins: staff, + admins, bans, downloads: { flintmc: addon?.downloads ?? 0, @@ -89,7 +87,7 @@ export async function saveMetrics() { ratings: { flintmc: addon?.rating.rating ?? 0 }, - dailyRequests: getRequests(), + daily_requests: getRequests(), positions, icons }).catch((error) => { diff --git a/src/libs/models.ts b/src/libs/models.ts new file mode 100644 index 0000000..d23d68f --- /dev/null +++ b/src/libs/models.ts @@ -0,0 +1,330 @@ +import { t } from "elysia"; +import { config } from "./config"; +import { generateSecureCode } from "./crypto"; +import { AccountLockType } from "../database/schemas/Player"; +import { ApplicationType } from "../database/schemas/Application"; +const { validation } = config; + +export const tId = t.String({ + default: generateSecureCode() +}); + +export const tUUID = t.String({ + default: '00000000-0000-0000-0000-000000000000' +}); + +export const tString = t.String({ default: '…' }); + +export const tTimestamp = t.Integer({ + default: Date.now() +}); + +export const tHeaders = t.Object({ + authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }), + 'x-language': t.Optional(t.String({ default: 'en_us', description: 'The language to use for translations' })) +}, { error: '$.error.notAllowed' }); + +export namespace tParams { + const id = (type: string) => t.Object({ id: t.String({ description: type }) }); + const uuidAnd = (params: any) => t.Object({ uuid: t.String({ description: 'A player UUID' }), ...params }); + const uuidAndId = (type: string) => uuidAnd({ id: id(type) }); + + export const uuid = uuidAnd({}); + export const uuidAndApiKeyId = uuidAndId('An API Key ID'); + export const uuidAndBanId = uuidAndId('A ban ID'); + export const uuidAndLockId = uuidAndId('A lock ID'); + export const uuidAndNoteId = uuidAndId('A note ID'); + export const uuidAndIconHash = uuidAnd({ hash: t.String({ description: 'An icon hash' }) }); + export const uuidAndReportId = uuidAndId('A report ID'); + export const giftCodeId = id('A gift code ID'); + export const applicationId = id('An application ID'); + export const reportId = id('A report ID'); + export const roleId = id('A role ID'); +} + +export namespace tRequestBody { + export const options = { + error: '$.error.invalidBody', + additionalProperties: true + }; + + export const ApiKey = t.Object({ + name: t.String({ error: '$.error.wrongType;;[["field", "name"], ["type", "string"]]', description: 'An API key name' }) + }, { description: 'An API key object', ...options }); + + export const AppealBan = t.Object({ + reason: t.String({ error: '$.error.wrongType;;[["field", "reason"], ["type", "string"]]' }) + }, { description: 'A ban appeal object', ...options }); + + export const CreateBan = t.Object({ + reason: t.String({ minLength: 1, error: '$.error.wrongType;;[["field", "reason"], ["type", "string"]]', description: 'A ban reason' }), + appealable: t.Optional(t.Boolean({ error: '$.error.wrongType;;[["field", "appealable"], ["type", "boolean"]]', description: 'A boolean indicating if the ban is appealable' })), + duration: t.Optional(t.Number({ error: '$.error.wrongType;;[["field", "duration"], ["type", "number"]]', description: 'A ban duration' })) + }, { description: 'A ban creation object', ...options }); + + export const EditBan = t.Object({ + reason: t.Optional(t.String({ minLength: 1, error: '$.error.wrongType;;[["field", "reason"], ["type", "string"]]', description: 'A ban reason' })), + appealable: t.Optional(t.Boolean({ error: '$.error.wrongType;;[["field", "appealable"], ["type", "boolean"]]', description: 'A boolean indicating if the ban is appealable' })) + }, { description: 'A ban edit object', ...options }); + + export const UploadCustomIcon = t.Object({ + image: t.File({ type: 'image/png', error: '$.error.wrongType;;[["field", "image"], ["type", "png file"]]', description: 'A png image file' }) + }, { description: 'A ban edit object', ...options }); + + export const CreateLock = t.Object({ + type: t.Enum(AccountLockType), + reason: t.String({ maxLength: validation.notes.maxLength, error: `$.error.wrongType;;[["field", "reason"], ["type", "string"]]`, description: 'A lock reason' }), + duration: t.Optional(t.Number({ error: '$.error.wrongType;;[["field", "duration"], ["type", "number"]]', description: 'A lock duration' })) + }, { description: 'A lock creation object', ...options }); + + export const EditLock = t.Object({ + reason: t.Optional(t.String({ maxLength: validation.notes.maxLength, error: `$.error.wrongType;;[["field", "reason"], ["type", "string"]]`, description: 'A lock reason' })) + }, { description: 'A lock edit object', ...options }); + + export const Note = t.Object({ + content: t.String({ maxLength: validation.notes.maxLength, error: `$.notes.create.max_length;;[["max", "${validation.notes.maxLength}"]]`, description: 'A player note' }) + }, { description: 'A note object', ...options }); + + export const Report = t.Object({ + reason: t.String({ error: '$.error.wrongType;;[["field", "reason"], ["type", "string"]]', description: 'A report reason' }) + }, { description: 'A report object', ...options }); + + export const TagSettings = t.Object({ + tag: t.Optional(t.Nullable(t.String({ error: '$.error.wrongType;;[["field", "tag"], ["type", "string"]]', description: 'The tag content' }))), + position: t.Optional(t.String({ error: '$.error.wrongType;;[["field", "position"], ["type", "string"]]', description: 'The position of the tag' })), + icon: t.Optional(t.Object({ + type: t.Optional(t.String({ error: '$.error.wrongType;;[["field", "icon.type"], ["type", "string"]]', description: 'The type of the icon' })), + hash: t.Optional(t.Nullable(t.String({ error: '$.error.wrongType;;[["field", "icon.hash"], ["type", "string"]]', description: 'The hash of the icon' }))) + }, { error: '$.error.wrongType;;[["field", "icon"], ["type", "object"]]' })), + }, { description: 'A tag settings object', ...options }); + + export const CreateGiftCode = t.Object({ + name: t.String({ error: '$.error.wrongType;;[["field", "name"], ["type", "string"]]' }), + code: t.Optional(t.String({ error: '$.error.wrongType;;[["field", "code"], ["type", "string"]]' })), + role: t.String({ error: '$.error.wrongType;;[["field", "role"], ["type", "string"]]' }), + max_uses: t.Number({ error: '$.error.wrongType;;[["field", "max_uses"], ["type", "number"]]' }), + code_expiration: t.Optional(t.Number({ error: '$.error.wrongType;;[["field", "code_expiration"], ["type", "number"]]' })), + gift_duration: t.Optional(t.Number({ error: '$.error.wrongType;;[["field", "gift_duration"], ["type", "number"]]' })) + }, { description: 'A gift code creation object', ...options }); + + export const Role = t.Object({ + name: t.String({ error: '$.error.wrongType;;[["field", "name"], ["type", "string"]]' }), + color: t.Optional(t.Nullable(t.String({ minLength: 6, maxLength: 6, error: '$.error.wrongType;;[["field", "color"], ["type", "string"]]' }))), + permissions: t.Optional(t.Integer({ error: '$.error.wrongType;;[["field", "permissions"], ["type", "integer"]]' })) + }, { description: 'A role object', ...options }); + + export const StaffCategory = t.Object({ + name: t.String({ minLength: 1, error: '$.error.wrongType;;[["field", "name"], ["type", "string"]]' }) + }, { description: 'A staff category object', ...options }); + + export const CreateStaffMember = t.Object({ + uuid: t.String({ error: '$.error.wrongType;;[["field", "uuid"], ["type", "string"]]' }), + category: t.String({ error: '$.error.wrongType;;[["field", "category"], ["type", "string"]]' }), + description: t.Optional(t.Nullable(t.String({ error: '$.error.wrongType;;[["field", "description"], ["type", "string"]]' }))) + }, { description: 'A staff member creation object', ...options }); + + export const EditStaffMember = t.Object({ + category: t.Optional(t.String({ error: '$.error.wrongType;;[["field", "category"], ["type", "string"]]' })), + description: t.Optional(t.Nullable(t.String({ error: '$.error.wrongType;;[["field", "description"], ["type", "string"]]' }))) + }, { description: 'A staff member edit object', ...options }); +} + +export namespace tResponseBody { + export const Message = t.Object({ + message: t.String({ default: 'Some message', description: 'The message to be returned' }) + }, { description: 'A message object' }); + + export const MessageWithExpiration = t.Object({ + message: t.String({ default: 'Some message', description: 'The message to be returned' }), + expires_at: t.Nullable(tTimestamp) + }, { description: 'A message object with an expiration timestamp' }); + + export const Error = t.Object({ + error: t.String({ default: 'Some error', description: 'The error message to be returned' }) + }, { description: 'An error object' }); + + export const TagData = t.Object({ + uuid: tUUID, + tag: t.Nullable(tString), + position: t.String({ default: 'above', description: 'The position of the tag' }), + icon: t.Object({ + type: t.String({ default: 'none', description: 'The type of the icon' }), + hash: t.Nullable(t.String({ default: generateSecureCode(32), description: 'The hash of the icon' })) + }), + referrals: t.Object({ + has_referred: t.Boolean(), + total_referrals: t.Integer(), + current_month_referrals: t.Integer() + }), + roleIcon: t.Nullable(tString), + roles: t.Array(t.String()), + permissions: t.Integer() + }, { description: 'A tag data object' }); + + export const EditTagSettings = t.Object({ + errors: t.Object({ + tag: t.Nullable(tString, { description: 'An error message for the tag' }), + position: t.Nullable(tString, { description: 'An error message for the position' }), + icon: t.Nullable(tString, { description: 'An error message for the icon' }) + }, { description: 'A list of errors that occurred during the update' }), + data: t.Object({ + tag: t.Nullable(tString), + position: t.String({ default: 'above', description: 'The position of the tag' }), + icon: t.Object({ + type: t.String({ default: 'none', description: 'The type of the icon' }), + hash: t.Optional(t.Nullable(t.String({ default: generateSecureCode(32), description: 'The hash of the icon' }))) + }) + }, { description: 'The updated tag settings' }) + }, { description: 'An edit tag settings response object' }); + + export const ApiInfo = t.Object({ + version: t.String({ default: config.version, description: 'The API version' }), + requests: t.Number({ default: 0, description: 'The amount of requests made since the start of the day' }) + }, { description: 'An API info object' }); + + export const StaffList = t.Array(t.Object({ + id: tId, + name: tString, + members: t.Array(t.Object({ + uuid: tUUID, + description: t.Nullable(tString), + avatar_url: tString, + joined_at: tTimestamp + })) + }), { description: 'A staff category list with members' }); +} + +export namespace tSchema { + export const PublicApiKey = t.Object({ + id: tId, + name: tString, + created_at: tTimestamp, + last_used: t.Nullable(tTimestamp) + }, { description: 'An API key object' }); + + export const PrivateApiKey = t.Object({ + id: tId, + name: tString, + key: t.String({ default: `sk_${generateSecureCode(32)}` }), + created_at: tTimestamp, + last_used: t.Nullable(tTimestamp) + }, { description: 'An API key object' }); + + export const Ban = t.Object({ + id: tId, + reason: tString, + staff: tUUID, + appealable: t.Boolean({ default: true }), + appealed: t.Boolean({ default: false }), + banned_at: tTimestamp, + expires_at: t.Nullable(tTimestamp) + }, { description: 'A ban object' }); + + export const Lock = t.Object({ + id: tId, + type: t.Enum(AccountLockType), + reason: tString, + staff: tUUID, + locked_at: tTimestamp, + expires_at: t.Nullable(tTimestamp) + }, { description: 'A lock object' }); + + export const Note = t.Object({ + id: tId, + text: tString, + author: tUUID, + created_at: tTimestamp + }, { description: 'A note object' }); + + export const TagContext = t.Object({ + tag: tString, + position: t.String({ default: 'above' }), + icon: t.Object({ + type: t.String({ default: 'none' }), + hash: t.Nullable(t.String({ default: generateSecureCode(32) })) + }) + }); + + export const Report = t.Object({ + id: tId, + reported_uuid: tUUID, + reporter_uuid: tUUID, + reason: tString, + context: TagContext, + is_resolved: t.Boolean(), + created_at: tTimestamp, + last_updated: tTimestamp + }); + + export const Application = t.Object({ + id: tId, + applicant: tUUID, + type: t.Enum(ApplicationType), + status: t.String(), + answers: t.Array(t.Object({ question: t.String(), answer: t.String() })), + review: t.Object({ + reviewer: t.Nullable(tUUID), + timestamp: t.Nullable(tTimestamp) + }), + submitted_at: tTimestamp + }); + + export const GiftCode = t.Object({ + id: tId, + name: tString, + code: t.String({ default: generateSecureCode(12) }), + uses: t.Array(t.String()), + max_uses: t.Number(), + gift: t.Object({ + type: t.String({ default: 'role' }), + value: tString, + duration: t.Nullable(t.Number()) + }), + created_by: tUUID, + created_at: tTimestamp, + expires_at: t.Nullable(tTimestamp) + }, { description: 'A gift code object' }); + + export const Metric = t.Object({ + time: tTimestamp, + users: t.Number(), + tags: t.Number(), + admins: t.Number(), + bans: t.Number(), + downloads: t.Object({ flintmc: t.Number(), modrinth: t.Number() }), + ratings: t.Object({ flintmc: t.Number() }), + daily_requests: t.Number(), + positions: t.Object({}, { default: {}, additionalProperties: true, description: 'All position counts' }), + icons: t.Object({}, { default: {}, additionalProperties: true, description: 'All icon counts' }) + }, { description: 'A metric object' }); + + export const Role = t.Object({ + id: tId, + name: tString, + position: t.Integer(), + hasIcon: t.Boolean(), + color: t.Nullable(t.String()), + permissions: t.Number() + }, { description: 'A role object' }); + + export const StaffMember = t.Object({ + uuid: tUUID, + username: tString, + category: tString, + description: t.Nullable(tString), + joined_at: tTimestamp + }, { description: 'A staff member object' }); + + export const StaffCategory = t.Object({ + id: tId, + name: tString, + position: t.Integer() + }, { description: 'A staff category object' }); + + export const MemberlistStaffCategory = t.Object({ + id: tId, + name: tString, + position: t.Integer(), + members: t.Integer() + }, { description: 'A staff category object' }); +} \ No newline at end of file diff --git a/src/routes/applications.ts b/src/routes/applications.ts new file mode 100644 index 0000000..c66e848 --- /dev/null +++ b/src/routes/applications.ts @@ -0,0 +1,78 @@ +import { t } from "elysia"; +import { ElysiaApp } from ".."; +import { Application } from "../database/schemas/Application"; +import { DocumentationCategory } from "../types/DocumentationCategory"; +import { Permission } from "../types/Permission"; +import { tHeaders, tParams, tResponseBody, tSchema } from "../libs/models"; + +export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status }) => { // Get all applications + if(!session?.player?.hasPermission(Permission.ViewApplications)) return status(403, { error: i18n('$.error.notAllowed') }); + + const applications = await Application.find(); + + return applications.map((application) => ({ + id: application.id, + applicant: application.applicant, + type: application.type, + status: application.status, + answers: application.answers, + review: { + reviewer: application.review.reviewer, + timestamp: application.review.timestamp?.getTime() || null + }, + submitted_at: application.submitted_at.getTime() + })); +}, { + detail: { + tags: [DocumentationCategory.Applications], + description: 'Get all applications' + }, + response: { + 200: t.Array(tSchema.Application, { description: 'An application list' }), + 403: tResponseBody.Error + }, + headers: tHeaders +}).get('/:id', async ({ session, params, i18n, status }) => { // Get a specific application + if(!session?.player?.hasPermission(Permission.ViewApplications)) return status(403, { error: i18n('$.error.notAllowed') }); + + const application = await Application.findOne({ id: params.id }); + if(!application) return status(404, { error: i18n('$.applications.not_found') }); + const { id, applicant, type, status: applicationStatus, answers, review, submitted_at } = application; + + return { id, applicant, type, status: applicationStatus, answers, review: { reviewer: review.reviewer, timestamp: review.timestamp?.getTime() || null }, submitted_at: submitted_at.getTime() }; +}, { + detail: { + tags: [DocumentationCategory.Applications], + description: 'Get a specific application' + }, + response: { + 200: tSchema.Application, + 403: tResponseBody.Error, + 404: tResponseBody.Error + }, + params: tParams.applicationId, + headers: tHeaders +}) // TODO: Add router to create and edit applications +.delete('/:id', async ({ session, params, i18n, status }) => { // Delete gift code + if(!session?.player?.hasPermission(Permission.DeleteApplications)) return status(403, { error: i18n('$.error.notAllowed') }); + + const application = await Application.findOne({ id: params.id }); + if(!application) return status(404, { error: i18n('$.applications.not_found') }); + await application.deleteOne(); + + // TODO: Add mod log + + return { message: i18n('$.applications.deleted') }; +}, { + detail: { + tags: [DocumentationCategory.Applications], + description: 'Delete an application' + }, + response: { + 200: tResponseBody.Message, + 403: tResponseBody.Error, + 404: tResponseBody.Error + }, + params: tParams.applicationId, + headers: tHeaders +}); \ No newline at end of file diff --git a/src/routes/gift-codes.ts b/src/routes/gift-codes.ts index 8efb2c8..c99f451 100644 --- a/src/routes/gift-codes.ts +++ b/src/routes/gift-codes.ts @@ -3,12 +3,14 @@ import { Permission } from "../types/Permission"; import { ElysiaApp } from ".."; import { ModLogType, sendGiftCodeRedeemMessage, sendModLogMessage } from "../libs/discord-notifier"; import { formatUUID } from "../libs/game-profiles"; -import giftCodes, { createGiftCode } from "../database/schemas/gift-codes"; +import { createGiftCode, GiftCode, GiftType } from "../database/schemas/GiftCode"; +import { tResponseBody, tHeaders, tParams, tRequestBody, tSchema } from "../libs/models"; +import { DocumentationCategory } from "../types/DocumentationCategory"; export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status }) => { // Get gift code list if(!session?.player?.hasPermission(Permission.ViewGiftCodes)) return status(403, { error: i18n('$.error.notAllowed') }); - const codes = await giftCodes.find(); + const codes = await GiftCode.find(); return codes.map((code) => ({ id: code.id, @@ -27,50 +29,44 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status } })); }, { detail: { - tags: ['Gift codes'], - description: 'Returns all gift codes' + tags: [DocumentationCategory.GiftCodes], + description: 'Get all gift codes' }, response: { - 200: t.Array(t.Object({ id: t.String(), name: t.String(), code: t.String(), uses: t.Array(t.String()), max_uses: t.Number(), gift: t.Object({ type: t.String(), value: t.String(), duration: t.Union([t.Number(), t.Null()]) }), created_at: t.Number(), expires_at: t.Union([t.Number(), t.Null()]) }), { description: 'A list of gift codes' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage gift codes' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: t.Array(tSchema.GiftCode, { description: 'A gift code list' }), + 403: tResponseBody.Error }, - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) -}).get('/:code', async ({ session, params, i18n, status }) => { // Get info of a specific code + headers: tHeaders +}).get('/:code', async ({ session, params, i18n, status }) => { // Get a specific code if(!session?.player?.hasPermission(Permission.ViewGiftCodes)) return status(403, { error: i18n('$.error.notAllowed') }); - const code = await giftCodes.findOne({ $or: [{ id: params.code }, { code: params.code }] }); + const code = await GiftCode.findOne({ $or: [{ id: params.id }, { code: params.id }] }); if(!code) return status(404, { error: i18n('$.gift_codes.not_found') }); const { id, name, code: giftCode, uses, max_uses, gift, created_by, created_at, expires_at } = code; return { id, name, code: giftCode, uses: uses.map((uuid) => formatUUID(uuid)), max_uses, gift: { type: gift.type, value: gift.value, duration: gift.duration || null }, created_by: formatUUID(created_by), created_at: created_at.getTime(), expires_at: expires_at?.getTime() || null }; }, { detail: { - tags: ['Gift codes'], - description: 'Returns info about a specific gift code' + tags: [DocumentationCategory.GiftCodes], + description: 'Get a specific gift code' }, response: { - 200: t.Object({ id: t.String(), name: t.String(), code: t.String(), uses: t.Array(t.String()), max_uses: t.Number(), gift: t.Object({ type: t.String(), value: t.String(), duration: t.Union([t.Number(), t.Null()]) }), created_by: t.String(), created_at: t.Number(), expires_at: t.Union([t.Number(), t.Null()]) }, { description: 'The gift code info' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage gift codes' }), - 404: t.Object({ error: t.String() }, { description: 'The gift code was not found' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tSchema.GiftCode, + 403: tResponseBody.Error, + 404: tResponseBody.Error }, - params: t.Object({ code: t.String({ description: 'The gift code' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) -}).post('/:code/redeem', async ({ session, params, i18n, status }) => { // Get info of a specific code + params: tParams.giftCodeId, + headers: tHeaders +}).post('/:code/redeem', async ({ session, params, i18n, status }) => { // Redeem code if(!session?.player) return status(403, { error: i18n('$.error.notAllowed') }); const { player } = session; if(!player) return status(403, { error: i18n('$.error.notAllowed') }); - const code = await giftCodes.findOne({ code: params.code }); + const code = await GiftCode.findOne({ code: params.id }); if(!code || !code.isValid()) return status(404, { error: i18n('$.gift_codes.not_found') }); - if(code.uses.includes(player.uuid)) return status(422, { error: i18n('$.gift_codes.already_redeemed') }); + if(code.uses.includes(player.uuid)) return status(409, { error: i18n('$.gift_codes.already_redeemed') }); - const { success, expiresAt } = player.addRole({ name: code.gift.value, reason: `Gift code: ${code.code}`, autoRemove: false, duration: code.gift.duration }); + const { success, expiresAt } = player.addRole({ id: code.gift.value, reason: `Gift code: ${code.code}`, autoRemove: false, duration: code.gift.duration }); if(!success) return status(409, { error: i18n('$.gift_codes.already_have_role') }); code.uses.push(player.uuid); await player.save(); @@ -81,20 +77,17 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status } return { message: i18n(expiresAt ? '$.gift_codes.redeemed_temporarily' : '$.gift_codes.redeemed_permanently').replace('', code.gift.value), expires_at: expiresAt?.getTime() || null }; }, { detail: { - tags: ['Gift codes'], - description: 'Redeems a gift code' + tags: [DocumentationCategory.GiftCodes], + description: 'Redeem a gift code' }, response: { - 200: t.Object({ message: t.String(), expires_at: t.Union([t.Number(), t.Null()]) }, { description: 'The gift code was redeemed' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not authenticated' }), - 404: t.Object({ error: t.String() }, { description: 'The gift code was not found' }), - 409: t.Object({ error: t.String() }, { description: 'You already have the reward role' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tResponseBody.MessageWithExpiration, + 403: tResponseBody.Error, + 404: tResponseBody.Error, + 409: tResponseBody.Error, }, - params: t.Object({ code: t.String({ description: 'The gift code' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) + params: tParams.giftCodeId, + headers: tHeaders }).post('/', async ({ session, body: { name, code, role, max_uses: maxUses, code_expiration: codeExpiration, gift_duration: giftDuration }, i18n, status }) => { // Create a gift code if(!session?.player?.hasPermission(Permission.CreateGiftCodes)) return status(403, { error: i18n('$.error.notAllowed') }); @@ -106,7 +99,7 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status } code: code?.trim() || undefined, maxUses, gift: { - type: 'role', + type: GiftType.Role, value: role, duration: giftExpiresAt }, @@ -138,22 +131,20 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status } }; }, { detail: { - tags: ['Gift codes'], - description: 'Creates a new gift code' + tags: [DocumentationCategory.GiftCodes], + description: 'Create a new gift code' }, response: { - 200: t.Object({ id: t.String(), name: t.String(), code: t.String(), uses: t.Array(t.String()), max_uses: t.Number(), gift: t.Object({ type: t.String(), value: t.String(), duration: t.Union([t.Number(), t.Null()]) }), created_at: t.Number(), expires_at: t.Union([t.Number(), t.Null()]) }, { description: 'The gift code was created' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage gift codes' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tSchema.GiftCode, + 403: tResponseBody.Error }, - body: t.Object({ name: t.String({ error: '$.error.wrongType;;[["field", "name"], ["type", "string"]]' }), code: t.Optional(t.String({ error: '$.error.wrongType;;[["field", "code"], ["type", "string"]]' })), role: t.String({ error: '$.error.wrongType;;[["field", "role"], ["type", "string"]]' }), max_uses: t.Number({ error: '$.error.wrongType;;[["field", "max_uses"], ["type", "number"]]' }), code_expiration: t.Optional(t.Number({ error: '$.error.wrongType;;[["field", "code_expiration"], ["type", "number"]]' })), gift_duration: t.Optional(t.Number({ error: '$.error.wrongType;;[["field", "gift_duration"], ["type", "number"]]' })) }, { error: '$.error.invalidBody', additionalProperties: true }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) -}).delete('/:code', async ({ session, params, i18n, status }) => { // Delete gift code + body: tRequestBody.CreateGiftCode, + headers: tHeaders +}) // TODO: Add route to patch an existing gift code +.delete('/:code', async ({ session, params, i18n, status }) => { // Delete gift code if(!session?.player?.hasPermission(Permission.DeleteGiftCodes)) return status(403, { error: i18n('$.error.notAllowed') }); - const code = await giftCodes.findOne({ id: params.code }); + const code = await GiftCode.findOne({ id: params.id }); if(!code) return status(404, { error: i18n('$.gift_codes.not_found') }); await code.deleteOne(); @@ -167,17 +158,14 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status } return { message: i18n('$.gift_codes.deleted') }; }, { detail: { - tags: ['Gift codes'], - description: 'Deletes a gift code' + tags: [DocumentationCategory.GiftCodes], + description: 'Delete a gift code' }, response: { - 200: t.Object({ message: t.String() }, { description: 'The gift code was deleted' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage gift codes' }), - 404: t.Object({ error: t.String() }, { description: 'The gift code was not found' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tResponseBody.Message, + 403: tResponseBody.Error, + 404: tResponseBody.Error }, - params: t.Object({ code: t.String({ description: 'The gift code' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) + params: tParams.giftCodeId, + headers: tHeaders }); \ No newline at end of file diff --git a/src/routes/index.ts b/src/routes/index.ts index bec3d41..7dc21e7 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,71 +1,55 @@ import { ElysiaApp } from ".."; import { getRequests } from "../libs/metrics"; -import { version } from "../../package.json"; import { Context, t } from "elysia"; -import Metrics from "../database/schemas/metrics"; import { formatUUID } from "../libs/game-profiles"; -import players from "../database/schemas/players"; +import { Metric } from "../database/schemas/Metric"; +import { Player } from "../database/schemas/Player"; +import { tResponseBody, tSchema } from "../libs/models"; +import { config } from "../libs/config"; +import { DocumentationCategory } from "../types/DocumentationCategory"; export default (app: ElysiaApp) => app.get('/', () => ({ - version, - requests: getRequests(), - commit: { - branch: 'deprecated', - sha: 'deprecated', - tree: 'deprecated' - } + version: config.version, + requests: getRequests() }), { detail: { - tags: ['API'], - description: 'Returns some basic info about the API' + tags: [DocumentationCategory.Api], + description: 'Get API information' }, response: { - 200: t.Object({ version: t.String(), requests: t.Number(), commit: t.Object({ branch: t.String(), sha: t.Union([t.String(), t.Null()]), tree: t.Union([t.String(), t.Null()]) }) }, { description: 'Some basic API info' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tResponseBody.ApiInfo } }).get('/metrics', async ({ query: { latest } }) => { - const metrics = await Metrics.find(); + const metrics = await Metric.find(); return metrics.filter((doc) => { if(latest != 'true') return true; return doc.id == (metrics.at(-1)?.id ?? 0); }).map((metric) => ({ - time: new Date(metric.createdAt).getTime(), + time: metric.created_at.getTime(), users: metric.players, tags: metric.tags, admins: metric.admins, bans: metric.bans, downloads: metric.downloads, ratings: metric.ratings, - dailyRequests: metric.dailyRequests ?? 0, + daily_requests: metric.daily_requests ?? 0, positions: metric.positions, icons: metric.icons })); }, { detail: { - tags: ['API'], + tags: [DocumentationCategory.Api], description: 'Get API statistics' }, response: { - 200: t.Array(t.Object({ - time: t.Number({ default: Date.now() }), - users: t.Number(), - tags: t.Number(), - admins: t.Number(), - bans: t.Number(), - downloads: t.Object({ flintmc: t.Number(), modrinth: t.Number() }, { additionalProperties: true }), - ratings: t.Object({ flintmc: t.Number() }, { additionalProperties: true }), - dailyRequests: t.Number(), - positions: t.Object({}, { default: {}, additionalProperties: true, description: 'All position counts' }), - icons: t.Object({}, { default: {}, additionalProperties: true, description: 'All icon counts' }) - }, { description: 'The server is reachable' })), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: t.Array(tSchema.Metric, { description: 'A metric list' }) }, query: t.Object({ latest: t.Optional(t.String({ error: '$.error.wrongType;;[["field", "element"], ["type", "string"]]' })) }, { additionalProperties: true }) }).get('/referrals', async () => { - const data = await players.find(); + const data = await Player.find(); const totalReferrals = data.filter((player) => player.referrals.total.length > 0).sort((a, b) => b.referrals.total.length - a.referrals.total.length).slice(0, 10); const monthReferrals = data.filter((player) => player.referrals.current_month > 0).sort((a, b) => b.referrals.current_month - a.referrals.current_month).slice(0, 10); @@ -83,23 +67,21 @@ export default (app: ElysiaApp) => app.get('/', () => ({ }; }, { detail: { - tags: ['API'], - description: 'Get the referral leaderboard' + tags: [DocumentationCategory.Referrals], + description: 'Get the referral leaderboards' }, response: { 200: t.Object({ total: t.Array(t.Object({ uuid: t.String(), total_referrals: t.Number(), current_month_referrals: t.Number() })), current_month: t.Array(t.Object({ uuid: t.String(), total_referrals: t.Number(), current_month_referrals: t.Number() })) - }, { description: 'The referral leaderboards' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + }, { description: 'The referral leaderboards' }) } }).get('/ping', ({ status }: Context) => { return status(204, '') }, { detail: { - tags: ['API'], - description: 'Used by uptime checkers. This route is not being logged' + tags: [DocumentationCategory.Api], + description: 'Check the status of the API. This route is not being logged' }, response: { - 204: t.Any({ description: 'The server is reachable' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 204: t.Any({ description: 'Empty response' }) } -}) \ No newline at end of file +}); \ No newline at end of file diff --git a/src/routes/players/[uuid]/api-keys.ts b/src/routes/players/[uuid]/api-keys.ts index 3709f09..8f4d872 100644 --- a/src/routes/players/[uuid]/api-keys.ts +++ b/src/routes/players/[uuid]/api-keys.ts @@ -1,15 +1,17 @@ import { t } from "elysia"; -import players from "../../../database/schemas/players"; import { ModLogType, sendModLogMessage } from "../../../libs/discord-notifier"; import { Permission } from "../../../types/Permission"; import { GameProfile, stripUUID } from "../../../libs/game-profiles"; import { ElysiaApp } from "../../.."; import { generateSecureCode } from "../../../libs/crypto"; +import { Player } from "../../../database/schemas/Player"; +import { tHeaders, tParams, tRequestBody, tResponseBody, tSchema } from "../../../libs/models"; +import { DocumentationCategory } from "../../../types/DocumentationCategory"; export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, status }) => { // Get api key list if(!session?.player?.hasPermission(Permission.ViewApiKeys)) return status(403, { error: i18n('$.error.notAllowed') }); - const player = await players.findOne({ uuid: stripUUID(params.uuid) }); + const player = await Player.findOne({ uuid: stripUUID(params.uuid) }).lean(); if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); return player.api_keys.map((key) => ({ @@ -20,23 +22,20 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, })); }, { detail: { - tags: ['Admin'], + tags: [DocumentationCategory.ApiKeys], description: 'Get all player API keys' }, response: { - 200: t.Array(t.Object({ id: t.String(), name: t.String(), created_at: t.Number(), last_used: t.Union([t.Number(), t.Null()]) }), { description: 'The API key list' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage API keys' }), - 404: t.Object({ error: t.String() }, { description: 'The player was not found' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: t.Array(tSchema.PublicApiKey, { description: 'An API key list' }), + 403: tResponseBody.Error, + 404: tResponseBody.Error }, - params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) + params: tParams.uuid, + headers: tHeaders }).get('/:id', async ({ session, params, i18n, status }) => { // Get info of specific api key if(!session?.player?.hasPermission(Permission.ViewApiKeys)) return status(403, { error: i18n('$.error.notAllowed') }); - const player = await players.findOne({ uuid: stripUUID(params.uuid) }); + const player = await Player.findOne({ uuid: stripUUID(params.uuid) }).lean(); if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); const key = player.getApiKey(params.id); @@ -46,23 +45,20 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, return { id, name, created_at: created_at.getTime(), last_used: last_used?.getTime() || null }; }, { detail: { - tags: ['Admin'], - description: 'Get info about a specific API key' + tags: [DocumentationCategory.ApiKeys], + description: 'Get a specific API key' }, response: { - 200: t.Object({ id: t.String(), name: t.String(), created_at: t.Number(), last_used: t.Union([t.Number(), t.Null()]) }, { description: 'The API key info' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage API keys' }), - 404: t.Object({ error: t.String() }, { description: 'The player or API key was not found' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tSchema.PublicApiKey, + 403: tResponseBody.Error, + 404: tResponseBody.Error }, - params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }), id: t.String({ description: 'The API key ID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) + params: tParams.uuidAndApiKeyId, + headers: tHeaders }).post('/', async ({ session, body: { name }, params, i18n, status }) => { // Create an API key if(!session?.player?.hasPermission(Permission.CreateApiKeys)) return status(403, { error: i18n('$.error.notAllowed') }); - const player = await players.findOne({ uuid: stripUUID(params.uuid) }); + const player = await Player.findOne({ uuid: stripUUID(params.uuid) }); if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); const key = player.createApiKey(name.trim()); @@ -79,30 +75,27 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, return { id: key.id, name: key.name, + key: key.key, created_at: key.created_at.getTime(), last_used: key.last_used?.getTime() || null }; }, { detail: { - tags: ['Admin'], + tags: [DocumentationCategory.ApiKeys], description: 'Create an API key' }, response: { - 200: t.Object({ id: t.String(), name: t.String(), created_at: t.Number(), last_used: t.Union([t.Number(), t.Null()]) }, { description: 'The created API key' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage API keys' }), - 404: t.Object({ error: t.String() }, { description: 'The player was not found' }), - 409: t.Object({ error: t.String() }, { description: 'An API key with this name already exists' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tSchema.PrivateApiKey, + 403: tResponseBody.Error, + 404: tResponseBody.Error, }, - body: t.Object({ name: t.String({ error: '$.error.wrongType;;[["field", "name"], ["type", "string"]]' }) }, { error: '$.error.invalidBody', additionalProperties: true }), - params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) + body: tRequestBody.ApiKey, + params: tParams.uuid, + headers: tHeaders }).post('/:id/regenerate', async ({ session, params, i18n, status }) => { // Regenerate API key if(!session?.player?.hasPermission(Permission.EditApiKeys)) return status(403, { error: i18n('$.error.notAllowed') }); - const player = await players.findOne({ uuid: stripUUID(params.uuid) }); + const player = await Player.findOne({ uuid: stripUUID(params.uuid) }); if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); const key = player.getApiKey(params.id); if(!key) return status(404, { error: i18n('$.api_keys.not_found') }); @@ -121,29 +114,26 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, return { id: key.id, name: key.name, + key: key.key, created_at: key.created_at.getTime(), last_used: key.last_used?.getTime() || null }; }, { detail: { - tags: ['Admin'], - description: 'Edit an existing API key' + tags: [DocumentationCategory.ApiKeys], + description: 'Regenerate an existing API key' }, response: { - 200: t.Object({ id: t.String(), name: t.String(), created_at: t.Number(), last_used: t.Union([t.Number(), t.Null()]) }, { description: 'The regenerated API key' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage API keys' }), - 404: t.Object({ error: t.String() }, { description: 'The player or key was not found' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tSchema.PrivateApiKey, + 403: tResponseBody.Error, + 404: tResponseBody.Error, }, - body: t.Object({ name: t.String({ error: '$.error.wrongType;;[["field", "name"], ["type", "string"]]' }) }, { error: '$.error.invalidBody', additionalProperties: true }), - params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }), id: t.String({ description: 'The API key ID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) + params: tParams.uuidAndApiKeyId, + headers: tHeaders }).patch('/:id', async ({ session, params, body: { name }, i18n, status }) => { // Edit API key if(!session?.player?.hasPermission(Permission.EditApiKeys)) return status(403, { error: i18n('$.error.notAllowed') }); - const player = await players.findOne({ uuid: stripUUID(params.uuid) }); + const player = await Player.findOne({ uuid: stripUUID(params.uuid) }); if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); const key = player.getApiKey(params.id); @@ -168,24 +158,21 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, }; }, { detail: { - tags: ['Admin'], - description: 'Regenerate an existing API key' + tags: [DocumentationCategory.ApiKeys], + description: 'Edit an existing API key' }, response: { - 200: t.Object({ id: t.String(), name: t.String(), created_at: t.Number(), last_used: t.Union([t.Number(), t.Null()]) }, { description: 'The edited API Key' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage API keys' }), - 404: t.Object({ error: t.String() }, { description: 'The player or key was not found' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tSchema.PublicApiKey, + 403: tResponseBody.Error, + 404: tResponseBody.Error, }, - body: t.Object({ name: t.String({ error: '$.error.wrongType;;[["field", "name"], ["type", "string"]]' }) }, { error: '$.error.invalidBody', additionalProperties: true }), - params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }), id: t.String({ description: 'The API key ID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) + body: tRequestBody.ApiKey, + params: tParams.uuidAndApiKeyId, + headers: tHeaders }).delete('/:id', async ({ session, params, i18n, status }) => { // Delete api key if(!session?.player?.hasPermission(Permission.DeleteApiKeys)) return status(403, { error: i18n('$.error.notAllowed') }); - const player = await players.findOne({ uuid: stripUUID(params.uuid) }); + const player = await Player.findOne({ uuid: stripUUID(params.uuid) }); if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); const key = player.getApiKey(params.id); if(!key || !player.deleteApiKey(key.id)) return status(404, { error: i18n('$.api_keys.not_found') }); @@ -203,17 +190,14 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, return { message: i18n('$.api_keys.deleted') }; }, { detail: { - tags: ['Admin'], + tags: [DocumentationCategory.ApiKeys], description: 'Delete an API key' }, response: { - 200: t.Object({ message: t.String() }, { description: 'The success message' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage API keys' }), - 404: t.Object({ error: t.String() }, { description: 'The player or key was not found' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tResponseBody.Message, + 403: tResponseBody.Error, + 404: tResponseBody.Error }, - params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }), id: t.String({ description: 'The API key ID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) + params: tParams.uuidAndApiKeyId, + headers: tHeaders }); \ No newline at end of file diff --git a/src/routes/players/[uuid]/bans.ts b/src/routes/players/[uuid]/bans.ts index 7ed2c0b..7deead2 100644 --- a/src/routes/players/[uuid]/bans.ts +++ b/src/routes/players/[uuid]/bans.ts @@ -1,77 +1,81 @@ import { t } from "elysia"; -import players from "../../../database/schemas/players"; import { getI18nFunctionByLanguage } from "../../../middleware/fetch-i18n"; import { ModLogType, sendBanAppealMessage, sendModLogMessage } from "../../../libs/discord-notifier"; import { sendBanEmail, sendUnbanEmail } from "../../../libs/mailer"; import { Permission } from "../../../types/Permission"; import { formatUUID, stripUUID } from "../../../libs/game-profiles"; import { ElysiaApp } from "../../.."; +import { Player } from "../../../database/schemas/Player"; +import { tHeaders, tResponseBody, tParams, tRequestBody, tSchema } from "../../../libs/models"; +import { DocumentationCategory } from "../../../types/DocumentationCategory"; export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, status }) => { // Get ban list if(!session?.player?.hasPermission(Permission.ViewBans)) return status(403, { error: i18n('$.error.notAllowed') }); - const player = await players.findOne({ uuid: stripUUID(params.uuid) }); + const player = await Player.findOne({ uuid: stripUUID(params.uuid) }); if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); return player.bans.map((ban) => ({ + id: ban.id, + reason: ban.reason, + staff: formatUUID(ban.staff), appealable: ban.appeal.appealable, appealed: ban.appeal.appealed, banned_at: ban.banned_at.getTime(), expires_at: ban.expires_at?.getTime() || null, - id: ban.id, - reason: ban.reason, - staff: formatUUID(ban.staff) })); }, { detail: { - tags: ['Admin'], - description: 'Returns all player bans' + tags: [DocumentationCategory.Bans], + description: 'Get all player bans' }, response: { - 200: t.Array(t.Object({ appealable: t.Boolean(), appealed: t.Boolean(), banned_at: t.Number(), expires_at: t.Union([t.Number(), t.Null()]), id: t.String(), reason: t.String({ default: '…' }), staff: t.String({ default: '00000000-0000-0000-0000-000000000000' }) }), { description: 'A list of bans' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage bans' }), - 404: t.Object({ error: t.String() }, { description: 'The player was not found' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: t.Array(tSchema.Ban, { description: 'A ban list' }), + 403: tResponseBody.Error, + 404: tResponseBody.Error, }, - params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) -}).get('/:id', async ({ session, params, i18n, status }) => { // Get ban info of specific ban + params: tParams.uuid, + headers: tHeaders +}).get('/:id', async ({ session, params, i18n, status }) => { // Get a specific ban if(!session?.player?.hasPermission(Permission.ViewBans)) return status(403, { error: i18n('$.error.notAllowed') }); - const player = await players.findOne({ uuid: stripUUID(params.uuid) }); + const player = await Player.findOne({ uuid: stripUUID(params.uuid) }); if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); const ban = player.bans.find(({ id }) => id === params.id); if(!ban) return status(404, { error: i18n('$.ban.not_found') }); - const { appeal, banned_at, expires_at, id, reason, staff } = ban; - - return { appealable: appeal.appealable, appealed: appeal.appealable, banned_at: banned_at.getTime(), expires_at: expires_at?.getTime() || null, id, reason, staff: formatUUID(staff) }; + const { id, reason, staff, appeal, banned_at, expires_at } = ban; + + return { + id, + reason, + staff: formatUUID(staff), + appealable: appeal.appealable, + appealed: appeal.appealable, + banned_at: banned_at.getTime(), + expires_at: expires_at?.getTime() || null, + }; }, { detail: { - tags: ['Admin'], - description: 'Returns info about a specific player ban' + tags: [DocumentationCategory.Bans], + description: 'Get a specific player ban' }, response: { - 200: t.Object({ appealable: t.Boolean(), appealed: t.Boolean(), banned_at: t.Number(), expires_at: t.Union([t.Number(), t.Null()]), id: t.String(), reason: t.String({ default: '…' }), staff: t.String({ default: '00000000-0000-0000-0000-000000000000' }) }, { description: 'The ban object' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage bans' }), - 404: t.Object({ error: t.String() }, { description: 'The player or ban was not found' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tSchema.Ban, + 403: tResponseBody.Error, + 404: tResponseBody.Error, }, - params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }), id: t.String({ description: 'The ban ID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) -}).post('/', async ({ session, body: { reason, appealable, duration }, params, i18n, status }) => { // Ban player + params: tParams.uuidAndBanId, + headers: tHeaders +}).post('/', async ({ session, body: { duration, reason, appealable }, params, i18n, status }) => { // Ban player if(!session?.player?.hasPermission(Permission.CreateBans)) return status(403, { error: i18n('$.error.notAllowed') }); - const player = await players.findOne({ uuid: stripUUID(params.uuid) }); + const player = await Player.findOne({ uuid: stripUUID(params.uuid) }); if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); if(player.isBanned()) return status(409, { error: i18n('$.ban.already_banned') }); const expires = duration ? new Date(Date.now() + duration) : null; - player.banPlayer({ reason, staff: session.uuid!, appealable, expiresAt: expires }); + const ban = player.banPlayer({ reason, staff: session.uuid!, appealable, expiresAt: expires })!; await player.save(); sendModLogMessage({ @@ -79,84 +83,106 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, staff: await session.player.getGameProfile(), user: await player.getGameProfile(), discord: false, - reason: reason, - appealable: appealable == undefined ? true : appealable, + reason, + appealable: ban.appeal.appealable, expires }); - if(player.isEmailVerified()) { + if(player.email.verified) { sendBanEmail({ - address: player.connections.email.address!, + address: player.email.address!, reason: reason || '---', - appealable: appealable == undefined ? true : appealable, + appealable: ban.appeal.appealable, duration: expires, - i18n: getI18nFunctionByLanguage(player.last_language) + i18n: getI18nFunctionByLanguage(player.preferred_language) }); } - return { message: i18n('$.ban.banned') }; + return { + id: ban.id, + reason: ban.reason, + staff: formatUUID(ban.staff), + appealable: ban.appeal.appealable, + appealed: ban.appeal.appealed, + banned_at: ban.banned_at.getTime(), + expires_at: ban.expires_at?.getTime() || null, + }; }, { detail: { - tags: ['Admin'], - description: 'Bans a player' + tags: [DocumentationCategory.Bans], + description: 'Ban a player' }, response: { - 200: t.Object({ message: t.String() }, { description: 'The player was banned' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage bans' }), - 404: t.Object({ error: t.String() }, { description: 'The player was not found' }), - 409: t.Object({ error: t.String() }, { description: 'The player is already banned' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tSchema.Ban, + 403: tResponseBody.Error, + 404: tResponseBody.Error, + 409: tResponseBody.Error, }, - body: t.Object({ appealable: t.Optional(t.Boolean({ error: '$.error.wrongType;;[["field", "appealable"], ["type", "boolean"]]' })), duration: t.Optional(t.Number({ error: '$.error.wrongType;;[["field", "duration"], ["type", "number"]]' })), reason: t.String({ minLength: 1, error: '$.error.wrongType;;[["field", "reason"], ["type", "string"]]' }) }, { error: '$.error.invalidBody', additionalProperties: true }), - params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) + body: tRequestBody.CreateBan, + params: tParams.uuid, + headers: tHeaders }).patch('/', async ({ session, body: { appealable, reason }, params, i18n, status }) => { // Update ban info if(!session?.player?.hasPermission(Permission.EditBans)) return status(403, { error: i18n('$.error.notAllowed') }); - const player = await players.findOne({ uuid: stripUUID(params.uuid) }); + const player = await Player.findOne({ uuid: stripUUID(params.uuid) }); if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); if(!player.isBanned()) return status(409, { error: i18n('$.ban.not_banned') }); - const ban = player.bans.at(0)!; - ban.reason = reason; - ban.appeal.appealable = appealable; - await player.save(); + const ban = player.bans.at(-1)!; + let changed = false; + reason = reason?.trim(); - sendModLogMessage({ - logType: ModLogType.EditBan, - staff: await session.player.getGameProfile(), - user: await player.getGameProfile(), - discord: false, - appealable: appealable, - reason - }); + if(reason !== undefined && ban.reason != reason) { + ban.reason = reason; + changed = true; + } + if(appealable !== undefined && ban.appeal.appealable !== appealable) { + ban.appeal.appealable = appealable; + changed = true; + } + if(changed) { + // sendModLogMessage({ + // logType: ModLogType.EditBan, + // staff: await session.player.getGameProfile(), + // user: await player.getGameProfile(), + // discord: false, + // appealable: appealable, + // reason + // }); + await player.save(); + } - return { message: i18n('$.editBan.success') }; + return { + id: ban.id, + reason: ban.reason, + staff: formatUUID(ban.staff), + appealable: ban.appeal.appealable, + appealed: ban.appeal.appealed, + banned_at: ban.banned_at.getTime(), + expires_at: ban.expires_at?.getTime() || null, + }; }, { detail: { - tags: ['Admin'], - description: 'Edits the ban info of a player' + tags: [DocumentationCategory.Bans], + description: 'Edit an existing player ban' }, response: { - 200: t.Object({ message: t.String() }, { description: 'The ban info was edited' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage bans' }), - 404: t.Object({ error: t.String() }, { description: 'The player was not found' }), - 409: t.Object({ error: t.String() }, { description: 'The player is not banned' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tSchema.Ban, + 403: tResponseBody.Error, + 404: tResponseBody.Error, + 409: tResponseBody.Error, }, - body: t.Object({ appealable: t.Boolean({ error: '$.error.wrongType;;[["field", "appealable"], ["type", "boolean"]]' }), reason: t.String({ error: '$.error.wrongType;;[["field", "reason"], ["type", "string"]]' }) }, { error: '$.error.invalidBody', additionalProperties: true }), - params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) + body: tRequestBody.EditBan, + params: tParams.uuid, + headers: tHeaders }).post('/appeal', async ({ session, body: { reason }, i18n, status }) => { // Appeal ban if(!session?.self) return status(403, { error: i18n('$.error.notAllowed') }); + reason = reason.trim(); + if(reason.length < 10 || reason.length > 100) return status(422, { error: i18n('$.appeal.reason_validation') }); const { player } = session; if(!player || !player.isBanned()) return status(404, { error: i18n('$.appeal.notBanned') }); - const ban = player.bans.at(0)!; + const ban = player.bans.at(-1)!; if(!ban.appeal.appealable) return status(403, { error: i18n('$.appeal.notAppealable') }); if(ban.appeal.appealed) return status(403, { error: i18n('$.appeal.alreadyAppealed') }); @@ -173,24 +199,22 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, return { message: i18n('$.appeal.success') }; }, { detail: { - tags: ['Admin'], - description: 'Requests to be unbanned by the admins' + tags: [DocumentationCategory.Bans], + description: 'Send a ban appeal' }, response: { - 200: t.Object({ message: t.String() }, { description: 'Your appeal was sent' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to send an appeal' }), - 404: t.Object({ error: t.String() }, { description: 'You\'re not banned' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tResponseBody.Message, + 403: tResponseBody.Error, + 404: tResponseBody.Error, + 422: tResponseBody.Error, }, - body: t.Object({ reason: t.String({ error: '$.error.wrongType;;[["field", "reason"], ["type", "string"]]' }) }, { error: '$.error.invalidBody', additionalProperties: true }), - params: t.Object({ uuid: t.String({ description: 'Your UUID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) + body: tRequestBody.AppealBan, + params: tParams.uuid, + headers: tHeaders }).delete('/', async ({ session, params, i18n, status }) => { // Unban player if(!session?.player?.hasPermission(Permission.DeleteBans)) return status(403, { error: i18n('$.error.notAllowed') }); - const player = await players.findOne({ uuid: stripUUID(params.uuid) }); + const player = await Player.findOne({ uuid: stripUUID(params.uuid) }); if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); if(!player.isBanned()) return status(409, { error: i18n('$.ban.not_banned') }); @@ -204,25 +228,22 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, discord: false }); - if(player.isEmailVerified()) { - sendUnbanEmail(player.connections.email.address!, getI18nFunctionByLanguage(player.last_language)); + if(player.email.verified) { + sendUnbanEmail(player.email.address!, getI18nFunctionByLanguage(player.preferred_language)); } return { message: i18n('$.ban.unbanned') }; }, { detail: { - tags: ['Admin'], - description: 'Unbans a player' + tags: [DocumentationCategory.Bans], + description: 'Unban a player' }, response: { - 200: t.Object({ message: t.String() }, { description: 'The player was unbanned' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage bans' }), - 404: t.Object({ error: t.String() }, { description: 'The player was not found' }), - 409: t.Object({ error: t.String() }, { description: 'The player is not banned' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tResponseBody.Message, + 403: tResponseBody.Error, + 404: tResponseBody.Error, + 409: tResponseBody.Error, }, - params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) + params: tParams.uuid, + headers: tHeaders }); \ No newline at end of file diff --git a/src/routes/players/[uuid]/connections.ts b/src/routes/players/[uuid]/connections.ts deleted file mode 100644 index aa6a95c..0000000 --- a/src/routes/players/[uuid]/connections.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { t } from "elysia"; -import { enabled as isMailerEnabled, sendEmail } from "../../../libs/mailer"; -import { config } from "../../../libs/config"; -import { sendEmailLinkMessage } from "../../../libs/discord-notifier"; -import { ElysiaApp } from "../../.."; -import { onDiscordUnlink } from "../../../libs/events"; -import { generateSecureCode } from "../../../libs/crypto"; -import { Permission } from "../../../types/Permission"; - -export default (app: ElysiaApp) => app.post('/discord', async ({ session, i18n, status }) => { // Get a discord linking code - if(!config.discordBot.notifications.accountConnections.enabled) return status(409, { error: i18n('$.connections.discord.disabled') }); - if(!session) return status(403, { error: i18n('$.error.notAllowed') }); - const { self, player } = session; - if(!self) return status(403, { error: i18n('$.error.notAllowed') }); - - if(!player) return status(404, { error: i18n('$.error.noTag') }); - if(player.connections.discord.id) return status(409, { error: i18n('$.connections.discord.alreadyConnected') }); - if(player.connections.discord.code) return { code: player.connections.discord.code }; - - player.connections.discord.code = generateSecureCode(12); - await player.save(); - - return { code: player.connections.discord.code }; -}, { - detail: { - tags: ['Connections'], - description: 'Returns a code to link your Discord account with \'/link\' with the bot' - }, - response: { - 200: t.Object({ code: t.String() }, { description: 'You received a linking code' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage connections for this player' }), - 404: t.Object({ error: t.String() }, { description: 'You don\'t have an account' }), - 409: t.Object({ error: t.String() }, { description: 'Account linking is deactivated / You already have a Discord account connected' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) - }, - params: t.Object({ uuid: t.String({ description: 'Your UUID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) -}).delete('/discord', async ({ session, i18n, status }) => { // Unlink discord account - if(!config.discordBot.notifications.accountConnections.enabled) return status(409, { error: i18n('$.connections.discord.disabled') }); - if(!session) return status(403, { error: i18n('$.error.notAllowed') }); - const { self, player } = session; - if(!self && !player?.hasPermission(Permission.RemoveConnections)) return status(403, { error: i18n('$.error.notAllowed') }); - - if(!player) return status(404, { error: i18n('$.error.noTag') }); - if(!player.connections.discord.id && !player.connections.discord.code) return status(409, { error: i18n('$.connections.discord.notConnected') }); - - await onDiscordUnlink(await player.getGameProfile(), player.connections.discord.id!); - - player.connections.discord.id = null; - player.connections.discord.code = null; - await player.save(); - - return { message: i18n('$.connections.discord.unlinked') }; -}, { - detail: { - tags: ['Connections'], - description: 'Unlinks your connected discord account' - }, - response: { - 200: t.Object({ message: t.String() }, { description: 'Your account was unlinked' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage connections for this player' }), - 404: t.Object({ error: t.String() }, { description: 'You don\'t have an account' }), - 409: t.Object({ error: t.String() }, { description: 'Account linking is deactivated / You don\'t have a Discord account connected' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) - }, - params: t.Object({ uuid: t.String({ description: 'Your UUID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) -}).post('/email', async ({ session, body: { email }, i18n, status }) => { // Send verification email - if(!isMailerEnabled) return status(409, { error: i18n('$.connections.email.disabled') }); - if(!session) return status(403, { error: i18n('$.error.notAllowed') }); - const { self, player } = session; - if(!self) return status(403, { error: i18n('$.error.notAllowed') }); - - if(!player) return status(404, { error: i18n('$.error.noTag') }); - if(player.connections.email.address) return status(409, { error: i18n('$.connections.email.alreadyConnected') }); - - player.connections.email.address = email; - player.connections.email.code = generateSecureCode(12); - await player.save(); - - sendEmail({ - recipient: email, - subject: i18n('$.email.verification.subject'), - template: 'verification', - variables: [ - ['title', i18n('$.email.verification.title')], - ['greeting', i18n('$.email.greeting')], - ['description', i18n('$.email.verification.description')], - ['code', player.connections.email.code], - ['note', i18n('$.email.verification.note')], - ['footer', i18n('$.email.footer')], - ] - }); - - return { message: i18n('$.connections.email.verificationSent') }; -}, { - detail: { - tags: ['Connections'], - description: 'Sends a verification email to your email address' - }, - response: { - 200: t.Object({ message: t.String() }, { description: 'The verification email was sent' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage connections for this player' }), - 404: t.Object({ error: t.String() }, { description: 'You don\'t have an account' }), - 409: t.Object({ error: t.String() }, { description: 'You already have an email address linked' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) - }, - body: t.Object({ email: t.String({ error: '$.connections.email.invalidEmail', format: 'email' }) }, { error: '$.error.invalidBody', additionalProperties: true }), - params: t.Object({ uuid: t.String({ description: 'Your UUID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) -}).post('/email/:code', async ({ session, params, i18n, status }) => { // Verify email - if(!isMailerEnabled) return status(409, { error: i18n('$.connections.email.disabled') }); - if(!session?.self) return status(403, { error: i18n('$.error.notAllowed') }); - - const { player } = session; - if(!player) return status(404, { error: i18n('$.error.noTag') }); - if(player.isEmailVerified()) return status(409, { error: i18n('$.connections.email.alreadyConnected') }); - if(player.connections.email.code != params.code) return status(403, { error: i18n('$.connections.email.invalidCode') }); - - player.connections.email.code = null; - await player.save(); - - sendEmailLinkMessage( - await player.getGameProfile(), - config.discordBot.notifications.accountConnections.hideEmails ? null : player.connections.email.address!, - true - ); - - sendEmail({ - recipient: player.connections.email.address!, - subject: i18n('$.email.verified.subject'), - template: 'verified', - variables: [ - ['title', i18n('$.email.verified.title')], - ['success', i18n('$.email.verified.success')], - ['questions', i18n('$.email.verified.questions')], - ['link', 'https://globaltags.xyz/discord'], - ['footer', i18n('$.email.footer')], - ] - }); - - return { message: i18n('$.connections.email.verified') }; -}, { - detail: { - tags: ['Connections'], - description: 'Verifies the verification code sent to your inbox' - }, - response: { - 200: t.Object({ message: t.String() }, { description: 'Your email address was verified' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage connections for this player' }), - 404: t.Object({ error: t.String() }, { description: 'You don\'t have an account' }), - 409: t.Object({ error: t.String() }, { description: 'You already have an email address linked' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) - }, - params: t.Object({ uuid: t.String({ description: 'Your UUID' }), code: t.String({ description: 'Your verification code' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) -}).delete('/email', async ({ session, i18n, status }) => { // Unlink email - if(!isMailerEnabled) return status(409, { error: i18n('$.connections.email.disabled') }); - if(!session) return status(403, { error: i18n('$.error.notAllowed') }); - const { self, player } = session; - if(!self && !player?.hasPermission(Permission.RemoveConnections)) return status(403, { error: i18n('$.error.notAllowed') }); - - if(!player) return status(404, { error: i18n('$.error.noTag') }); - if(!player.connections.email.address && !player.connections.email.code) return status(400, { error: i18n('$.connections.email.notConnected') }); - - sendEmailLinkMessage( - await player.getGameProfile(), - config.discordBot.notifications.accountConnections.hideEmails ? null : player.connections.email.address!, - false - ); - - player.connections.email.address = null; - player.connections.email.code = null; - await player.save(); - - return { message: i18n('$.connections.email.unlinked') }; -}, { - detail: { - tags: ['Connections'], - description: 'Unlinks your email' - }, - response: { - 200: t.Object({ message: t.String() }, { description: 'Your email was unlinked' }), - 400: t.Object({ error: t.String() }, { description: 'You don\'t have an email address connected' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage connections for this player' }), - 404: t.Object({ error: t.String() }, { description: 'You don\'t have an account' }), - 409: t.Object({ error: t.String() }, { description: 'Email linking is deactivated' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) - }, - params: t.Object({ uuid: t.String({ description: 'Your UUID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) -}); \ No newline at end of file diff --git a/src/routes/players/[uuid]/icon.ts b/src/routes/players/[uuid]/icon.ts deleted file mode 100644 index 9c73773..0000000 --- a/src/routes/players/[uuid]/icon.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { t } from "elysia"; -import players, { getOrCreatePlayer } from "../../../database/schemas/players"; -import { join } from "path"; -import { capitalCase, snakeCase } from "change-case"; -import { config } from "../../../libs/config"; -import { Permission } from "../../../types/Permission"; -import { GlobalIcon } from "../../../types/GlobalIcon"; -import { stripUUID } from "../../../libs/game-profiles"; -import { ElysiaApp } from "../../.."; -import { ModLogType, sendCustomIconUploadMessage, sendModLogMessage } from "../../../libs/discord-notifier"; -import { sendTagChangeEmail } from "../../../libs/mailer"; -import { getI18nFunctionByLanguage } from "../../../middleware/fetch-i18n"; -import sharp from "sharp"; -import Logger from "../../../libs/Logger"; -import { generateSecureCode } from "../../../libs/crypto"; - -export function getCustomIconUrl(uuid: string, hash: string) { - return `${config.baseUrl}/players/${uuid}/icon/${hash}`; -} - -export default (app: ElysiaApp) => app.get('/:hash', async ({ params: { uuid, hash }, i18n, status }) => { // Get custom icon - const player = await players.findOne({ uuid: stripUUID(uuid) }); - if(!player) return status(404, { error: i18n('$.error.noTag') }); - if(player.isBanned()) return status(403, { error: i18n('$.error.playerBanned') }); - - const file = Bun.file(join('data', 'icons', player.uuid, `${hash.trim()}.png`)); - if(!(await file.exists())) return status(404, { error: i18n('$.error.noIcon') }); - - return file; -}, { - detail: { - tags: ['Settings'], - description: 'Returns a custom icon by its owner and its hash' - }, - response: { - 200: t.File({ description: 'The custom icon' }), - 403: t.Object({ error: t.String() }, { description: 'The player is banned' }), - 404: t.Object({ error: t.String() }, { description: 'The icon was not found' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) - }, - params: t.Object({ uuid: t.String({ description: 'The uuid of the image owner' }), hash: t.String({ description: 'The image hash' }) }) -}).post('/', async ({ session, body: { icon }, params, i18n, status }) => { // Change icon - if(!session || !session.self && !session.player?.hasPermission(Permission.ManagePlayerIcons)) return status(403, { error: i18n('error.notAllowed') }); - - icon = icon.toLowerCase(); - const player = await getOrCreatePlayer(params.uuid); - if(session.self && player.isBanned()) return status(403, { error: i18n('$.error.banned') }); - - const isCustomIconDisallowed = session.self && snakeCase(GlobalIcon[GlobalIcon.Custom]) == icon && !session.player?.hasPermission(Permission.CustomIcon); - if(!session.player?.hasPermission(Permission.BypassValidation) && (isCustomIconDisallowed || !(capitalCase(icon) in GlobalIcon) || config.validation.icon.blacklist.includes(capitalCase(icon)))) return status(403, { error: i18n('$.icon.notAllowed') }); - - if(player.isBanned()) return status(403, { error: i18n('$.error.banned') }); - if(snakeCase(player.icon.name) == icon) return status(400, { error: i18n('$.icon.sameIcon') }); - - const oldIcon = player.icon; - player.icon.name = icon; - await player.save(); - - if(!session.self && session.player) { - sendModLogMessage({ - logType: ModLogType.ChangeIconType, - staff: await session.player.getGameProfile(), - user: await player.getGameProfile(), - discord: false, - icons: { - old: oldIcon?.name || '---', - new: icon - } - }); - - if(player.isEmailVerified()) { - sendTagChangeEmail(player.connections.email.address!, oldIcon?.name || '---', icon, getI18nFunctionByLanguage(player.last_language)); - } - } - - return { message: i18n(session.self ? `$.icon.success.self` : '$.icon.success.admin') }; -}, { - detail: { - tags: ['Settings'], - description: 'Changes your GlobalTag icon' - }, - response: { - 200: t.Object({ message: t.String() }, { description: 'The icon was updated' }), - 400: t.Object({ error: t.String() }, { description: 'You\'re already using that icon' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to change your icon' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) - }, - body: t.Object({ icon: t.String({ error: '$.error.wrongType;;[["field", "icon"], ["type", "string"]]' }) }, { error: '$.error.invalidBody', additionalProperties: true }), - params: t.Object({ uuid: t.String({ description: 'Your UUID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) -}).post('/upload', async ({ session, body: { image }, params, i18n, status }) => { // Upload custom icon - if(!session || !session.self) return status(403, { error: i18n('$.error.notAllowed') }); - - const player = await players.findOne({ uuid: stripUUID(params.uuid) }); - if(!player) return status(404, { error: i18n('$.error.noTag') }); - if(player.isBanned()) return status(403, { error: i18n('$.error.banned') }); - if(!player.hasPermission(Permission.CustomIcon)) return status(403, { error: i18n('$.icon.upload.notAllowed') }); - - const metadata = await sharp(await image.arrayBuffer()).metadata().catch((err: Error) => { - Logger.error('Failed to read image metadata:', err.message); - return null; - }); - - if(!metadata) return status(422, { error: i18n('$.icon.upload.invalidMetadata') }); - if(metadata.format != 'png') return status(422, { error: i18n('$.icon.upload.wrongFormat')}); - if(!metadata.height || metadata.height != metadata.width) return status(422, { error: i18n('$.icon.upload.wrongResolution')}); - if(metadata.height > config.validation.icon.maxResolution) return status(422, { error: i18n('$.icon.upload.exceedsMaxResolution').replaceAll('', config.validation.icon.maxResolution.toString()) }); - - player.icon.name = snakeCase(GlobalIcon[GlobalIcon.Custom]); - player.icon.hash = generateSecureCode(32); - await player.save(); - await Bun.write(Bun.file(join('data', 'icons', player.uuid, `${player.icon.hash}.png`)), await image.arrayBuffer(), { createPath: true }); - - if(!player.hasPermission(Permission.BypassValidation)) sendCustomIconUploadMessage( - await player.getGameProfile(), - player.icon.hash - ); - - return { message: i18n('$.icon.upload.success'), hash: player.icon.hash }; -}, { - detail: { - tags: ['Settings'], - description: 'Upload a custom icon' - }, - response: { - 200: t.Object({ message: t.String(), hash: t.String() }, { description: 'The icon was uploaded' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to change your icon' }), - 404: t.Object({ error: t.String() }, { description: 'You don\'t have an account' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) - }, - body: t.Object({ image: t.File({ type: 'image/png', error: '$.error.wrongType;;[["field", "image"], ["type", "png file"]]' }) }, { error: '$.error.invalidBody', additionalProperties: true }), - params: t.Object({ uuid: t.String({ description: 'Your UUID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) -}).patch('/role-visibility', async ({ session, body: { visible }, params, i18n, status }) => { // Toggle role icon - if(!session || !session.self && !session.player?.hasPermission(Permission.ManagePlayerIcons)) return status(403, { error: i18n('$.error.notAllowed') }); - - const player = await players.findOne({ uuid: stripUUID(params.uuid) }); - if(!player) return status(404, { error: i18n('$.error.noTag') }); - if(session.self && player.isBanned()) return status(403, { error: i18n('$.error.banned') }); - if(player.hide_role_icon == !visible) return status(409, { error: i18n(player.hide_role_icon ? '$.icon.role_icon.already_hidden' : '$.icon.role_icon.already_shown') }); - - player.hide_role_icon = !visible; - await player.save(); - - return { message: i18n(player.hide_role_icon ? '$.icon.role_icon.success.hidden' : '$.icon.role_icon.success.shown') }; -}, { - detail: { - tags: ['Settings'], - description: 'Toggles the visibility of your role icon' - }, - response: { - 200: t.Object({ message: t.String() }, { description: 'The role icon visibility was updated' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to toggle your role icon' }), - 404: t.Object({ error: t.String() }, { description: 'You don\'t have an account' }), - 409: t.Object({ error: t.String() }, { description: 'That role icon visibility is already set' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) - }, - body: t.Object({ visible: t.Boolean({ error: '$.error.wrongType;;[["field", "visible"], ["type", "boolean"]]' }) }, { error: '$.error.invalidBody', additionalProperties: true }), - params: t.Object({ uuid: t.String({ description: 'Your UUID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) -}); \ No newline at end of file diff --git a/src/routes/players/[uuid]/icons.ts b/src/routes/players/[uuid]/icons.ts new file mode 100644 index 0000000..1fb9598 --- /dev/null +++ b/src/routes/players/[uuid]/icons.ts @@ -0,0 +1,83 @@ +import { t } from "elysia"; +import { config } from "../../../libs/config"; +import { Permission } from "../../../types/Permission"; +import { GlobalIcon, icons } from "../../../types/GlobalIcon"; +import { stripUUID } from "../../../libs/game-profiles"; +import { ElysiaApp } from "../../.."; +import { sendCustomIconUploadMessage } from "../../../libs/discord-notifier"; +import sharp from "sharp"; +import Logger from "../../../libs/Logger"; +import { generateSecureCode } from "../../../libs/crypto"; +import { Player } from "../../../database/schemas/Player"; +import { tResponseBody, tHeaders, tParams, tRequestBody } from "../../../libs/models"; +import { DocumentationCategory } from "../../../types/DocumentationCategory"; +import { customIconFile } from "../../../libs/data-accessor"; + +export function getCustomIconUrl(uuid: string, hash: string) { + return `${config.baseUrl}/players/${uuid}/icon/${hash}`; +} + +export default (app: ElysiaApp) => app.get('/:hash', async ({ params: { uuid, hash }, i18n, status }) => { // Get custom icon + const player = await Player.findOne({ uuid: stripUUID(uuid) }); + if(!player) return status(404, { error: i18n('$.error.noTag') }); + if(player.isBanned()) return status(403, { error: i18n('$.error.playerBanned') }); + + const file = customIconFile(player.uuid, hash); + if(!(await file.exists())) return status(404, { error: i18n('$.error.noIcon') }); + + return file; +}, { + detail: { + tags: [DocumentationCategory.Tags], + description: 'Get a custom icon' + }, + response: { + 200: t.File({ description: 'An image file' }), + 403: tResponseBody.Error, + 404: tResponseBody.Error + }, + params: tParams.uuidAndIconHash +}).post('/upload', async ({ session, body: { image }, params, i18n, status }) => { // Upload custom icon + if(!session || !session.self) return status(403, { error: i18n('$.error.notAllowed') }); + + const player = await Player.findOne({ uuid: stripUUID(params.uuid) }); + if(!player) return status(404, { error: i18n('$.error.noTag') }); + if(player.isBanned()) return status(403, { error: i18n('$.error.banned') }); + if(!player.hasPermission(Permission.CustomIcon)) return status(403, { error: i18n('$.icon.upload.notAllowed') }); + + const metadata = await sharp(await image.arrayBuffer()).metadata().catch((err: Error) => { + Logger.error('Failed to read image metadata:', err.message); + return null; + }); + + if(!metadata) return status(422, { error: i18n('$.icon.upload.invalidMetadata') }); + if(metadata.format != 'png') return status(422, { error: i18n('$.icon.upload.wrongFormat')}); + if(!metadata.height || metadata.height != metadata.width) return status(422, { error: i18n('$.icon.upload.wrongResolution')}); + if(metadata.height > config.validation.icon.maxResolution) return status(422, { error: i18n('$.icon.upload.exceedsMaxResolution').replaceAll('', config.validation.icon.maxResolution.toString()) }); + + player.icon.type = GlobalIcon.Custom; + player.icon.hash = generateSecureCode(32); + await player.save(); + await Bun.write(customIconFile(player.uuid, player.icon.hash), await image.arrayBuffer(), { createPath: true }); + + if(!player.hasPermission(Permission.BypassValidation)) sendCustomIconUploadMessage( + await player.getGameProfile(), + player.icon.hash + ); + + return { message: i18n('$.icon.upload.success'), hash: player.icon.hash }; +}, { + detail: { + tags: [DocumentationCategory.Tags], + description: 'Upload a custom icon' + }, + response: { + 200: t.Object({ message: t.String(), hash: t.String() }, { description: 'A message and icon hash' }), + 403: tResponseBody.Error, + 404: tResponseBody.Error, + 422: tResponseBody.Error + }, + body: tRequestBody.UploadCustomIcon, + params: tParams.uuid, + headers: tHeaders +}); \ No newline at end of file diff --git a/src/routes/players/[uuid]/index.ts b/src/routes/players/[uuid]/index.ts index 9ce4001..610b184 100644 --- a/src/routes/players/[uuid]/index.ts +++ b/src/routes/players/[uuid]/index.ts @@ -1,39 +1,31 @@ import { t } from "elysia"; -import players, { getOrCreatePlayer } from "../../../database/schemas/players"; -import Logger from "../../../libs/Logger"; -import { ModLogType, sendModLogMessage, sendWatchlistAddMessage, sendWatchlistTagUpdateMessage } from "../../../libs/discord-notifier"; -import { getI18nFunctionByLanguage } from "../../../middleware/fetch-i18n"; import { colorCodesWithSpaces, hexColorCodesWithSpaces, stripColors } from "../../../libs/chat-color"; -import { snakeCase } from "change-case"; -import { sendTagChangeEmail, sendTagClearEmail } from "../../../libs/mailer"; -import { saveLastLanguage } from "../../../libs/i18n"; import { config } from "../../../libs/config"; -import { Permission, permissions } from "../../../types/Permission"; -import { GlobalIcon } from "../../../types/GlobalIcon"; -import { GlobalPosition } from "../../../types/GlobalPosition"; +import { Permission } from "../../../types/Permission"; +import { GlobalIcon, icons } from "../../../types/GlobalIcon"; import { formatUUID, stripUUID } from "../../../libs/game-profiles"; import { ElysiaApp } from "../../.."; +import { getOrCreatePlayer, Player } from "../../../database/schemas/Player"; +import { tHeaders, tParams, tRequestBody, tResponseBody } from "../../../libs/models"; +import { DocumentationCategory } from "../../../types/DocumentationCategory"; +import { customIconFile } from "../../../libs/data-accessor"; +import { GlobalPosition, positions } from "../../../types/GlobalPosition"; const { validation, strictAuth } = config; const { min, max, blacklist, watchlist } = validation.tag; const multipleSpaces = /\s{2,}/g; -export default (app: ElysiaApp) => app.get('/', async ({ session, language, params, i18n, status }) => { // Get player info - if(!!session?.uuid && !!language) saveLastLanguage(session.uuid, language); +export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, status }) => { // Get player info if(strictAuth) { if(!session?.uuid) return status(403, { error: i18n('$.error.notAllowed') }); } - const showBan = session?.self || session?.player?.hasPermission(Permission.ViewBans) || false; - const showRoleIconVisibility = session?.self || session?.player?.hasPermission(Permission.ManagePlayerIcons) || false; - const player = await players.findOne({ uuid: stripUUID(params.uuid) }); + const player = await Player.findOne({ uuid: stripUUID(params.uuid) }); if(!player) return status(404, { error: i18n('$.error.playerNoTag') }); - const playerIcon = snakeCase(player.icon.name); - - if(playerIcon == snakeCase(GlobalIcon[GlobalIcon.Custom])) { + if(player.icon.type == GlobalIcon.Custom) { if(!player.hasPermission(Permission.CustomIcon)) { - player.icon.name = snakeCase(GlobalIcon[GlobalIcon.None]); + player.icon.type = GlobalIcon.None; await player.save(); } } @@ -41,178 +33,180 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, language, para return { uuid: formatUUID(player.uuid), tag: player.isBanned() ? null : player.tag || null, - position: snakeCase(player.position || GlobalPosition[GlobalPosition.Above]), - icon: { - type: playerIcon, - hash: player.icon.hash || null - }, - roleIcon: !player.hide_role_icon ? player.getActiveRoles().find((role) => role.role.hasIcon)?.role.name || null : null, - hideRoleIcon: showRoleIconVisibility ? player.hide_role_icon : false, + position: player.position, + icon: player.icon, + roleIcon: player.getActiveRoles().find((role) => role.role.hasIcon)?.role.name || null, roles: player.getActiveRoles().map((role) => role.role.name), permissions: player.getActiveRoles().reduce((acc, role) => acc | role.role.permissions, 0), referrals: { - has_referred: player.referrals.has_referred, + has_referred: await player.hasReferrer(), total_referrals: player.referrals.total.length, current_month_referrals: player.referrals.current_month - }, - ban: showBan && player.isBanned() ? (() => { - const { appeal, banned_at, expires_at, id, reason, staff } = player.bans.at(0)!; - return { - appealable: appeal.appealable, - appealed: appeal.appealed, - banned_at: banned_at.getTime(), - expires_at: expires_at?.getTime() || null, - id, - reason, - staff: formatUUID(staff) - } - })() : null + } }; }, { detail: { - tags: ['Interactions'], - description: 'Returns a players\' tag info' + tags: [DocumentationCategory.Tags], + description: 'Get a players\' tag info' }, response: { - 200: t.Object({ uuid: t.String(), tag: t.Union([t.String(), t.Null()]), position: t.String(), icon: t.Object({ type: t.String(), hash: t.Union([t.String(), t.Null()]) }), referrals: t.Object({ has_referred: t.Boolean(), total_referrals: t.Integer(), current_month_referrals: t.Integer() }), roleIcon: t.Union([t.String(), t.Null()]), hideRoleIcon: t.Boolean(), roles: t.Array(t.String()), permissions: t.Integer(), ban: t.Union([t.Object({ appealable: t.Boolean(), appealed: t.Boolean(), banned_at: t.Number(), expires_at: t.Union([t.Number(), t.Null()]), id: t.String(), reason: t.Union([t.String(), t.Null()]), staff: t.String() }), t.Null()]) }, { description: 'The tag data' }), - 403: t.Object({ error: t.String() }, { description: 'The player is banned' }), - 404: t.Object({ error: t.String() }, { description: 'The player was not found' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tResponseBody.TagData, + 403: tResponseBody.Error, + 404: tResponseBody.Error, }, - params: t.Object({ uuid: t.String({ description: 'The uuid of the player you want to fetch the info of' }) }), - headers: t.Object({ authorization: strictAuth ? t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) : t.Optional(t.String({ description: 'Your authentication token' })) }, { error: '$.error.notAllowed' }), -}).get('/history', async ({ session, params, i18n, status }) => { // Get player's tag history + params: tParams.uuid, + headers: tHeaders, +}).get('/history', async ({ session, params, i18n, status }) => { // Get player's tag and icon history if(!session || session?.self && !session.player?.hasPermission(Permission.ViewTagHistory)) return status(403, { error: i18n('$.error.notAllowed') }); - const player = await players.findOne({ uuid: stripUUID(params.uuid) }); + const player = await Player.findOne({ uuid: stripUUID(params.uuid) }); if(!player) return status(404, { error: i18n('$.error.playerNoTag') }); - return player.history.map((tag) => ({ - tag, - flaggedWords: watchlist.filter((entry) => stripColors(tag).trim().toLowerCase().includes(entry)) + return player.tag_history.map((tag) => ({ + tag: tag.content, + timestamp: tag.timestamp.getTime(), + flagged_words: watchlist.filter((entry) => stripColors(tag.content).trim().toLowerCase().includes(entry)) })); }, { detail: { - tags: ['Interactions'], - description: 'Returns a players\' tag history' + tags: [DocumentationCategory.Tags], + description: 'Get a players\' tag history' }, response: { - 200: t.Array(t.Object({ tag: t.String(), flaggedWords: t.Array(t.String()) }), { description: 'The tag history' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage tags' }), - 404: t.Object({ error: t.String() }, { description: 'The player is not in the database' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: t.Array(t.Object({ tag: t.String(), timestamp: t.Number(), flagged_words: t.Array(t.String()) }), { description: 'The tag history' }), + 403: tResponseBody.Error, + 404: tResponseBody.Error, }, - params: t.Object({ uuid: t.String({ description: 'The uuid of the player you want to fetch the info of' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }), -}).post('/', async ({ session, body: { tag }, params, i18n, status }) => { // Change tag + params: tParams.uuid, + headers: tHeaders, +}).post('/', async ({ session, params, i18n, status }) => { // Create account if(!session || !session.self && !session.player?.hasPermission(Permission.ManagePlayerTags)) return status(403, { error: i18n('$.error.notAllowed') }); - const player = await getOrCreatePlayer(params.uuid); - if(session.self && player.isBanned()) return status(403, { error: i18n('$.error.banned') }); - - let isWatched = false; - let isWatchedInitially = false; - const gameProfile = await player.getGameProfile(); - if(!session.player?.hasPermission(Permission.BypassValidation)) { - tag = tag.trim().replace(multipleSpaces, ' ').replace(colorCodesWithSpaces, '').replace(hexColorCodesWithSpaces, ''); - const strippedTag = stripColors(tag); - if(strippedTag == '') return status(422, { error: i18n('$.setTag.empty') }); - if(strippedTag.length < min || strippedTag.length > max) return status(422, { error: i18n('$.setTag.validation').replace('', String(min)).replace('', String(max)) }); - const blacklistedWord = blacklist.find((word) => strippedTag.toLowerCase().includes(word)); - if(blacklistedWord) return status(422, { error: i18n('$.setTag.blacklisted').replaceAll('', blacklistedWord) }); - isWatched = (player && player.watchlist) || watchlist.some((word) => { - if(strippedTag.toLowerCase().includes(word)) { - Logger.warn(`Now watching ${player.uuid} for matching "${word}" in "${tag}".`); - sendWatchlistAddMessage({ player: gameProfile, tag, word }); - isWatchedInitially = true; - return true; - } - return false; - }); - } - - if(player.tag == tag) return status(409, { error: i18n('$.setTag.sameTag') }); + if(await Player.exists({ uuid: stripUUID(params.uuid) })) return status(409, { error: i18n('$.account.create.account_already_exists') }); + (await getOrCreatePlayer(params.uuid)).save(); - const oldTag = player.tag; - player.tag = tag; - if(isWatched) player.watchlist = true; - if(player.history.at(-1) != tag) player.history.push(tag); - await player.save(); - if(!session.self && session.player) { - sendModLogMessage({ - logType: ModLogType.ChangeTag, - staff: await session.player.getGameProfile(), - user: gameProfile, - discord: false, - tags: { - old: oldTag || 'None', - new: tag - } - }); - - if(player.isEmailVerified()) { - sendTagChangeEmail(player.connections.email.address!, oldTag || '---', tag, getI18nFunctionByLanguage(player.last_language)); - } + // TODO: Reimplement mod log } - if(isWatched && !isWatchedInitially) sendWatchlistTagUpdateMessage(gameProfile, tag); - return { message: i18n(session.self ? '$.setTag.success.self' : '$.setTag.success.admin') }; + return { + message: i18n('$.account.create.success'), + } }, { detail: { - tags: ['Settings'], - description: 'Changes your global tag' + tags: [DocumentationCategory.Tags], + description: 'Create an account' }, response: { - 200: t.Object({ message: t.String() }, { description: 'Your tag was changed' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re banned' }), - 409: t.Object({ error: t.String() }, { description: 'You already have this tag' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tResponseBody.Message, + 403: tResponseBody.Error, + 409: tResponseBody.Error }, - params: t.Object({ uuid: t.String({ description: 'Your UUID' }) }), - body: t.Object({ tag: t.String({ error: '$.error.wrongType;;[["field", "tag"], ["type", "string"]]' }) }, { error: '$.error.invalidBody', additionalProperties: true }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) -}).delete('/', async ({ session, params, i18n, status }) => { // Delete tag + params: tParams.uuid, + headers: tHeaders +}).patch('/', async ({ session, body: { tag, position, icon }, params, i18n, status }) => { // Update settings if(!session || !session.self && !session.player?.hasPermission(Permission.ManagePlayerTags)) return status(403, { error: i18n('$.error.notAllowed') }); - const player = await players.findOne({ uuid: stripUUID(params.uuid) }); - if(!player) return status(404, { error: i18n('$.error.noTag') }); + const player = await getOrCreatePlayer(params.uuid); if(session.self && player.isBanned()) return status(403, { error: i18n('$.error.banned') }); - if(!player.tag) return status(404, { error: i18n('$.error.noTag') }); - if(!session.self && session.player) { - sendModLogMessage({ - logType: ModLogType.ClearTag, - staff: await session.player.getGameProfile(), - user: await player.getGameProfile(), - discord: false - }); - if(player.isEmailVerified()) { - sendTagClearEmail(player.connections.email.address!, player.tag, getI18nFunctionByLanguage(player.last_language)); + const errors: { + tag: string | null; + position: string | null; + icon: string | null; + } = { + tag: null, + position: null, + icon: null + } + + let changed = false; + if(tag !== undefined) { + if(tag !== null && !session.player?.hasPermission(Permission.BypassValidation)) { + tag = tag.trim() + .replace(multipleSpaces, ' ') + .replace(colorCodesWithSpaces, '') + .replace(hexColorCodesWithSpaces, ''); + + const strippedTag = stripColors(tag); + const blacklistedWord = blacklist.find((word) => strippedTag.toLowerCase().includes(word)); + + if(strippedTag.length < min || strippedTag.length > max){ + errors.tag = i18n('$.set_tag.validation').replace('', String(min)).replace('', String(max)); + } else if(blacklistedWord) { + errors.tag = i18n('$.set_tag.blacklisted_word').replaceAll('', blacklistedWord); + } + } + if(!errors.tag && player.tag !== tag) { + player.changeTag(tag); + changed = true; + } + } + if(position !== undefined) { + const globalPosition = position.toLowerCase() as GlobalPosition; + if(!positions.includes(globalPosition)) { + errors.position = i18n('$.position.invalid'); + } else { + player.position = globalPosition; + changed = true; + } + } + if(icon !== undefined) { + // TODO: Remove comment when permissions are done + const hasCustomIconPermission = !session.self || true//session.player?.hasPermission(Permission.CustomIcon) || session.player?.hasPermission(Permission.BypassValidation); + if(icon.hash !== undefined) { + if(!hasCustomIconPermission) { + errors.icon = i18n('$.icon.upload.notAllowed'); + } else if(icon.hash != null && (icon.hash.trim().length < 1 || !(await customIconFile(player.uuid, icon.hash).exists()))) { + errors.icon = i18n('$.icon.upload.notFound'); + } else if(player.icon.hash !== icon.hash) { + player.icon.hash = icon.hash; + changed = true; + } } - player.clearTag(session.uuid!); - } else { - player.tag = null; + if(icon.type !== undefined && !errors.icon) { + const globalIcon = icon.type.toLowerCase() as GlobalIcon; + + if(!icons.includes(globalIcon)) { + errors.icon = i18n('$.icon.not_allowed'); + } else if(!hasCustomIconPermission && globalIcon === GlobalIcon.Custom) { + errors.icon = i18n('$.icon.upload.notAllowed'); + } else if(globalIcon === GlobalIcon.Custom && !player.icon.hash) { + if(player.icon.type === GlobalIcon.Custom) { + player.icon.type = GlobalIcon.None; + changed = true; + } + errors.icon = i18n('$.icon.upload.noHash'); + } else if(player.icon.type !== globalIcon) { + player.icon.type = globalIcon; + changed = true; + } + } + } + if(changed) player.save(); + + if(!session.self && session.player) { + // TODO: Reimplement logs and email notifications } - await player.save(); - return { message: i18n(session.self ? '$.resetTag.success.self' : '$.resetTag.success.admin') }; + return { + errors, + data: { + tag: player.tag, + position: player.position, + icon: player.icon, + } + }; }, { detail: { - tags: ['Settings'], - description: 'Deletes your global tag' + tags: [DocumentationCategory.Tags], + description: 'Change your GlobalTag settings' }, response: { - 200: t.Object({ message: t.String() }, { description: 'Your tag was deleted' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re banned' }), - 404: t.Object({ error: t.String() }, { description: 'You don\'t have an account' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tResponseBody.EditTagSettings, + 403: tResponseBody.Error }, - params: t.Object({ uuid: t.String({ description: 'Your UUID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) + params: tParams.uuid, + body: tRequestBody.TagSettings, + headers: tHeaders }); \ No newline at end of file diff --git a/src/routes/players/[uuid]/locks.ts b/src/routes/players/[uuid]/locks.ts new file mode 100644 index 0000000..004f657 --- /dev/null +++ b/src/routes/players/[uuid]/locks.ts @@ -0,0 +1,173 @@ +import { t } from "elysia"; +import { ElysiaApp } from "../../.."; +import { AccountLockType, Player } from "../../../database/schemas/Player"; +import { formatUUID, stripUUID } from "../../../libs/game-profiles"; +import { DocumentationCategory } from "../../../types/DocumentationCategory"; +import { Permission } from "../../../types/Permission"; +import { tHeaders, tParams, tRequestBody, tResponseBody, tSchema } from "../../../libs/models"; + +const lockTypes = Object.values(AccountLockType); + +export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, status }) => { // Get all player locks + if(!session?.player?.hasPermission(Permission.ViewLocks)) return status(403, { error: i18n('$.error.notAllowed') }); + + const player = await Player.findOne({ uuid: stripUUID(params.uuid) }); + if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); + + return player.locks.map((lock) => ({ + id: lock.id, + type: lock.type, + reason: lock.reason, + staff: formatUUID(lock.staff), + locked_at: lock.locked_at.getTime(), + expires_at: lock.expires_at?.getTime() || null, + })); +}, { + detail: { + tags: [DocumentationCategory.Locks], + description: 'Get all player locks' + }, + response: { + 200: t.Array(tSchema.Lock, { description: 'A lock list' }), + 403: tResponseBody.Error, + 404: tResponseBody.Error, + }, + params: tParams.uuid, + headers: tHeaders +}).get('/:id', async ({ session, params, i18n, status }) => { // Get a specific player lock + if(!session?.player?.hasPermission(Permission.ViewLocks)) return status(403, { error: i18n('$.error.notAllowed') }); + + const player = await Player.findOne({ uuid: stripUUID(params.uuid) }); + if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); + + const lock = player.locks.find(({ id }) => id === params.id); + if(!lock) return status(404, { error: i18n('$.locks.not_found') }); + const { id, type, reason, staff, locked_at, expires_at } = lock; + + return { + id, + type, + reason, + staff: formatUUID(staff), + locked_at: locked_at.getTime(), + expires_at: expires_at?.getTime() || null, + }; +}, { + detail: { + tags: [DocumentationCategory.Locks], + description: 'Get a specific player lock' + }, + response: { + 200: tSchema.Lock, + 403: tResponseBody.Error, + 404: tResponseBody.Error, + }, + params: tParams.uuidAndLockId, + headers: tHeaders +}).post('/', async ({ session, body: { type, reason, duration }, params, i18n, status }) => { // Create player lock + if(!session?.player?.hasPermission(Permission.ManageLocks)) return status(403, { error: i18n('$.error.notAllowed') }); + + const lockType = type.toLowerCase() as AccountLockType; + if(!lockTypes.includes(lockType)) return status(400, { error: i18n('$.locks.invalid_type') }); + + const player = await Player.findOne({ uuid: stripUUID(params.uuid) }); + if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); + if(player.hasLock(type)) return status(409, { error: i18n('$.locks.already_exists') }); + + const expires = duration ? new Date(Date.now() + duration) : null; + const lock = player.createLock({ type, reason: reason.trim(), staff: session.uuid!, expiresAt: expires })!; + await player.save(); + + // TODO: Send mod log message + + return { + id: lock.id, + type: lock.type, + reason: lock.reason, + staff: formatUUID(lock.staff), + locked_at: lock.locked_at.getTime(), + expires_at: lock.expires_at?.getTime() || null, + }; +}, { + detail: { + tags: [DocumentationCategory.Locks], + description: 'Create a player lock' + }, + response: { + 200: tSchema.Lock, + 400: tResponseBody.Error, + 403: tResponseBody.Error, + 404: tResponseBody.Error, + 409: tResponseBody.Error + }, + body: tRequestBody.CreateLock, + params: tParams.uuid, + headers: tHeaders +}).patch('/:id', async ({ session, body: { reason }, params, i18n, status }) => { // Update lock info + if(!session?.player?.hasPermission(Permission.ManageLocks)) return status(403, { error: i18n('$.error.notAllowed') }); + + const player = await Player.findOne({ uuid: stripUUID(params.uuid) }); + if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); + + const lock = player.locks.find(({ id }) => id === params.id); + if(!lock) return status(404, { error: i18n('$.locks.not_found') }); + + reason = reason?.trim(); + + if(reason !== undefined && lock.reason != reason) { + lock.reason = reason; + // TODO: Send mod log message + await player.save(); + } + + return { + id: lock.id, + type: lock.type, + reason: lock.reason, + staff: formatUUID(lock.staff), + locked_at: lock.locked_at.getTime(), + expires_at: lock.expires_at?.getTime() || null + }; +}, { + detail: { + tags: [DocumentationCategory.Locks], + description: 'Edit an existing player lock' + }, + response: { + 200: tSchema.Lock, + 403: tResponseBody.Error, + 404: tResponseBody.Error, + 409: tResponseBody.Error, + }, + body: tRequestBody.EditLock, + params: tParams.uuid, + headers: tHeaders +}).delete('/:id', async ({ session, params, i18n, status }) => { // Remove a player lock + if(!session?.player?.hasPermission(Permission.ManageLocks)) return status(403, { error: i18n('$.error.notAllowed') }); + + const player = await Player.findOne({ uuid: stripUUID(params.uuid) }); + if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); + + const lock = player.locks.find(({ id }) => id === params.id); + if(!lock) return status(404, { error: i18n('$.locks.not_found') }); + + lock.expires_at = new Date(); + await player.save(); + + // TODO: Add mod log message + + return { message: i18n('$.locks.removed') }; +}, { + detail: { + tags: [DocumentationCategory.Locks], + description: 'Remove a player lock' + }, + response: { + 200: tResponseBody.Message, + 403: tResponseBody.Error, + 404: tResponseBody.Error, + 409: tResponseBody.Error, + }, + params: tParams.uuid, + headers: tHeaders +}); \ No newline at end of file diff --git a/src/routes/players/[uuid]/notes.ts b/src/routes/players/[uuid]/notes.ts index 4f596ac..d1c399c 100644 --- a/src/routes/players/[uuid]/notes.ts +++ b/src/routes/players/[uuid]/notes.ts @@ -1,44 +1,40 @@ import { t } from "elysia"; -import players from "../../../database/schemas/players"; import { ModLogType, sendModLogMessage } from "../../../libs/discord-notifier"; -import { config } from "../../../libs/config"; import { Permission } from "../../../types/Permission"; -import { formatUUID, GameProfile, stripUUID } from "../../../libs/game-profiles"; +import { formatUUID, stripUUID } from "../../../libs/game-profiles"; import { ElysiaApp } from "../../.."; - -const { validation } = config; +import { Player } from "../../../database/schemas/Player"; +import { tResponseBody, tHeaders, tParams, tRequestBody, tSchema } from "../../../libs/models"; +import { DocumentationCategory } from "../../../types/DocumentationCategory"; export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, status }) => { // Get notes if(!session?.player?.hasPermission(Permission.ViewNotes)) return status(403, { error: i18n('$.error.notAllowed') }); - const player = await players.findOne({ uuid: stripUUID(params.uuid) }); + const player = await Player.findOne({ uuid: stripUUID(params.uuid) }); if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); return player.notes.map((note) => ({ id: note.id, - text: note.text, + text: note.content, author: formatUUID(note.author), - createdAt: note.createdAt.getTime() + created_at: note.created_at.getTime() })); }, { detail: { - tags: ['Admin'], - description: 'Returns all player notes' + tags: [DocumentationCategory.Notes], + description: 'Get all player notes' }, response: { - 200: t.Array(t.Object({ id: t.String(), text: t.String(), author: t.String(), createdAt: t.Number() }), { description: 'The notes of the player' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage notes' }), - 404: t.Object({ error: t.String() }, { description: 'The player was not found' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: t.Array(tSchema.Note, { description: 'A note list' }), + 403: tResponseBody.Error, + 404: tResponseBody.Error, }, - params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) + params: tParams.uuid, + headers: tHeaders }).get('/:id', async ({ session, params: { uuid, id }, i18n, status }) => { // Get specific note if(!session?.player?.hasPermission(Permission.ViewNotes)) return status(403, { error: i18n('$.error.notAllowed') }); - const player = await players.findOne({ uuid: stripUUID(uuid) }); + const player = await Player.findOne({ uuid: stripUUID(uuid) }); if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); const note = player.notes.find((note) => note.id == id); @@ -46,33 +42,30 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, return { id: note.id, - text: note.text, + text: note.content, author: formatUUID(note.author), - createdAt: note.createdAt.getTime() + created_at: note.created_at.getTime() }; }, { detail: { - tags: ['Admin'], - description: 'Returns a specific player note' + tags: [DocumentationCategory.Notes], + description: 'Get a specific player note' }, response: { - 200: t.Object({ id: t.String(), text: t.String(), author: t.String(), createdAt: t.Number() }, { description: 'The note info' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage notes' }), - 404: t.Object({ error: t.String() }, { description: 'The player or the note was not found' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tSchema.Note, + 403: tResponseBody.Error, + 404: tResponseBody.Error, }, - params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }), id: t.String({ description: 'The note ID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) -}).post('/', async ({ session, body: { note }, params, i18n, status }) => { // Add note to player + params: tParams.uuidAndApiKeyId, + headers: tHeaders +}).post('/', async ({ session, body: { content }, params, i18n, status }) => { // Add note to player if(!session?.player?.hasPermission(Permission.CreateNotes)) return status(403, { error: i18n('$.error.notAllowed') }); const uuid = stripUUID(params.uuid); - const player = await players.findOne({ uuid }); + const player = await Player.findOne({ uuid }); if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); - player.createNote({ text: note, author: session.uuid! }); + const note = player.createNote({ content, author: session.uuid! }); await player.save(); sendModLogMessage({ @@ -80,30 +73,32 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, staff: await session.player.getGameProfile(), user: await player.getGameProfile(), discord: false, - note + note: content }); - return { message: i18n('$.notes.create.success') }; + return status(201, { + id: note.id, + text: note.content, + author: formatUUID(note.author), + created_at: note.created_at.getTime() + }); }, { detail: { - tags: ['Admin'], - description: 'Creates a player note' + tags: [DocumentationCategory.Notes], + description: 'Create a new player note' }, response: { - 200: t.Object({ message: t.String() }, { description: 'The note was created' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage notes' }), - 404: t.Object({ error: t.String() }, { description: 'The player was not found' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 201: tSchema.Note, + 403: tResponseBody.Error, + 404: tResponseBody.Error, }, - body: t.Object({ note: t.String({ maxLength: validation.notes.maxLength, error: `$.notes.create.max_length;;[["max", "${validation.notes.maxLength}"]]` }) }, { error: '$.error.invalidBody', additionalProperties: true }), - params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) + body: tRequestBody.Note, + params: tParams.uuid, + headers: tHeaders }).delete('/:id', async ({ session, params: { uuid, id }, i18n, status }) => { // Delete note if(!session?.player?.hasPermission(Permission.DeleteNotes)) return status(403, { error: i18n('$.error.notAllowed') }); - const player = await players.findOne({ uuid: stripUUID(uuid) }); + const player = await Player.findOne({ uuid: stripUUID(uuid) }); if(!player) return status(404, { error: i18n(`error.playerNotFound`) }); const note = player.notes.find((note) => note.id == id); @@ -117,23 +112,20 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, staff: await session.player.getGameProfile(), user: await player.getGameProfile(), discord: false, - note: note.text + note: note.content }); return { message: i18n('$.notes.delete.success') }; }, { detail: { - tags: ['Admin'], - description: 'Deletes a specific player note' + tags: [DocumentationCategory.Notes], + description: 'Delete a specific player note' }, response: { - 200: t.Object({ message: t.String() }, { description: 'The note was deleted' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage notes' }), - 404: t.Object({ error: t.String() }, { description: 'The player or the note was not found' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tResponseBody.Message, + 403: tResponseBody.Error, + 404: tResponseBody.Error, }, - params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }), id: t.String({ description: 'The note ID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) + params: tParams.uuidAndApiKeyId, + headers: tHeaders }); \ No newline at end of file diff --git a/src/routes/players/[uuid]/position.ts b/src/routes/players/[uuid]/position.ts deleted file mode 100644 index 57d12f4..0000000 --- a/src/routes/players/[uuid]/position.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { t } from "elysia"; -import { getOrCreatePlayer } from "../../../database/schemas/players"; -import { Permission } from "../../../types/Permission"; -import { GlobalPosition } from "../../../types/GlobalPosition"; -import { capitalCase, snakeCase } from "change-case"; -import { GameProfile } from "../../../libs/game-profiles"; -import { ElysiaApp } from "../../.."; -import { ModLogType, sendModLogMessage } from "../../../libs/discord-notifier"; -import { sendTagChangeEmail } from "../../../libs/mailer"; -import { getI18nFunctionByLanguage } from "../../../middleware/fetch-i18n"; - -export default (app: ElysiaApp) => app.post('/', async ({ session, body: { position }, params, i18n, status }) => { // Change tag position - if(!session || !session.self && !session.player?.hasPermission(Permission.ManagePlayerPositions)) return status(403, { error: i18n('$.error.notAllowed') }); - - position = position.toLowerCase(); - if(!(capitalCase(position) in GlobalPosition)) return status(422, { error: i18n('$.position.invalid') }); - - const player = await getOrCreatePlayer(params.uuid); - if(session.self && player.isBanned()) return status(403, { error: i18n('$.error.banned') }); - if(snakeCase(player.position) == position) return status(400, { error: i18n('$.position.samePosition') }); - - const oldPosition = player.position; - player.position = position; - await player.save(); - - if(!session.self && session.player) { - sendModLogMessage({ - logType: ModLogType.EditPosition, - staff: await session.player.getGameProfile(), - user: await player.getGameProfile(), - discord: false, - positions: { - old: oldPosition || '---', - new: position - } - }); - - if(player.isEmailVerified()) { - sendTagChangeEmail(player.connections.email.address!, oldPosition || '---', position, getI18nFunctionByLanguage(player.last_language)); - } - } - - return { message: i18n(session.self ? '$.position.success.self' : '$.position.success.admin') }; -}, { - detail: { - tags: ['Settings'], - description: 'Changes your GlobalTag position' - }, - response: { - 200: t.Object({ message: t.String() }, { description: 'The tag position was updated' }), - 400: t.Object({ error: t.String() }, { description: 'You provided an invalid position' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to change your tag position' }), - 409: t.Object({ error: t.String() }, { description: 'Your tag is already in that position' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) - }, - body: t.Object({ position: t.String({ error: '$.error.wrongType;;[["field", "position"], ["type", "string"]]' }) }, { error: '$.error.invalidBody', additionalProperties: true }), - params: t.Object({ uuid: t.String({ description: 'Your UUID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) -}); \ No newline at end of file diff --git a/src/routes/players/[uuid]/referral.ts b/src/routes/players/[uuid]/referral.ts index b0e4402..543dcda 100644 --- a/src/routes/players/[uuid]/referral.ts +++ b/src/routes/players/[uuid]/referral.ts @@ -1,41 +1,36 @@ -import { t } from "elysia"; -import players, { getOrCreatePlayer } from "../../../database/schemas/players"; import { sendReferralMessage } from "../../../libs/discord-notifier"; import { stripUUID } from "../../../libs/game-profiles"; import { ElysiaApp } from "../../.."; +import { getOrCreatePlayer, Player } from "../../../database/schemas/Player"; +import { tResponseBody, tHeaders, tParams } from "../../../libs/models"; +import { DocumentationCategory } from "../../../types/DocumentationCategory"; export default (app: ElysiaApp) => app.post('/', async ({ session, params, i18n, status }) => { // Mark player as referrer if(!session?.uuid) return status(403, { error: i18n('$.error.notAllowed') }); if(session.self) return status(403, { error: i18n('$.referral.self') }); - const player = await players.findOne({ uuid: stripUUID(params.uuid) }); + const player = await Player.findOne({ uuid: stripUUID(params.uuid) }); if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); const executor = await getOrCreatePlayer(session.uuid); - if(executor.referrals.has_referred) return status(409, { error: i18n('$.referral.alreadyReferred') }); + if(await executor.hasReferrer()) return status(409, { error: i18n('$.referral.alreadyReferred') }); player.addReferral(session.uuid); await player.save(); - executor.referrals.has_referred = true; - executor.save(); - sendReferralMessage(await player.getGameProfile(), await executor.getGameProfile()); return { message: i18n('$.referral.success') }; }, { detail: { - tags: ['Interactions'], - description: 'Marks another player as your referrer' + tags: [DocumentationCategory.Referrals], + description: 'Mark another player as your referrer' }, response: { - 200: t.Object({ message: t.String() }, { description: 'The player was successfully marked as your referrer' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to mark this player as your referrer' }), - 404: t.Object({ error: t.String() }, { description: 'The player is not a GlobalTags user' }), - 409: t.Object({ error: t.String() }, { description: 'You have already marked someone as your referrer' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tResponseBody.Message, + 403: tResponseBody.Error, + 404: tResponseBody.Error, + 409: tResponseBody.Error }, - params: t.Object({ uuid: t.String({ description: 'The UUID of the player you want to refer to' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) + params: tParams.uuid, + headers: tHeaders }); \ No newline at end of file diff --git a/src/routes/players/[uuid]/reports.ts b/src/routes/players/[uuid]/reports.ts index 2bd6961..19b1943 100644 --- a/src/routes/players/[uuid]/reports.ts +++ b/src/routes/players/[uuid]/reports.ts @@ -1,43 +1,82 @@ import { t } from "elysia"; -import players, { getOrCreatePlayer } from "../../../database/schemas/players"; +import { getOrCreatePlayer, Player } from "../../../database/schemas/Player"; import { Permission } from "../../../types/Permission"; -import { ModLogType, sendModLogMessage, sendReportMessage } from "../../../libs/discord-notifier"; -import { formatUUID, stripUUID } from "../../../libs/game-profiles"; +import { sendReportMessage } from "../../../libs/discord-notifier"; +import { stripUUID } from "../../../libs/game-profiles"; import { ElysiaApp } from "../../.."; +import { Report } from "../../../database/schemas/Report"; +import { tResponseBody, tHeaders, tParams, tRequestBody, tSchema } from "../../../libs/models"; +import { DocumentationCategory } from "../../../types/DocumentationCategory"; -export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, status }) => { // Get reports - if(!session?.player?.hasPermission(Permission.ViewReports)) return status(403, { error: i18n('$.error.notAllowed') }); +export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, status }) => { // Get player made reports + if(!session?.self && !session?.player?.hasPermission(Permission.ViewReports)) return status(403, { error: i18n('$.error.notAllowed') }); - const player = await players.findOne({ uuid: stripUUID(params.uuid) }); + const player = await Player.findOne({ uuid: stripUUID(params.uuid) }); if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); - return player.reports.map((report) => ({ + const reports = await Report.find({ reporter_uuid: player.uuid }); + + return reports.map(report => ({ id: report.id, - reportedTag: report.reported_tag, - by: formatUUID(report.by), + reported_uuid: report.reported_uuid, + reporter_uuid: report.reporter_uuid, reason: report.reason, - createdAt: report.created_at.getTime() + context: report.context, + is_resolved: report.isResolved(), + created_at: report.created_at.getTime(), + last_updated: report.last_updated.getTime(), })); }, { detail: { - tags: ['Admin'], - description: 'Returns all player reports' + tags: [DocumentationCategory.Reports], + description: 'Get all player reports' + }, + response: { + 200: t.Array(tSchema.Report, { description: 'A report list' }), + 403: tResponseBody.Error, + 404: tResponseBody.Error, + }, + params: tParams.uuid, + headers: tHeaders +}).get('/:id', async ({ session, params, i18n, status }) => { // Get player made reports + if(!session?.self && !session?.player?.hasPermission(Permission.ViewReports)) return status(403, { error: i18n('$.error.notAllowed') }); + + const player = await Player.findOne({ uuid: stripUUID(params.uuid) }); + if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); + + const report = await Report.findOne({ id: params.id, reporter_uuid: player.uuid }); + if(!report) return status(404, { error: i18n('$.reports.not_found') }); + + return { + id: report.id, + reported_uuid: report.reported_uuid, + reporter_uuid: report.reporter_uuid, + reason: report.reason, + context: report.context, + is_resolved: report.isResolved(), + created_at: report.created_at.getTime(), + last_updated: report.last_updated.getTime(), + }; +}, { + detail: { + tags: [DocumentationCategory.Reports], + description: 'Get a single player report' }, response: { - 200: t.Array(t.Object({ id: t.String(), reportedTag: t.String(), by: t.String(), reason: t.String(), createdAt: t.Number() }), { description: 'The reports of the player' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage reports' }), - 404: t.Object({ error: t.String() }, { description: 'The player was not found' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tSchema.Report, + 403: tResponseBody.Error, + 404: tResponseBody.Error, }, - params: t.Object({ uuid: t.String({ description: 'The UUID of the player you want to get the reports of' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) + params: tParams.uuidAndReportId, + headers: tHeaders }).post('/', async ({ session, body: { reason }, params, i18n, status }) => { // Report player if(!session?.player) return status(403, { error: i18n('$.error.notAllowed') }); - if(session.self) return status(403, { error: i18n('$.report.self') }); - const player = await players.findOne({ uuid: stripUUID(params.uuid) }); + + reason = reason.trim(); + if(reason.length < 2 || reason.length > 200) return status(422, { error: i18n('$.error.invalid_reason').replace('', '2').replace('', '200') }); + + const player = await Player.findOne({ uuid: stripUUID(params.uuid) }); if(!player) return status(404, { error: i18n('$.error.playerNoTag') }); if(player.isBanned()) return status(403, { error: i18n('$.ban.already_banned') }); if(player.hasPermission(Permission.ReportImmunity)) return status(403, { error: i18n('$.report.immune') }); @@ -45,74 +84,39 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, const reporter = await getOrCreatePlayer(session.uuid!); if(reporter.isBanned()) return status(403, { error: i18n('$.error.banned') }); - if(player.reports.some((report) => report.by == reporter.uuid && report.reported_tag == player.tag)) return status(409, { error: i18n('$.report.alreadyReported') }); - if(reason.trim() == '') return status(422, { error: i18n('$.report.invalidReason') }); + if(await Report.exists({ reporter_uuid: reporter.uuid, 'context.tag': player.tag })) return status(409, { error: i18n('$.report.alreadyReported') }); - player.createReport({ - by: reporter.uuid, - reported_tag: player.tag, - reason - }); - await player.save(); + const report = await player.createReport(reporter.uuid, reason); sendReportMessage({ player: await player.getGameProfile(), reporter: await reporter.getGameProfile(), - tag: player.tag, - reason - }); - return { message: i18n('$.report.success') }; -}, { - detail: { - tags: ['Interactions'], - description: 'Reports another player' - }, - response: { - 200: t.Object({ message: t.String() }, { description: 'The player was successfully reported' }), - 403: t.Object({ error: t.String() }, { description: 'You are not authorized to report this player' }), - 404: t.Object({ error: t.String() }, { description: 'The player does not have a tag' }), - 409: t.Object({ error: t.String() }, { description: 'You already reported that tag' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) - }, - body: t.Object({ reason: t.String({ minLength: 2, maxLength: 200, error: '$.report.validation;;[["min", "2"], ["max", "200"]]', description: 'The report reason' }) }, { error: '$.error.invalidBody', additionalProperties: true }), - params: t.Object({ uuid: t.String({ description: 'The UUID of the player you want to report' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) -}).delete('/:id', async ({ session, params: { uuid, id }, i18n, status }) => { // Delete report - if(!session?.player?.hasPermission(Permission.DeleteReports)) return status(403, { error: i18n('$.error.notAllowed') }); - - const player = await players.findOne({ uuid: stripUUID(uuid) }); - if(!player) return status(404, { error: i18n(`$.error.playerNotFound`) }); - - const report = player.reports.find((report) => report.id == id.trim()); - if(!report) return status(404, { error: i18n(`$.report.delete.not_found`) }); - - player.deleteReport(report.id); - await player.save(); - - sendModLogMessage({ - logType: ModLogType.DeleteReport, - staff: await session.player.getGameProfile(), - user: await player.getGameProfile(), - discord: false, - report: `\`${report.reason}\` (\`#${report.id}\`)` + report }); - return { message: i18n(`$.report.delete.success`) }; + return { + id: report.id, + reported_uuid: report.reported_uuid, + reporter_uuid: report.reporter_uuid, + reason: report.reason, + context: report.context, + is_resolved: report.isResolved(), + created_at: report.created_at.getTime(), + last_updated: report.last_updated.getTime(), + }; }, { detail: { - tags: ['Admin'], - description: 'Deletes a specific player report' + tags: [DocumentationCategory.Reports], + description: 'Report another player' }, response: { - 200: t.Object({ message: t.String() }, { description: 'The report was deleted' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage reports' }), - 404: t.Object({ error: t.String() }, { description: 'The player or the report was not found' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tSchema.Report, + 403: tResponseBody.Error, + 404: tResponseBody.Error, + 409: tResponseBody.Error, + 422: tResponseBody.Error }, - params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }), id: t.String({ description: 'The report ID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) -});; \ No newline at end of file + body: tRequestBody.Report, + params: tParams.uuid, + headers: tHeaders +}); \ No newline at end of file diff --git a/src/routes/players/[uuid]/watchlist.ts b/src/routes/players/[uuid]/watchlist.ts deleted file mode 100644 index 3f3c337..0000000 --- a/src/routes/players/[uuid]/watchlist.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { t } from "elysia"; -import { ElysiaApp } from "../../.."; -import players from "../../../database/schemas/players"; -import { ModLogType, sendModLogMessage } from "../../../libs/discord-notifier"; -import { stripUUID } from "../../../libs/game-profiles"; -import { Permission } from "../../../types/Permission"; - -export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, status }) => { // Watch player - if(!session?.player?.hasPermission(Permission.ManageWatchlistEntries)) return status(403, { error: i18n('$.error.notAllowed') }); - - const player = await players.findOne({ uuid: stripUUID(params.uuid) }); - if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); - - return { watched: player.watchlist }; -}, { - detail: { - tags: ['Admin'], - description: 'Returns the player\'s watchlist status' - }, - response: { - 200: t.Object({ watched: t.Boolean() }, { description: 'The player\'s watchlist status' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage the watchlist' }), - 404: t.Object({ error: t.String() }, { description: 'The player was not found' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) - }, - params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) -}).patch('/', async ({ session, body: { watched }, params, i18n, status }) => { // Watch player - if(!session?.player?.hasPermission(Permission.ManageWatchlistEntries)) return status(403, { error: i18n('$.error.notAllowed') }); - - const player = await players.findOne({ uuid: stripUUID(params.uuid) }); - if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); - if(player.watchlist == watched) return status(409, { error: i18n(player.watchlist ? '$.watchlist.already_watched' : '$.watchlist.not_watched') }); - - player.watchlist = watched; - await player.save(); - - sendModLogMessage({ - logType: player.watchlist ? ModLogType.Watch : ModLogType.Unwatch, - staff: await session.player.getGameProfile(), - user: await player.getGameProfile(), - discord: false - }); - - return { message: i18n(player.watchlist ? `$.watchlist.success.watch` : '$.watchlist.success.unwatch') }; -}, { - detail: { - tags: ['Admin'], - description: 'Toggles the watchlist status of a player' - }, - response: { - 200: t.Object({ message: t.String() }, { description: 'The player\'s watchlist status was updated' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage the watchlist' }), - 404: t.Object({ error: t.String() }, { description: 'The player was not found' }), - 409: t.Object({ error: t.String() }, { description: 'This watchlist state is already set' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) - }, - body: t.Object({ watched: t.Boolean({ error: '$.error.wrongType;;[["field", "watched"], ["type", "boolean"]]' }) }, { error: '$.error.invalidBody', additionalProperties: true }), - params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) -}); \ No newline at end of file diff --git a/src/routes/reports.ts b/src/routes/reports.ts new file mode 100644 index 0000000..f6b3dd6 --- /dev/null +++ b/src/routes/reports.ts @@ -0,0 +1,107 @@ +import { t } from "elysia"; +import { ElysiaApp } from ".."; +import { Report } from "../database/schemas/Report"; +import { formatUUID } from "../libs/game-profiles"; +import { tHeaders, tParams, tResponseBody, tSchema } from "../libs/models"; +import { Permission } from "../types/Permission"; +import { DocumentationCategory } from "../types/DocumentationCategory"; + +export default (app: ElysiaApp) => app.get('/', async ({ session, status, i18n }) => { + if(!session?.player?.hasPermission(Permission.ViewReports)) return status(403, { error: i18n('$.error.notAllowed') }); + + const reports = await Report.find(); + + return reports.map(report => ({ + id: report.id, + reported_uuid: formatUUID(report.reported_uuid), + reporter_uuid: formatUUID(report.reporter_uuid), + reason: report.reason, + actions: report.actions.length, + context: { + tag: report.context.tag, + position: report.context.position, + icon: { + type: report.context.icon.type, + hash: report.context.icon.hash + } + }, + is_resolved: report.isResolved(), + created_at: report.created_at.getTime(), + last_updated: report.last_updated.getTime() + })); +}, { + detail: { + tags: [DocumentationCategory.Reports], + description: 'Get a specific report' + }, + response: { + 200: t.Array(tSchema.Report, { description: 'A report list' }), + 403: tResponseBody.Error, + 404: tResponseBody.Error + }, + params: tParams.reportId, + headers: tHeaders +}).get('/:id', async ({ session, params, status, i18n }) => { + if(!session?.player?.hasPermission(Permission.ViewReports)) return status(403, { error: i18n('$.error.notAllowed') }); + + const report = await Report.findOne({ id: params.id }); + if(!report) return status(404, { error: i18n('$.reports.not_found') }); + + return { + id: report.id, + reported_uuid: formatUUID(report.reported_uuid), + reporter_uuid: formatUUID(report.reporter_uuid), + reason: report.reason, + actions: report.actions.map(action => ({ + user: action.user, + type: action.type, + comment: action.comment, + added_at: action.added_at + })), + context: { + tag: report.context.tag, + position: report.context.position, + icon: { + type: report.context.icon.type, + hash: report.context.icon.hash + } + }, + is_resolved: report.isResolved(), + created_at: report.created_at.getTime(), + last_updated: report.last_updated.getTime(), + }; +}, { + detail: { + tags: [DocumentationCategory.Reports], + description: 'Get a specific report' + }, + response: { + 200: tSchema.Report, + 403: tResponseBody.Error, + 404: tResponseBody.Error + }, + params: tParams.reportId, + headers: tHeaders +}) // TODO: Add route to manage report actions and status +.delete('/:id', async ({ session, params, status, i18n }) => { + if(!session?.player?.hasPermission(Permission.DeleteReports)) return status(403, { error: i18n('$.error.notAllowed') }); + + const report = await Report.findOne({ id: params.id }); + if(!report) return status(404, { error: i18n('$.reports.not_found') }); + + await report.deleteOne(); + + return { message: i18n('$.reports.deleted') }; +}, { + detail: { + tags: [DocumentationCategory.Reports], + description: 'Delete a report' + }, + response: { + 200: tResponseBody.Message, + 403: tResponseBody.Error, + 404: tResponseBody.Error + }, + params: tParams.reportId, + headers: tHeaders +}); \ No newline at end of file diff --git a/src/routes/roles.ts b/src/routes/roles.ts index 2fe76c4..db03de6 100644 --- a/src/routes/roles.ts +++ b/src/routes/roles.ts @@ -1,8 +1,10 @@ import { t } from "elysia"; import { Permission } from "../types/Permission"; import { ModLogType, sendModLogMessage } from "../libs/discord-notifier"; -import roles, { getCachedRoles, getNextPosition, updateRoleCache } from "../database/schemas/roles"; +import { getCachedRoles, getNextPosition, Role, updateRoleCache } from "../database/schemas/Role"; import { ElysiaApp } from ".."; +import { tHeaders, tParams, tRequestBody, tResponseBody, tSchema } from "../libs/models"; +import { DocumentationCategory } from "../types/DocumentationCategory"; export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status }) => { // Get roles if(!session?.player?.hasPermission(Permission.ViewRoles)) return status(403, { error: i18n('$.error.notAllowed') }); @@ -17,17 +19,14 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status } })); }, { detail: { - tags: ['Roles'], + tags: [DocumentationCategory.Roles], description: 'Get all roles' }, response: { - 200: t.Array(t.Object({ id: t.String(), name: t.String(), position: t.Integer(), hasIcon: t.Boolean(), color: t.Nullable(t.String()), permissions: t.Number() }), { description: 'The role list' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage roles' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: t.Array(tSchema.Role, { description: 'A role list' }), + 403: tResponseBody.Error }, - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) + headers: tHeaders }).get('/:id', async ({ session, params: { id }, i18n, status }) => { // Get specific role if(!session?.player?.hasPermission(Permission.ViewRoles)) return status(403, { error: i18n('$.error.notAllowed') }); @@ -44,23 +43,20 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status } }; }, { detail: { - tags: ['Roles'], + tags: [DocumentationCategory.Roles], description: 'Get a specific role' }, response: { - 200: t.Object({ id: t.String(), name: t.String(), position: t.Integer(), hasIcon: t.Boolean(), color: t.Nullable(t.String()), permissions: t.Number() }, { description: 'The role data' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage roles' }), - 404: t.Object({ error: t.String() }, { description: 'There is no role with the name you provided' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tSchema.Role, + 403: tResponseBody.Error, + 404: tResponseBody.Error }, - params: t.Object({ id: t.String({ description: 'The role ID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) + params: tParams.roleId, + headers: tHeaders }).post('/', async ({ session, body, i18n, status }) => { // Create role if(!session?.player?.hasPermission(Permission.CreateRoles)) return status(403, { error: i18n('$.error.notAllowed') }); - const role = await roles.insertOne({ + const role = await Role.insertOne({ name: body.name.trim(), position: await getNextPosition(), hasIcon: false, @@ -85,23 +81,19 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status } }; }, { detail: { - tags: ['Roles'], - description: 'Create a new role' + tags: [DocumentationCategory.Roles], + description: 'Create a new role', }, response: { - 200: t.Object({ id: t.String(), name: t.String(), position: t.Integer(), hasIcon: t.Boolean(), color: t.Nullable(t.String()), permissions: t.Number() }, { description: 'The created role' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage roles' }), - 409: t.Object({ error: t.String() }, { description: 'A role with the provided name already exists' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tSchema.Role, + 403: tResponseBody.Error }, - body: t.Object({ name: t.String({ error: '$.error.wrongType;;[["field", "name"], ["type", "string"]]' }), color: t.Optional(t.Nullable(t.String({ minLength: 6, maxLength: 6, error: '$.error.wrongType;;[["field", "color"], ["type", "string"]]' }))), permissions: t.Optional(t.Integer({ error: '$.error.wrongType;;[["field", "permissions"], ["type", "integer"]]' })) }, { error: '$.error.invalidBody', additionalProperties: true }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) + body: tRequestBody.Role, + headers: tHeaders }).patch('/:id', async ({ session, params, body: { name, color, permissions }, i18n, status }) => { // Edit role if(!session?.player?.hasPermission(Permission.DeleteRoles)) return status(403, { error: i18n('$.error.notAllowed') }); - const role = await roles.findOne({ id: params.id }); + const role = await Role.findOne({ id: params.id }); if(!role) return status(404, { error: i18n('$.roles.not_found') }); let updated = false; @@ -141,25 +133,23 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status } }; }, { detail: { - tags: ['Roles'], + tags: [DocumentationCategory.Roles], description: 'Edit a role' }, response: { - 200: t.Object({ id: t.String(), name: t.String(), position: t.Integer(), hasIcon: t.Boolean(), color: t.Nullable(t.String()), permissions: t.Number() }, { description: 'The edited role' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage roles' }), - 404: t.Object({ error: t.String() }, { description: 'There is no role with the name you provided' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tSchema.Role, + 403: tResponseBody.Error, + 404: tResponseBody.Error, + 422: tResponseBody.Error, }, - body: t.Object({ name: t.Optional(t.String({ error: '$.error.wrongType;;[["field", "name"], ["type", "string"]]' })), color: t.Optional(t.Nullable(t.String({ minLength: 6, maxLength: 6, error: '$.error.wrongType;;[["field", "color"], ["type", "string"]]' }))), permissions: t.Optional(t.Integer({ error: '$.error.wrongType;;[["field", "permissions"], ["type", "integer"]]' })) }, { error: '$.error.invalidBody', additionalProperties: true }), - params: t.Object({ id: t.String({ description: 'The role ID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) + body: tRequestBody.Role, + params: tParams.roleId, + headers: tHeaders }) // TODO: Implement route to patch all roles at once .delete('/:id', async ({ session, params, i18n, status }) => { // Delete role if(!session?.player?.hasPermission(Permission.DeleteRoles)) return status(403, { error: i18n('$.error.notAllowed') }); - const role = await roles.findOne({ id: params.id }); + const role = await Role.findOne({ id: params.id }); if(!role) return status(404, { error: i18n('$.roles.not_found') }); sendModLogMessage({ @@ -175,17 +165,14 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status } return { message: i18n('$.roles.delete.success') }; }, { detail: { - tags: ['Roles'], + tags: [DocumentationCategory.Roles], description: 'Delete a role' }, response: { - 200: t.Object({ message: t.String() }, { description: 'The success message' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage roles' }), - 404: t.Object({ error: t.String() }, { description: 'There is no role with the name you provided' }), - 422: t.Object({ error: t.String() }, { description: 'You\'re lacking the validation requirements' }), - 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tResponseBody.Message, + 403: tResponseBody.Error, + 404: tResponseBody.Error, }, - params: t.Object({ id: t.String({ description: 'The role ID' }) }), - headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) + params: tParams.roleId, + headers: tHeaders }); \ No newline at end of file diff --git a/src/routes/staff.ts b/src/routes/staff.ts index 5688607..3cfa95a 100644 --- a/src/routes/staff.ts +++ b/src/routes/staff.ts @@ -1,21 +1,22 @@ import { t } from "elysia"; import { ElysiaApp } from ".."; -import staffCategories, { getNextPosition } from "../database/schemas/staff-categories"; -import staffMembers from "../database/schemas/staff-members"; import { formatUUID, GameProfile, stripUUID, uuidRegex } from "../libs/game-profiles"; import { generateSecureCode } from "../libs/crypto"; import { Permission } from "../types/Permission"; +import { getNextPosition, StaffCategory } from "../database/schemas/StaffCategory"; +import { StaffMember } from "../database/schemas/StaffMember"; +import { tRequestBody, tResponseBody, tSchema } from "../libs/models"; +import { DocumentationCategory } from "../types/DocumentationCategory"; export default (app: ElysiaApp) => app.get('/', async () => { - const categories = await staffCategories.find().sort({ position: 1 }).lean(); + const categories = await StaffCategory.find().sort({ position: 1 }).lean(); return await Promise.all( categories.map(async (category) => { - const members = await staffMembers.find({ category: category.id }).sort({ joinedAt: 1 }).lean(); + const members = await StaffMember.find({ category: category.id }).sort({ joinedAt: 1 }).lean(); const mappedMembers = await Promise.all(members.map(async (member) => ({ uuid: member.uuid, - username: (await GameProfile.getProfileByUUID(member.uuid)).username || 'Failed to load', description: member.description || null, avatar_url: 'https://example.com/avatar.png', // TODO: Replace with actual avatar URL logic joined_at: member.joined_at.getTime() @@ -30,39 +31,37 @@ export default (app: ElysiaApp) => app.get('/', async () => { ); }, { detail: { - tags: ['API'], - description: 'Gets the staff team members' + tags: [DocumentationCategory.Staff], + description: 'Get the staff team overview' }, response: { - 200: t.Array(t.Object({ id: t.String(), name: t.String(), members: t.Array(t.Object({ uuid: t.String(), username: t.String(), description: t.Union([t.String(), t.Null()]), avatar_url: t.String(), joined_at: t.Integer() })) }), { description: 'The team categories with its members' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tResponseBody.StaffList } }).group('/categories', (app) => app.get('/', async ({ session, i18n, status }) => { if(!session?.player?.hasPermission(Permission.ViewStaffCategories)) return status(403, { error: i18n('$.error.notAllowed') }); - return Promise.all((await staffCategories.find().sort({ position: 1 }).lean()).map(async (category) => ({ + return Promise.all((await StaffCategory.find().sort({ position: 1 }).lean()).map(async (category) => ({ id: category.id, name: category.name, position: category.position, - members: (await staffMembers.find({ category: category.id }).sort({ joinedAt: 1 }).lean()).length + members: (await StaffMember.find({ category: category.id }).sort({ joinedAt: 1 }).lean()).length }))); }, { detail: { - tags: ['API'], - description: 'Gets the staff team categories' + tags: [DocumentationCategory.Staff], + description: 'Get all staff categories' }, response: { - 200: t.Array(t.Object({ id: t.String(), name: t.String(), position: t.Integer(), members: t.Integer() }), { description: 'The team categories' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage staff categories' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: t.Array(tSchema.MemberlistStaffCategory, { description: 'A staff category list' }), + 403: tResponseBody.Error, } }).get('/:category', async ({ session, params: { category: id }, i18n, status }) => { if(!session?.player?.hasPermission(Permission.ViewStaffCategories)) return status(403, { error: i18n('$.error.notAllowed') }); - const category = await staffCategories.findOne({ id }).lean(); + const category = await StaffCategory.findOne({ id }).lean(); if(!category) return status(404, { error: i18n('$.staff.categories.not_found') }); - const members = await staffMembers.find({ category: category.id }).sort({ joinedAt: 1 }).lean(); + const members = await StaffMember.find({ category: category.id }).sort({ joinedAt: 1 }).lean(); return { id: category.id, @@ -72,21 +71,20 @@ export default (app: ElysiaApp) => app.get('/', async () => { }; }, { detail: { - tags: ['API'], - description: 'Gets a specific staff team category by ID' + tags: [DocumentationCategory.Staff], + description: 'Get a specific staff category' }, response: { - 200: t.Object({ id: t.String(), name: t.String(), position: t.Integer(), members: t.Integer() }, { description: 'The team category' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage staff categories' }), - 404: t.Object({ error: t.String() }, { description: 'Category not found' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tSchema.MemberlistStaffCategory, + 403: tResponseBody.Error, + 404: tResponseBody.Error, }, params: t.Object({ category: t.String({ description: 'The category ID' }) }), headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }), }).post('/', async ({ session, body: { name }, i18n, status }) => { if(!session?.player?.hasPermission(Permission.CreateStaffCategories)) return status(403, { error: i18n('$.error.notAllowed') }); - const category = await staffCategories.insertOne({ + const category = await StaffCategory.insertOne({ id: generateSecureCode(), name: name.trim(), position: await getNextPosition() @@ -99,22 +97,19 @@ export default (app: ElysiaApp) => app.get('/', async () => { }); }, { detail: { - tags: ['API'], - description: 'Creates a new staff team category' + tags: [DocumentationCategory.Staff], + description: 'Create a new staff category' }, response: { - 201: t.Object({ id: t.String(), name: t.String(), position: t.Integer() }, { description: 'The created team category' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage staff categories' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 201: tSchema.StaffCategory, + 403: tResponseBody.Error, }, - body: t.Object({ - name: t.String({ minLength: 1, error: '$.error.wrongType;;[["field", "name"], ["type", "string"]]' }) - }, { error: '$.error.invalidBody', additionalProperties: true }), + body: tRequestBody.StaffCategory, headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).patch('/:id', async ({ session, body: { name }, params: { id }, i18n, status }) => { // Edit category if(!session?.player?.hasPermission(Permission.EditStaffMembers)) return status(403, { error: i18n('$.error.notAllowed') }); - const category = await staffCategories.findOne({ id }); + const category = await StaffCategory.findOne({ id }); if(!category) return status(404, { error: i18n('$.staff.categories.not_found') }); if(name && category.name !== name.trim()) { @@ -124,32 +119,29 @@ export default (app: ElysiaApp) => app.get('/', async () => { // TODO: Add mod log } - return status(200, { + return { id: category.id, name: category.name, position: category.position - }); + }; }, { detail: { - tags: ['API'], - description: 'Edits an existing staff category' + tags: [DocumentationCategory.Staff], + description: 'Edit an existing staff category' }, response: { - 200: t.Object({ id: t.String(), name: t.String(), position: t.Integer() }, { description: 'The edited team category' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage staff categories' }), - 404: t.Object({ error: t.String() }, { description: 'Category not found' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tSchema.StaffCategory, + 403: tResponseBody.Error, + 404: tResponseBody.Error, }, - body: t.Object({ - name: t.Optional(t.String({ error: '$.error.wrongType;;[["field", "name"], ["type", "string"]]' })) - }, { error: '$.error.invalidBody', additionalProperties: true }), + body: tRequestBody.StaffCategory, params: t.Object({ id: t.String({ description: 'The category ID' }) }), headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }) // TODO: Implement route to patch all categories at once .delete('/:category', async ({ session, params: { category: id }, i18n, status }) => { if(!session?.player?.hasPermission(Permission.DeleteStaffCategories)) return status(403, { error: i18n('$.error.notAllowed') }); - const category = await staffCategories.findOne({ id }); + const category = await StaffCategory.findOne({ id }); if(!category) return status(404, { error: i18n('$.staff.categories.not_found') }); await category.deleteOne(); @@ -157,14 +149,13 @@ export default (app: ElysiaApp) => app.get('/', async () => { return { message: i18n('$.staff.categories.delete.success').replace('', category.name) }; }, { detail: { - tags: ['API'], - description: 'Deletes a specific staff team category by ID' + tags: [DocumentationCategory.Staff], + description: 'Delete a specific staff category' }, response: { - 200: t.Object({ message: t.String() }, { description: 'The success message' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage staff categories' }), - 404: t.Object({ error: t.String() }, { description: 'Category not found' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tResponseBody.Message, + 403: tResponseBody.Error, + 404: tResponseBody.Error, }, params: t.Object({ category: t.String({ description: 'The category ID' }) }), headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }), @@ -173,7 +164,7 @@ export default (app: ElysiaApp) => app.get('/', async () => { app.get('/', async ({ session, i18n, status }) => { if(!session?.player?.hasPermission(Permission.ViewStaffMembers)) return status(403, { error: i18n('$.error.notAllowed') }); - return Promise.all((await staffMembers.find().sort({ joinedAt: 1 }).lean()).map(async member => ({ + return Promise.all((await StaffMember.find().sort({ joinedAt: 1 }).lean()).map(async member => ({ uuid: formatUUID(member.uuid), username: (await GameProfile.getProfileByUUID(member.uuid)).username || 'Unknown', category: member.category, @@ -182,18 +173,17 @@ export default (app: ElysiaApp) => app.get('/', async () => { }))); }, { detail: { - tags: ['API'], - description: 'Gets all staff team members' + tags: [DocumentationCategory.Staff], + description: 'Get all staff members' }, response: { - 200: t.Array(t.Object({ uuid: t.String(), username: t.String(), category: t.String(), description: t.Union([t.String(), t.Null()]), joined_at: t.Integer() }), { description: 'The team members' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage staff members' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: t.Array(tSchema.StaffMember, { description: 'A staff member list' }), + 403: tResponseBody.Error, } }).get('/:uuid', async ({ session, params: { uuid }, i18n, status }) => { if(!session?.player?.hasPermission(Permission.ViewStaffMembers)) return status(403, { error: i18n('$.error.notAllowed') }); - const member = await staffMembers.findOne({ uuid: stripUUID(uuid) }); + const member = await StaffMember.findOne({ uuid: stripUUID(uuid) }); if(!member) return status(404, { error: i18n('$.staff.members.not_found') }); return { @@ -205,14 +195,13 @@ export default (app: ElysiaApp) => app.get('/', async () => { }; }, { detail: { - tags: ['API'], - description: 'Gets a specific staff team member by UUID' + tags: [DocumentationCategory.Staff], + description: 'Get a specific staff member' }, response: { - 200: t.Object({ uuid: t.String(), username: t.String(), category: t.String(), description: t.Union([t.String(), t.Null()]), joined_at: t.Integer() }, { description: 'The team member' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage staff members' }), - 404: t.Object({ error: t.String() }, { description: 'Member not found' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tSchema.StaffMember, + 403: tResponseBody.Error, + 404: tResponseBody.Error, }, params: t.Object({ uuid: t.String({ description: 'The member UUID' }) }), headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }), @@ -223,15 +212,15 @@ export default (app: ElysiaApp) => app.get('/', async () => { if(!uuidRegex.test(uuid)) return status(400, { error: i18n('$.staff.members.invalid_uuid') }); - const existingMember = await staffMembers.findOne({ uuid }); + const existingMember = await StaffMember.findOne({ uuid }); if(existingMember) return status(409, { error: i18n('$.staff.members.already_exists') }); - if(!(await staffCategories.exists({ id: category }))) return status(404, { error: i18n('$.staff.categories.not_found') }); + if(!(await StaffCategory.exists({ id: category }))) return status(404, { error: i18n('$.staff.categories.not_found') }); const joinedAt = new Date(); joinedAt.setHours(0, 0, 0, 0); - const newMember = await staffMembers.insertOne({ + const newMember = await StaffMember.insertOne({ uuid: stripUUID(uuid.trim()), category, description: description?.trim() || null, @@ -247,32 +236,27 @@ export default (app: ElysiaApp) => app.get('/', async () => { }); }, { detail: { - tags: ['API'], - description: 'Adds a new staff team member' + tags: [DocumentationCategory.Staff], + description: 'Create a new staff member' }, response: { - 201: t.Object({ uuid: t.String(), username: t.String(), category: t.String(), description: t.Union([t.String(), t.Null()]), joined_at: t.Integer() }, { description: 'The created team member' }), - 400: t.Object({ error: t.String() }, { description: 'An invalid UUID was passed' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage staff members' }), - 404: t.Object({ error: t.String() }, { description: 'Category not found' }), - 409: t.Object({ error: t.String() }, { description: 'Member already exists' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 201: tSchema.StaffMember, + 400: tResponseBody.Error, + 403: tResponseBody.Error, + 404: tResponseBody.Error, + 409: tResponseBody.Error, }, - body: t.Object({ - uuid: t.String({ error: '$.error.wrongType;;[["field", "uuid"], ["type", "string"]]' }), - category: t.String({ error: '$.error.wrongType;;[["field", "category"], ["type", "string"]]' }), - description: t.Union([t.String(), t.Null()], { error: '$.error.wrongType;;[["field", "description"], ["type", "string"]]' }) - }, { error: '$.error.invalidBody', additionalProperties: true }), + body: tRequestBody.CreateStaffMember, headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).patch('/:uuid', async ({ session, body: { category, description }, params: { uuid }, i18n, status }) => { if(!session?.player?.hasPermission(Permission.EditStaffMembers)) return status(403, { error: i18n('$.error.notAllowed') }); - const member = await staffMembers.findOne({ uuid: stripUUID(uuid) }); + const member = await StaffMember.findOne({ uuid: stripUUID(uuid) }); if(!member) return status(404, { error: i18n('$.staff.members.not_found') }); let updated = false; if(category !== undefined && member.category !== category) { - if(!(await staffCategories.exists({ id: category }))) return status(404, { error: i18n('$.staff.categories.not_found') }); + if(!(await StaffCategory.exists({ id: category }))) return status(404, { error: i18n('$.staff.categories.not_found') }); member.category = category; updated = true; } @@ -282,34 +266,30 @@ export default (app: ElysiaApp) => app.get('/', async () => { } if(updated) member.save(); // TODO: Add mod log - return status(200, { + return { uuid: formatUUID(member.uuid), username: (await GameProfile.getProfileByUUID(member.uuid)).username || 'Unknown', category: member.category, description: member.description || null, joined_at: member.joined_at.getTime() - }); + }; }, { detail: { - tags: ['API'], - description: 'Edits an existing staff team member' + tags: [DocumentationCategory.Staff], + description: 'Edit an existing staff member' }, response: { - 200: t.Object({ uuid: t.String(), username: t.String(), category: t.String(), description: t.Union([t.String(), t.Null()]), joined_at: t.Integer() }, { description: 'The edited team member' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage staff members' }), - 404: t.Object({ error: t.String() }, { description: 'Mmeber or category not found' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tSchema.StaffMember, + 403: tResponseBody.Error, + 404: tResponseBody.Error, }, - body: t.Object({ - category: t.Optional(t.String({ error: '$.error.wrongType;;[["field", "category"], ["type", "string"]]' })), - description: t.Optional(t.Union([t.String(), t.Null()], { error: '$.error.wrongType;;[["field", "description"], ["type", "string"]]' })) - }, { error: 'error.invalidBody', additionalProperties: true }), + body: tRequestBody.EditStaffMember, params: t.Object({ uuid: t.String({ description: 'The member UUID' }) }), headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).delete('/:uuid', async ({ session, params: { uuid }, i18n, status }) => { if(!session?.player?.hasPermission(Permission.DeleteStaffMembers)) return status(403, { error: i18n('$.error.notAllowed') }); - const member = await staffMembers.findOne({ uuid: stripUUID(uuid) }); + const member = await StaffMember.findOne({ uuid: stripUUID(uuid) }); if(!member) return status(404, { error: i18n('$.staff.members.not_found') }); await member.deleteOne(); @@ -317,14 +297,13 @@ export default (app: ElysiaApp) => app.get('/', async () => { return { message: i18n('$.staff.members.delete.success').replace('', (await GameProfile.getProfileByUUID(uuid)).username || 'Unknown') }; }, { detail: { - tags: ['API'], - description: 'Deletes a specific staff team member by UUID' + tags: [DocumentationCategory.Staff], + description: 'Delete a specific staff member' }, response: { - 200: t.Object({ message: t.String() }, { description: 'The success message' }), - 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage staff members' }), - 404: t.Object({ error: t.String() }, { description: 'Member not found' }), - 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) + 200: tResponseBody.Message, + 403: tResponseBody.Error, + 404: tResponseBody.Error, }, params: t.Object({ uuid: t.String({ description: 'The member UUID' }) }), headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) diff --git a/src/types/DocumentationCategory.ts b/src/types/DocumentationCategory.ts new file mode 100644 index 0000000..a893116 --- /dev/null +++ b/src/types/DocumentationCategory.ts @@ -0,0 +1,15 @@ +export enum DocumentationCategory { + Api = 'API', + ApiKeys = 'API Keys', + Applications = 'Applications', + Bans = 'Bans', + GiftCodes = 'Gift codes', + Locks = 'Locks', + Notes = 'Notes', + Partners = 'Partners', + Referrals = 'Referrals', + Reports = 'Reports', + Roles = 'Roles', + Staff = 'Staff', + Tags = 'Tags', +} \ No newline at end of file diff --git a/src/types/GlobalIcon.ts b/src/types/GlobalIcon.ts index 99fe970..72c677e 100644 --- a/src/types/GlobalIcon.ts +++ b/src/types/GlobalIcon.ts @@ -1,49 +1,49 @@ export enum GlobalIcon { - None, - Custom, - Android, - Apple, - Bereal, - Cashapp, - Crown, - Discord, - Duolingo, - Ebio, - Epicgames, - Facebook, - Gamescom, - Github, - Gitlab, - Globaltags, - Heart, - Instagram, - Kick, - Labymod, - Labynet, - Linkedin, - Namemc, - Mastodon, - Patreon, - Paypal, - Pinterest, - Playstation, - Reddit, - Snapchat, - Soundcloud, - Spotify, - Star, - Statsfm, - Steam, - Telegram, - Tellonym, - Threads, - Tiktok, - Twitch, - Venmo, - Wechat, - X, - Xbox, - Youtube + None = 'none', + Custom = 'custom', + Android = 'android', + Apple = 'apple', + Bereal = 'bereal', + Cashapp = 'cashapp', + Crown = 'crown', + Discord = 'discord', + Duolingo = 'duolingo', + Ebio = 'ebio', + Epicgames = 'epicgames', + Facebook = 'facebook', + Gamescom = 'gamescom', + Github = 'github', + Gitlab = 'gitlab', + Globaltags = 'globaltags', + Heart = 'heart', + Instagram = 'instagram', + Kick = 'kick', + Labymod = 'labymod', + Labynet = 'labynet', + Linkedin = 'linkedin', + Namemc = 'namemc', + Mastodon = 'mastodon', + Patreon = 'patreon', + Paypal = 'paypal', + Pinterest = 'pinterest', + Playstation = 'playstation', + Reddit = 'reddit', + Snapchat = 'snapchat', + Soundcloud = 'soundcloud', + Spotify = 'spotify', + Star = 'star', + Statsfm = 'statsfm', + Steam = 'steam', + Telegram = 'telegram', + Tellonym = 'tellonym', + Threads = 'threads', + Tiktok = 'tiktok', + Twitch = 'twitch', + Venmo = 'venmo', + Wechat = 'wechat', + X = 'x', + Xbox = 'xbox', + Youtube = 'youtube', } -export const icons = Object.keys(GlobalIcon).filter(key => isNaN(Number(key))).map((icon) => GlobalIcon[icon as keyof typeof GlobalIcon]); \ No newline at end of file +export const icons = Object.values(GlobalIcon); \ No newline at end of file diff --git a/src/types/GlobalPosition.ts b/src/types/GlobalPosition.ts index 95b1fc4..67ffda5 100644 --- a/src/types/GlobalPosition.ts +++ b/src/types/GlobalPosition.ts @@ -1,8 +1,8 @@ export enum GlobalPosition { - Above, - Below, - Right, - Left + Above = 'above', + Below = 'below', + Right = 'right', + Left = 'left' } -export const positions = Object.keys(GlobalPosition).filter(key => isNaN(Number(key))).map((icon) => GlobalPosition[icon as keyof typeof GlobalPosition]); \ No newline at end of file +export const positions = Object.values(GlobalPosition); \ No newline at end of file diff --git a/src/types/MailTemplate.ts b/src/types/MailTemplate.ts new file mode 100644 index 0000000..35a96b7 --- /dev/null +++ b/src/types/MailTemplate.ts @@ -0,0 +1,11 @@ +export enum MailTemplate { // TODO: Add more templates + Banned = 'banned', + IconChanged = 'icon_changed', + IconCleared = 'icon_cleared', + PositionChanged = 'position_changed', + TagChanged = 'tag_changed', + TagCleared = 'tag_cleared', + Unbanned = 'unbanned', + Verification = 'verification', + Verified = 'verified' +} \ No newline at end of file diff --git a/src/types/Permission.ts b/src/types/Permission.ts index 8258a59..f26e8d0 100644 --- a/src/types/Permission.ts +++ b/src/types/Permission.ts @@ -18,6 +18,12 @@ export enum Permission { // TODO: Replace real bitfield values EditStaffMembers = 1 << 0, DeleteStaffMembers = 1 << 0, + //* Applications + + ViewApplications = 1 << 0, + ReviewApplications = 1 << 0, + DeleteApplications = 1 << 0, + //* Bans ViewBans = 1 << 0, @@ -44,6 +50,10 @@ export enum Permission { // TODO: Replace real bitfield values EditGiftCodes = 1 << 0, DeleteGiftCodes = 1 << 0, + //* Locks + ViewLocks = 1 << 0, + ManageLocks = 1 << 0, + //* Notes ViewNotes = 1 << 0, @@ -51,14 +61,6 @@ export enum Permission { // TODO: Replace real bitfield values EditNotes = 1 << 0, DeleteNotes = 1 << 0, - //* Roles - - ViewRoles = 1 << 0, - CreateRoles = 1 << 0, - EditRoles = 1 << 0, - DeleteRoles = 1 << 0, - ManagePlayerRoles = 1 << 0, - //* Player management ViewTagHistory = 1 << 0, @@ -78,6 +80,14 @@ export enum Permission { // TODO: Replace real bitfield values ReviewReports = 1 << 0, DeleteReports = 1 << 0, + //* Roles + + ViewRoles = 1 << 0, + CreateRoles = 1 << 0, + EditRoles = 1 << 0, + DeleteRoles = 1 << 0, + ManagePlayerRoles = 1 << 0, + //* Watchlist ViewWatchlist = 1 << 0,