diff --git a/.forgejo/issue_template/bug_report.yaml b/.gitea/issue_template/bug_report.yaml similarity index 98% rename from .forgejo/issue_template/bug_report.yaml rename to .gitea/issue_template/bug_report.yaml index 6deb616..bf17745 100644 --- a/.forgejo/issue_template/bug_report.yaml +++ b/.gitea/issue_template/bug_report.yaml @@ -2,7 +2,7 @@ name: 🐛 Bug Report description: Something isn't working as expected? Let us know! title: '[BUG] - ' labels: - - "🚦 status: awaiting triage" + - "status/awaiting triage" body: - type: checkboxes id: attestations diff --git a/.forgejo/issue_template/config.yml b/.gitea/issue_template/config.yml similarity index 100% rename from .forgejo/issue_template/config.yml rename to .gitea/issue_template/config.yml diff --git a/.forgejo/issue_template/feature_proposal.yml b/.gitea/issue_template/feature_proposal.yml similarity index 97% rename from .forgejo/issue_template/feature_proposal.yml rename to .gitea/issue_template/feature_proposal.yml index d833b94..b3fae97 100644 --- a/.forgejo/issue_template/feature_proposal.yml +++ b/.gitea/issue_template/feature_proposal.yml @@ -2,7 +2,7 @@ name: 💭 Feature Proposal description: Have an idea for how we can improve? Share it here! title: '[FEAT] - ' labels: - - "🚦 status: awaiting triage" + - "status/awaiting triage" body: - type: checkboxes id: attestations diff --git a/.forgejo/issue_template/other.yml b/.gitea/issue_template/other.yml similarity index 97% rename from .forgejo/issue_template/other.yml rename to .gitea/issue_template/other.yml index 680e7a7..2f1335f 100644 --- a/.forgejo/issue_template/other.yml +++ b/.gitea/issue_template/other.yml @@ -2,7 +2,7 @@ name: ❓ Other Issue description: I have something that is neither a bug nor a feature request. title: '[OTHER] - ' labels: - - "🚦 status: awaiting triage" + - "status/awaiting triage" body: - type: checkboxes id: attestations diff --git a/.forgejo/pull_request_template.yml b/.gitea/pull_request_template.yml similarity index 98% rename from .forgejo/pull_request_template.yml rename to .gitea/pull_request_template.yml index 37d43f4..2d2fbf3 100644 --- a/.forgejo/pull_request_template.yml +++ b/.gitea/pull_request_template.yml @@ -1,7 +1,5 @@ name: "Pull Request Template" about: "Template for pulls" -labels: - - "🔍 pull: ready for review" body: - type: textarea id: explain diff --git a/package.json b/package.json index 7d73853..23a6bb3 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,8 @@ "typescript": "5.7.3" }, "dependencies": { + "@fastify/formbody": "8.0.2", "@prisma/client": "6.2.1", - "discord.js": "14.17.3", "fastify": "5.2.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 060d259..732f07c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,12 +8,12 @@ importers: .: dependencies: + '@fastify/formbody': + specifier: 8.0.2 + version: 8.0.2 '@prisma/client': specifier: 6.2.1 version: 6.2.1(prisma@6.2.1) - discord.js: - specifier: 14.17.3 - version: 14.17.3 fastify: specifier: 5.2.1 version: 5.2.1 @@ -47,34 +47,6 @@ packages: resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} - '@discordjs/builders@1.10.0': - resolution: {integrity: sha512-ikVZsZP+3shmVJ5S1oM+7SveUCK3L9fTyfA8aJ7uD9cNQlTqF+3Irbk2Y22KXTb3C3RNUahRkSInClJMkHrINg==} - engines: {node: '>=16.11.0'} - - '@discordjs/collection@1.5.3': - resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==} - engines: {node: '>=16.11.0'} - - '@discordjs/collection@2.1.1': - resolution: {integrity: sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==} - engines: {node: '>=18'} - - '@discordjs/formatters@0.6.0': - resolution: {integrity: sha512-YIruKw4UILt/ivO4uISmrGq2GdMY6EkoTtD0oS0GvkJFRZbTSdPhzYiUILbJ/QslsvC9H9nTgGgnarnIl4jMfw==} - engines: {node: '>=16.11.0'} - - '@discordjs/rest@2.4.2': - resolution: {integrity: sha512-9bOvXYLQd5IBg/kKGuEFq3cstVxAMJ6wMxO2U3wjrgO+lHv8oNCT+BBRpuzVQh7BoXKvk/gpajceGvQUiRoJ8g==} - engines: {node: '>=18'} - - '@discordjs/util@1.1.1': - resolution: {integrity: sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==} - engines: {node: '>=18'} - - '@discordjs/ws@1.2.0': - resolution: {integrity: sha512-QH5CAFe3wHDiedbO+EI3OOiyipwWd+Q6BdoFZUw/Wf2fw5Cv2fgU/9UEtJRmJa9RecI+TAhdGPadMaEIur5yJg==} - engines: {node: '>=16.11.0'} - '@es-joy/jsdoccomment@0.49.0': resolution: {integrity: sha512-xjZTSFgECpb9Ohuk5yMX5RhUEbfeQcuOp8IF60e+wyzWEF0M5xeSgqsfLtvPEX8BIyOX9saZqzuGPmZ8oWc+5Q==} engines: {node: '>=16'} @@ -291,6 +263,9 @@ packages: '@fastify/fast-json-stringify-compiler@5.0.2': resolution: {integrity: sha512-YdR7gqlLg1xZAQa+SX4sMNzQHY5pC54fu9oC5aYSUqBhyn6fkLkrdtKlpVdCNPlwuUuXA1PjFTEmvMF6ZVXVGw==} + '@fastify/formbody@8.0.2': + resolution: {integrity: sha512-84v5J2KrkXzjgBpYnaNRPqwgMsmY7ZDjuj0YVuMR3NXCJRCgKEZy/taSP1wUYGn0onfxJpLyRGDLa+NMaDJtnA==} + '@fastify/forwarded@3.0.0': resolution: {integrity: sha512-kJExsp4JCms7ipzg7SJ3y8DwmePaELHxKYtg+tZow+k0znUTf3cb+npgyqm8+ATZOdmfgfydIebPDWM172wfyA==} @@ -477,18 +452,6 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - '@sapphire/async-queue@1.5.5': - resolution: {integrity: sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==} - engines: {node: '>=v14.0.0', npm: '>=7.0.0'} - - '@sapphire/shapeshift@4.0.0': - resolution: {integrity: sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==} - engines: {node: '>=v16'} - - '@sapphire/snowflake@3.5.3': - resolution: {integrity: sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==} - engines: {node: '>=v14.0.0', npm: '>=7.0.0'} - '@stylistic/eslint-plugin@2.12.1': resolution: {integrity: sha512-fubZKIHSPuo07FgRTn6S4Nl0uXPRPYVNpyZzIDGfp7Fny6JjNus6kReLD7NI380JXi4HtUTSOZ34LBuNPO1XLQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -510,9 +473,6 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} - '@types/ws@8.5.13': - resolution: {integrity: sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==} - '@typescript-eslint/eslint-plugin@8.19.0': resolution: {integrity: sha512-NggSaEZCdSrFddbctrVjkVZvFC6KGfKfNK0CU7mNK/iKHGKbzT4Wmgm08dKpcZECBu9f5FypndoMyRHkdqfT1Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -654,10 +614,6 @@ packages: '@vitest/utils@3.0.2': resolution: {integrity: sha512-Qu01ZYZlgHvDP02JnMBRpX43nRaZtNpIzw3C1clDXmn8eakgX6iQVGzTQ/NjkIr64WD8ioqOjkaYRVvHQI5qiw==} - '@vladfrangu/async_event_emitter@2.4.6': - resolution: {integrity: sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==} - engines: {node: '>=v14.0.0', npm: '>=7.0.0'} - abstract-logging@2.0.1: resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} @@ -894,13 +850,6 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} - discord-api-types@0.37.117: - resolution: {integrity: sha512-d+Z6RKd7v3q22lsil7yASucqMfVVV0s0XSqu3cw7kyHVXiDO/mAnqMzqma26IYnIm2mk3TlupYJDGrdL908ZKA==} - - discord.js@14.17.3: - resolution: {integrity: sha512-8/j8udc3CU7dz3Eqch64UaSHoJtUT6IXK4da5ixjbav4NAXJicloWswD/iwn1ImZEMoAV3LscsdO0zhBh6H+0Q==} - engines: {node: '>=18'} - doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -1120,6 +1069,9 @@ packages: fast-uri@3.0.6: resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + fastify-plugin@5.0.1: + resolution: {integrity: sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==} + fastify@5.2.1: resolution: {integrity: sha512-rslrNBF67eg8/Gyn7P2URV8/6pz8kSAscFL4EThZJ8JBMaXacVdVE4hmUcnPNKERl5o/xTiBSLfdowBRhVF1WA==} @@ -1460,12 +1412,6 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.snakecase@4.1.1: - resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} - - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -1473,9 +1419,6 @@ packages: loupe@3.1.2: resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} - magic-bytes.js@1.10.0: - resolution: {integrity: sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==} - magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -1963,9 +1906,6 @@ packages: peerDependencies: typescript: '>=4.8.4' - ts-mixer@6.0.4: - resolution: {integrity: sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==} - tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} @@ -2016,10 +1956,6 @@ packages: undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} - undici@6.19.8: - resolution: {integrity: sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==} - engines: {node: '>=18.17'} - update-browserslist-db@1.1.2: resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==} hasBin: true @@ -2132,18 +2068,6 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - ws@8.18.0: - resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -2158,53 +2082,6 @@ snapshots: '@babel/helper-validator-identifier@7.25.9': {} - '@discordjs/builders@1.10.0': - dependencies: - '@discordjs/formatters': 0.6.0 - '@discordjs/util': 1.1.1 - '@sapphire/shapeshift': 4.0.0 - discord-api-types: 0.37.117 - fast-deep-equal: 3.1.3 - ts-mixer: 6.0.4 - tslib: 2.8.1 - - '@discordjs/collection@1.5.3': {} - - '@discordjs/collection@2.1.1': {} - - '@discordjs/formatters@0.6.0': - dependencies: - discord-api-types: 0.37.117 - - '@discordjs/rest@2.4.2': - dependencies: - '@discordjs/collection': 2.1.1 - '@discordjs/util': 1.1.1 - '@sapphire/async-queue': 1.5.5 - '@sapphire/snowflake': 3.5.3 - '@vladfrangu/async_event_emitter': 2.4.6 - discord-api-types: 0.37.117 - magic-bytes.js: 1.10.0 - tslib: 2.8.1 - undici: 6.19.8 - - '@discordjs/util@1.1.1': {} - - '@discordjs/ws@1.2.0': - dependencies: - '@discordjs/collection': 2.1.1 - '@discordjs/rest': 2.4.2 - '@discordjs/util': 1.1.1 - '@sapphire/async-queue': 1.5.5 - '@types/ws': 8.5.13 - '@vladfrangu/async_event_emitter': 2.4.6 - discord-api-types: 0.37.117 - tslib: 2.8.1 - ws: 8.18.0 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - '@es-joy/jsdoccomment@0.49.0': dependencies: comment-parser: 1.4.1 @@ -2352,6 +2229,11 @@ snapshots: dependencies: fast-json-stringify: 6.0.1 + '@fastify/formbody@8.0.2': + dependencies: + fast-querystring: 1.1.2 + fastify-plugin: 5.0.1 + '@fastify/forwarded@3.0.0': {} '@fastify/merge-json-schemas@0.2.1': @@ -2509,15 +2391,6 @@ snapshots: '@rtsao/scc@1.1.0': {} - '@sapphire/async-queue@1.5.5': {} - - '@sapphire/shapeshift@4.0.0': - dependencies: - fast-deep-equal: 3.1.3 - lodash: 4.17.21 - - '@sapphire/snowflake@3.5.3': {} - '@stylistic/eslint-plugin@2.12.1(eslint@9.18.0)(typescript@5.7.3)': dependencies: '@typescript-eslint/utils': 8.21.0(eslint@9.18.0)(typescript@5.7.3) @@ -2542,10 +2415,6 @@ snapshots: '@types/normalize-package-data@2.4.4': {} - '@types/ws@8.5.13': - dependencies: - '@types/node': 22.10.7 - '@typescript-eslint/eslint-plugin@8.19.0(@typescript-eslint/parser@8.19.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0)(typescript@5.7.3)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -2746,8 +2615,6 @@ snapshots: loupe: 3.1.2 tinyrainbow: 2.0.0 - '@vladfrangu/async_event_emitter@2.4.6': {} - abstract-logging@2.0.1: {} acorn-jsx@5.3.2(acorn@7.4.1): @@ -3005,26 +2872,6 @@ snapshots: dependencies: path-type: 4.0.0 - discord-api-types@0.37.117: {} - - discord.js@14.17.3: - dependencies: - '@discordjs/builders': 1.10.0 - '@discordjs/collection': 1.5.3 - '@discordjs/formatters': 0.6.0 - '@discordjs/rest': 2.4.2 - '@discordjs/util': 1.1.1 - '@discordjs/ws': 1.2.0 - '@sapphire/snowflake': 3.5.3 - discord-api-types: 0.37.117 - fast-deep-equal: 3.1.3 - lodash.snakecase: 4.1.1 - tslib: 2.8.1 - undici: 6.19.8 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -3416,6 +3263,8 @@ snapshots: fast-uri@3.0.6: {} + fastify-plugin@5.0.1: {} + fastify@5.2.1: dependencies: '@fastify/ajv-compiler': 4.0.2 @@ -3780,18 +3629,12 @@ snapshots: lodash.merge@4.6.2: {} - lodash.snakecase@4.1.1: {} - - lodash@4.17.21: {} - loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 loupe@3.1.2: {} - magic-bytes.js@1.10.0: {} - magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -4319,8 +4162,6 @@ snapshots: dependencies: typescript: 5.7.3 - ts-mixer@6.0.4: {} - tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 @@ -4384,8 +4225,6 @@ snapshots: undici-types@6.20.0: {} - undici@6.19.8: {} - update-browserslist-db@1.1.2(browserslist@4.24.4): dependencies: browserslist: 4.24.4 @@ -4520,6 +4359,4 @@ snapshots: word-wrap@1.2.5: {} - ws@8.18.0: {} - yocto-queue@0.1.0: {} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1009fa9..826f4ed 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,14 +8,22 @@ datasource db { } model Sanctions { - id String @id @default(auto()) @map("_id") @db.ObjectId - date DateTime @default(now()) - number Int @unique + id String @id @default(auto()) @map("_id") @db.ObjectId + date DateTime @default(now()) + number Int @unique uuid String platform String action String + moderator String reason String - revoked Boolean @default(false) + revoked Boolean @default(false) + revokedBy String? revokeReason String? revokeDate DateTime? } + +model Tokens { + id String @id @default(auto()) @map("_id") @db.ObjectId + username String @unique + token String @unique +} diff --git a/src/commands/log.ts b/src/commands/log.ts deleted file mode 100644 index fdcfea1..0000000 --- a/src/commands/log.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { - ApplicationIntegrationType, - InteractionContextType, - SlashCommandBuilder, -} from "discord.js"; -import { errorHandler } from "../utils/errorHandler.js"; -import type { Command } from "../interfaces/command.js"; - -/** - * @copyright nhcarrigan - * @license Naomi's Public License - * @author Naomi Carrigan - */ -export const log: Command = { - data: new SlashCommandBuilder(). - setName("log"). - setDescription("Log a moderation action."). - setContexts([ - InteractionContextType.BotDM, - InteractionContextType.Guild, - InteractionContextType.PrivateChannel, - ]). - setIntegrationTypes([ ApplicationIntegrationType.UserInstall ]). - addStringOption((option) => { - return option. - setName("action"). - setDescription("The action to log."). - setRequired(true). - addChoices([ - { name: "Ban", value: "ban" }, - { name: "Kick", value: "kick" }, - { name: "Mute", value: "mute" }, - { name: "Warn", value: "warn" }, - { name: "Block", value: "block" }, - { name: "Suspend", value: "suspend" }, - ]); - }). - addStringOption((option) => { - return option. - setName("platform"). - setDescription("The platform to log the action on."). - setRequired(true). - addChoices([ - { name: "Discord", value: "discord" }, - { name: "Twitch", value: "twitch" }, - { name: "Codeberg", value: "codeberg" }, - { name: "Bluesky", value: "bluesky" }, - { name: "Forum", value: "forum" }, - { name: "Reddit", value: "reddit" }, - { name: "IRC", value: "irc" }, - { name: "Slack", value: "slack" }, - { name: "Mastodon", value: "mastodon" }, - { name: "Twitter", value: "twitter" }, - { name: "GitHub", value: "github" }, - { name: "LinkedIn", value: "linkedin" }, - { name: "Peerlist", value: "peerlist" }, - { name: "Signal", value: "signal" }, - { name: "WhatsApp", value: "whatsapp" }, - { name: "Snapchat", value: "snapchat" }, - ]); - }). - addStringOption((option) => { - return option. - setName("username"). - setDescription("Username or UUID of the targeted user."). - setRequired(true); - }). - addStringOption((option) => { - return option. - setName("reason"). - setDescription("Reason for the action."). - setRequired(true); - }), - run: async(app, interaction) => { - try { - await interaction.deferReply({ ephemeral: true }); - if (interaction.user.id !== "465650873650118659") { - await interaction.editReply({ - content: - // eslint-disable-next-line stylistic/max-len -- This is a single string. - "Only Naomi can use this app. Sorry! Feel free to join our Discord? https://chat.nhcarrigan.com", - }); - return; - } - - const action = interaction.options.getString("action", true); - const platform = interaction.options.getString("platform", true); - const uuid = interaction.options.getString("username", true); - const reason = interaction.options.getString("reason", true); - - const number = await app.database.sanctions.count() + 1; - await app.database.sanctions.create({ - data: { - action, - number, - platform, - reason, - uuid, - }, - }); - await interaction.editReply({ - content: `Logged ${action} #${String( - number, - )} on ${platform} for user ${uuid}.`, - }); - } catch (error) { - await errorHandler("log command", error); - await interaction.editReply({ - content: - "An error occurred while processing your command. Please try again.", - }); - } - }, -}; diff --git a/src/commands/revoke.ts b/src/commands/revoke.ts deleted file mode 100644 index a3d151d..0000000 --- a/src/commands/revoke.ts +++ /dev/null @@ -1,84 +0,0 @@ -/** - * @copyright nhcarrigan - * @license Naomi's Public License - * @author Naomi Carrigan - */ -import { - ApplicationIntegrationType, - InteractionContextType, - SlashCommandBuilder, -} from "discord.js"; -import { errorHandler } from "../utils/errorHandler.js"; -import type { Command } from "../interfaces/command.js"; - -export const revoke: Command = { - data: new SlashCommandBuilder(). - setName("revoke"). - setDescription("Revoke a moderation action."). - setContexts([ - InteractionContextType.BotDM, - InteractionContextType.Guild, - InteractionContextType.PrivateChannel, - ]). - setIntegrationTypes([ ApplicationIntegrationType.UserInstall ]). - addIntegerOption((option) => { - return option. - setName("case"). - setDescription("The case number to revoke."). - setRequired(true); - }). - addStringOption((option) => { - return option. - setName("reason"). - setDescription("The reason for revoking the case."). - setRequired(true); - }), - run: async(app, interaction) => { - try { - await interaction.deferReply({ ephemeral: true }); - if (interaction.user.id !== "465650873650118659") { - await interaction.editReply({ - content: - // eslint-disable-next-line stylistic/max-len -- This is a single string. - "Only Naomi can use this app. Sorry! Feel free to join our Discord? https://chat.nhcarrigan.com", - }); - return; - } - - const number = interaction.options.getInteger("case", true); - const reason = interaction.options.getString("reason", true); - - const exists = await app.database.sanctions.findUnique({ - where: { number }, - }); - - if (!exists) { - await interaction.editReply({ - content: `Case ${String(number)} does not exist.`, - }); - return; - } - - await app.database.sanctions.update({ - data: { - revokeDate: new Date(), - revokeReason: reason, - revoked: true, - }, - where: { - number, - }, - }); - - await interaction.editReply({ - content: `Revoked action ${String(number)} for ${reason}.`, - }); - } catch (error) { - await errorHandler("log command", error); - await interaction.editReply({ - content: - "An error occurred while processing your command. Please try again.", - }); - } - }, -}; diff --git a/src/config/form.ts b/src/config/form.ts new file mode 100644 index 0000000..ce7ea5d --- /dev/null +++ b/src/config/form.ts @@ -0,0 +1,74 @@ +/** + * @copyright nhcarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ + +const formHtml = ` + + + + Moderation Logs + + + + + + +
+

Log A Sanction

+
+

This page allows our staff to log an official sanction from one of our platforms.

+
+
+ + + + + + +
+
+ + +`; + +const revokeHtml = ` + + + + Moderation Logs + + + + + + +
+

Revoke A Sanction

+
+

This page allows our staff to indicate a sanction has been successfully appealed.

+
+
+ + + + +
+
+ + +`; + +export { formHtml, revokeHtml }; diff --git a/src/config/icons.ts b/src/config/icons.ts index 30f1706..58a8194 100644 --- a/src/config/icons.ts +++ b/src/config/icons.ts @@ -8,27 +8,14 @@ * Turn a platform name into a font-awesome icon. */ const platformIcons: Record = { - bluesky: "", - codeberg: "", - discord: "", - forum: "", - github: "", - irc: "", - linkedin: "", - mastodon: "", - peerlist: "", - reddit: "", - signal: "", - slack: "", - snapchat: "", - twitch: "", - twitter: "", - whatsapp: "", + fediverse: "", + forum: "", + gitea: "", + irc: "", }; const actionIcons: Record = { ban: "", - block: "", kick: "", mute: "", revoked: "", diff --git a/src/config/landing.ts b/src/config/landing.ts index f6ddb4a..86d7208 100644 --- a/src/config/landing.ts +++ b/src/config/landing.ts @@ -25,17 +25,17 @@ export const landingHtml = ` margin-bottom: 10px; border-radius: 50px; } -.ban, .block { +.ban { background-color: #ffcccc; color: #660000; } -.kick { +.kick, .suspend { background-color: #ffddcc; color: #662200; } -.mute, .suspend { +.mute { background-color: #fff1cc; color: #664b00; } @@ -69,6 +69,7 @@ iframe {

Moderation Logs

This page lists all of our community moderation sanctions.

+

This log was last updated at {{ timestamp }}.

Links

diff --git a/src/index.ts b/src/index.ts index 3f45cd0..77eaa99 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,45 +4,14 @@ * @author Naomi Carrigan */ import { PrismaClient } from "@prisma/client"; -import { Client, Events, GatewayIntentBits } from "discord.js"; -import { log } from "./commands/log.js"; -import { revoke } from "./commands/revoke.js"; -import { updateCache } from "./modules/updateCache.js"; import { serve } from "./server/serve.js"; -import { sendDebugLog } from "./utils/sendDebugLog.js"; +import type { App } from "./interfaces/app.js"; const database = new PrismaClient(); -const app = { +const app: App = { cacheUpdated: new Date(), database: database, - discord: new Client({ - intents: [ GatewayIntentBits.Guilds ], - }), - sanctions: await database.sanctions.findMany(), + sanctions: await database.sanctions.findMany(), }; -app.discord.once(Events.ClientReady, () => { - void sendDebugLog({ content: "Bot is online!" }); - setInterval(() => { - void updateCache(app); - }, 1000 * 60 * 60 * 24); -}); - -app.discord.on(Events.InteractionCreate, (interaction) => { - if (interaction.isChatInputCommand()) { - const target = [ log, revoke ].find((command) => { - return command.data.name === interaction.commandName; - }); - if (!target) { - void interaction.reply({ - content: `Command ${interaction.commandName} not found.`, - ephemeral: true, - }); - return; - } - void target.run(app, interaction); - } -}); - -await app.discord.login(process.env.DISCORD_TOKEN); -serve(app); +await serve(app); diff --git a/src/interfaces/app.ts b/src/interfaces/app.ts index 50831a4..9527466 100644 --- a/src/interfaces/app.ts +++ b/src/interfaces/app.ts @@ -4,10 +4,8 @@ * @author Naomi Carrigan */ import type { PrismaClient, Sanctions } from "@prisma/client"; -import type { Client } from "discord.js"; export interface App { - discord: Client; database: PrismaClient; sanctions: Array; cacheUpdated: Date; diff --git a/src/interfaces/command.ts b/src/interfaces/command.ts deleted file mode 100644 index 9497726..0000000 --- a/src/interfaces/command.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @copyright nhcarrigan - * @license Naomi's Public License - * @author Naomi Carrigan - */ -import type { App } from "./app.js"; -import type { - ChatInputCommandInteraction, - SlashCommandOptionsOnlyBuilder, -} from "discord.js"; - -export interface Command { - data: SlashCommandOptionsOnlyBuilder; - run: (app: App, interaction: ChatInputCommandInteraction)=> Promise; -} diff --git a/src/modules/generateHtml.ts b/src/modules/generateHtml.ts index 1dc8306..17c6bd7 100644 --- a/src/modules/generateHtml.ts +++ b/src/modules/generateHtml.ts @@ -16,27 +16,48 @@ export const generateHtml = (app: App): string => { const sanctions = app.sanctions.toSorted((a, b) => { return b.number - a.number; }); + const sanctionHtml = sanctions.map((sanction) => { return sanction.revoked + && sanction.revokedBy !== null + && sanction.revokeDate !== null + && sanction.revokeReason !== null ? `
- ${actionIcons.revoked ?? ""} #${sanction.number.toString()}: ${sanction.action} - REVOKED ${platformIcons[sanction.platform.toLowerCase()] ?? ""} + ${ + actionIcons.revoked ?? "" + } #${sanction.number.toString()}: ${sanction.action} - REVOKED ${ + platformIcons[sanction.platform.toLowerCase()] + ?? "" + }

${sanction.uuid}

${sanction.reason}

+

Performed by: ${sanction.moderator} on ${sanction.date.toLocaleString("en-GB")}

Revoked:

-

${sanction.revokeReason ?? "Undocumented."}

+

${sanction.revokeReason}

+

Revoked by: ${ + sanction.revokedBy + } on ${sanction.revokeDate.toLocaleString("en-GB")}

` : `
- ${actionIcons[sanction.action.toLowerCase()] ?? ""} #${sanction.number.toString()}: ${sanction.action} ${platformIcons[sanction.platform.toLowerCase()] ?? ""} + ${ + actionIcons[sanction.action.toLowerCase()] ?? "" + } #${sanction.number.toString()}: ${sanction.action} ${ + platformIcons[sanction.platform.toLowerCase()] + ?? "" + }

${sanction.uuid}

${sanction.reason}

+

Performed by: ${sanction.moderator} on ${sanction.date.toLocaleString("en-GB")}

`; }); - const html = landingHtml.replace("{{ logs }}", sanctionHtml.join("\n")); + const html = landingHtml. + replace("{{ logs }}", sanctionHtml.join("\n")). + replace("{{ timestamp }}", app.cacheUpdated.toLocaleString("en-GB")); return html; }; diff --git a/src/modules/updateCache.ts b/src/modules/updateCache.ts index 54d80e7..43d2b89 100644 --- a/src/modules/updateCache.ts +++ b/src/modules/updateCache.ts @@ -3,7 +3,6 @@ * @license Naomi's Public License * @author Naomi Carrigan */ -import { errorHandler } from "../utils/errorHandler.js"; import type { App } from "../interfaces/app.js"; /** @@ -11,11 +10,7 @@ import type { App } from "../interfaces/app.js"; * @param app - The application instance. */ export const updateCache = async(app: App): Promise => { - try { - app.cacheUpdated = new Date(); - // eslint-disable-next-line require-atomic-updates -- We're allowing this so we can update the cache on an interval. - app.sanctions = await app.database.sanctions.findMany(); - } catch (error) { - await errorHandler("update cache module", error); - } + app.cacheUpdated = new Date(); + // eslint-disable-next-line require-atomic-updates -- We're allowing this so we can update the cache on an interval. + app.sanctions = await app.database.sanctions.findMany(); }; diff --git a/src/server/serve.ts b/src/server/serve.ts index d4708d6..e3a9848 100644 --- a/src/server/serve.ts +++ b/src/server/serve.ts @@ -3,28 +3,156 @@ * @license Naomi's Public License * @author Naomi Carrigan */ +import formParser from "@fastify/formbody"; import fastify from "fastify"; +import { formHtml, revokeHtml } from "../config/form.js"; import { generateHtml } from "../modules/generateHtml.js"; -import { sendDebugLog } from "../utils/sendDebugLog.js"; +import { updateCache } from "../modules/updateCache.js"; import type { App } from "../interfaces/app.js"; /** * Instantiates the fastify server. * @param app - The application instance. */ -export const serve = (app: App): void => { +// eslint-disable-next-line max-lines-per-function -- May refactor? +export const serve = async(app: App): Promise => { const server = fastify({ logger: false, }); + server.register(formParser); + server.get("/", (_request, response) => { response.header("Content-Type", "text/html"); response.send(generateHtml(app)); }); - server.listen({ port: 12_443 }, () => { - void sendDebugLog({ - content: "Server listening on port 12443.", - }); + server.get("/log", (_request, response) => { + response.header("Content-Type", "text/html"); + response.send(formHtml); }); + + server.get("/revoke", (_request, response) => { + response.header("Content-Type", "text/html"); + response.send(revokeHtml); + }); + + server.post<{ + // eslint-disable-next-line @typescript-eslint/naming-convention -- Required convention for Fastify. + Body: { + uuid: string; + platform: string; + action: string; + reason: string; + token: string; + }; + }>("/log", async(request, response) => { + try { + const { body } = request; + + const token = await app.database.tokens.findUnique({ + where: { + token: body.token, + }, + }); + + if (!token) { + response.status(401); + response.send("Invalid token."); + return; + } + + const number = await app.database.sanctions.count(); + await app.database.sanctions.create({ + data: { + action: body.action, + moderator: token.username, + number: number + 1, + platform: body.platform, + reason: body.reason, + uuid: body.uuid, + }, + }); + + response.status(200); + response.send( + `Logged #${String(number + 1)} ${body.action} against ${body.uuid} on ${ + body.platform + }.`, + ); + await updateCache(app).catch(() => { + return null; + }); + } catch (error) { + response.status(500); + response.header("Content-Type", "application/json"); + response.send(JSON.stringify(error)); + } + }); + + server.post<{ + // eslint-disable-next-line @typescript-eslint/naming-convention -- Required convention for Fastify. + Body: { + case: string; + reason: string; + token: string; + }; + }>("/revoke", async(request, response) => { + try { + const { body } = request; + + const token = await app.database.tokens.findUnique({ + where: { + token: body.token, + }, + }); + + if (!token) { + response.status(401); + response.send("Invalid token."); + return; + } + + const number = Number.parseInt(body.case, 10); + + const sanction = await app.database.sanctions.findUnique({ + where: { + number, + }, + }); + + if (!sanction) { + response.status(404); + response.send(`Sanction #${String(number)} not found.`); + return; + } + + await app.database.sanctions.update({ + data: { + revokeDate: new Date(), + revokeReason: body.reason, + revoked: true, + revokedBy: token.username, + }, + where: { + number, + }, + }); + response.status(200); + response.send( + `Revoked #${String(number)} ${sanction.action} against ${ + sanction.uuid + } on ${sanction.platform}.`, + ); + await updateCache(app).catch(() => { + return null; + }); + } catch (error) { + response.status(500); + response.header("Content-Type", "application/json"); + response.send(JSON.stringify(error, null, 2)); + } + }); + + await server.listen({ port: 12_443 }); }; diff --git a/src/utils/errorHandler.ts b/src/utils/errorHandler.ts deleted file mode 100644 index 5e39deb..0000000 --- a/src/utils/errorHandler.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * @copyright nhcarrigan - * @license Naomi's Public License - * @author Naomi Carrigan - */ -import { EmbedBuilder, SnowflakeUtil } from "discord.js"; -import { sendDebugLog } from "./sendDebugLog.js"; - -/** - * Parses an error event (re: from a catch block). If it - * is a proper Error object, extrapolates data. Sends information - * to the debug webhook, and assigns the error a Snowflake ID. - * @param context -- A brief description of the code module that threw the error. - * @param error -- The error payload, typed as unknown to comply with TypeScript's typedef. - * @returns The Snowflake ID assigned to the error. - */ -export const errorHandler = async( - context: string, - error: unknown, -): Promise => { - const id = SnowflakeUtil.generate(); - const embed = new EmbedBuilder(); - embed.setFooter({ text: `Error ID: ${id.toString()}` }); - embed.setTitle(`Error: ${context}`); - if (error instanceof Error) { - embed.setDescription(error.message); - embed.addFields([ - { name: "Stack", value: `\`\`\`\n${String(error.stack).slice(0, 1000)}` }, - ]); - } else { - embed.setDescription(String(error).slice(0, 2000)); - } - await sendDebugLog({ embeds: [ embed ] }); - return id.toString(); -}; diff --git a/src/utils/sendDebugLog.ts b/src/utils/sendDebugLog.ts deleted file mode 100644 index 3b74916..0000000 --- a/src/utils/sendDebugLog.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @copyright nhcarrigan - * @license Naomi's Public License - * @author Naomi Carrigan - */ - -import { WebhookClient, type MessageCreateOptions } from "discord.js"; - -const hook = new WebhookClient({ - url: process.env.DISCORD_DEBUG_WEBHOOK ?? "", -}); - -/** - * Quick wrapper to send a debug message to the webhook. - * Modularised for future expansion if needed. - * @param message -- The message payload, compatible with Discord's API. - */ -export const sendDebugLog = async( - message: MessageCreateOptions, -): Promise => { - await hook.send({ - ...message, - avatarURL: "https://cdn.nhcarrigan.com/art/logs.png", - username: "Moderation Logs", - }); -};