diff --git a/docker-compose.yml b/docker-compose.yml index a0b5c32..10feb4b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,6 +22,7 @@ services: build: context: . dockerfile: Dockerfile + container_name: arabot depends_on: - postgres - redis diff --git a/package-lock.json b/package-lock.json index 1c44373..7865267 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,16 +9,17 @@ "version": "0.1.0", "license": "GPL-3.0-or-later", "dependencies": { - "@prisma/client": "^4.0.0", + "@prisma/client": "^4.10.1", "@sapphire/discord.js-utilities": "^6.0.0", - "@sapphire/framework": "^4.0.1", + "@sapphire/framework": "^4.1.0", "@sapphire/plugin-logger": "^3.0.1", - "@sapphire/plugin-scheduled-tasks": "^4.0.0", + "@sapphire/plugin-scheduled-tasks": "^6.0.0", "@sapphire/plugin-subcommands": "^4.0.0", - "@sapphire/stopwatch": "^1.4.1", + "@sapphire/stopwatch": "^1.5.0", + "@sapphire/time-utilities": "^1.7.8", "@sapphire/utilities": "^3.9.2", "@types/node": "^18.0.3", - "bullmq": "^1.89.1", + "bullmq": "^3.6.6", "discord.js": "^14.7.1", "dotenv": "^16.0.1", "ts-node": "^10.8.2", @@ -34,7 +35,7 @@ "eslint-config-airbnb-typescript": "^17.0.0", "eslint-import-resolver-typescript": "^3.5.3", "eslint-plugin-import": "^2.27.5", - "prisma": "^4.4.0" + "prisma": "^4.10.1" } }, "node_modules/@cspotcode/source-map-support": { @@ -306,12 +307,12 @@ } }, "node_modules/@prisma/client": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.9.0.tgz", - "integrity": "sha512-bz6QARw54sWcbyR1lLnF2QHvRW5R/Jxnbbmwh3u+969vUKXtBkXgSgjDA85nji31ZBlf7+FrHDy5x+5ydGyQDg==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.10.1.tgz", + "integrity": "sha512-VonXLJZybdt8e5XZH5vnIGCRNnIh6OMX1FS3H/yzMGLT3STj5TJ/OkMcednrvELgk8PK89Vo3aSh51MWNO0axA==", "hasInstallScript": true, "dependencies": { - "@prisma/engines-version": "4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5" + "@prisma/engines-version": "4.10.1-2.aead147aa326ccb985dcfed5b065b4fdabd44b19" }, "engines": { "node": ">=14.17" @@ -326,16 +327,16 @@ } }, "node_modules/@prisma/engines": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.9.0.tgz", - "integrity": "sha512-t1pt0Gsp+HcgPJrHFc+d/ZSAaKKWar2G/iakrE07yeKPNavDP3iVKPpfXP22OTCHZUWf7OelwKJxQgKAm5hkgw==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.10.1.tgz", + "integrity": "sha512-B3tcTxjx196nuAu1GOTKO9cGPUgTFHYRdkPkTS4m5ptb2cejyBlH9X7GOfSt3xlI7p4zAJDshJP4JJivCg9ouA==", "devOptional": true, "hasInstallScript": true }, "node_modules/@prisma/engines-version": { - "version": "4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5.tgz", - "integrity": "sha512-M16aibbxi/FhW7z1sJCX8u+0DriyQYY5AyeTH7plQm9MLnURoiyn3CZBqAyIoQ+Z1pS77usCIibYJWSgleBMBA==" + "version": "4.10.1-2.aead147aa326ccb985dcfed5b065b4fdabd44b19", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.10.1-2.aead147aa326ccb985dcfed5b065b4fdabd44b19.tgz", + "integrity": "sha512-tsjTho7laDhf9EJ9EnDxAPEf7yrigSMDhniXeU4YoWc7azHAs4GPxRi2P9LTFonmHkJLMOLjR77J1oIP8Ife1w==" }, "node_modules/@sapphire/async-queue": { "version": "1.5.0", @@ -346,6 +347,18 @@ "npm": ">=7.0.0" } }, + "node_modules/@sapphire/cron": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/cron/-/cron-1.0.0.tgz", + "integrity": "sha512-pKYfpnHiDFknur3yoquA8cqbJZS140y2oqjshwGGmtjiuIbUngQhPHGwdWHNDKDrF6EKbOK06nd2URE+0eUrfQ==", + "dependencies": { + "@sapphire/utilities": "^3.9.3" + }, + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, "node_modules/@sapphire/discord-utilities": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@sapphire/discord-utilities/-/discord-utilities-3.0.0.tgz", @@ -383,9 +396,9 @@ } }, "node_modules/@sapphire/framework": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@sapphire/framework/-/framework-4.0.2.tgz", - "integrity": "sha512-IoSZGBPJjiINJKJKaBfnpEB1IxPv7yitunnvJ6V5XcTdxP51I/KsVJX2ELxiH7sslg8ZrQQMRIcluGLbVwv4KA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@sapphire/framework/-/framework-4.1.0.tgz", + "integrity": "sha512-jtwZPysF13Sn8h2p8nkIPETveGAxRmYmiqxYkd3VXV8VPWwKBG8IriuI4oExpSnuCqxIs5HRpo3M+Gl+f/mdCg==", "dependencies": { "@discordjs/builders": "^1.4.0", "@sapphire/discord-utilities": "^3.0.0", @@ -443,12 +456,12 @@ } }, "node_modules/@sapphire/plugin-scheduled-tasks": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@sapphire/plugin-scheduled-tasks/-/plugin-scheduled-tasks-4.0.1.tgz", - "integrity": "sha512-vLxfHBu2vKaJZ9v2f4z+VDZaPeDqS8bm+Sc2minRwJPw1hWAHiPqmxCBPIONY7eOQ9qKayvhKYTIwwruxgO/Mg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/plugin-scheduled-tasks/-/plugin-scheduled-tasks-6.0.0.tgz", + "integrity": "sha512-R9rga1aZk3GSXkmGfBMQR8Ng4ou36l5WGWoDvPaq1xNb56wZJ2zPZPsQHV6lxNoyOT++M8GFhVhh9OUB+xwEXg==", "dependencies": { - "@sapphire/stopwatch": "^1.4.1", - "@sapphire/utilities": "^3.9.3", + "@sapphire/stopwatch": "^1.5.0", + "@sapphire/utilities": "^3.11.0", "tslib": "^2.4.0" }, "engines": { @@ -524,6 +537,21 @@ "npm": ">=7.0.0" } }, + "node_modules/@sapphire/time-utilities": { + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/@sapphire/time-utilities/-/time-utilities-1.7.8.tgz", + "integrity": "sha512-T6X/nwCvKhxmNRexgmA3KwLt3Z+xzlErkre4viflx46hHOmNNb3hoIyQtekgHYrabEaHWNbqW4PW7gC3hBc+ag==", + "dependencies": { + "@sapphire/cron": "^1.0.0", + "@sapphire/duration": "^1.0.0", + "@sapphire/timer-manager": "^1.0.0", + "@sapphire/timestamp": "^1.0.0" + }, + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, "node_modules/@sapphire/timer-manager": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@sapphire/timer-manager/-/timer-manager-1.0.0.tgz", @@ -612,9 +640,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.11.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.19.tgz", - "integrity": "sha512-YUgMWAQBWLObABqrvx8qKO1enAvBUdjZOAWQ5grBAkp5LQv45jBvYKZ3oFS9iKRCQyFjqw6iuEa1vmFqtxYLZw==" + "version": "18.13.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.13.0.tgz", + "integrity": "sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==" }, "node_modules/@types/semver": { "version": "7.3.13", @@ -631,14 +659,14 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.51.0.tgz", - "integrity": "sha512-wcAwhEWm1RgNd7dxD/o+nnLW8oH+6RK1OGnmbmkj/GGoDPV1WWMVP0FXYQBivKHdwM1pwii3bt//RC62EriIUQ==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.52.0.tgz", + "integrity": "sha512-lHazYdvYVsBokwCdKOppvYJKaJ4S41CgKBcPvyd0xjZNbvQdhn/pnJlGtQksQ/NhInzdaeaSarlBjDXHuclEbg==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.51.0", - "@typescript-eslint/type-utils": "5.51.0", - "@typescript-eslint/utils": "5.51.0", + "@typescript-eslint/scope-manager": "5.52.0", + "@typescript-eslint/type-utils": "5.52.0", + "@typescript-eslint/utils": "5.52.0", "debug": "^4.3.4", "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", @@ -665,14 +693,14 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.51.0.tgz", - "integrity": "sha512-fEV0R9gGmfpDeRzJXn+fGQKcl0inIeYobmmUWijZh9zA7bxJ8clPhV9up2ZQzATxAiFAECqPQyMDB4o4B81AaA==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.52.0.tgz", + "integrity": "sha512-e2KiLQOZRo4Y0D/b+3y08i3jsekoSkOYStROYmPUnGMEoA0h+k2qOH5H6tcjIc68WDvGwH+PaOrP1XRzLJ6QlA==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.51.0", - "@typescript-eslint/types": "5.51.0", - "@typescript-eslint/typescript-estree": "5.51.0", + "@typescript-eslint/scope-manager": "5.52.0", + "@typescript-eslint/types": "5.52.0", + "@typescript-eslint/typescript-estree": "5.52.0", "debug": "^4.3.4" }, "engines": { @@ -692,13 +720,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.51.0.tgz", - "integrity": "sha512-gNpxRdlx5qw3yaHA0SFuTjW4rxeYhpHxt491PEcKF8Z6zpq0kMhe0Tolxt0qjlojS+/wArSDlj/LtE69xUJphQ==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.52.0.tgz", + "integrity": "sha512-AR7sxxfBKiNV0FWBSARxM8DmNxrwgnYMPwmpkC1Pl1n+eT8/I2NAUPuwDy/FmDcC6F8pBfmOcaxcxRHspgOBMw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.51.0", - "@typescript-eslint/visitor-keys": "5.51.0" + "@typescript-eslint/types": "5.52.0", + "@typescript-eslint/visitor-keys": "5.52.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -709,13 +737,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.51.0.tgz", - "integrity": "sha512-QHC5KKyfV8sNSyHqfNa0UbTbJ6caB8uhcx2hYcWVvJAZYJRBo5HyyZfzMdRx8nvS+GyMg56fugMzzWnojREuQQ==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.52.0.tgz", + "integrity": "sha512-tEKuUHfDOv852QGlpPtB3lHOoig5pyFQN/cUiZtpw99D93nEBjexRLre5sQZlkMoHry/lZr8qDAt2oAHLKA6Jw==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.51.0", - "@typescript-eslint/utils": "5.51.0", + "@typescript-eslint/typescript-estree": "5.52.0", + "@typescript-eslint/utils": "5.52.0", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -736,9 +764,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.51.0.tgz", - "integrity": "sha512-SqOn0ANn/v6hFn0kjvLwiDi4AzR++CBZz0NV5AnusT2/3y32jdc0G4woXPWHCumWtUXZKPAS27/9vziSsC9jnw==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.52.0.tgz", + "integrity": "sha512-oV7XU4CHYfBhk78fS7tkum+/Dpgsfi91IIDy7fjCyq2k6KB63M6gMC0YIvy+iABzmXThCRI6xpCEyVObBdWSDQ==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -749,13 +777,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.51.0.tgz", - "integrity": "sha512-TSkNupHvNRkoH9FMA3w7TazVFcBPveAAmb7Sz+kArY6sLT86PA5Vx80cKlYmd8m3Ha2SwofM1KwraF24lM9FvA==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.52.0.tgz", + "integrity": "sha512-WeWnjanyEwt6+fVrSR0MYgEpUAuROxuAH516WPjUblIrClzYJj0kBbjdnbQXLpgAN8qbEuGywiQsXUVDiAoEuQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.51.0", - "@typescript-eslint/visitor-keys": "5.51.0", + "@typescript-eslint/types": "5.52.0", + "@typescript-eslint/visitor-keys": "5.52.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -776,16 +804,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.51.0.tgz", - "integrity": "sha512-76qs+5KWcaatmwtwsDJvBk4H76RJQBFe+Gext0EfJdC3Vd2kpY2Pf//OHHzHp84Ciw0/rYoGTDnIAr3uWhhJYw==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.52.0.tgz", + "integrity": "sha512-As3lChhrbwWQLNk2HC8Ree96hldKIqk98EYvypd3It8Q1f8d5zWyIoaZEp2va5667M4ZyE7X8UUR+azXrFl+NA==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.51.0", - "@typescript-eslint/types": "5.51.0", - "@typescript-eslint/typescript-estree": "5.51.0", + "@typescript-eslint/scope-manager": "5.52.0", + "@typescript-eslint/types": "5.52.0", + "@typescript-eslint/typescript-estree": "5.52.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0", "semver": "^7.3.7" @@ -802,12 +830,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.51.0.tgz", - "integrity": "sha512-Oh2+eTdjHjOFjKA27sxESlA87YPSOJafGCR0md5oeMdh1ZcCfAGCIOL216uTBAkAIptvLIfKQhl7lHxMJet4GQ==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.52.0.tgz", + "integrity": "sha512-qMwpw6SU5VHCPr99y274xhbm+PRViK/NATY6qzt+Et7+mThGuFSl/ompj2/hrBlRP/kq+BFdgagnOSgw9TB0eA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.51.0", + "@typescript-eslint/types": "5.52.0", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -1001,14 +1029,13 @@ } }, "node_modules/bullmq": { - "version": "1.91.1", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-1.91.1.tgz", - "integrity": "sha512-u7dat9I8ZwouZ651AMZkBSvB6NVUPpnAjd4iokd9DM41whqIBnDjuL11h7+kEjcpiDKj6E+wxZiER00FqirZQg==", + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-3.6.6.tgz", + "integrity": "sha512-W71jXrcTdcT3Y5tzMyTx22Cd8O3dTML7vl6KG3YdGVGrO3+UmKRLYfGLn1QwIhIoTQJVvIrSB4qfGs1hgqYRVw==", "dependencies": { "cron-parser": "^4.6.0", - "get-port": "6.1.2", "glob": "^8.0.3", - "ioredis": "^5.2.2", + "ioredis": "^5.3.0", "lodash": "^4.17.21", "msgpackr": "^1.6.2", "semver": "^7.3.7", @@ -1170,9 +1197,9 @@ } }, "node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", "dev": true, "dependencies": { "has-property-descriptors": "^1.0.0", @@ -1214,9 +1241,9 @@ } }, "node_modules/discord-api-types": { - "version": "0.37.31", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.31.tgz", - "integrity": "sha512-k9DQQ7Wv+ehiF7901qk/FnP47k6O2MHm3meQFee4gUzi5dfGAVLf7SfLNtb4w7G2dmukJyWQtVJEDF9oMb9yuQ==" + "version": "0.37.33", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.33.tgz", + "integrity": "sha512-ZMH5RU3q1pvYS+2wGUJ5Zvy8jMGTQ4wCpbDlIQDkbIL/k6kJwBPsXnCg81g2GywlOuf0f8ezakxVSe+sZuY6ig==" }, "node_modules/discord.js": { "version": "14.7.1", @@ -1976,17 +2003,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-port": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-6.1.2.tgz", - "integrity": "sha512-BrGGraKm2uPqurfGVj/z97/zv8dPleC6x9JBNRTrDNtCkkRF4rPwrQXFgL7+I+q8QSdU4ntLQX2D7KIxSy8nGw==", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-symbol-description": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", @@ -2296,12 +2312,12 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/internal-slot": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.4.tgz", - "integrity": "sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", "dev": true, "dependencies": { - "get-intrinsic": "^1.1.3", + "get-intrinsic": "^1.2.0", "has": "^1.0.3", "side-channel": "^1.0.4" }, @@ -2310,9 +2326,9 @@ } }, "node_modules/ioredis": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.0.tgz", - "integrity": "sha512-Id9jKHhsILuIZpHc61QkagfVdUj2Rag5GzG1TGEvRNeM7dtTOjICgjC+tvqYxi//PuX2wjQ+Xjva2ONBuf92Pw==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.1.tgz", + "integrity": "sha512-C+IBcMysM6v52pTLItYMeV4Hz7uriGtoJdz7SSBDX6u+zwSYGirLdQh3L7t/OItWITcw3gTFMjJReYUwS4zihg==", "dependencies": { "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", @@ -2741,9 +2757,9 @@ } }, "node_modules/minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2882,9 +2898,9 @@ } }, "node_modules/open": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", - "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.1.tgz", + "integrity": "sha512-/4b7qZNhv6Uhd7jjnREh1NjnPxlTq+XNWPG88Ydkj5AILcA5m3ajvcg57pB24EQjKv0dK62XnDqk9c/hkIG5Kg==", "dev": true, "dependencies": { "define-lazy-prop": "^2.0.0", @@ -3039,13 +3055,13 @@ } }, "node_modules/prisma": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.9.0.tgz", - "integrity": "sha512-bS96oZ5oDFXYgoF2l7PJ3Mp1wWWfLOo8B/jAfbA2Pn0Wm5Z/owBHzaMQKS3i1CzVBDWWPVnOohmbJmjvkcHS5w==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.10.1.tgz", + "integrity": "sha512-0jDxgg+DruB1kHVNlcspXQB9au62IFfVg9drkhzXudszHNUAQn0lVuu+T8np0uC2z1nKD5S3qPeCyR8u5YFLnA==", "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/engines": "4.9.0" + "@prisma/engines": "4.10.1" }, "bin": { "prisma": "build/index.js", @@ -3537,9 +3553,9 @@ } }, "node_modules/ts-mixer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.2.tgz", - "integrity": "sha512-zvHx3VM83m2WYCE8XL99uaM7mFwYSkjR2OZti98fabHrwkjsCvgwChda5xctein3xGOyaQhtTeDq/1H/GNvF3A==" + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.3.tgz", + "integrity": "sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==" }, "node_modules/ts-node": { "version": "10.9.1", @@ -3687,9 +3703,9 @@ } }, "node_modules/undici": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.18.0.tgz", - "integrity": "sha512-1iVwbhonhFytNdg0P4PqyIAXbdlVZVebtPDvuM36m66mRw4OGrCm2MYynJv/UENFLdP13J1nPVQzVE2zTs1OeA==", + "version": "5.19.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.19.1.tgz", + "integrity": "sha512-YiZ61LPIgY73E7syxCDxxa3LV2yl3sN8spnIuTct60boiiRaE1J8mNWHO8Im2Zi/sFrPusjLlmRPrsyraSqX6A==", "dependencies": { "busboy": "^1.6.0" }, @@ -3796,9 +3812,9 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.0.tgz", - "integrity": "sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz", + "integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==", "engines": { "node": ">=10.0.0" }, @@ -4029,29 +4045,37 @@ } }, "@prisma/client": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.9.0.tgz", - "integrity": "sha512-bz6QARw54sWcbyR1lLnF2QHvRW5R/Jxnbbmwh3u+969vUKXtBkXgSgjDA85nji31ZBlf7+FrHDy5x+5ydGyQDg==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.10.1.tgz", + "integrity": "sha512-VonXLJZybdt8e5XZH5vnIGCRNnIh6OMX1FS3H/yzMGLT3STj5TJ/OkMcednrvELgk8PK89Vo3aSh51MWNO0axA==", "requires": { - "@prisma/engines-version": "4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5" + "@prisma/engines-version": "4.10.1-2.aead147aa326ccb985dcfed5b065b4fdabd44b19" } }, "@prisma/engines": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.9.0.tgz", - "integrity": "sha512-t1pt0Gsp+HcgPJrHFc+d/ZSAaKKWar2G/iakrE07yeKPNavDP3iVKPpfXP22OTCHZUWf7OelwKJxQgKAm5hkgw==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.10.1.tgz", + "integrity": "sha512-B3tcTxjx196nuAu1GOTKO9cGPUgTFHYRdkPkTS4m5ptb2cejyBlH9X7GOfSt3xlI7p4zAJDshJP4JJivCg9ouA==", "devOptional": true }, "@prisma/engines-version": { - "version": "4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5.tgz", - "integrity": "sha512-M16aibbxi/FhW7z1sJCX8u+0DriyQYY5AyeTH7plQm9MLnURoiyn3CZBqAyIoQ+Z1pS77usCIibYJWSgleBMBA==" + "version": "4.10.1-2.aead147aa326ccb985dcfed5b065b4fdabd44b19", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.10.1-2.aead147aa326ccb985dcfed5b065b4fdabd44b19.tgz", + "integrity": "sha512-tsjTho7laDhf9EJ9EnDxAPEf7yrigSMDhniXeU4YoWc7azHAs4GPxRi2P9LTFonmHkJLMOLjR77J1oIP8Ife1w==" }, "@sapphire/async-queue": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.0.tgz", "integrity": "sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==" }, + "@sapphire/cron": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/cron/-/cron-1.0.0.tgz", + "integrity": "sha512-pKYfpnHiDFknur3yoquA8cqbJZS140y2oqjshwGGmtjiuIbUngQhPHGwdWHNDKDrF6EKbOK06nd2URE+0eUrfQ==", + "requires": { + "@sapphire/utilities": "^3.9.3" + } + }, "@sapphire/discord-utilities": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@sapphire/discord-utilities/-/discord-utilities-3.0.0.tgz", @@ -4077,9 +4101,9 @@ "integrity": "sha512-B+6nKYnBmIlqqbamcR4iBvbQHz6/Kq2JUVM0rA3lQ+aYUYDdcA1Spt66CKtPWwdTYEtSv0VY6Jv27WCtFNYTUg==" }, "@sapphire/framework": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@sapphire/framework/-/framework-4.0.2.tgz", - "integrity": "sha512-IoSZGBPJjiINJKJKaBfnpEB1IxPv7yitunnvJ6V5XcTdxP51I/KsVJX2ELxiH7sslg8ZrQQMRIcluGLbVwv4KA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@sapphire/framework/-/framework-4.1.0.tgz", + "integrity": "sha512-jtwZPysF13Sn8h2p8nkIPETveGAxRmYmiqxYkd3VXV8VPWwKBG8IriuI4oExpSnuCqxIs5HRpo3M+Gl+f/mdCg==", "requires": { "@discordjs/builders": "^1.4.0", "@sapphire/discord-utilities": "^3.0.0", @@ -4121,12 +4145,12 @@ } }, "@sapphire/plugin-scheduled-tasks": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@sapphire/plugin-scheduled-tasks/-/plugin-scheduled-tasks-4.0.1.tgz", - "integrity": "sha512-vLxfHBu2vKaJZ9v2f4z+VDZaPeDqS8bm+Sc2minRwJPw1hWAHiPqmxCBPIONY7eOQ9qKayvhKYTIwwruxgO/Mg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/plugin-scheduled-tasks/-/plugin-scheduled-tasks-6.0.0.tgz", + "integrity": "sha512-R9rga1aZk3GSXkmGfBMQR8Ng4ou36l5WGWoDvPaq1xNb56wZJ2zPZPsQHV6lxNoyOT++M8GFhVhh9OUB+xwEXg==", "requires": { - "@sapphire/stopwatch": "^1.4.1", - "@sapphire/utilities": "^3.9.3", + "@sapphire/stopwatch": "^1.5.0", + "@sapphire/utilities": "^3.11.0", "tslib": "^2.4.0" } }, @@ -4174,6 +4198,17 @@ "tslib": "^2.4.0" } }, + "@sapphire/time-utilities": { + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/@sapphire/time-utilities/-/time-utilities-1.7.8.tgz", + "integrity": "sha512-T6X/nwCvKhxmNRexgmA3KwLt3Z+xzlErkre4viflx46hHOmNNb3hoIyQtekgHYrabEaHWNbqW4PW7gC3hBc+ag==", + "requires": { + "@sapphire/cron": "^1.0.0", + "@sapphire/duration": "^1.0.0", + "@sapphire/timer-manager": "^1.0.0", + "@sapphire/timestamp": "^1.0.0" + } + }, "@sapphire/timer-manager": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@sapphire/timer-manager/-/timer-manager-1.0.0.tgz", @@ -4246,9 +4281,9 @@ "dev": true }, "@types/node": { - "version": "18.11.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.19.tgz", - "integrity": "sha512-YUgMWAQBWLObABqrvx8qKO1enAvBUdjZOAWQ5grBAkp5LQv45jBvYKZ3oFS9iKRCQyFjqw6iuEa1vmFqtxYLZw==" + "version": "18.13.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.13.0.tgz", + "integrity": "sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==" }, "@types/semver": { "version": "7.3.13", @@ -4265,14 +4300,14 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.51.0.tgz", - "integrity": "sha512-wcAwhEWm1RgNd7dxD/o+nnLW8oH+6RK1OGnmbmkj/GGoDPV1WWMVP0FXYQBivKHdwM1pwii3bt//RC62EriIUQ==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.52.0.tgz", + "integrity": "sha512-lHazYdvYVsBokwCdKOppvYJKaJ4S41CgKBcPvyd0xjZNbvQdhn/pnJlGtQksQ/NhInzdaeaSarlBjDXHuclEbg==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.51.0", - "@typescript-eslint/type-utils": "5.51.0", - "@typescript-eslint/utils": "5.51.0", + "@typescript-eslint/scope-manager": "5.52.0", + "@typescript-eslint/type-utils": "5.52.0", + "@typescript-eslint/utils": "5.52.0", "debug": "^4.3.4", "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", @@ -4283,53 +4318,53 @@ } }, "@typescript-eslint/parser": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.51.0.tgz", - "integrity": "sha512-fEV0R9gGmfpDeRzJXn+fGQKcl0inIeYobmmUWijZh9zA7bxJ8clPhV9up2ZQzATxAiFAECqPQyMDB4o4B81AaA==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.52.0.tgz", + "integrity": "sha512-e2KiLQOZRo4Y0D/b+3y08i3jsekoSkOYStROYmPUnGMEoA0h+k2qOH5H6tcjIc68WDvGwH+PaOrP1XRzLJ6QlA==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.51.0", - "@typescript-eslint/types": "5.51.0", - "@typescript-eslint/typescript-estree": "5.51.0", + "@typescript-eslint/scope-manager": "5.52.0", + "@typescript-eslint/types": "5.52.0", + "@typescript-eslint/typescript-estree": "5.52.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.51.0.tgz", - "integrity": "sha512-gNpxRdlx5qw3yaHA0SFuTjW4rxeYhpHxt491PEcKF8Z6zpq0kMhe0Tolxt0qjlojS+/wArSDlj/LtE69xUJphQ==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.52.0.tgz", + "integrity": "sha512-AR7sxxfBKiNV0FWBSARxM8DmNxrwgnYMPwmpkC1Pl1n+eT8/I2NAUPuwDy/FmDcC6F8pBfmOcaxcxRHspgOBMw==", "dev": true, "requires": { - "@typescript-eslint/types": "5.51.0", - "@typescript-eslint/visitor-keys": "5.51.0" + "@typescript-eslint/types": "5.52.0", + "@typescript-eslint/visitor-keys": "5.52.0" } }, "@typescript-eslint/type-utils": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.51.0.tgz", - "integrity": "sha512-QHC5KKyfV8sNSyHqfNa0UbTbJ6caB8uhcx2hYcWVvJAZYJRBo5HyyZfzMdRx8nvS+GyMg56fugMzzWnojREuQQ==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.52.0.tgz", + "integrity": "sha512-tEKuUHfDOv852QGlpPtB3lHOoig5pyFQN/cUiZtpw99D93nEBjexRLre5sQZlkMoHry/lZr8qDAt2oAHLKA6Jw==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "5.51.0", - "@typescript-eslint/utils": "5.51.0", + "@typescript-eslint/typescript-estree": "5.52.0", + "@typescript-eslint/utils": "5.52.0", "debug": "^4.3.4", "tsutils": "^3.21.0" } }, "@typescript-eslint/types": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.51.0.tgz", - "integrity": "sha512-SqOn0ANn/v6hFn0kjvLwiDi4AzR++CBZz0NV5AnusT2/3y32jdc0G4woXPWHCumWtUXZKPAS27/9vziSsC9jnw==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.52.0.tgz", + "integrity": "sha512-oV7XU4CHYfBhk78fS7tkum+/Dpgsfi91IIDy7fjCyq2k6KB63M6gMC0YIvy+iABzmXThCRI6xpCEyVObBdWSDQ==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.51.0.tgz", - "integrity": "sha512-TSkNupHvNRkoH9FMA3w7TazVFcBPveAAmb7Sz+kArY6sLT86PA5Vx80cKlYmd8m3Ha2SwofM1KwraF24lM9FvA==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.52.0.tgz", + "integrity": "sha512-WeWnjanyEwt6+fVrSR0MYgEpUAuROxuAH516WPjUblIrClzYJj0kBbjdnbQXLpgAN8qbEuGywiQsXUVDiAoEuQ==", "dev": true, "requires": { - "@typescript-eslint/types": "5.51.0", - "@typescript-eslint/visitor-keys": "5.51.0", + "@typescript-eslint/types": "5.52.0", + "@typescript-eslint/visitor-keys": "5.52.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -4338,28 +4373,28 @@ } }, "@typescript-eslint/utils": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.51.0.tgz", - "integrity": "sha512-76qs+5KWcaatmwtwsDJvBk4H76RJQBFe+Gext0EfJdC3Vd2kpY2Pf//OHHzHp84Ciw0/rYoGTDnIAr3uWhhJYw==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.52.0.tgz", + "integrity": "sha512-As3lChhrbwWQLNk2HC8Ree96hldKIqk98EYvypd3It8Q1f8d5zWyIoaZEp2va5667M4ZyE7X8UUR+azXrFl+NA==", "dev": true, "requires": { "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.51.0", - "@typescript-eslint/types": "5.51.0", - "@typescript-eslint/typescript-estree": "5.51.0", + "@typescript-eslint/scope-manager": "5.52.0", + "@typescript-eslint/types": "5.52.0", + "@typescript-eslint/typescript-estree": "5.52.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0", "semver": "^7.3.7" } }, "@typescript-eslint/visitor-keys": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.51.0.tgz", - "integrity": "sha512-Oh2+eTdjHjOFjKA27sxESlA87YPSOJafGCR0md5oeMdh1ZcCfAGCIOL216uTBAkAIptvLIfKQhl7lHxMJet4GQ==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.52.0.tgz", + "integrity": "sha512-qMwpw6SU5VHCPr99y274xhbm+PRViK/NATY6qzt+Et7+mThGuFSl/ompj2/hrBlRP/kq+BFdgagnOSgw9TB0eA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.51.0", + "@typescript-eslint/types": "5.52.0", "eslint-visitor-keys": "^3.3.0" } }, @@ -4492,14 +4527,13 @@ } }, "bullmq": { - "version": "1.91.1", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-1.91.1.tgz", - "integrity": "sha512-u7dat9I8ZwouZ651AMZkBSvB6NVUPpnAjd4iokd9DM41whqIBnDjuL11h7+kEjcpiDKj6E+wxZiER00FqirZQg==", + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-3.6.6.tgz", + "integrity": "sha512-W71jXrcTdcT3Y5tzMyTx22Cd8O3dTML7vl6KG3YdGVGrO3+UmKRLYfGLn1QwIhIoTQJVvIrSB4qfGs1hgqYRVw==", "requires": { "cron-parser": "^4.6.0", - "get-port": "6.1.2", "glob": "^8.0.3", - "ioredis": "^5.2.2", + "ioredis": "^5.3.0", "lodash": "^4.17.21", "msgpackr": "^1.6.2", "semver": "^7.3.7", @@ -4623,9 +4657,9 @@ "dev": true }, "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", "dev": true, "requires": { "has-property-descriptors": "^1.0.0", @@ -4652,9 +4686,9 @@ } }, "discord-api-types": { - "version": "0.37.31", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.31.tgz", - "integrity": "sha512-k9DQQ7Wv+ehiF7901qk/FnP47k6O2MHm3meQFee4gUzi5dfGAVLf7SfLNtb4w7G2dmukJyWQtVJEDF9oMb9yuQ==" + "version": "0.37.33", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.33.tgz", + "integrity": "sha512-ZMH5RU3q1pvYS+2wGUJ5Zvy8jMGTQ4wCpbDlIQDkbIL/k6kJwBPsXnCg81g2GywlOuf0f8ezakxVSe+sZuY6ig==" }, "discord.js": { "version": "14.7.1", @@ -5247,11 +5281,6 @@ "has-symbols": "^1.0.3" } }, - "get-port": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-6.1.2.tgz", - "integrity": "sha512-BrGGraKm2uPqurfGVj/z97/zv8dPleC6x9JBNRTrDNtCkkRF4rPwrQXFgL7+I+q8QSdU4ntLQX2D7KIxSy8nGw==" - }, "get-symbol-description": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", @@ -5465,20 +5494,20 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "internal-slot": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.4.tgz", - "integrity": "sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", "dev": true, "requires": { - "get-intrinsic": "^1.1.3", + "get-intrinsic": "^1.2.0", "has": "^1.0.3", "side-channel": "^1.0.4" } }, "ioredis": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.0.tgz", - "integrity": "sha512-Id9jKHhsILuIZpHc61QkagfVdUj2Rag5GzG1TGEvRNeM7dtTOjICgjC+tvqYxi//PuX2wjQ+Xjva2ONBuf92Pw==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.1.tgz", + "integrity": "sha512-C+IBcMysM6v52pTLItYMeV4Hz7uriGtoJdz7SSBDX6u+zwSYGirLdQh3L7t/OItWITcw3gTFMjJReYUwS4zihg==", "requires": { "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", @@ -5780,9 +5809,9 @@ } }, "minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true }, "ms": { @@ -5886,9 +5915,9 @@ } }, "open": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", - "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.1.tgz", + "integrity": "sha512-/4b7qZNhv6Uhd7jjnREh1NjnPxlTq+XNWPG88Ydkj5AILcA5m3ajvcg57pB24EQjKv0dK62XnDqk9c/hkIG5Kg==", "dev": true, "requires": { "define-lazy-prop": "^2.0.0", @@ -5991,12 +6020,12 @@ "dev": true }, "prisma": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.9.0.tgz", - "integrity": "sha512-bS96oZ5oDFXYgoF2l7PJ3Mp1wWWfLOo8B/jAfbA2Pn0Wm5Z/owBHzaMQKS3i1CzVBDWWPVnOohmbJmjvkcHS5w==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.10.1.tgz", + "integrity": "sha512-0jDxgg+DruB1kHVNlcspXQB9au62IFfVg9drkhzXudszHNUAQn0lVuu+T8np0uC2z1nKD5S3qPeCyR8u5YFLnA==", "devOptional": true, "requires": { - "@prisma/engines": "4.9.0" + "@prisma/engines": "4.10.1" } }, "punycode": { @@ -6308,9 +6337,9 @@ } }, "ts-mixer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.2.tgz", - "integrity": "sha512-zvHx3VM83m2WYCE8XL99uaM7mFwYSkjR2OZti98fabHrwkjsCvgwChda5xctein3xGOyaQhtTeDq/1H/GNvF3A==" + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.3.tgz", + "integrity": "sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==" }, "ts-node": { "version": "10.9.1", @@ -6410,9 +6439,9 @@ } }, "undici": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.18.0.tgz", - "integrity": "sha512-1iVwbhonhFytNdg0P4PqyIAXbdlVZVebtPDvuM36m66mRw4OGrCm2MYynJv/UENFLdP13J1nPVQzVE2zTs1OeA==", + "version": "5.19.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.19.1.tgz", + "integrity": "sha512-YiZ61LPIgY73E7syxCDxxa3LV2yl3sN8spnIuTct60boiiRaE1J8mNWHO8Im2Zi/sFrPusjLlmRPrsyraSqX6A==", "requires": { "busboy": "^1.6.0" } @@ -6495,9 +6524,9 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "ws": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.0.tgz", - "integrity": "sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz", + "integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==", "requires": {} }, "yallist": { diff --git a/package.json b/package.json index 6521980..6a701a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "arabot", - "version": "0.1.0", + "version": "0.2.1", "description": "A Discord bot for Animal Rights Advocates", "main": "dist/index.js", "scripts": { @@ -29,16 +29,17 @@ }, "homepage": "https://github.com/veganhacktivists/arabot#readme", "dependencies": { - "@prisma/client": "^4.0.0", + "@prisma/client": "^4.10.1", "@sapphire/discord.js-utilities": "^6.0.0", - "@sapphire/framework": "^4.0.1", + "@sapphire/framework": "^4.1.0", "@sapphire/plugin-logger": "^3.0.1", - "@sapphire/plugin-scheduled-tasks": "^4.0.0", + "@sapphire/plugin-scheduled-tasks": "^6.0.0", "@sapphire/plugin-subcommands": "^4.0.0", - "@sapphire/stopwatch": "^1.4.1", + "@sapphire/stopwatch": "^1.5.0", + "@sapphire/time-utilities": "^1.7.8", "@sapphire/utilities": "^3.9.2", "@types/node": "^18.0.3", - "bullmq": "^1.89.1", + "bullmq": "^3.6.6", "discord.js": "^14.7.1", "dotenv": "^16.0.1", "ts-node": "^10.8.2", @@ -54,6 +55,6 @@ "eslint-config-airbnb-typescript": "^17.0.0", "eslint-import-resolver-typescript": "^3.5.3", "eslint-plugin-import": "^2.27.5", - "prisma": "^4.4.0" + "prisma": "^4.10.1" } } diff --git a/prisma/migrations/20230211001033_vcmute/migration.sql b/prisma/migrations/20230211001033_vcmute/migration.sql new file mode 100644 index 0000000..fe45f2f --- /dev/null +++ b/prisma/migrations/20230211001033_vcmute/migration.sql @@ -0,0 +1,17 @@ +-- CreateTable +CREATE TABLE "VCMute" ( + "id" SERIAL NOT NULL, + "userId" TEXT NOT NULL, + "modId" TEXT NOT NULL, + "time" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "endTime" TIMESTAMP(3), + "reason" TEXT, + + CONSTRAINT "VCMute_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "VCMute" ADD CONSTRAINT "VCMute_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VCMute" ADD CONSTRAINT "VCMute_modId_fkey" FOREIGN KEY ("modId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20230211163443_restrict/migration.sql b/prisma/migrations/20230211163443_restrict/migration.sql new file mode 100644 index 0000000..a9f047e --- /dev/null +++ b/prisma/migrations/20230211163443_restrict/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "Restrict" ADD COLUMN "endModId" TEXT; + +-- AddForeignKey +ALTER TABLE "Restrict" ADD CONSTRAINT "Restrict_endModId_fkey" FOREIGN KEY ("endModId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20230211163734_fix_typo/migration.sql b/prisma/migrations/20230211163734_fix_typo/migration.sql new file mode 100644 index 0000000..930458b --- /dev/null +++ b/prisma/migrations/20230211163734_fix_typo/migration.sql @@ -0,0 +1,15 @@ +/* + Warnings: + + - You are about to drop the column `endedTime` on the `Restrict` table. All the data in the column will be lost. + - You are about to drop the column `endedTime` on the `TempBan` table. All the data in the column will be lost. + - Added the required column `endTime` to the `TempBan` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Restrict" DROP COLUMN "endedTime", +ADD COLUMN "endTime" TIMESTAMP(3); + +-- AlterTable +ALTER TABLE "TempBan" DROP COLUMN "endedTime", +ADD COLUMN "endTime" TIMESTAMP(3) NOT NULL; diff --git a/prisma/migrations/20230214000132_restrict_section/migration.sql b/prisma/migrations/20230214000132_restrict_section/migration.sql new file mode 100644 index 0000000..5c80fad --- /dev/null +++ b/prisma/migrations/20230214000132_restrict_section/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `section` to the `Restrict` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Restrict" ADD COLUMN "section" INTEGER NOT NULL; diff --git a/prisma/migrations/20230214100026_warnings/migration.sql b/prisma/migrations/20230214100026_warnings/migration.sql new file mode 100644 index 0000000..d16de9c --- /dev/null +++ b/prisma/migrations/20230214100026_warnings/migration.sql @@ -0,0 +1,17 @@ +-- CreateTable +CREATE TABLE "Warnings" ( + "id" SERIAL NOT NULL, + "userId" TEXT NOT NULL, + "modId" TEXT NOT NULL, + "time" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "active" BOOLEAN NOT NULL DEFAULT true, + "note" TEXT NOT NULL, + + CONSTRAINT "Warnings_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Warnings" ADD CONSTRAINT "Warnings_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Warnings" ADD CONSTRAINT "Warnings_modId_fkey" FOREIGN KEY ("modId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20230215125223_fix_warning_naming/migration.sql b/prisma/migrations/20230215125223_fix_warning_naming/migration.sql new file mode 100644 index 0000000..32daa4a --- /dev/null +++ b/prisma/migrations/20230215125223_fix_warning_naming/migration.sql @@ -0,0 +1,20 @@ +/* + Warnings: + + - You are about to drop the `Warnings` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "Warnings" DROP CONSTRAINT "Warnings_modId_fkey"; + +-- DropForeignKey +ALTER TABLE "Warnings" DROP CONSTRAINT "Warnings_userId_fkey"; + +-- RenameTable +ALTER TABLE "Warnings" RENAME TO "Warning"; + +-- AddForeignKey +ALTER TABLE "Warning" ADD CONSTRAINT "Warning_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Warning" ADD CONSTRAINT "Warning_modId_fkey" FOREIGN KEY ("modId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20230215125632_fix_warning_naming/migration.sql b/prisma/migrations/20230215125632_fix_warning_naming/migration.sql new file mode 100644 index 0000000..14f1583 --- /dev/null +++ b/prisma/migrations/20230215125632_fix_warning_naming/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Warning" RENAME CONSTRAINT "Warnings_pkey" TO "Warning_pkey"; diff --git a/prisma/migrations/20230216152545_temp_ban_end_mod/migration.sql b/prisma/migrations/20230216152545_temp_ban_end_mod/migration.sql new file mode 100644 index 0000000..026518f --- /dev/null +++ b/prisma/migrations/20230216152545_temp_ban_end_mod/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "TempBan" ADD COLUMN "endModId" TEXT; + +-- AddForeignKey +ALTER TABLE "TempBan" ADD CONSTRAINT "TempBan_endModId_fkey" FOREIGN KEY ("endModId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 129ad31..1ec76c7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -39,13 +39,19 @@ model User { VerifyVerifier Verify[] @relation("verVerifier") SusUser Sus[] @relation("susUser") SusMod Sus[] @relation("susMod") + WarnUser Warning[] @relation("warnUser") + WarnMod Warning[] @relation("warnMod") RestrictUser Restrict[] @relation("restUser") RestrictMod Restrict[] @relation("restMod") + RestrictEndMod Restrict[] @relation("endRestMod") BanUser Ban[] @relation("banUser") BanMod Ban[] @relation("banMod") BanEndMod Ban[] @relation("endBanMod") TempBanUser TempBan[] @relation("tbanUser") TempBanMod TempBan[] @relation("tbanMod") + TempBanEndMod TempBan[] @relation("endTbanMod") + VCMuteUser VCMute[] @relation("vcMuteUser") + VCMuteMod VCMute[] @relation("vcMuteMod") } model Verify { @@ -88,6 +94,17 @@ model Sus { note String } +model Warning { + id Int @id @default(autoincrement()) + user User @relation("warnUser", fields: [userId], references: [id]) + userId String + mod User @relation("warnMod", fields: [modId], references: [id]) + modId String + time DateTime @default(now()) + active Boolean @default(true) + note String +} + model Restrict { id Int @id @default(autoincrement()) user User @relation("restUser", fields: [userId], references: [id]) @@ -95,8 +112,11 @@ model Restrict { mod User @relation("restMod", fields: [modId], references: [id]) modId String startTime DateTime @default(now()) - endedTime DateTime? + endMod User? @relation("endRestMod", fields: [endModId], references: [id]) + endModId String? + endTime DateTime? reason String + section Int } model Ban { @@ -120,7 +140,20 @@ model TempBan { mod User @relation("tbanMod", fields: [modId], references: [id]) modId String startTime DateTime @default(now()) - endedTime DateTime + endMod User? @relation("endTbanMod", fields: [endModId], references: [id]) + endModId String? + endTime DateTime active Boolean @default(true) reason String } + +model VCMute { + id Int @id @default(autoincrement()) + user User @relation("vcMuteUser", fields: [userId], references: [id]) + userId String + mod User @relation("vcMuteMod", fields: [modId], references: [id]) + modId String + time DateTime @default(now()) + endTime DateTime? + reason String? +} diff --git a/src/commands/mod/ban.ts b/src/commands/mod/ban/ban.ts similarity index 51% rename from src/commands/mod/ban.ts rename to src/commands/mod/ban/ban.ts index cff4f06..3a4cdb7 100644 --- a/src/commands/mod/ban.ts +++ b/src/commands/mod/ban/ban.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later /* Animal Rights Advocates Discord Bot - Copyright (C) 2022 Anthony Berg + Copyright (C) 2023 Anthony Berg This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -18,10 +18,18 @@ */ import { Args, Command, RegisterBehavior } from '@sapphire/framework'; -import type { User, Message, TextChannel } from 'discord.js'; +import type { + User, + Message, + Snowflake, + TextChannel, + Guild, +} from 'discord.js'; +import { EmbedBuilder } from 'discord.js'; import IDs from '#utils/ids'; -import { addBan, checkActive } from '#utils/database/ban'; -import { addEmptyUser, addExistingUser, userExists } from '#utils/database/dbExistingUser'; +import { addBan, checkBan } from '#utils/database/ban'; +import { addEmptyUser, updateUser, userExists } from '#utils/database/dbExistingUser'; +import { checkTempBan, removeTempBan } from '#utils/database/tempBan'; export class BanCommand extends Command { public constructor(context: Command.Context, options: Command.Options) { @@ -54,13 +62,13 @@ export class BanCommand extends Command { // Command run public async chatInputRun(interaction: Command.ChatInputCommandInteraction) { // Get the arguments - const user = interaction.options.getUser('user'); - const reason = interaction.options.getString('reason'); + const user = interaction.options.getUser('user', true); + const reason = interaction.options.getString('reason', true); const mod = interaction.member; const { guild } = interaction; // Checks if all the variables are of the right type - if (user === null || guild === null || reason === null || mod === null) { + if (guild === null || mod === null) { await interaction.reply({ content: 'Error fetching user!', ephemeral: true, @@ -69,87 +77,9 @@ export class BanCommand extends Command { return; } - // Gets mod's GuildMember - const modGuildMember = guild.members.cache.get(mod.user.id); + const ban = await this.ban(user.id, mod.user.id, reason, guild); - // Checks if guildMember is null - if (modGuildMember === undefined) { - await interaction.reply({ - content: 'Error fetching mod!', - ephemeral: true, - fetchReply: true, - }); - return; - } - - if (await checkActive(user.id)) { - await interaction.reply(`${user} is already banned!`); - return; - } - - // Check if mod is in database - if (!await userExists(modGuildMember.id)) { - await addExistingUser(modGuildMember); - } - - // Gets guildMember - let guildMember = guild.members.cache.get(user.id); - - if (guildMember === undefined) { - guildMember = await guild.members.fetch(user.id) - .catch(() => undefined); - } - - if (guildMember !== undefined) { - // Checks if the user is not restricted - if (guildMember.roles.cache.has(IDs.roles.vegan.vegan)) { - await interaction.reply({ - content: 'You need to restrict the user first!', - ephemeral: true, - fetchReply: true, - }); - return; - } - - // Check if user and mod are on the database - if (!await userExists(guildMember.id)) { - await addExistingUser(guildMember); - } - - // Send DM for reason of ban - await user.send(`You have been banned from ARA for: ${reason}` - + '\n\nhttps://vbcamp.org/ARA') - .catch(() => {}); - - // Ban the user - await guildMember.ban({ reason }); - } else if (!await userExists(user.id)) { - await addEmptyUser(user.id); - } - - await interaction.reply({ - content: `${user} has been banned.`, - ephemeral: true, - fetchReply: true, - }); - - // Add ban to database - await addBan(user.id, mod.user.id, reason); - - // Log the ban - let logChannel = guild.channels.cache - .get(IDs.channels.logs.restricted) as TextChannel | undefined; - - if (logChannel === undefined) { - logChannel = await guild.channels - .fetch(IDs.channels.logs.restricted) as TextChannel | undefined; - if (logChannel === undefined) { - this.container.logger.error('Ban Error: Could not fetch log channel'); - return; - } - } - - await logChannel.send(`${user} was banned for: ${reason} by ${mod}`); + await interaction.reply({ content: ban.message }); } // Non Application Command method of banning a user @@ -186,62 +116,85 @@ export class BanCommand extends Command { return; } - if (await checkActive(user.id)) { - await message.react('❌'); - await message.reply(`${user} is already banned!`); - return; - } - if (message.channel.id !== IDs.channels.restricted.moderators) { await message.react('❌'); await message.reply(`You can only run this command in <#${IDs.channels.restricted.moderators}> ` - + 'or alternatively use the slash command!'); + + 'or alternatively use the slash command!'); return; } - // Check if mod is in database - if (!await userExists(mod.id)) { - await addExistingUser(mod); + const ban = await this.ban(user.id, mod.user.id, reason, guild); + + await message.reply(ban.message); + await message.react(ban.success ? '✅' : '❌'); + } + + private async ban(userId: Snowflake, modId: Snowflake, reason: string, guild: Guild) { + const info = { + message: '', + success: false, + }; + + let user = guild.client.users.cache.get(userId); + + if (user === undefined) { + user = await guild.client.users.fetch(userId) as User; } - // Gets guildMember - let guildMember = await guild.members.cache.get(user.id); + // Gets mod's GuildMember + const mod = guild.members.cache.get(modId); - if (guildMember === undefined) { - guildMember = await guild.members.fetch(user.id) + // Checks if guildMember is null + if (mod === undefined) { + info.message = 'Error fetching mod!'; + return info; + } + + if (await checkBan(userId)) { + info.message = `${user} is already banned!`; + return info; + } + + // Check if mod is in database + await updateUser(mod); + + // Gets guildMember + let member = guild.members.cache.get(userId); + + if (member === undefined) { + member = await guild.members.fetch(userId) .catch(() => undefined); } - if (guildMember !== undefined) { + if (member !== undefined) { // Checks if the user is not restricted - if (guildMember.roles.cache.has(IDs.roles.vegan.vegan)) { - await message.react('❌'); - await message.reply({ - content: 'You need to restrict the user first!', - }); - return; + if (member.roles.cache.has(IDs.roles.vegan.vegan)) { + info.message = 'You need to restrict the user first!'; + return info; } - // Check if user and mod are on the database - if (!await userExists(guildMember.id)) { - await addExistingUser(guildMember); - } + await updateUser(member); // Send DM for reason of ban - await user.send(`You have been banned from ARA for: ${reason}` + await member.send(`You have been banned from ARA for: ${reason}` + '\n\nhttps://vbcamp.org/ARA') .catch(() => {}); // Ban the user - await guildMember.ban({ reason }); - } else if (!await userExists(user.id)) { - await addEmptyUser(user.id); + await member.ban({ reason }); + } else if (!await userExists(userId)) { + await addEmptyUser(userId); } // Add ban to database - await addBan(user.id, mod.id, reason); + await addBan(userId, modId, reason); - await message.react('✅'); + if (await checkTempBan(userId)) { + await removeTempBan(userId); + } + + info.message = `${user} has been banned.`; + info.success = true; // Log the ban let logChannel = guild.channels.cache @@ -252,10 +205,24 @@ export class BanCommand extends Command { .fetch(IDs.channels.logs.restricted) as TextChannel | undefined; if (logChannel === undefined) { this.container.logger.error('Ban Error: Could not fetch log channel'); - return; + info.message = `${user} has been banned. This hasn't been logged in a text channel as log channel could not be found`; + return info; } } - await logChannel.send(`${user} was banned for: ${reason} by ${mod}`); + const log = new EmbedBuilder() + .setColor('#FF0000') + .setAuthor({ name: `Banned ${user.tag}`, iconURL: `${user.avatarURL()}` }) + .addFields( + { name: 'User', value: `${user}`, inline: true }, + { name: 'Moderator', value: `${mod}`, inline: true }, + { name: 'Reason', value: reason }, + ) + .setTimestamp() + .setFooter({ text: `ID: ${user.id}` }); + + await logChannel.send({ embeds: [log] }); + + return info; } } diff --git a/src/commands/mod/ban/tempBan.ts b/src/commands/mod/ban/tempBan.ts new file mode 100644 index 0000000..760e9eb --- /dev/null +++ b/src/commands/mod/ban/tempBan.ts @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +/* + Animal Rights Advocates Discord Bot + Copyright (C) 2023 Anthony Berg + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import { Command, RegisterBehavior } from '@sapphire/framework'; +import { Duration, DurationFormatter } from '@sapphire/time-utilities'; +import type { + User, + Snowflake, + TextChannel, + Guild, +} from 'discord.js'; +import { EmbedBuilder } from 'discord.js'; +import IDs from '#utils/ids'; +import { addTempBan, checkTempBan } from '#utils/database/tempBan'; +import { addEmptyUser, updateUser, userExists } from '#utils/database/dbExistingUser'; + +export class TempBanCommand extends Command { + public constructor(context: Command.Context, options: Command.Options) { + super(context, { + ...options, + name: 'tempban', + description: 'Bans a user for a certain amount of time', + preconditions: ['RestrictedAccessOnly'], + }); + } + + // Registers that this is a slash command + public override registerApplicationCommands(registry: Command.Registry) { + registry.registerChatInputCommand( + (builder) => builder + .setName(this.name) + .setDescription(this.description) + .addUserOption((option) => option.setName('user') + .setDescription('User to ban') + .setRequired(true)) + .addStringOption((option) => option.setName('duration') + .setDescription('How long to ban the user for') + .setRequired(true)) + .addStringOption((option) => option.setName('reason') + .setDescription('Note about the user') + .setRequired(true)), + { + behaviorWhenNotIdentical: RegisterBehavior.Overwrite, + }, + ); + } + + // Command run + public async chatInputRun(interaction: Command.ChatInputCommandInteraction) { + // Get the arguments + const user = interaction.options.getUser('user', true); + const duration = interaction.options.getString('duration', true); + const reason = interaction.options.getString('reason', true); + const mod = interaction.member; + const { guild } = interaction; + + // Checks if all the variables are of the right type + if (guild === null || mod === null) { + await interaction.reply({ + content: 'Error fetching user!', + ephemeral: true, + fetchReply: true, + }); + return; + } + + const time = new Duration(duration); + + if (Number.isNaN(time.offset)) { + await interaction.reply({ + content: 'Invalid ban duration input', + }); + return; + } + + const ban = await this.ban(user.id, mod.user.id, time, reason, guild); + + await interaction.reply({ content: ban.message }); + } + + private async ban( + userId: Snowflake, + modId: Snowflake, + time: Duration, + reason: string, + guild: Guild, + ) { + const info = { + message: '', + success: false, + }; + + const banLength = new DurationFormatter().format(time.offset); + + let user = guild.client.users.cache.get(userId); + + if (user === undefined) { + user = await guild.client.users.fetch(userId) as User; + } + + // Gets mod's GuildMember + const mod = guild.members.cache.get(modId); + + // Checks if guildMember is null + if (mod === undefined) { + info.message = 'Error fetching mod!'; + return info; + } + + if (await checkTempBan(userId)) { + info.message = `${user} is already temp banned!`; + return info; + } + + // Check if mod is in database + await updateUser(mod); + + // Gets guildMember + let member = guild.members.cache.get(userId); + + if (member === undefined) { + member = await guild.members.fetch(userId) + .catch(() => undefined); + } + + if (member !== undefined) { + // Checks if the user is not restricted + if (member.roles.cache.has(IDs.roles.vegan.vegan)) { + info.message = 'You need to restrict the user first!'; + return info; + } + + await updateUser(member); + + // Send DM for reason of ban + await member.send(`You have been temporarily banned from ARA for ${banLength}. Reason: ${reason}` + + '\n\nhttps://vbcamp.org/ARA') + .catch(() => {}); + + // Ban the user + await member.ban({ reason }); + } else if (!await userExists(userId)) { + await addEmptyUser(userId); + } + + // Add ban to database + await addTempBan(userId, modId, time.fromNow, reason); + + // Create scheduled task to unban + this.container.tasks.create('tempBan', { + userId: user.id, + guildId: guild.id, + }, time.offset); + + info.message = `${user} has been temporarily banned for ${banLength}.`; + info.success = true; + + // Log the ban + let logChannel = guild.channels.cache + .get(IDs.channels.logs.restricted) as TextChannel | undefined; + + if (logChannel === undefined) { + logChannel = await guild.channels + .fetch(IDs.channels.logs.restricted) as TextChannel | undefined; + if (logChannel === undefined) { + this.container.logger.error('Temp Ban Error: Could not fetch log channel'); + info.message = `${user} has been temporarily banned for ${banLength}. ` + + 'This hasn\'t been logged in a text channel as log channel could not be found'; + return info; + } + } + + const log = new EmbedBuilder() + .setColor('#FF0000') + .setAuthor({ name: `Temp Banned ${user.tag}`, iconURL: `${user.avatarURL()}` }) + .addFields( + { name: 'User', value: `${user}`, inline: true }, + { name: 'Moderator', value: `${mod}`, inline: true }, + { name: 'Duration', value: banLength }, + { name: 'Reason', value: reason }, + ) + .setTimestamp() + .setFooter({ text: `ID: ${user.id}` }); + + await logChannel.send({ embeds: [log] }); + + return info; + } +} diff --git a/src/commands/mod/unban.ts b/src/commands/mod/ban/unban.ts similarity index 53% rename from src/commands/mod/unban.ts rename to src/commands/mod/ban/unban.ts index 209770a..62bf738 100644 --- a/src/commands/mod/unban.ts +++ b/src/commands/mod/ban/unban.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later /* Animal Rights Advocates Discord Bot - Copyright (C) 2022 Anthony Berg + Copyright (C) 2023 Anthony Berg This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -21,11 +21,15 @@ import { Args, Command, RegisterBehavior } from '@sapphire/framework'; import type { User, Message, + Snowflake, TextChannel, + Guild, GuildBan, } from 'discord.js'; +import { EmbedBuilder } from 'discord.js'; import IDs from '#utils/ids'; -import { removeBan, checkActive, addBan } from '#utils/database/ban'; +import { removeBan, checkBan, addBan } from '#utils/database/ban'; +import { checkTempBan, removeTempBan } from '#utils/database/tempBan'; import { addEmptyUser, addExistingUser, userExists } from '#utils/database/dbExistingUser'; export class UnbanCommand extends Command { @@ -56,12 +60,12 @@ export class UnbanCommand extends Command { // Command run public async chatInputRun(interaction: Command.ChatInputCommandInteraction) { // Get the arguments - const user = interaction.options.getUser('user'); + const user = interaction.options.getUser('user', true); const mod = interaction.member; const { guild } = interaction; // Checks if all the variables are of the right type - if (user === null || guild === null || mod === null) { + if (guild === null || mod === null) { await interaction.reply({ content: 'Error fetching user!', ephemeral: true, @@ -70,80 +74,9 @@ export class UnbanCommand extends Command { return; } - // Gets mod's GuildMember - const modGuildMember = guild.members.cache.get(mod.user.id); + const unban = await this.unban(user.id, mod.user.id, guild); - // Checks if guildMember is null - if (modGuildMember === undefined) { - await interaction.reply({ - content: 'Error fetching mod!', - ephemeral: true, - fetchReply: true, - }); - return; - } - - // Check if mod is in database - if (!await userExists(modGuildMember.id)) { - await addExistingUser(modGuildMember); - } - - if (!await checkActive(user.id)) { - let ban: GuildBan; - try { - ban = await guild.bans.fetch(user.id); - } catch { - try { - ban = await guild.bans.fetch({ user, force: true }); - } catch { - await interaction.reply({ - content: `${user} is not banned.`, - }); - return; - } - } - let { reason } = ban; - - if (reason === null || reason === undefined) { - reason = ''; - } - - // Check if user and mod are on the database - if (!await userExists(user.id)) { - await addEmptyUser(user.id); - } - - // Add missing ban - await addBan(user.id, mod.user.id, `(Mod who banned is not accurate) - ${reason}`); - } - - // Unban the user - await guild.members.unban(user) - .catch(() => {}); - - // Add unban to database - await removeBan(user.id, mod.user.id); - - await interaction.reply({ - content: `${user} has been unbanned.`, - ephemeral: true, - fetchReply: true, - }); - - // Log the ban - let modRestrict = guild.channels.cache - .get(IDs.channels.restricted.moderators) as TextChannel | undefined; - - if (modRestrict === undefined) { - modRestrict = await guild.channels - .fetch(IDs.channels.restricted.moderators) as TextChannel | undefined; - if (modRestrict === undefined) { - this.container.logger.error('Unban Error: Could not fetch mod channel'); - return; - } - } - - await modRestrict.send(`${user} was unbanned by ${mod}`); + await interaction.reply({ content: unban.message }); } // Non Application Command method of banning a user @@ -174,24 +107,55 @@ export class UnbanCommand extends Command { return; } + const unban = await this.unban(user.id, mod.user.id, guild); + + await message.reply(unban.message); + await message.react(unban.success ? '✅' : '❌'); + } + + private async unban(userId: Snowflake, modId: Snowflake, guild: Guild) { + const info = { + message: '', + success: false, + }; + + // Gets mod's GuildMember + const mod = guild.members.cache.get(modId); + + // Checks if guildMember is null + if (mod === undefined) { + info.message = 'Error fetching mod!'; + return info; + } + // Check if mod is in database - if (!await userExists(mod.id)) { + if (!await userExists(modId)) { await addExistingUser(mod); } - if (!await checkActive(user.id)) { + let user = guild.client.users.cache.get(userId); + + if (user === undefined) { + user = await guild.client.users.fetch(userId); + if (user === undefined) { + info.message = 'Could not fetch the user!'; + return info; + } + } + + let dbBan = await checkBan(userId); + const dbTempBan = await checkTempBan(userId); + + if (!dbBan && !dbTempBan) { let ban: GuildBan; try { - ban = await guild.bans.fetch(user.id); + ban = await guild.bans.fetch(userId); } catch { try { ban = await guild.bans.fetch({ user, force: true }); } catch { - await message.react('❌'); - await message.reply({ - content: `${user} is not banned.`, - }); - return; + info.message = `${user} is not banned.`; + return info; } } let { reason } = ban; @@ -206,35 +170,50 @@ export class UnbanCommand extends Command { } // Add missing ban - await addBan(user.id, mod.user.id, `(Mod who banned is not accurate) - ${reason}`); + await addBan(userId, modId, `(Mod who banned is not accurate) - ${reason}`); + dbBan = true; } // Unban the user await guild.members.unban(user) .catch(() => {}); - // Add unban to database - await removeBan(user.id, mod.id); + if (dbBan) { + // Add unban to database + await removeBan(user.id, mod.user.id); + } else if (dbTempBan) { + await removeTempBan(user.id, mod.user.id); + } - await message.react('✅'); + info.message = `${user} has been unbanned.`; + info.success = true; - await message.reply({ - content: `${user} has been unbanned.`, - }); + // Log unban + let logChannel = guild.channels.cache + .get(IDs.channels.logs.restricted) as TextChannel | undefined; - // Log the ban - let modRestrict = guild.channels.cache - .get(IDs.channels.restricted.moderators) as TextChannel | undefined; - - if (modRestrict === undefined) { - modRestrict = await guild.channels - .fetch(IDs.channels.restricted.moderators) as TextChannel | undefined; - if (modRestrict === undefined) { - this.container.logger.error('Unban Error: Could not fetch mod channel'); - return; + if (logChannel === undefined) { + logChannel = await guild.channels + .fetch(IDs.channels.logs.restricted) as TextChannel | undefined; + if (logChannel === undefined) { + this.container.logger.error('Ban Error: Could not fetch log channel'); + info.message = `${user} has been banned. This hasn't been logged in a text channel as log channel could not be found`; + return info; } } - await modRestrict.send(`${user} was unbanned by ${mod}`); + const log = new EmbedBuilder() + .setColor('#28A745') + .setAuthor({ name: `Unbanned ${user.tag}`, iconURL: `${user.avatarURL()}` }) + .addFields( + { name: 'User', value: `${user}`, inline: true }, + { name: 'Moderator', value: `${mod}`, inline: true }, + ) + .setTimestamp() + .setFooter({ text: `ID: ${user.id}` }); + + await logChannel.send({ embeds: [log] }); + + return info; } } diff --git a/src/commands/mod/rename.ts b/src/commands/mod/rename.ts index d5f2dcb..a0181db 100644 --- a/src/commands/mod/rename.ts +++ b/src/commands/mod/rename.ts @@ -53,14 +53,14 @@ export class RenameUserCommand extends Command { public async chatInputRun(interaction: Command.ChatInputCommandInteraction) { // TODO add database updates // Get the arguments - const user = interaction.options.getUser('user'); + const user = interaction.options.getUser('user', true); const nickname = interaction.options.getString('nickname'); const { guild } = interaction; // Checks if all the variables are of the right type - if (user === null || guild === null) { + if (guild === null) { await interaction.reply({ - content: 'Error fetching user!', + content: 'Error fetching guild!', ephemeral: true, fetchReply: true, }); @@ -68,10 +68,10 @@ export class RenameUserCommand extends Command { } // Gets guildMember whilst removing the ability of each other variables being null - const guildMember = guild.members.cache.get(user.id); + const member = guild.members.cache.get(user?.id); // Checks if guildMember is null - if (guildMember === undefined) { + if (member === undefined) { await interaction.reply({ content: 'Error fetching user!', ephemeral: true, @@ -81,7 +81,16 @@ export class RenameUserCommand extends Command { } // Change nickname - await guildMember.setNickname(nickname); + try { + await member.setNickname(nickname); + } catch { + await interaction.reply({ + content: 'Bot doesn\'t have permission to change the user\'s name!', + ephemeral: true, + fetchReply: true, + }); + return; + } await interaction.reply({ content: `Changed ${user}'s nickname`, fetchReply: true, @@ -91,9 +100,9 @@ export class RenameUserCommand extends Command { public async messageRun(message: Message, args: Args) { // Get arguments - let user: GuildMember; + let member: GuildMember; try { - user = await args.pick('member'); + member = await args.pick('member'); } catch { await message.react('❌'); await message.reply('User was not provided!'); @@ -108,7 +117,13 @@ export class RenameUserCommand extends Command { return; } - await user.setNickname(nickname); + try { + await member.setNickname(nickname); + } catch { + await message.react('❌'); + await message.reply('Bot doesn\'t have permission to change the user\'s name!'); + return; + } await message.react('✅'); } diff --git a/src/commands/mod/restriction/restrict.ts b/src/commands/mod/restriction/restrict.ts new file mode 100644 index 0000000..cfc3915 --- /dev/null +++ b/src/commands/mod/restriction/restrict.ts @@ -0,0 +1,361 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +/* + Animal Rights Advocates Discord Bot + Copyright (C) 2023 Anthony Berg + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import { + Args, + Command, + RegisterBehavior, + container, +} from '@sapphire/framework'; +import { + ChannelType, + EmbedBuilder, + PermissionsBitField, + time, +} from 'discord.js'; +import type { + User, + Message, + TextChannel, + Guild, + Snowflake, +} from 'discord.js'; +import IDs from '#utils/ids'; +import { + addEmptyUser, + updateUser, + userExists, + fetchRoles, +} from '#utils/database/dbExistingUser'; +import { restrict, checkActive } from '#utils/database/restriction'; +import { randint } from '#utils/maths'; + +export async function restrictRun( + userId: Snowflake, + modId: Snowflake, + reason: string, + guild: Guild, + tolerance = false, +) { + const info = { + message: '', + success: false, + }; + + let user = guild.client.users.cache.get(userId); + + if (user === undefined) { + user = await guild.client.users.fetch(userId); + if (user === undefined) { + info.message = 'Error fetching user'; + return info; + } + } + + // Gets mod's GuildMember + const mod = guild.members.cache.get(modId); + + // Checks if guildMember is null + if (mod === undefined) { + info.message = 'Error fetching mod'; + return info; + } + + // Check if mod is in database + await updateUser(mod); + + if (await checkActive(userId)) { + info.message = `<@${userId}> is already restricted!`; + return info; + } + + // Gets guildMember + let member = guild.members.cache.get(userId); + + if (member === undefined) { + member = await guild.members.fetch(userId) + .catch(() => undefined); + } + + const restrictRoles = IDs.roles.restrictions.restricted; + + let section = tolerance ? randint(3, 4) : randint(1, 2); + + if (member !== undefined) { + // Checks if the user is not restricted + if (member.roles.cache.hasAny(...restrictRoles)) { + info.message = `${member} is already restricted!`; + return info; + } + + // Check if user and mod are on the database + await updateUser(member); + + if (member.roles.cache.has(IDs.roles.vegan.vegan)) { + section = 5; + } + + await member.roles.add(restrictRoles[section - 1]); + + if (member.roles.cache.has(IDs.roles.vegan.vegan)) { + const voiceChannel = await guild.channels.create({ + name: 'Restricted Voice Channel', + type: ChannelType.GuildVoice, + parent: IDs.categories.restricted, + permissionOverwrites: [ + { + id: guild.roles.everyone, + deny: [PermissionsBitField.Flags.ViewChannel], + }, + { + id: member.id, + allow: [PermissionsBitField.Flags.ViewChannel], + }, + { + id: IDs.roles.staff.restricted, + allow: [PermissionsBitField.Flags.SendMessages, + PermissionsBitField.Flags.ViewChannel, + PermissionsBitField.Flags.Connect, + PermissionsBitField.Flags.MuteMembers], + }, + ], + }); + + let restrictedChannel: TextChannel; + let bannedName = false; + try { + restrictedChannel = await guild.channels.create({ + name: `⛔┃${member.user.username}-restricted`, + type: ChannelType.GuildText, + topic: `Restricted channel. ${member.id} ${voiceChannel.id} (Please do not change this)`, + parent: IDs.categories.restricted, + permissionOverwrites: [ + { + id: guild.roles.everyone, + allow: [PermissionsBitField.Flags.ReadMessageHistory], + deny: [PermissionsBitField.Flags.ViewChannel], + }, + { + id: member.id, + allow: [PermissionsBitField.Flags.ViewChannel], + }, + { + id: IDs.roles.staff.restricted, + allow: [PermissionsBitField.Flags.SendMessages, + PermissionsBitField.Flags.ViewChannel], + }, + ], + }); + } catch { + restrictedChannel = await guild.channels.create({ + name: `⛔┃${member.user.id}-restricted`, + type: ChannelType.GuildText, + topic: `Restricted channel. ${member.id} ${voiceChannel.id} (Please do not change this)`, + parent: IDs.categories.restricted, + permissionOverwrites: [ + { + id: guild.roles.everyone, + allow: [PermissionsBitField.Flags.ReadMessageHistory], + deny: [PermissionsBitField.Flags.ViewChannel], + }, + { + id: member.id, + allow: [PermissionsBitField.Flags.ViewChannel], + }, + { + id: IDs.roles.staff.restricted, + allow: [PermissionsBitField.Flags.SendMessages, + PermissionsBitField.Flags.ViewChannel], + }, + ], + }); + bannedName = true; + } + + if (!bannedName) { + await voiceChannel.setName(`${member.user.username}-restricted`); + } else { + await voiceChannel.setName(`${member.user.id}-restricted`); + } + + const joinTime = time(member.joinedAt!); + const registerTime = time(member.user.createdAt); + + const embed = new EmbedBuilder() + .setColor(member.displayHexColor) + .setTitle(`Restricted channel for ${member.user.username}`) + .setDescription(`${member}`) + .setThumbnail(member.user.avatarURL()!) + .addFields( + { name: 'Joined:', value: `${joinTime}`, inline: true }, + { name: 'Created:', value: `${registerTime}`, inline: true }, + ); + + await restrictedChannel.send({ embeds: [embed] }); + } + + await member.roles.remove([ + IDs.roles.vegan.vegan, + IDs.roles.vegan.plus, + IDs.roles.vegan.activist, + IDs.roles.trusted, + IDs.roles.nonvegan.nonvegan, + IDs.roles.nonvegan.convinced, + IDs.roles.nonvegan.vegCurious, + ]); + } else if (!await userExists(userId)) { + await addEmptyUser(userId); + } else { + const dbRoles = await fetchRoles(userId); + if (dbRoles.includes(IDs.roles.vegan.vegan)) { + section = 5; + } + } + + // Restrict the user on the database + await restrict(userId, modId, reason, section); + + info.message = `Restricted ${user}`; + info.success = true; + + // Log the ban + let logChannel = guild.channels.cache + .get(IDs.channels.logs.restricted) as TextChannel | undefined; + + if (logChannel === undefined) { + logChannel = await guild.channels + .fetch(IDs.channels.logs.restricted) as TextChannel | undefined; + if (logChannel === undefined) { + container.logger.error('Restrict Error: Could not fetch log channel'); + info.message = `Restricted ${user} but could not find the log channel. This has been logged to the database.`; + return info; + } + } + + const message = new EmbedBuilder() + .setColor('#FF6700') + .setAuthor({ name: `Restricted ${user.tag}`, iconURL: `${user.avatarURL()}` }) + .addFields( + { name: 'User', value: `${user}`, inline: true }, + { name: 'Moderator', value: `${mod}`, inline: true }, + { name: 'Reason', value: reason }, + ) + .setTimestamp() + .setFooter({ text: `ID: ${userId}` }); + + await logChannel.send({ embeds: [message] }); + + return info; +} + +export class RestrictCommand extends Command { + public constructor(context: Command.Context, options: Command.Options) { + super(context, { + ...options, + name: 'restrict', + aliases: ['r', 'rest', 'rr', 'rv'], + description: 'Restricts a user', + preconditions: ['ModOnly'], + }); + } + + // Registers that this is a slash command + public override registerApplicationCommands(registry: Command.Registry) { + registry.registerChatInputCommand( + (builder) => builder + .setName(this.name) + .setDescription(this.description) + .addUserOption((option) => option.setName('user') + .setDescription('User to restrict') + .setRequired(true)) + .addStringOption((option) => option.setName('reason') + .setDescription('Reason for restricting the user') + .setRequired(true)), + { + behaviorWhenNotIdentical: RegisterBehavior.Overwrite, + }, + ); + } + + // Command run + public async chatInputRun(interaction: Command.ChatInputCommandInteraction) { + // Get the arguments + const user = interaction.options.getUser('user', true); + const reason = interaction.options.getString('reason', true); + const mod = interaction.member; + const { guild } = interaction; + + // Checks if all the variables are of the right type + if (guild === null || mod === null) { + await interaction.reply({ + content: 'Error fetching user!', + ephemeral: true, + fetchReply: true, + }); + return; + } + + const info = await restrictRun(user?.id, mod.user.id, reason, guild); + + await interaction.reply({ + content: info.message, + fetchReply: true, + }); + } + + // Non Application Command method of banning a user + public async messageRun(message: Message, args: Args) { + // Get arguments + let user: User; + try { + user = await args.pick('user'); + } catch { + await message.react('❌'); + await message.reply('User was not provided!'); + return; + } + const reason = args.finished ? null : await args.rest('string'); + const mod = message.member; + + if (reason === null) { + await message.react('❌'); + await message.reply('Restrict reason was not provided!'); + return; + } + + if (mod === null) { + await message.react('❌'); + await message.reply('Moderator not found! Try again or contact a developer!'); + return; + } + + const { guild } = message; + + if (guild === null) { + await message.react('❌'); + await message.reply('Guild not found! Try again or contact a developer!'); + return; + } + + const info = await restrictRun(user?.id, mod.user.id, reason, guild); + + await message.reply(info.message); + await message.react(info.success ? '✅' : '❌'); + } +} diff --git a/src/commands/mod/restriction/restrictTolerance.ts b/src/commands/mod/restriction/restrictTolerance.ts new file mode 100644 index 0000000..113e2d5 --- /dev/null +++ b/src/commands/mod/restriction/restrictTolerance.ts @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +/* + Animal Rights Advocates Discord Bot + Copyright (C) 2023 Anthony Berg + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import { Args, Command, RegisterBehavior } from '@sapphire/framework'; +import type { User, Message } from 'discord.js'; +import { restrictRun } from './restrict'; + +export class RestrictToleranceCommand extends Command { + public constructor(context: Command.Context, options: Command.Options) { + super(context, { + ...options, + name: 'restricttolerance', + aliases: ['rt'], + description: 'Restricts a user for bigoted reasons', + preconditions: ['ModOnly'], + }); + } + + // Registers that this is a slash command + public override registerApplicationCommands(registry: Command.Registry) { + registry.registerChatInputCommand( + (builder) => builder + .setName(this.name) + .setDescription(this.description) + .addUserOption((option) => option.setName('user') + .setDescription('User to restrict') + .setRequired(true)) + .addStringOption((option) => option.setName('reason') + .setDescription('Reason for restricting the user') + .setRequired(true)), + { + behaviorWhenNotIdentical: RegisterBehavior.Overwrite, + }, + ); + } + + // Command run + public async chatInputRun(interaction: Command.ChatInputCommandInteraction) { + // Get the arguments + const user = interaction.options.getUser('user', true); + const reason = interaction.options.getString('reason', true); + const mod = interaction.member; + const { guild } = interaction; + + // Checks if all the variables are of the right type + if (guild === null || mod === null) { + await interaction.reply({ + content: 'Error fetching user!', + ephemeral: true, + fetchReply: true, + }); + return; + } + + const info = await restrictRun(user?.id, mod.user.id, reason, guild, true); + + await interaction.reply({ + content: info.message, + fetchReply: true, + }); + } + + // Non Application Command method of banning a user + public async messageRun(message: Message, args: Args) { + // Get arguments + let user: User; + try { + user = await args.pick('user'); + } catch { + await message.react('❌'); + await message.reply('User was not provided!'); + return; + } + const reason = args.finished ? null : await args.rest('string'); + const mod = message.member; + + if (reason === null) { + await message.react('❌'); + await message.reply('Restrict reason was not provided!'); + return; + } + + if (mod === null) { + await message.react('❌'); + await message.reply('Moderator not found! Try again or contact a developer!'); + return; + } + + const { guild } = message; + + if (guild === null) { + await message.react('❌'); + await message.reply('Guild not found! Try again or contact a developer!'); + return; + } + + const info = await restrictRun(user?.id, mod.user.id, reason, guild, true); + + await message.reply(info.message); + await message.react(info.success ? '✅' : '❌'); + } +} diff --git a/src/commands/mod/restriction/unrestrict.ts b/src/commands/mod/restriction/unrestrict.ts new file mode 100644 index 0000000..42c3d86 --- /dev/null +++ b/src/commands/mod/restriction/unrestrict.ts @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +/* + Animal Rights Advocates Discord Bot + Copyright (C) 2023 Anthony Berg + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import { Args, Command, RegisterBehavior } from '@sapphire/framework'; +import { CategoryChannel, ChannelType, EmbedBuilder } from 'discord.js'; +import type { + User, + Message, + TextChannel, + Guild, + Snowflake, +} from 'discord.js'; +import IDs from '#utils/ids'; +import { fetchRoles, addExistingUser, userExists } from '#utils/database/dbExistingUser'; +import { unRestrict, checkActive, unRestrictLegacy } from '#utils/database/restriction'; + +export class UnRestrictCommand extends Command { + public constructor(context: Command.Context, options: Command.Options) { + super(context, { + ...options, + name: 'unrestrict', + aliases: ['ur', 'urv'], + description: 'Unrestricts a user', + preconditions: ['ModOnly'], + }); + } + + // Registers that this is a slash command + public override registerApplicationCommands(registry: Command.Registry) { + registry.registerChatInputCommand( + (builder) => builder + .setName(this.name) + .setDescription(this.description) + .addUserOption((option) => option.setName('user') + .setDescription('User to unrestrict') + .setRequired(true)), + { + behaviorWhenNotIdentical: RegisterBehavior.Overwrite, + }, + ); + } + + // Command run + public async chatInputRun(interaction: Command.ChatInputCommandInteraction) { + // Get the arguments + const user = interaction.options.getUser('user', true); + const mod = interaction.member; + const { guild } = interaction; + + // Checks if all the variables are of the right type + if (guild === null || mod === null) { + await interaction.reply({ + content: 'Error fetching user!', + ephemeral: true, + fetchReply: true, + }); + return; + } + + const info = await this.unRestrictRun(user?.id, mod.user.id, guild); + + await interaction.reply({ + content: info.message, + fetchReply: true, + }); + } + + // Non Application Command method of banning a user + public async messageRun(message: Message, args: Args) { + // Get arguments + let user: User; + try { + user = await args.pick('user'); + } catch { + await message.react('❌'); + await message.reply('User was not provided!'); + return; + } + + const mod = message.member; + + if (mod === null) { + await message.react('❌'); + await message.reply('Moderator not found! Try again or contact a developer!'); + return; + } + + const { guild } = message; + + if (guild === null) { + await message.react('❌'); + await message.reply('Guild not found! Try again or contact a developer!'); + return; + } + + const info = await this.unRestrictRun(user?.id, mod.user.id, guild); + + await message.reply(info.message); + await message.react(info.success ? '✅' : '❌'); + } + + private async unRestrictRun(userId: Snowflake, modId: Snowflake, guild: Guild) { + const info = { + message: '', + success: false, + }; + + let user = guild.client.users.cache.get(userId); + + if (user === undefined) { + user = await guild.client.users.fetch(userId); + if (user === undefined) { + info.message = 'Error fetching user'; + return info; + } + } + + // Gets mod's GuildMember + const mod = guild.members.cache.get(modId); + + // Checks if guildMember is null + if (mod === undefined) { + info.message = 'Error fetching mod'; + return info; + } + + // Check if mod is in database + if (!await userExists(modId)) { + await addExistingUser(mod); + } + + // Gets guildMember + let member = guild.members.cache.get(userId); + + if (member === undefined) { + member = await guild.members.fetch(userId) + .catch(() => undefined); + } + + if (member === undefined) { + info.message = 'Can\'t unrestrict the user as they are not on this server'; + return info; + } + + // Check if mod is in database + if (!await userExists(userId)) { + await addExistingUser(member); + } + + const restrictRoles = IDs.roles.restrictions.restricted; + + // Checks if the user is not restricted + if (!member.roles.cache.hasAny(...restrictRoles)) { + info.message = `${user} is not restricted!`; + return info; + } + + if (await checkActive(userId)) { + const roles = await fetchRoles(userId); + await member.roles.add(roles); + // Unrestricts the user on the database + await unRestrict(userId, modId); + } else { + let section = 1; + for (let i = 0; i < restrictRoles.length; i += 1) { + if (member.roles.cache.has(restrictRoles[i])) { + section = i + 1; + } + } + await member.roles.add(IDs.roles.nonvegan.nonvegan); + // Unrestricts the user on the database but for restricts done on the old bot + await unRestrictLegacy(userId, modId, section); + } + + await member.roles.remove(restrictRoles); + + // Remove vegan restrict channels + if (member.roles.cache.has(IDs.roles.vegan.vegan)) { + const category = guild.channels.cache + .get(IDs.categories.restricted) as CategoryChannel | undefined; + + let topic: string[]; + + if (category !== undefined) { + const textChannels = category.children.cache + .filter((c) => c.type === ChannelType.GuildText); + textChannels.forEach((c) => { + const textChannel = c as TextChannel; + // Checks if the channel topic has the user's snowflake + if (textChannel.topic?.includes(userId)) { + topic = textChannel.topic.split(' '); + const vcId = topic[topic.indexOf(userId) + 1]; + const voiceChannel = guild.channels.cache.get(vcId); + + if (voiceChannel !== undefined + && voiceChannel.parentId === IDs.categories.restricted) { + voiceChannel.delete(); + } + textChannel.delete(); + } + }); + } + } + + info.success = true; + + // Log the ban + let logChannel = guild.channels.cache + .get(IDs.channels.logs.restricted) as TextChannel | undefined; + + if (logChannel === undefined) { + logChannel = await guild.channels + .fetch(IDs.channels.logs.restricted) as TextChannel | undefined; + if (logChannel === undefined) { + this.container.logger.error('Restrict Error: Could not fetch log channel'); + info.message = `Unrestricted ${user} but could not find the log channel. This has been logged to the database.`; + return info; + } + } + + const message = new EmbedBuilder() + .setColor('#28A745') + .setAuthor({ name: `Unrestricted ${user.tag}`, iconURL: `${user.avatarURL()}` }) + .addFields( + { name: 'User', value: `${user}`, inline: true }, + { name: 'Moderator', value: `${mod}`, inline: true }, + ) + .setTimestamp() + .setFooter({ text: `ID: ${userId}` }); + + await logChannel.send({ embeds: [message] }); + + info.message = `Unrestricted ${user}`; + return info; + } +} diff --git a/src/commands/mod/slowmode.ts b/src/commands/mod/slowmode.ts new file mode 100644 index 0000000..074ed5a --- /dev/null +++ b/src/commands/mod/slowmode.ts @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +/* + Animal Rights Advocates Discord Bot + Copyright (C) 2023 Anthony Berg + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import { Args, Command, RegisterBehavior } from '@sapphire/framework'; +import type { Message, TextBasedChannel } from 'discord.js'; +import { ChannelType } from 'discord.js'; +import { Duration, DurationFormatter } from '@sapphire/time-utilities'; +import { isNumber } from '#utils/maths'; + +export class SlowmodeCommand extends Command { + public constructor(context: Command.Context, options: Command.Options) { + super(context, { + ...options, + name: 'slowmode', + description: 'Sets slowmode for a channel', + preconditions: ['ModOnly'], + }); + } + + // Registers that this is a slash command + public override registerApplicationCommands(registry: Command.Registry) { + registry.registerChatInputCommand( + (builder) => builder + .setName(this.name) + .setDescription(this.description) + .addStringOption((option) => option.setName('duration') + .setDescription('Set the slowmode time') + .setRequired(true)), + { + behaviorWhenNotIdentical: RegisterBehavior.Overwrite, + }, + ); + } + + // Command run + public async chatInputRun(interaction: Command.ChatInputCommandInteraction) { + // Get the arguments + const duration = interaction.options.getString('duration', true); + const { channel } = interaction; + + if (channel === null) { + await interaction.reply({ + content: 'Could not fetch channel!', + ephemeral: true, + fetchReply: true, + }); + return; + } + + const slowmode = await this.slowmode(duration, channel); + + await interaction.reply({ content: slowmode.message }); + } + + public async messageRun(message: Message, args: Args) { + // Get arguments + const duration = args.finished ? null : await args.rest('string'); + const { channel } = message; + + if (duration === null) { + await message.react('❌'); + await message.reply('Slowmode length was not provided!'); + return; + } + + const slowmode = await this.slowmode(duration, channel); + + await message.reply(slowmode.message); + await message.react(slowmode.success ? '✅' : '❌'); + } + + private async slowmode(duration: string, channel: TextBasedChannel) { + const info = { + message: '', + success: false, + }; + if (channel.type !== ChannelType.GuildText) { + info.message = 'Channel is not a text channel!'; + return info; + } + + let durationCheck = duration; + + if (isNumber(durationCheck)) { + durationCheck += 's'; + } + this.container.logger.debug(durationCheck); + + const durationParsed = new Duration(durationCheck); + let time = 0; + + if (Number.isNaN(durationParsed.offset)) { + if (duration !== 'off') { + info.message = 'Invalid time format!'; + return info; + } + time = 0; + } else { + time = durationParsed.offset; + } + + await channel.setRateLimitPerUser(time / 1000); + + info.success = true; + if (time === 0) { + info.message = `${channel} is no longer in slowmode.`; + return info; + } + + info.message = `${channel} has now been set to a post every ${new DurationFormatter().format(time)}.`; + return info; + } +} diff --git a/src/commands/mod/vcMute.ts b/src/commands/mod/vcMute.ts new file mode 100644 index 0000000..bfeae20 --- /dev/null +++ b/src/commands/mod/vcMute.ts @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +/* + Animal Rights Advocates Discord Bot + Copyright (C) 2023 Anthony Berg + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import { Args, Command, RegisterBehavior } from '@sapphire/framework'; +import type { GuildMember, Message } from 'discord.js'; +import { addMute, removeMute, checkActive } from '#utils/database/vcMute'; +import { addExistingUser, userExists } from '#utils/database/dbExistingUser'; + +export class VCMuteCommand extends Command { + public constructor(context: Command.Context, options: Command.Options) { + super(context, { + ...options, + name: 'vcmute', + aliases: ['vmute'], + description: 'Persists a server mute if a user is trying to bypass mute', + preconditions: [['CoordinatorOnly', 'ModOnly']], + }); + } + + // Registers that this is a slash command + public override registerApplicationCommands(registry: Command.Registry) { + registry.registerChatInputCommand( + (builder) => builder + .setName(this.name) + .setDescription(this.description) + .addUserOption((option) => option.setName('user') + .setDescription('User to persistently mute') + .setRequired(true)) + .addStringOption((option) => option.setName('reason') + .setDescription('Reason for persistently muting the user')), + { + behaviorWhenNotIdentical: RegisterBehavior.Overwrite, + }, + ); + } + + // Command run + public async chatInputRun(interaction: Command.ChatInputCommandInteraction) { + // Get the arguments + const user = interaction.options.getUser('user', true); + const reason = interaction.options.getString('reason'); + const modUser = interaction.member; + const { guild } = interaction; + + // Checks if all the variables are of the right type + if (modUser === null || guild === null) { + await interaction.reply({ + content: 'Error fetching user!', + ephemeral: true, + fetchReply: true, + }); + return; + } + + // Gets guildMember whilst removing the ability of each other variables being null + const member = guild.members.cache.get(user.id); + const mod = guild.members.cache.get(modUser.user.id); + + // Checks if guildMember is null + if (member === undefined || mod === undefined) { + await interaction.reply({ + content: 'Error fetching user!', + ephemeral: true, + fetchReply: true, + }); + return; + } + + // Check if removing VC Mute + if (await checkActive(member.id)) { + await removeMute(member.id); + if (member.voice.channel !== null) { + await member.voice.setMute(false, reason === null ? undefined : reason); + } + + await interaction.reply({ + content: `Removed server mute from ${user}`, + fetchReply: true, + ephemeral: true, + }); + return; + } + + // Check if mod is in database + if (!await userExists(mod.id)) { + await addExistingUser(mod); + } + + // Add VC Mute + if (member.voice.channel !== null) { + await member.voice.setMute(true, reason === null ? undefined : reason); + } + await addMute(member.id, mod.id, reason); + await interaction.reply({ + content: `Server muted ${user}`, + fetchReply: true, + ephemeral: true, + }); + } + + public async messageRun(message: Message, args: Args) { + // Get arguments + let member: GuildMember; + try { + member = await args.pick('member'); + } catch { + await message.react('❌'); + await message.reply('User was not provided!'); + return; + } + + const reason = args.finished ? null : await args.rest('string'); + const mod = message.member; + + if (mod === null) { + await message.react('❌'); + await message.reply('Moderator not found! Try again or contact a developer!'); + return; + } + + // Check if removing VC Mute + if (await checkActive(member.id)) { + await removeMute(member.id); + if (member.voice.channel !== null) { + await member.voice.setMute(false, reason === null ? undefined : reason); + } + + await message.reply(`Removed server mute from ${member}`); + await message.react('✅'); + return; + } + + // Check if mod is in database + if (!await userExists(mod.id)) { + await addExistingUser(mod); + } + + // Add VC Mute + if (member.voice.channel !== null) { + await member.voice.setMute(true, reason === null ? undefined : reason); + } + await addMute(member.id, mod.id, reason); + await message.reply(`Server muted ${member}`); + + await message.react('✅'); + } +} diff --git a/src/commands/mod/warn.ts b/src/commands/mod/warn.ts new file mode 100644 index 0000000..2557e11 --- /dev/null +++ b/src/commands/mod/warn.ts @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +/* + Animal Rights Advocates Discord Bot + Copyright (C) 2023 Anthony Berg + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import { Args, Command } from '@sapphire/framework'; +import type { + User, + Message, + Snowflake, + Guild, +} from 'discord.js'; +import { addExistingUser, updateUser, userExists } from '#utils/database/dbExistingUser'; +import { addWarn } from '#utils/database/warnings'; + +/* + This command is not intended to be functional for now, this is purely to log + warnings onto a database, so if we were to switch purely to ARA Bot, it would + mean we would have a lot of the warns already in the database. +*/ +export class WarnCommand extends Command { + public constructor(context: Command.Context, options: Command.Options) { + super(context, { + ...options, + name: 'warn', + description: 'Warns a user (only used for logging to a database for now)', + preconditions: [['CoordinatorOnly', 'ModOnly']], + }); + } + + public async messageRun(message: Message, args: Args) { + // Get arguments + let user: User; + try { + user = await args.pick('user'); + } catch { + await message.react('❌'); + await message.reply('User was not provided!'); + return; + } + const reason = args.finished ? null : await args.rest('string'); + const mod = message.member; + + if (reason === null) { + await message.react('❌'); + await message.reply('Warn reason was not provided!'); + return; + } + + if (mod === null) { + await message.react('❌'); + await message.reply('Moderator not found! Try again or contact a developer!'); + return; + } + + const { guild } = message; + + if (guild === null) { + await message.react('❌'); + await message.reply('Guild not found! Try again or contact a developer!'); + return; + } + + const warn = await this.warn(user.id, mod.id, reason, guild); + + if (!warn.success) { + await message.react('❌'); + } + + // await message.react('✅'); + } + + private async warn(userId: Snowflake, modId: Snowflake, reason: string, guild: Guild) { + const info = { + message: '', + success: false, + }; + + // Gets mod's GuildMember + const mod = guild.members.cache.get(modId); + + // Checks if guildMember is null + if (mod === undefined) { + info.message = 'Error fetching mod!'; + return info; + } + + // Check if mod is in database + await updateUser(mod); + + // Gets guildMember + let member = guild.members.cache.get(userId); + + if (member === undefined) { + member = await guild.members.fetch(userId) + .catch(() => undefined); + } + + if (member === undefined) { + info.message = 'User is not on this server'; + return info; + } + + if (!(await userExists(userId))) { + await addExistingUser(member); + } + + await addWarn(userId, modId, reason); + + info.message = `Warned ${member}`; + info.success = true; + return info; + } +} diff --git a/src/listeners/ban/ban.ts b/src/listeners/ban/ban.ts new file mode 100644 index 0000000..4b9b745 --- /dev/null +++ b/src/listeners/ban/ban.ts @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +/* + Animal Rights Advocates Discord Bot + Copyright (C) 2022 Anthony Berg + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import { Listener } from '@sapphire/framework'; +import type { GuildBan } from 'discord.js'; +import { AuditLogEvent, EmbedBuilder, TextChannel } from 'discord.js'; +import { addBan, checkBan } from '#utils/database/ban'; +import IDs from '#utils/ids'; +import { addEmptyUser, addExistingUser, userExists } from '#utils/database/dbExistingUser'; + +export class BanListener extends Listener { + public constructor(context: Listener.Context, options: Listener.Options) { + super(context, { + ...options, + event: 'guildBanAdd', + }); + } + + public async run(ban: GuildBan) { + if (await checkBan(ban.user.id)) { + return; + } + + // Get the audit logs for the ban + const logs = await ban.guild.fetchAuditLogs({ + limit: 1, + type: AuditLogEvent.MemberBanAdd, + }); + + const banLog = logs.entries.first(); + + if (banLog === undefined) { + this.container.logger.error('BanListener: banLog is undefined.'); + return; + } + + const { executor, target } = banLog; + + if (ban.user !== target) { + this.container.logger.error('BanListener: ban.user !== target.'); + return; + } + + if (executor === null) { + this.container.logger.error('BanListener: mod not found.'); + return; + } + + if (this.container.client.user === null) { + this.container.logger.error('BanListener: client.user is null.'); + return; + } + + // Check if the bot banned the user + if (executor.id === this.container.client.user.id) { + this.container.logger.error('BanListener: got past the checkActive and bot banned this user.'); + return; + } + + const { user } = ban; + const { guild } = ban; + + // Gets mod's GuildMember + let mod = guild.members.cache.get(executor.id); + + // Checks if GuildMember is null + if (mod === undefined) { + mod = await guild.members.fetch(executor.id) + .catch(() => undefined); + if (mod === undefined) { + this.container.logger.error('UnbanListener: Could not fetch moderator.'); + return; + } + } + + // Check if mod is in database + if (!await userExists(mod.id)) { + await addExistingUser(mod); + } + + if (await checkBan(user.id)) { + this.container.logger.error('BanListener: got past the checkActive at the start.'); + return; + } + + // Check if user and mod are on the database + if (!await userExists(user.id)) { + await addEmptyUser(user.id); + } + + let { reason } = banLog; + + if (reason === null) { + reason = 'Was banned without using the bot, reason was not given'; + } + + // Add missing ban + await addBan(user.id, mod.id, `${reason}`); + + // Log the ban + let logChannel = guild.channels.cache + .get(IDs.channels.logs.restricted) as TextChannel | undefined; + + if (logChannel === undefined) { + logChannel = await guild.channels + .fetch(IDs.channels.logs.restricted) as TextChannel | undefined; + if (logChannel === undefined) { + this.container.logger.error('BanListener: Could not fetch log channel'); + return; + } + } + + const log = new EmbedBuilder() + .setColor('#FF0000') + .setAuthor({ name: `Banned ${user.tag} (not done via bot)`, iconURL: `${user.avatarURL()}` }) + .addFields( + { name: 'User', value: `${user}`, inline: true }, + { name: 'Moderator', value: `${mod}`, inline: true }, + { name: 'Reason', value: reason }, + ) + .setTimestamp() + .setFooter({ text: `ID: ${user.id}` }); + + await logChannel.send({ embeds: [log] }); + } +} diff --git a/src/listeners/ban.ts b/src/listeners/ban/banJoin.ts similarity index 82% rename from src/listeners/ban.ts rename to src/listeners/ban/banJoin.ts index 1697ee8..c8182d0 100644 --- a/src/listeners/ban.ts +++ b/src/listeners/ban/banJoin.ts @@ -19,9 +19,10 @@ import { Listener } from '@sapphire/framework'; import type { GuildMember } from 'discord.js'; -import { checkActive, getReason } from '#utils/database/ban'; +import { checkBan, getBanReason } from '#utils/database/ban'; +import { checkTempBan } from '#utils/database/tempBan'; -export class BanJoin extends Listener { +export class BanJoinListener extends Listener { public constructor(context: Listener.Context, options: Listener.Options) { super(context, { ...options, @@ -31,12 +32,13 @@ export class BanJoin extends Listener { public async run(user: GuildMember) { // Check if the user is banned - if (!await checkActive(user.id)) { + if (!await checkBan(user.id) + && !await checkTempBan(user.id)) { return; } // Get reason from database - const reason = await getReason(user.id); + const reason = await getBanReason(user.id); // Send DM for ban reason await user.send(`You have been banned from ARA for: ${reason}` diff --git a/src/listeners/ban/unban.ts b/src/listeners/ban/unban.ts new file mode 100644 index 0000000..bf2d8bd --- /dev/null +++ b/src/listeners/ban/unban.ts @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +/* + Animal Rights Advocates Discord Bot + Copyright (C) 2022 Anthony Berg + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import { Listener } from '@sapphire/framework'; +import type { GuildBan } from 'discord.js'; +import { AuditLogEvent, EmbedBuilder, TextChannel } from 'discord.js'; +import { addBan, checkBan, removeBan } from '#utils/database/ban'; +import IDs from '#utils/ids'; +import { addEmptyUser, addExistingUser, userExists } from '#utils/database/dbExistingUser'; + +export class UnbanListener extends Listener { + public constructor(context: Listener.Context, options: Listener.Options) { + super(context, { + ...options, + event: 'guildBanRemove', + }); + } + + public async run(ban: GuildBan) { + // Check if the bot unbanned the user + const logs = await ban.guild.fetchAuditLogs({ + limit: 1, + type: AuditLogEvent.MemberBanRemove, + }); + + const banLog = logs.entries.first(); + + if (banLog === undefined) { + return; + } + + const { executor, target } = banLog; + + if (ban.user !== target) { + return; + } + + if (executor === null) { + return; + } + + if (this.container.client.user === null) { + this.container.logger.error('UnbanListener: client.user is null.'); + return; + } + + if (executor.id === this.container.client.user.id) { + return; + } + + const { user } = ban; + const { guild } = ban; + + // Gets mod's GuildMember + let mod = guild.members.cache.get(executor.id); + + // Checks if GuildMember is null + if (mod === undefined) { + mod = await guild.members.fetch(executor.id) + .catch(() => undefined); + if (mod === undefined) { + this.container.logger.error('UnbanListener: Could not fetch moderator.'); + return; + } + } + + // Check if mod is in database + if (!await userExists(mod.id)) { + await addExistingUser(mod); + } + + // Check for missing ban on database + if (!await checkBan(user.id)) { + // Check if user and mod are on the database + if (!await userExists(user.id)) { + await addEmptyUser(user.id); + } + // Add missing ban + await addBan(user.id, mod.id, '(Mod who banned is not accurate) - '); + } + + // Add unban to database + await removeBan(user.id, mod.id); + + // Log the ban + let logChannel = guild.channels.cache + .get(IDs.channels.logs.restricted) as TextChannel | undefined; + + if (logChannel === undefined) { + logChannel = await guild.channels + .fetch(IDs.channels.logs.restricted) as TextChannel | undefined; + if (logChannel === undefined) { + this.container.logger.error('UnbanListener: Could not fetch log channel'); + return; + } + } + + const log = new EmbedBuilder() + .setColor('#28A745') + .setAuthor({ name: `Unbanned ${user.tag} (not done via bot)`, iconURL: `${user.avatarURL()}` }) + .addFields( + { name: 'User', value: `${user}`, inline: true }, + { name: 'Moderator', value: `${mod}`, inline: true }, + ) + .setTimestamp() + .setFooter({ text: `ID: ${user.id}` }); + + await logChannel.send({ embeds: [log] }); + } +} diff --git a/src/listeners/botAppreciation.ts b/src/listeners/botAppreciation.ts new file mode 100644 index 0000000..06d75f5 --- /dev/null +++ b/src/listeners/botAppreciation.ts @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +/* + Animal Rights Advocates Discord Bot + Copyright (C) 2023 Anthony Berg + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import { Listener } from '@sapphire/framework'; +import type { Message } from 'discord.js'; + +export class BotAppreciationListener extends Listener { + public constructor(context: Listener.Context, options: Listener.Options) { + super(context, { + ...options, + event: 'messageCreate', + }); + } + + public async run(message: Message) { + const content = message.content.toLowerCase(); + if (!content.includes('thanks arabot') + && !content.includes('thanks ara bot') + && !content.includes('thank you arabot') + && !content.includes('thank you ara bot')) { + return; + } + + await message.react('💚'); + } +} diff --git a/src/listeners/dbLeaveServer.ts b/src/listeners/dbLeaveServer.ts new file mode 100644 index 0000000..fece687 --- /dev/null +++ b/src/listeners/dbLeaveServer.ts @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +/* + Animal Rights Advocates Discord Bot + Copyright (C) 2022 Anthony Berg + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import { Listener } from '@sapphire/framework'; +import type { GuildMember } from 'discord.js'; +import IDs from '#utils/ids'; +import { updateUser } from '#utils/database/dbExistingUser'; + +export class DbLeaveServerListener extends Listener { + public constructor(context: Listener.Context, options: Listener.Options) { + super(context, { + ...options, + event: 'guildMemberRemove', + }); + } + + public async run(member: GuildMember) { + if (!member.roles.cache.hasAny( + IDs.roles.vegan.vegan, + IDs.roles.nonvegan.nonvegan, + )) { + return; + } + + await updateUser(member); + } +} diff --git a/src/listeners/verification/joinServer.ts b/src/listeners/rolesJoinServer.ts similarity index 68% rename from src/listeners/verification/joinServer.ts rename to src/listeners/rolesJoinServer.ts index d9ae243..a4e7216 100644 --- a/src/listeners/verification/joinServer.ts +++ b/src/listeners/rolesJoinServer.ts @@ -19,11 +19,12 @@ import { Listener } from '@sapphire/framework'; import type { GuildMember } from 'discord.js'; -// import { fetchRoles } from '../../utils/database/dbExistingUser'; +// import { fetchRoles } from '#utils/database/dbExistingUser'; import IDs from '#utils/ids'; import { blockTime } from '#utils/database/verification'; +import { checkActive, getSection } from '#utils/database/restriction'; -export class VerificationReady extends Listener { +export class RolesJoinServerListener extends Listener { public constructor(context: Listener.Context, options: Listener.Options) { super(context, { ...options, @@ -31,20 +32,26 @@ export class VerificationReady extends Listener { }); } - public async run(user: GuildMember) { + public async run(member: GuildMember) { // Add basic roles - // Removed this because it can give restricted people access back, - // Currently using another bot for this - // const roles = await fetchRoles(user.id); - const roles: string[] = []; + + const roles = []; + // const roles = await fetchRoles(member.id); + + // Check if the user is restricted + if (await checkActive(member.id)) { + const section = await getSection(member.id); + roles.length = 0; + roles.push(IDs.roles.restrictions.restricted[section - 1]); + } // Check if the user has a verification block - const timeout = await blockTime(user.id); + const timeout = await blockTime(member.id); if (timeout > 0) { roles.push(IDs.roles.verifyBlock); } // Add roles if they don't have verification block - await user.roles.add(roles); + await member.roles.add(roles); } } diff --git a/src/listeners/vcMute.ts b/src/listeners/vcMute.ts new file mode 100644 index 0000000..f6ce33d --- /dev/null +++ b/src/listeners/vcMute.ts @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +/* + Animal Rights Advocates Discord Bot + Copyright (C) 2023 Anthony Berg + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import { Listener } from '@sapphire/framework'; +import type { VoiceState } from 'discord.js'; +import { checkActive, removeMute } from '#utils/database/vcMute'; + +export class VCMuteListener extends Listener { + public constructor(context: Listener.Context, options: Listener.Options) { + super(context, { + ...options, + event: 'voiceStateUpdate', + }); + } + + public async run(oldState: VoiceState, newState: VoiceState) { + // Check the user joining the channel if they need to be muted + if (oldState.channel === null && newState.channel !== null) { + const { member } = newState; + + if (member === null) { + this.container.logger.error('VCMute Listener - GuildMember not found when joining'); + return; + } + + // Check if user is already muted + if (member.voice.serverMute) { + return; + } + + // Check if user is muted on the database + if (!await checkActive(member.id)) { + return; + } + + // Server mute the user + await member.voice.setMute(true); + return; + } + + // Check if the user has been unmuted by a mod + if (oldState.channel !== null && newState.channel !== null) { + const { member } = newState; + + if (member === null) { + this.container.logger.error('VCMute Listener - GuildMember not found when unmuting'); + return; + } + + // Check if user is muted on the database + if (!await checkActive(member.id)) { + return; + } + + // Checks if the user has been unmuted + if (oldState.serverMute && !newState.serverMute) { + await removeMute(member.id); + } + } + } +} diff --git a/src/listeners/verification/joinVC.ts b/src/listeners/verification/joinVC.ts index 4873420..f5489d3 100644 --- a/src/listeners/verification/joinVC.ts +++ b/src/listeners/verification/joinVC.ts @@ -128,7 +128,6 @@ export class VerificationJoinVCListener extends Listener { ]); // Start 15-minute timer if verifier does not join - // @ts-ignore this.container.tasks.create('verifyTimeout', { channelId: channel.id, userId: member.id, diff --git a/src/listeners/verification/leaveVC.ts b/src/listeners/verification/leaveVC.ts index 6d67298..e110d71 100644 --- a/src/listeners/verification/leaveVC.ts +++ b/src/listeners/verification/leaveVC.ts @@ -25,7 +25,7 @@ import { time, ChannelType, PermissionsBitField } from 'discord.js'; import { maxVCs, leaveBan } from '#utils/verificationConfig'; import { getUser, checkFinish, countIncomplete } from '#utils/database/verification'; import { fetchRoles } from '#utils/database/dbExistingUser'; -import { fibonacci } from '#utils/mathsSeries'; +import { fibonacci } from '#utils/maths'; import IDs from '#utils/ids'; export class VerificationLeaveVCListener extends Listener { @@ -88,7 +88,6 @@ export class VerificationLeaveVCListener extends Listener { // Creates the length of the time for the ban const banLength = fibonacci(incompleteCount) * 3600_000; - // @ts-ignore this.container.tasks.create('verifyUnblock', { userId: user.id, guildId: guild.id, diff --git a/src/scheduled-tasks/diversityMon.ts b/src/scheduled-tasks/messages/diversityMon.ts similarity index 98% rename from src/scheduled-tasks/diversityMon.ts rename to src/scheduled-tasks/messages/diversityMon.ts index 1755b65..53e0c8b 100644 --- a/src/scheduled-tasks/diversityMon.ts +++ b/src/scheduled-tasks/messages/diversityMon.ts @@ -26,7 +26,7 @@ export class DiversityMonMessageTask extends ScheduledTask { public constructor(context: ScheduledTask.Context, options: ScheduledTask.Options) { super(context, { ...options, - cron: '0 15 * * 1', + pattern: '0 15 * * 1', }); } @@ -53,6 +53,6 @@ export class DiversityMonMessageTask extends ScheduledTask { declare module '@sapphire/plugin-scheduled-tasks' { interface ScheduledTasks { - cron: never; + pattern: never; } } diff --git a/src/scheduled-tasks/diversityWed.ts b/src/scheduled-tasks/messages/diversityWed.ts similarity index 98% rename from src/scheduled-tasks/diversityWed.ts rename to src/scheduled-tasks/messages/diversityWed.ts index 9e5e597..f40ee0b 100644 --- a/src/scheduled-tasks/diversityWed.ts +++ b/src/scheduled-tasks/messages/diversityWed.ts @@ -26,7 +26,7 @@ export class DiversityWedMessageTask extends ScheduledTask { public constructor(context: ScheduledTask.Context, options: ScheduledTask.Options) { super(context, { ...options, - cron: '0 15 * * 3', + pattern: '0 15 * * 3', }); } @@ -54,6 +54,6 @@ export class DiversityWedMessageTask extends ScheduledTask { declare module '@sapphire/plugin-scheduled-tasks' { interface ScheduledTasks { - cron: never; + pattern: never; } } diff --git a/src/scheduled-tasks/restrictedMessage.ts b/src/scheduled-tasks/messages/restrictedMessage.ts similarity index 97% rename from src/scheduled-tasks/restrictedMessage.ts rename to src/scheduled-tasks/messages/restrictedMessage.ts index 2840f4c..d24f1a0 100644 --- a/src/scheduled-tasks/restrictedMessage.ts +++ b/src/scheduled-tasks/messages/restrictedMessage.ts @@ -26,7 +26,7 @@ export class RestrictedMessageTask extends ScheduledTask { public constructor(context: ScheduledTask.Context, options: ScheduledTask.Options) { super(context, { ...options, - cron: '0 17 * * *', + pattern: '0 17 * * *', }); } @@ -49,6 +49,6 @@ export class RestrictedMessageTask extends ScheduledTask { declare module '@sapphire/plugin-scheduled-tasks' { interface ScheduledTasks { - cron: never; + pattern: never; } } diff --git a/src/scheduled-tasks/standup.ts b/src/scheduled-tasks/messages/standup.ts similarity index 97% rename from src/scheduled-tasks/standup.ts rename to src/scheduled-tasks/messages/standup.ts index 5f49584..6731464 100644 --- a/src/scheduled-tasks/standup.ts +++ b/src/scheduled-tasks/messages/standup.ts @@ -26,7 +26,7 @@ export class StandupTask extends ScheduledTask { public constructor(context: ScheduledTask.Context, options: ScheduledTask.Options) { super(context, { ...options, - cron: '0 12 * * 1', + pattern: '0 12 * * 1', }); } @@ -42,6 +42,6 @@ export class StandupTask extends ScheduledTask { declare module '@sapphire/plugin-scheduled-tasks' { interface ScheduledTasks { - cron: never; + pattern: never; } } diff --git a/src/scheduled-tasks/verifyReminder.ts b/src/scheduled-tasks/messages/verifyReminder.ts similarity index 97% rename from src/scheduled-tasks/verifyReminder.ts rename to src/scheduled-tasks/messages/verifyReminder.ts index 5fc6228..9054c52 100644 --- a/src/scheduled-tasks/verifyReminder.ts +++ b/src/scheduled-tasks/messages/verifyReminder.ts @@ -26,7 +26,7 @@ export class VerifyReminder extends ScheduledTask { public constructor(context: ScheduledTask.Context, options: ScheduledTask.Options) { super(context, { ...options, - cron: '0 */1 * * *', + pattern: '0 */1 * * *', }); } @@ -43,6 +43,6 @@ export class VerifyReminder extends ScheduledTask { declare module '@sapphire/plugin-scheduled-tasks' { interface VerifyReminder { - cron: never; + pattern: never; } } diff --git a/src/scheduled-tasks/tempBan.ts b/src/scheduled-tasks/tempBan.ts new file mode 100644 index 0000000..1b49818 --- /dev/null +++ b/src/scheduled-tasks/tempBan.ts @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +/* + Animal Rights Advocates Discord Bot + Copyright (C) 2023 Anthony Berg + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import { ScheduledTask } from '@sapphire/plugin-scheduled-tasks'; +import IDs from '#utils/ids'; +import { + TextChannel, + EmbedBuilder, +} from 'discord.js'; +import { checkBan } from '#utils/database/ban'; +import { checkTempBan, removeTempBan } from '#utils/database/tempBan'; + +export class TempBan extends ScheduledTask { + public constructor(context: ScheduledTask.Context, options: ScheduledTask.Options) { + super(context, options); + } + + public async run(payload: { userId: string, guildId: string }) { + this.container.logger.debug('Temp Unban Task: Currently running unban'); + // Get the guild where the user is in + let guild = this.container.client.guilds.cache.get(payload.guildId); + if (guild === undefined) { + guild = await this.container.client.guilds.fetch(payload.guildId); + if (guild === undefined) { + this.container.logger.error('Temp Unban Task: Guild not found!'); + return; + } + } + + const { userId } = payload; + + let user = guild.client.users.cache.get(userId); + + if (user === undefined) { + user = await guild.client.users.fetch(userId); + if (user === undefined) { + this.container.logger.error('Temp Unban Task: Could not fetch banned user!'); + return; + } + } + + if (await checkBan(userId) + || !await checkTempBan(userId)) { + this.container.logger.debug('Temp Unban Task: User is either permanently banned or no longer temporarily banned.'); + return; + } + + // Unban the user + await guild.members.unban(user) + .catch(() => {}); + + await removeTempBan(userId); + + // Log unban + let logChannel = guild.channels.cache + .get(IDs.channels.logs.restricted) as TextChannel | undefined; + + if (logChannel === undefined) { + logChannel = await guild.channels + .fetch(IDs.channels.logs.restricted) as TextChannel | undefined; + if (logChannel === undefined) { + this.container.logger.error(`Temp Ban Listener: Could not fetch log channel. User Snowflake: ${userId}`); + return; + } + } + + const log = new EmbedBuilder() + .setColor('#28A745') + .setAuthor({ name: `Unbanned ${user.tag} (tempban)`, iconURL: `${user.avatarURL()}` }) + .addFields( + { name: 'User', value: `${user}`, inline: true }, + ) + .setTimestamp() + .setFooter({ text: `ID: ${user.id}` }); + + await logChannel.send({ embeds: [log] }); + } +} + +declare module '@sapphire/plugin-scheduled-tasks' { + interface ScheduledTasks { + tempBan: never; + } +} diff --git a/src/scheduled-tasks/verifyTimeout.ts b/src/scheduled-tasks/verifyTimeout.ts index bbf452c..a0bd253 100644 --- a/src/scheduled-tasks/verifyTimeout.ts +++ b/src/scheduled-tasks/verifyTimeout.ts @@ -51,6 +51,6 @@ export class VerifyTimeout extends ScheduledTask { declare module '@sapphire/plugin-scheduled-tasks' { interface ScheduledTasks { - verifyUnblock: never; + verifyTimeout: never; } } diff --git a/src/utils/database/ban.ts b/src/utils/database/ban.ts index 5ad7222..2319d57 100644 --- a/src/utils/database/ban.ts +++ b/src/utils/database/ban.ts @@ -46,7 +46,7 @@ export async function removeBan(userId: string, modId: string) { }); } -export async function checkActive(userId: string) { +export async function checkBan(userId: string) { const ban = await container.database.ban.findFirst({ where: { userId, @@ -63,7 +63,7 @@ export async function checkActive(userId: string) { return ban.active; } -export async function getReason(userId: string) { +export async function getBanReason(userId: string) { const ban = await container.database.ban.findFirst({ where: { userId, diff --git a/src/utils/database/dbExistingUser.ts b/src/utils/database/dbExistingUser.ts index f64c58d..153c2c2 100644 --- a/src/utils/database/dbExistingUser.ts +++ b/src/utils/database/dbExistingUser.ts @@ -17,12 +17,12 @@ along with this program. If not, see . */ -import type { GuildMember, GuildMemberRoleManager } from 'discord.js'; +import type { GuildMember, GuildMemberRoleManager, Snowflake } from 'discord.js'; import { container } from '@sapphire/framework'; import IDs from '#utils/ids'; // Checks if the user exists on the database -export async function userExists(userId: string) { +export async function userExists(userId: Snowflake) { // Counts if the user is on the database by their snowflake const userQuery = await container.database.user.count({ where: { @@ -51,11 +51,11 @@ function getRoles(roles: GuildMemberRoleManager) { } // Adds the user to the database if they were already on the server before the bot/database -export async function addExistingUser(user: GuildMember) { +export async function addExistingUser(member: GuildMember) { // Counts if the user is on the database by their snowflake const userQuery = await container.database.user.count({ where: { - id: user.id, + id: member.id, }, }); @@ -65,12 +65,12 @@ export async function addExistingUser(user: GuildMember) { } // Parse all the roles into a dictionary - const roles = getRoles(user.roles); + const roles = getRoles(member.roles); // Create the user in the database await container.database.user.create({ data: { - id: user.id, + id: member.id, vegan: roles.vegan, trusted: roles.trusted, activist: roles.activist, @@ -84,7 +84,7 @@ export async function addExistingUser(user: GuildMember) { } // Add an empty user to database in case they are not on the server -export async function addEmptyUser(userId: string) { +export async function addEmptyUser(userId: Snowflake) { // Counts if the user is on the database by their snowflake const userQuery = await container.database.user.count({ where: { @@ -105,22 +105,22 @@ export async function addEmptyUser(userId: string) { }); } -export async function updateUser(user: GuildMember) { +export async function updateUser(member: GuildMember) { // Check if the user is already on the database - if (!(await userExists(user.id))) { - await addExistingUser(user); + if (!(await userExists(member.id))) { + await addExistingUser(member); return; } // Parse all the roles into a dictionary - const roles = getRoles(user.roles); + const roles = getRoles(member.roles); await container.database.user.update({ where: { - id: user.id, + id: member.id, }, data: { - id: user.id, + id: member.id, vegan: roles.vegan, trusted: roles.trusted, activist: roles.activist, @@ -133,11 +133,11 @@ export async function updateUser(user: GuildMember) { }); } -export async function fetchRoles(user: string) { +export async function fetchRoles(userId: Snowflake) { // Get the user's roles const roleQuery = await container.database.user.findUnique({ where: { - id: user, + id: userId, }, select: { vegan: true, diff --git a/src/utils/database/restriction.ts b/src/utils/database/restriction.ts new file mode 100644 index 0000000..5117401 --- /dev/null +++ b/src/utils/database/restriction.ts @@ -0,0 +1,124 @@ +import { container } from '@sapphire/framework'; +import type { Snowflake } from 'discord.js'; + +export async function restrict( + userId: Snowflake, + modId: Snowflake, + reason: string, + section: number, +) { + // Add the user to the database + await container.database.restrict.create({ + data: { + user: { + connect: { + id: userId, + }, + }, + mod: { + connect: { + id: modId, + }, + }, + reason, + section, + }, + }); +} + +export async function unRestrict(userId: Snowflake, modId: Snowflake) { + const restriction = await container.database.restrict.findFirst({ + where: { + userId, + }, + select: { + id: true, + }, + orderBy: { + id: 'desc', + }, + }); + + if (restriction === null) { + return; + } + + await container.database.restrict.update({ + where: { + id: restriction.id, + }, + data: { + endMod: { + connect: { + id: modId, + }, + }, + endTime: new Date(), + }, + }); +} + +export async function checkActive(userId: Snowflake) { + const restriction = await container.database.restrict.findFirst({ + where: { + userId, + }, + select: { + endTime: true, + }, + orderBy: { + id: 'desc', + }, + }); + + if (restriction === null) { + return false; + } + + return restriction.endTime === null; +} + +export async function getSection(userId: Snowflake) { + const restriction = await container.database.restrict.findFirst({ + where: { + userId, + }, + select: { + section: true, + }, + orderBy: { + id: 'desc', + }, + }); + + if (restriction === null) { + return 0; + } + + return restriction.section; +} + +// This is only for restrictions created with the old bot +export async function unRestrictLegacy(userId: Snowflake, modId: Snowflake, section: number) { + await container.database.restrict.create({ + data: { + user: { + connect: { + id: userId, + }, + }, + mod: { + connect: { + id: modId, + }, + }, + endMod: { + connect: { + id: modId, + }, + }, + reason: 'This user was restricted with the old bot. Restrict reason, time and mod unknown, check old bot logs.', + section, + }, + }); +} diff --git a/src/utils/database/tempBan.ts b/src/utils/database/tempBan.ts new file mode 100644 index 0000000..00d73e8 --- /dev/null +++ b/src/utils/database/tempBan.ts @@ -0,0 +1,98 @@ +import { container } from '@sapphire/framework'; +import type { Snowflake } from 'discord.js'; + +export async function addTempBan( + userId: Snowflake, + modId: Snowflake, + endTime: Date, + reason: string, +) { + // Add the user to the database + await container.database.tempBan.create({ + data: { + user: { + connect: { + id: userId, + }, + }, + mod: { + connect: { + id: modId, + }, + }, + endTime, + reason, + }, + }); +} + +export async function removeTempBan(userId: Snowflake, modId?: Snowflake) { + const ban = await container.database.tempBan.findFirst({ + where: { + userId, + }, + orderBy: { + id: 'desc', + }, + }); + + if (ban === null) { + return; + } + + if (modId !== undefined) { + await container.database.tempBan.update({ + where: { + id: ban.id, + }, + data: { + endModId: modId, + active: false, + }, + }); + return; + } + + await container.database.tempBan.update({ + where: { + id: ban.id, + }, + data: { + active: false, + }, + }); +} + +export async function checkTempBan(userId: Snowflake) { + const ban = await container.database.tempBan.findFirst({ + where: { + userId, + }, + orderBy: { + id: 'desc', + }, + }); + + if (ban === null) { + return false; + } + + return ban.active; +} + +export async function getTempBanReason(userId: Snowflake) { + const ban = await container.database.tempBan.findFirst({ + where: { + userId, + }, + orderBy: { + id: 'desc', + }, + }); + + if (ban === null) { + return ''; + } + + return ban.reason; +} diff --git a/src/utils/database/vcMute.ts b/src/utils/database/vcMute.ts new file mode 100644 index 0000000..e57e63b --- /dev/null +++ b/src/utils/database/vcMute.ts @@ -0,0 +1,69 @@ +import { container } from '@sapphire/framework'; +import type { Snowflake } from 'discord.js'; + +export async function addMute(userId: Snowflake, modId: Snowflake, reason: string | null) { + // Add the user to the database + await container.database.vCMute.create({ + data: { + user: { + connect: { + id: userId, + }, + }, + mod: { + connect: { + id: modId, + }, + }, + reason, + }, + }); +} + +export async function removeMute(userId: Snowflake) { + const mute = await container.database.vCMute.findFirst({ + where: { + userId, + }, + select: { + id: true, + }, + orderBy: { + id: 'desc', + }, + }); + + if (mute === null) { + return; + } + + // Query to deactivate the specific sus note + await container.database.vCMute.update({ + where: { + id: mute.id, + }, + data: { + endTime: new Date(), + }, + }); +} + +export async function checkActive(userId: Snowflake) { + const mute = await container.database.vCMute.findFirst({ + where: { + userId, + }, + select: { + endTime: true, + }, + orderBy: { + id: 'desc', + }, + }); + + if (mute === null) { + return false; + } + + return mute.endTime === null; +} diff --git a/src/utils/database/verification.ts b/src/utils/database/verification.ts index 065f555..d1c11e8 100644 --- a/src/utils/database/verification.ts +++ b/src/utils/database/verification.ts @@ -21,7 +21,7 @@ import type { GuildMember } from 'discord.js'; import { container } from '@sapphire/framework'; import { updateUser } from '#utils/database/dbExistingUser'; import { leaveBan } from '#utils/verificationConfig'; -import { fibonacci } from '#utils/mathsSeries'; +import { fibonacci } from '#utils/maths'; export async function joinVerification(channelId: string, user: GuildMember) { // Update the user on the database with the current roles they have diff --git a/src/utils/database/warnings.ts b/src/utils/database/warnings.ts new file mode 100644 index 0000000..ee2b1b0 --- /dev/null +++ b/src/utils/database/warnings.ts @@ -0,0 +1,20 @@ +import { container } from '@sapphire/framework'; +import type { Snowflake } from 'discord.js'; + +export async function addWarn(userId: Snowflake, modId: Snowflake, message: string) { + await container.database.warning.create({ + data: { + user: { + connect: { + id: userId, + }, + }, + mod: { + connect: { + id: modId, + }, + }, + note: message, + }, + }); +} diff --git a/src/utils/devIDs.ts b/src/utils/devIDs.ts index efe6e5d..f8e2fb4 100644 --- a/src/utils/devIDs.ts +++ b/src/utils/devIDs.ts @@ -38,6 +38,13 @@ const devIDs = { restricted2: '999431674997788676', restricted3: '999431674997788675', restricted4: '999431674997788674', + restricted: [ + '999431674997788677', // Restricted 1 + '999431674997788676', // Restricted 2 + '999431674997788675', // Restricted 3 + '999431674997788674', // Restricted 4 + '1075952207091994726', // Restricted Vegan + ], }, staff: { coordinator: '999431675165556822', diff --git a/src/utils/ids.ts b/src/utils/ids.ts index 98a282d..e09e7d2 100644 --- a/src/utils/ids.ts +++ b/src/utils/ids.ts @@ -41,6 +41,13 @@ let IDs = { restricted2: '872482843304001566', restricted3: '856582673258774538', restricted4: '872472182888992858', + restricted: [ + '809769217477050369', // Restricted 1 + '872482843304001566', // Restricted 2 + '856582673258774538', // Restricted 3 + '872472182888992858', // Restricted 4 + '1075951477379567646', // Restricted Vegan + ], }, staff: { coordinator: '993636242019323904', diff --git a/src/utils/mathsSeries.ts b/src/utils/maths.ts similarity index 65% rename from src/utils/mathsSeries.ts rename to src/utils/maths.ts index 2a99459..cbb9859 100644 --- a/src/utils/mathsSeries.ts +++ b/src/utils/maths.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later /* Animal Rights Advocates Discord Bot - Copyright (C) 2022 Anthony Berg + Copyright (C) 2023 Anthony Berg This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,6 +17,25 @@ along with this program. If not, see . */ +/** + * Checks if any parsed value is a number. + * @param number check if variable is a number + * @returns {boolean} true if it is a number + */ +export function isNumber(number: any) { + return !Number.isNaN(+number); +} + +/** + * Creates a (PRNG) random integer between minimum and maximum both inclusive + * @param min minimum integer + * @param max maximum integer + * @returns number a random integer between min and max + */ +export function randint(min: number, max: number) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + // Created because Stove loves Fibonacci sequences // A fibonacci sequence where n = 0 => 1 export function fibonacci(position: number) {