From a0b668cb90afbc72c55aa9ecb92ed9a785ce50e5 Mon Sep 17 00:00:00 2001 From: Michel Fedde Date: Fri, 28 Mar 2025 23:19:54 +0100 Subject: [PATCH] Adds initial progress --- .gitignore | 9 + build/build-cli.mjs | 4 + build/context.mjs | 16 + build/watch.mjs | 3 + package-lock.json | 1432 +++++++++++++++++ package.json | 26 + source/Container/Container.ts | 26 + source/Container/Services.ts | 60 + source/Database/DatabaseConnection.ts | 44 + source/Database/DatabaseDefinition.ts | 13 + source/Database/DatabaseUpdater.ts | 81 + source/Database/definitions.ts | 10 + source/Database/tables/Groups.ts | 39 + source/Database/tables/Playdate.ts | 35 + .../Discord/CommandPartials/GroupSelection.ts | 46 + source/Discord/Commands/Command.ts | 17 + source/Discord/Commands/Commands.ts | 41 + source/Discord/Commands/Groups.ts | 100 ++ source/Discord/Commands/HelloWorldCommand.ts | 25 + source/Discord/Commands/Playdates.ts | 206 +++ source/Discord/DiscordClient.ts | 120 ++ source/Discord/UserError.ts | 3 + source/Environment.ts | 34 + source/Models/GroupModel.ts | 8 + source/Models/Model.ts | 3 + source/Models/PlaydateModel.ts | 9 + source/Repositories/GroupRepository.ts | 82 + source/Repositories/PlaydateRepository.ts | 53 + source/Repositories/Repository.ts | 52 + source/deploy.ts | 46 + source/main.ts | 12 + source/types/DiscordTypes.ts | 9 + source/types/Nullable.ts | 1 + tsconfig.json | 15 + 34 files changed, 2680 insertions(+) create mode 100644 .gitignore create mode 100644 build/build-cli.mjs create mode 100644 build/context.mjs create mode 100644 build/watch.mjs create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 source/Container/Container.ts create mode 100644 source/Container/Services.ts create mode 100644 source/Database/DatabaseConnection.ts create mode 100644 source/Database/DatabaseDefinition.ts create mode 100644 source/Database/DatabaseUpdater.ts create mode 100644 source/Database/definitions.ts create mode 100644 source/Database/tables/Groups.ts create mode 100644 source/Database/tables/Playdate.ts create mode 100644 source/Discord/CommandPartials/GroupSelection.ts create mode 100644 source/Discord/Commands/Command.ts create mode 100644 source/Discord/Commands/Commands.ts create mode 100644 source/Discord/Commands/Groups.ts create mode 100644 source/Discord/Commands/HelloWorldCommand.ts create mode 100644 source/Discord/Commands/Playdates.ts create mode 100644 source/Discord/DiscordClient.ts create mode 100644 source/Discord/UserError.ts create mode 100644 source/Environment.ts create mode 100644 source/Models/GroupModel.ts create mode 100644 source/Models/Model.ts create mode 100644 source/Models/PlaydateModel.ts create mode 100644 source/Repositories/GroupRepository.ts create mode 100644 source/Repositories/PlaydateRepository.ts create mode 100644 source/Repositories/Repository.ts create mode 100644 source/deploy.ts create mode 100644 source/main.ts create mode 100644 source/types/DiscordTypes.ts create mode 100644 source/types/Nullable.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e618249 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.idea/ +.vscode/ + +/node_modules/ +/dist/ + +/environment/ +/data/ +/logs/ diff --git a/build/build-cli.mjs b/build/build-cli.mjs new file mode 100644 index 0000000..089060b --- /dev/null +++ b/build/build-cli.mjs @@ -0,0 +1,4 @@ +import context from './context.mjs'; + +await context.rebuild(); +await context.dispose(); \ No newline at end of file diff --git a/build/context.mjs b/build/context.mjs new file mode 100644 index 0000000..e20481b --- /dev/null +++ b/build/context.mjs @@ -0,0 +1,16 @@ +import * as esbuild from "esbuild"; +import path from "path"; + +const context = await esbuild.context({ + entryPoints: [ + path.join('source', 'main.ts'), + path.join('source', 'deploy.ts') + ], + bundle: true, + outdir: './dist/', + platform: 'node', + target: 'node10.4', + sourcemap: 'linked', +}) + +export default context \ No newline at end of file diff --git a/build/watch.mjs b/build/watch.mjs new file mode 100644 index 0000000..a2b52a6 --- /dev/null +++ b/build/watch.mjs @@ -0,0 +1,3 @@ +import context from './context.mjs' + +await context.watch(); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6eb6cbd --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1432 @@ +{ + "name": "dsa-scheduler", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dsa-scheduler", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@types/better-sqlite3": "^7.6.12", + "@types/log4js": "^0.0.33", + "@types/node": "^22.13.9", + "better-sqlite3": "^11.8.1", + "discord.js": "^14.18.0", + "dotenv": "^16.4.7", + "esbuild": "^0.25.0", + "log4js": "^6.9.1" + } + }, + "node_modules/@discordjs/builders": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.10.1.tgz", + "integrity": "sha512-OWo1fY4ztL1/M/DUyRPShB4d/EzVfuUvPTRRHRIt/YxBrUYSz0a+JicD5F5zHFoNs2oTuWavxCOVFV1UljHTng==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/formatters": "^0.6.0", + "@discordjs/util": "^1.1.1", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "^0.37.119", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/collection": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/formatters": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.0.tgz", + "integrity": "sha512-YIruKw4UILt/ivO4uISmrGq2GdMY6EkoTtD0oS0GvkJFRZbTSdPhzYiUILbJ/QslsvC9H9nTgGgnarnIl4jMfw==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.37.114" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.4.3.tgz", + "integrity": "sha512-+SO4RKvWsM+y8uFHgYQrcTl/3+cY02uQOH7/7bKbVZsTfrfpoE62o5p+mmV+s7FVhTX82/kQUGGbu4YlV60RtA==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.1.1", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.37.119", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/util": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz", + "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.1.tgz", + "integrity": "sha512-PBvenhZG56a6tMWF/f4P6f4GxZKJTBG95n7aiGSPTnodmz4N5g60t79rSIAq7ywMbv8A4jFtexMruH+oe51aQQ==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.4.3", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@types/ws": "^8.5.10", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "^0.37.119", + "tslib": "^2.6.2", + "ws": "^8.17.0" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", + "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", + "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", + "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", + "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", + "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", + "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", + "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", + "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", + "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", + "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", + "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", + "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", + "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", + "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", + "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", + "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", + "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", + "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", + "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", + "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sapphire/async-queue": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/shapeshift": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v16" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", + "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.12", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.12.tgz", + "integrity": "sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz", + "integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "license": "MIT" + }, + "node_modules/@types/log4js": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/log4js/-/log4js-0.0.33.tgz", + "integrity": "sha512-AiV2aDM8FZoSh/cRzWA3y9kffXjzTMS1x002FfwzYzbg8cYJpTgTGyjWuJl0b0bZxluOpgqcA248xmtXrJ1TBg==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.13.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz", + "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vladfrangu/async_event_emitter": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz", + "integrity": "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.8.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.8.1.tgz", + "integrity": "sha512-9BxNaBkblMjhJW8sMRZxnxVTRgbRmssZW0Oxc1MPBTfiR+WW21e2Mk4qu8CzrcZb1LwPCnFsfDEzq+SNcBU8eg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/discord-api-types": { + "version": "0.37.119", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.119.tgz", + "integrity": "sha512-WasbGFXEB+VQWXlo6IpW3oUv73Yuau1Ig4AZF/m13tXcTKnMpc/mHjpztIlz4+BM9FG9BHQkEXiPto3bKduQUg==", + "license": "MIT" + }, + "node_modules/discord.js": { + "version": "14.18.0", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.18.0.tgz", + "integrity": "sha512-SvU5kVUvwunQhN2/+0t55QW/1EHfB1lp0TtLZUSXVHDmyHTrdOj5LRKdR0zLcybaA15F+NtdWuWmGOX9lE+CAw==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/builders": "^1.10.1", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.6.0", + "@discordjs/rest": "^2.4.3", + "@discordjs/util": "^1.1.1", + "@discordjs/ws": "^1.2.1", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "^0.37.119", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "tslib": "^2.6.3", + "undici": "6.21.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/esbuild": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.0", + "@esbuild/android-arm": "0.25.0", + "@esbuild/android-arm64": "0.25.0", + "@esbuild/android-x64": "0.25.0", + "@esbuild/darwin-arm64": "0.25.0", + "@esbuild/darwin-x64": "0.25.0", + "@esbuild/freebsd-arm64": "0.25.0", + "@esbuild/freebsd-x64": "0.25.0", + "@esbuild/linux-arm": "0.25.0", + "@esbuild/linux-arm64": "0.25.0", + "@esbuild/linux-ia32": "0.25.0", + "@esbuild/linux-loong64": "0.25.0", + "@esbuild/linux-mips64el": "0.25.0", + "@esbuild/linux-ppc64": "0.25.0", + "@esbuild/linux-riscv64": "0.25.0", + "@esbuild/linux-s390x": "0.25.0", + "@esbuild/linux-x64": "0.25.0", + "@esbuild/netbsd-arm64": "0.25.0", + "@esbuild/netbsd-x64": "0.25.0", + "@esbuild/openbsd-arm64": "0.25.0", + "@esbuild/openbsd-x64": "0.25.0", + "@esbuild/sunos-x64": "0.25.0", + "@esbuild/win32-arm64": "0.25.0", + "@esbuild/win32-ia32": "0.25.0", + "@esbuild/win32-x64": "0.25.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "license": "ISC" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "license": "MIT" + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "license": "Apache-2.0", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/magic-bytes.js": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz", + "integrity": "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==", + "license": "MIT" + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.74.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", + "integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "license": "MIT", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-mixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/undici": { + "version": "6.21.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz", + "integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "license": "MIT", + "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 + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..42c6a26 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "dsa-scheduler", + "version": "1.0.0", + "description": "", + "license": "ISC", + "author": "", + "type": "commonjs", + "main": "main.ts", + "scripts": { + "build": "node ./build/build-cli.mjs", + "watch": "node ./build/watch.mjs", + "deploy": "node ./dist/deploy.js", + "start": "node ./dist/main.js", + "edge-start": "npm run deploy && npm run start" + }, + "dependencies": { + "@types/better-sqlite3": "^7.6.12", + "@types/log4js": "^0.0.33", + "@types/node": "^22.13.9", + "better-sqlite3": "^11.8.1", + "discord.js": "^14.18.0", + "dotenv": "^16.4.7", + "esbuild": "^0.25.0", + "log4js": "^6.9.1" + } +} diff --git a/source/Container/Container.ts b/source/Container/Container.ts new file mode 100644 index 0000000..536e7cd --- /dev/null +++ b/source/Container/Container.ts @@ -0,0 +1,26 @@ +export class Container { + static instance: Container; + + private instances: Map = new Map(); + + public set(instance: T, name: string|null = null): void + { + this.instances.set(name ?? instance.constructor.name, instance); + } + + public get(name: string): T + { + return this.instances.get(name); + } + + static getInstance(): Container { + if (!Container.instance) { + Container.instance = new Container(); + } + + return Container.instance; + } + public static get(name: string): T { + return Container.instance.get(name); + } +} \ No newline at end of file diff --git a/source/Container/Services.ts b/source/Container/Services.ts new file mode 100644 index 0000000..1865775 --- /dev/null +++ b/source/Container/Services.ts @@ -0,0 +1,60 @@ +import {Environment} from "../Environment"; +import {Container} from "./Container"; +import {DatabaseConnection} from "../Database/DatabaseConnection"; +import {getLogger, configure, Logger} from "log4js"; +import path from "node:path"; +import {GroupRepository} from "../Repositories/GroupRepository"; +import {PlaydateRepository} from "../Repositories/PlaydateRepository"; +import {GuildEmojiRoleManager} from "discord.js"; + +export enum ServiceHint { + App, + Deploy +} + +export class Services { + public static setup(container: Container, hint: ServiceHint) { + const env = new Environment(); + env.setup(); + container.set(env); + + const database = new DatabaseConnection(env.database); + container.set(database); + + // @ts-ignore + configure({ + appenders: { + out: { type: "stdout" }, + appLogFile: { type: "file", filename: path.resolve("logs/run.log")}, + deployLogFile: { type: "file", filename: path.resolve("logs/deploy.log")}, + }, + categories: { + default: { appenders: ['out'], level: 'debug' }, + app: { appenders: ["out", "appLogFile"], level: "debug" }, + deploy: { appenders: ["out", "deployLogFile"], level: "debug" }, + } + }) + + let loggername = ''; + switch (hint) { + case ServiceHint.App: + loggername = "app"; + break; + case ServiceHint.Deploy: + loggername = "deploy"; + break; + + } + const logger = getLogger(loggername); + + container.set(logger, 'logger'); + + this.setupRepositories(container); + } + + private static setupRepositories(container: Container) { + const db = container.get(DatabaseConnection.name); + container.set(new GroupRepository(db)); + container.set(new PlaydateRepository(db, container.get(GroupRepository.name))) + } +} \ No newline at end of file diff --git a/source/Database/DatabaseConnection.ts b/source/Database/DatabaseConnection.ts new file mode 100644 index 0000000..1da0a43 --- /dev/null +++ b/source/Database/DatabaseConnection.ts @@ -0,0 +1,44 @@ +import {DatabaseEnvironment, Environment} from "../Environment"; +import Sqlite3 from "better-sqlite3"; +import Database from "better-sqlite3"; +import {Container} from "../Container/Container"; +import {Logger} from "log4js"; + +export class DatabaseConnection { + private static connection: DatabaseConnection; + + private database: Sqlite3.Database; + + constructor(private readonly env: DatabaseEnvironment) { + this.database = new Database(env.path, { + nativeBinding: "node_modules/better-sqlite3/build/Release/better_sqlite3.node", + }) + this.database.pragma('journal_mode = WAL'); + } + + public execute(query: string, ...args: any[]): Sqlite3.RunResult { + try { + const preparedQuery = this.database.prepare(query); + return preparedQuery.run(args); + } catch (error) { + Container.get("logger").error("Failed to execute database connection", error, query, args); + throw error; + } + } + + public fetch(query: string, ...args: any[]): Result|undefined { + const preparedQuery = this.database.prepare(query); + return preparedQuery.get(args); + } + public fetchAll(query: string, ...args: any[]): Result[] { + const preparedQuery = this.database.prepare(query); + return preparedQuery.all(args); + } + + public hasTable(tableName: string): boolean { + const sql = "SELECT COUNT(*) as tableCount FROM sqlite_master WHERE type='table' AND name=? LIMIT 1;"; + + const result = this.fetch(sql, tableName); + return result != undefined && result.tableCount > 0; + } +} \ No newline at end of file diff --git a/source/Database/DatabaseDefinition.ts b/source/Database/DatabaseDefinition.ts new file mode 100644 index 0000000..f83419d --- /dev/null +++ b/source/Database/DatabaseDefinition.ts @@ -0,0 +1,13 @@ +export type DatabaseColumnDefinition = { + name: string; + type: string; + primaryKey?: boolean; + autoIncrement?: boolean; + notNull?: boolean; + options?: string; +} + +export type DatabaseDefinition = { + name: string; + columns: DatabaseColumnDefinition[]; +} \ No newline at end of file diff --git a/source/Database/DatabaseUpdater.ts b/source/Database/DatabaseUpdater.ts new file mode 100644 index 0000000..a9f58fe --- /dev/null +++ b/source/Database/DatabaseUpdater.ts @@ -0,0 +1,81 @@ +import {DatabaseConnection} from "./DatabaseConnection"; +import {DatabaseColumnDefinition, DatabaseDefinition} from "./DatabaseDefinition"; +import {Container} from "../Container/Container"; +import {Logger} from "log4js"; + +export class DatabaseUpdater { + constructor(private readonly database: DatabaseConnection) {} + + public ensureAvaliablity(definitions: Iterable) { + for (const definition of definitions) { + this.ensureDefinition(definition); + } + } + + private ensureDefinition(definition: DatabaseDefinition) { + if (this.database.hasTable(definition.name)) { + this.ensureTableColumns(definition); + return; + } + + this.createTable(definition); + } + + private ensureTableColumns(definition: DatabaseDefinition) { + const DBSQLColumns = this.database.fetchAll<{}, {name: string, type: string}>( + `PRAGMA table_info("${definition.name}")` + ); + + if (!DBSQLColumns) { + Container.get("logger").log("Request failed..."); + return; + } + + const missingColumns = definition.columns.filter( + (column: DatabaseColumnDefinition) => { + return !DBSQLColumns.some((dbColumn: DatabaseColumnDefinition) => { + return column.name === dbColumn.name + }); + } + ) + + if (missingColumns.length < 1) { + Container.get("logger").log(`No new columns found for ${definition.name}`) + return; + } + + const columnsSQL = missingColumns.map((column: DatabaseColumnDefinition) => { + const values = [ + "ADD", + column.name, + column.type, + column.primaryKey ? `PRIMARY KEY` : '', + column.notNull ? 'NOT NULL' : '', + column.autoIncrement ? 'AUTOINCREMENT' : '', + ] + + return values.join(' '); + }).join(', '); + + const sql = `ALTER TABLE ${definition.name} ${columnsSQL}`; + this.database.execute(sql); + } + + + private createTable(definition: DatabaseDefinition) { + const columnsSQL = definition.columns.map((column: DatabaseColumnDefinition) => { + const values = [ + column.name, + column.type, + column.primaryKey ? `PRIMARY KEY` : '', + column.notNull ? 'NOT NULL' : '', + column.autoIncrement ? 'AUTOINCREMENT' : '', + ] + + return values.join(' '); + }).join(', '); + + const sql = `CREATE TABLE IF NOT EXISTS ${definition.name} (${columnsSQL})`; + const result = this.database.execute(sql); + } +} \ No newline at end of file diff --git a/source/Database/definitions.ts b/source/Database/definitions.ts new file mode 100644 index 0000000..d975cac --- /dev/null +++ b/source/Database/definitions.ts @@ -0,0 +1,10 @@ +import Groups from "./tables/Groups"; +import {DatabaseDefinition} from "./DatabaseDefinition"; +import Playdate from "./tables/Playdate"; + +const definitions = new Set([ + Groups, + Playdate +]); + +export default definitions; \ No newline at end of file diff --git a/source/Database/tables/Groups.ts b/source/Database/tables/Groups.ts new file mode 100644 index 0000000..61aae0c --- /dev/null +++ b/source/Database/tables/Groups.ts @@ -0,0 +1,39 @@ +import {DatabaseDefinition} from "../DatabaseDefinition"; + +export type DBGroup = { + id: number; + name: string; + server: string; + leader: string; + role: string; +} + +const dbDefinition: DatabaseDefinition = { + name: "groups", + columns: [ + { + name: "id", + type: "INTEGER", + autoIncrement: true, + primaryKey: true, + }, + { + name: "server", + type: "VARCHAR(32)" + }, + { + name: "name", + type: "VARCHAR(32)", + }, + { + name: "leader", + type: "VARCHAR(32)", + }, + { + name: "role", + type: "VARCHAR(32)", + } + ] +} + +export default dbDefinition; \ No newline at end of file diff --git a/source/Database/tables/Playdate.ts b/source/Database/tables/Playdate.ts new file mode 100644 index 0000000..889653c --- /dev/null +++ b/source/Database/tables/Playdate.ts @@ -0,0 +1,35 @@ +import {DatabaseDefinition} from "../DatabaseDefinition"; + +export type DBPlaydate = { + id: number; + groupid: number; + + time_from: number; + time_to: number; +} + +const dbDefinition: DatabaseDefinition = { + name: "playdates", + columns: [ + { + name: "id", + type: "INTEGER", + autoIncrement: true, + primaryKey: true, + }, + { + name: "groupid", + type: "INTEGER", + }, + { + name: "time_from", + type: "TIMESTAMP", + }, + { + name: "time_to", + type: "TIMESTAMP", + } + ] +} + +export default dbDefinition; \ No newline at end of file diff --git a/source/Discord/CommandPartials/GroupSelection.ts b/source/Discord/CommandPartials/GroupSelection.ts new file mode 100644 index 0000000..84f0a0c --- /dev/null +++ b/source/Discord/CommandPartials/GroupSelection.ts @@ -0,0 +1,46 @@ +import { + AutocompleteInteraction, + ChatInputCommandInteraction, + CommandInteraction, + GuildMember, + SlashCommandStringOption +} from "discord.js"; +import {Container} from "../../Container/Container"; +import {GroupRepository} from "../../Repositories/GroupRepository"; +import {GroupModel} from "../../Models/GroupModel"; +import {UserError} from "../UserError"; + +export class GroupSelection { + public static createOptionSetup(): SlashCommandStringOption { + return new SlashCommandStringOption() + .setName("group") + .setDescription("Defines the group you want to manage the playdates for") + .setRequired(true) + .setAutocomplete(true) + } + + public static async handleAutocomplete(interaction: AutocompleteInteraction): Promise { + const value = interaction.options.getFocused(); + const repo = Container.get(GroupRepository.name); + const groups = repo.findGroupsByMember(interaction.member); + await interaction.respond( + groups + .filter((group) => group.name.startsWith(value)) + .map((group) => ({name: group.name, value: group.name })) + ) + } + + public static getGroup(interaction: CommandInteraction): GroupModel { + const groupname = interaction.options.get("group"); + if (!groupname) { + throw new UserError("No group name provided"); + } + + const group = Container.get(GroupRepository.name).findGroupByName((groupname.value ?? '').toString()); + if (!group) { + throw new UserError("No group found"); + } + + return group; + } +} \ No newline at end of file diff --git a/source/Discord/Commands/Command.ts b/source/Discord/Commands/Command.ts new file mode 100644 index 0000000..72043e4 --- /dev/null +++ b/source/Discord/Commands/Command.ts @@ -0,0 +1,17 @@ +import {ChatInputCommandInteraction, Interaction, SlashCommandBuilder} from "discord.js"; +import Commands from "./Commands"; + +export interface Command { + definition(): SlashCommandBuilder; +} + +export interface ChatInteractionCommand { + execute(interaction: ChatInputCommandInteraction): Promise; +} + +export interface AutocompleteCommand { + handleAutocomplete(interaction: Interaction): Promise; +} + +export type CommandUnion = + Command | Partial | Partial; \ No newline at end of file diff --git a/source/Discord/Commands/Commands.ts b/source/Discord/Commands/Commands.ts new file mode 100644 index 0000000..2b1c193 --- /dev/null +++ b/source/Discord/Commands/Commands.ts @@ -0,0 +1,41 @@ +import {HelloWorldCommand} from "./HelloWorldCommand"; +import {Command, CommandUnion} from "./Command"; +import {GroupCommand} from "./Groups"; +import {PlaydatesCommand} from "./Playdates"; + +const commands: Set = new Set([ + new HelloWorldCommand(), + new GroupCommand(), + new PlaydatesCommand() +]); + +export default class Commands { + private mappedCommands: Map = new Map(); + + constructor() { + this.mappedCommands = this.getMap(); + } + + public getCommand(commandName: string): CommandUnion|undefined { + if (!this.mappedCommands.has(commandName)) { + throw new Error(`Unknown command: ${commandName}`); + } + + return this.mappedCommands.get(commandName); + } + + public get allCommands(): Set { + return commands; + } + + private getMap(): Map + { + const map = new Map(); + for (const command of commands) { + const definition = command.definition() + map.set(definition.name, command); + } + + return map; + } +} \ No newline at end of file diff --git a/source/Discord/Commands/Groups.ts b/source/Discord/Commands/Groups.ts new file mode 100644 index 0000000..809a35e --- /dev/null +++ b/source/Discord/Commands/Groups.ts @@ -0,0 +1,100 @@ +import { + SlashCommandBuilder, + Interaction, + CommandInteraction, + ChatInputCommandInteraction, + MessageFlags, GuildMemberRoleManager, InteractionReplyOptions, GuildMember +} from "discord.js"; +import {ChatInteractionCommand, Command} from "./Command"; +import {GroupModel} from "../../Models/GroupModel"; +import {GroupRepository} from "../../Repositories/GroupRepository"; +import {DatabaseConnection} from "../../Database/DatabaseConnection"; +import {Container} from "../../Container/Container"; + +export class GroupCommand implements Command, ChatInteractionCommand { + definition(): SlashCommandBuilder { + // @ts-ignore + return new SlashCommandBuilder() + .setName('groups') + .setDescription(`Manages groups`) + .addSubcommand(create => + create.setName("create") + .setDescription("Creates a new group, with executing user being the leader") + .addStringOption((option) => + option.setName("name") + .setDescription("Defines the name for the group.") + .setRequired(true) + ) + .addRoleOption((builder) => + builder.setName("role") + .setDescription("Defines the role, where all the members are located in.") + .setRequired(true) + ) + ) + .addSubcommand(listCommand => + listCommand + .setName("list") + .setDescription("Displays the groups you are apart of.") + ); + + } + execute(interaction: ChatInputCommandInteraction): Promise { + switch (interaction.options.getSubcommand()) { + case "create": + this.create(interaction); + break; + case "list": + this.list(interaction); + break; + default: + throw new Error("Unsupported command"); + } + + return Promise.resolve(); + } + + private create(interaction: ChatInputCommandInteraction): void { + const name = interaction.options.getString("name") ?? ''; + const role = interaction.options.getRole("role"); + + const group: GroupModel = { + id: -1, + name: name, + leader: { + server: interaction.guildId ?? '', + memberid: interaction.member?.user.id ?? '' + }, + role: { + server: interaction.guildId ?? '', + roleid: role?.id ?? '' + } + } + + Container.get(GroupRepository.name).create(group); + + interaction.reply({content: `:white_check_mark: Created group \`${name}\``, flags: MessageFlags.Ephemeral }) + } + + private list(interaction: ChatInputCommandInteraction) { + const repo = Container.get(GroupRepository.name); + const groups = repo.findGroupsByMember(interaction.member); + + const reply: InteractionReplyOptions = { + embeds: [ + { + title: "Your groups on this server:", + + fields: groups.map((group) => { + return { + name: group.name, + value: "" + } + }) + } + ], + flags: MessageFlags.Ephemeral + } + + interaction.reply(reply); + } +} \ No newline at end of file diff --git a/source/Discord/Commands/HelloWorldCommand.ts b/source/Discord/Commands/HelloWorldCommand.ts new file mode 100644 index 0000000..7191c47 --- /dev/null +++ b/source/Discord/Commands/HelloWorldCommand.ts @@ -0,0 +1,25 @@ +import {SlashCommandBuilder, Interaction, CommandInteraction} from "discord.js"; +import {Command} from "./Command"; + +export class HelloWorldCommand implements Command { + private static RESPONSES: string[] = [ + 'Hello :)', + 'zzzZ... ZzzzZ... huh? I am awake. I am awake!', + 'Roll for initiative!', + 'I was an adventurerer like you...', + 'Hello :) How are you?', + ] + + definition(): SlashCommandBuilder + { + return new SlashCommandBuilder() + .setName("hello") + .setDescription("Displays a random response. (commonly used to test if the bot is online)") + } + async execute(interaction: CommandInteraction): Promise { + const random = Math.floor(Math.random() * HelloWorldCommand.RESPONSES.length); + + await interaction.reply(HelloWorldCommand.RESPONSES[random]); + return Promise.resolve(); + } +} \ No newline at end of file diff --git a/source/Discord/Commands/Playdates.ts b/source/Discord/Commands/Playdates.ts new file mode 100644 index 0000000..2e7c5b5 --- /dev/null +++ b/source/Discord/Commands/Playdates.ts @@ -0,0 +1,206 @@ +import { + SlashCommandBuilder, + Interaction, + CommandInteraction, + AutocompleteInteraction, + GuildMember, + EmbedBuilder, MessageFlags, ChatInputCommandInteraction, ModalSubmitFields +} from "discord.js"; +import {AutocompleteCommand, ChatInteractionCommand, Command} from "./Command"; +import {Container} from "../../Container/Container"; +import {GroupRepository} from "../../Repositories/GroupRepository"; +import {GroupSelection} from "../CommandPartials/GroupSelection"; +import {setFlagsFromString} from "node:v8"; +import {UserError} from "../UserError"; +import Playdate from "../../Database/tables/Playdate"; +import {PlaydateModel} from "../../Models/PlaydateModel"; +import {PlaydateRepository} from "../../Repositories/PlaydateRepository"; +import {GroupModel} from "../../Models/GroupModel"; +import playdate from "../../Database/tables/Playdate"; + +export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInteractionCommand { + static REGEX = [ + + ] + + definition(): SlashCommandBuilder { + // @ts-ignore + return new SlashCommandBuilder() + .setName("playdates") + .setDescription("Manage your playdates") + .addSubcommand((subcommand) => subcommand + .setName("create") + .setDescription("Creates a new playdate") + .addStringOption(GroupSelection.createOptionSetup()) + .addStringOption((option) => option + .setName("from") + .setDescription("Defines the start date & time. Format: YYYY-MM-DD HH:mm") + ) + .addStringOption((option) => option + .setName("to") + .setDescription("Defines the end date & time. Format: YYYY-MM-DD HH:mm") + ) + .addAttachmentOption((option) => option + .setName("calendar-entry") + .setDescription("Optional, you can upload a iCal file and the from and to-values are read from it.") + ) + ) + .addSubcommand((subcommand) => subcommand + .setName("list") + .setDescription("Lists all playdates") + .addStringOption(GroupSelection.createOptionSetup()) + ) + .addSubcommand((subcommand) => subcommand + .setName("remove") + .setDescription("Removes a playdate") + .addStringOption(GroupSelection.createOptionSetup()) + .addIntegerOption((option) => option + .setName("playdate") + .setDescription("Selects a playdate") + .setRequired(true) + .setAutocomplete(true) + ) + ); + } + + async execute(interaction: ChatInputCommandInteraction): Promise { + const group = GroupSelection.getGroup(interaction); + + switch (interaction.options.getSubcommand()) { + case "create": + await this.create(interaction, group); + break; + case "remove": + await this.delete(interaction, group); + break; + case "list": + await this.list(interaction, group); + break; + default: + throw new UserError("This subcommand is not yet implemented."); + } + } + + async create(interaction: CommandInteraction, group: GroupModel): Promise { + const fromDate = Date.parse(interaction.options.get("from")?.value ?? ''); + const toDate = Date.parse(interaction.options.get("to")?.value ?? ''); + + if (isNaN(fromDate)) { + throw new UserError("No date or invalid date format for the from parameter."); + } + + if (isNaN(toDate)) { + throw new UserError("No date or invalid date format for the to parameter."); + } + + const playdate: Partial = { + group: group, + from_time: new Date(fromDate), + to_time: new Date(toDate), + } + + const id = Container.get(PlaydateRepository.name).create(playdate); + + const embed = new EmbedBuilder() + .setTitle("Created a play-date.") + .setDescription(":white_check_mark: Your playdate has been created! You and your group get notified, when its time.") + .setFooter({ + text: `Group: ${group.name}` + }) + + await interaction.reply({ + embeds: [ + embed + ], + flags: MessageFlags.Ephemeral, + }) + } + + async handleAutocomplete(interaction: AutocompleteInteraction): Promise { + const option = interaction.options.getFocused(true); + if (option.name == "group") { + await GroupSelection.handleAutocomplete(interaction); + return; + } + + if (option.name != 'playdate') { + return; + } + + const groupname = interaction.options.getString("group") + const group = Container.get(GroupRepository.name).findGroupByName((groupname ?? '').toString()); + if (!group) { + throw new UserError("No group found"); + } + + const playdates = Container.get(PlaydateRepository.name).findFromGroup(group); + await interaction.respond( + playdates.map(playdate => { + return { + name: `${playdate.from_time.toLocaleString()} - ${playdate.to_time.toLocaleString()}`, + value: playdate.id + } + }) + ) + } + + private async list(interaction: ChatInputCommandInteraction, group: GroupModel) { + const playdates = Container.get(PlaydateRepository.name).findFromGroup(group); + + const embed = new EmbedBuilder() + .setTitle("The next playdates:") + .setFields( + playdates.map((playdate) => + { + return { + name: `${playdate.from_time.toLocaleString()} - ${playdate.to_time.toLocaleString()}`, + value: `` + } + }) + ) + .setFooter({ + text: `Group: ${group.name}` + }) + + await interaction.reply({ + embeds: [ + embed + ], + flags: MessageFlags.Ephemeral, + }) + } + + private async delete(interaction: ChatInputCommandInteraction, group: GroupModel): Promise { + const playdateId = interaction.options.getInteger("playdate", true) + + const repo = Container.get(PlaydateRepository.name); + const selected = repo.getById(playdateId); + if (!selected) { + throw new UserError("No playdate found"); + } + + console.log(selected, group); + + if (selected.group?.id != group.id) { + throw new UserError("No playdate found"); + } + + repo.delete(playdateId); + + const embed = new EmbedBuilder() + .setTitle("Playdate deleted") + .setDescription( + `:x: Deleted \`${selected.from_time.toLocaleString()} - ${selected.to_time.toLocaleString()}\`` + ) + .setFooter({ + text: `Group: ${group.name}` + }) + + await interaction.reply({ + embeds: [ + embed + ], + flags: MessageFlags.Ephemeral, + }) + } +} \ No newline at end of file diff --git a/source/Discord/DiscordClient.ts b/source/Discord/DiscordClient.ts new file mode 100644 index 0000000..76f77c9 --- /dev/null +++ b/source/Discord/DiscordClient.ts @@ -0,0 +1,120 @@ +import { + Client, + GatewayIntentBits, + Events, + Interaction, + ChatInputCommandInteraction, + MessageFlags, + Activity, + ActivityType +} from "discord.js"; +import Commands from "./Commands/Commands"; +import {Container} from "../Container/Container"; +import {Logger} from "log4js"; +import {UserError} from "./UserError"; + +export class DiscordClient { + private readonly client: Client; + private commands: Commands; + + public get Client (): Client { + return this.client; + } + + constructor() { + this.client = new Client({ + intents: [GatewayIntentBits.Guilds] + }) + + this.commands = new Commands(); + } + + applyEvents() { + this.client.once(Events.ClientReady, () => { + Container.get("logger").info(`Ready! Logged in as ${this.client.user.tag}`); + this.client.user.setActivity('your PnP playdates', { + type: ActivityType.Watching, + }); + }) + + this.client.on(Events.InteractionCreate, async (interaction: Interaction) => { + const command = this.commands.getCommand(interaction.commandName); + + if (command === null) { + Container.get("logger").error(`Could not find command for '${interaction.commandName}'`); + return; + } + + const method = this.findCommandMethod(interaction); + if (!method) { + Container.get("logger").error(`Could not find method for '${interaction.commandName}'`); + return; + } + + await method(); + }) + } + + connect(token: string) { + this.client.login(token); + } + + private findCommandMethod(interaction: Interaction) { + if (interaction.isChatInputCommand()) { + const command = this.commands.getCommand(interaction.commandName); + + if (!command) { + return null; + } + + if (!('execute' in command)) { + return null; + } + + return async () => { + Container.get("logger").debug(`Found chat command ${interaction.commandName}: running...`); + + try { + await command.execute(interaction) + } + catch (e: Error) { + Container.get("logger").error(e) + + let userMessage = ":x: There was an error while executing this command!"; + if (e.constructor.name === UserError.name) { + userMessage = `:x: \`${e.message}\` - Please validate your request!` + } + if (interaction.replied || interaction.deferred) { + await interaction.followUp({ content: userMessage, flags: MessageFlags.Ephemeral }); + } else { + await interaction.reply({ content: userMessage, flags: MessageFlags.Ephemeral }); + } + } + } + } + + if (interaction.isAutocomplete()) { + const command = this.commands.getCommand(interaction.commandName); + + if (!command) { + return null; + } + + if (!('handleAutocomplete' in command)) { + return null; + } + + return async () => { + Container.get("logger").debug(`Found command ${interaction.commandName} for autocomplete: handling...`); + + try { + await command.handleAutocomplete(interaction); + } catch (e: any) { + Container.get('logger').error(e); + } + } + } + + return null; + } +} \ No newline at end of file diff --git a/source/Discord/UserError.ts b/source/Discord/UserError.ts new file mode 100644 index 0000000..4d10465 --- /dev/null +++ b/source/Discord/UserError.ts @@ -0,0 +1,3 @@ +export class UserError extends Error { + +} \ No newline at end of file diff --git a/source/Environment.ts b/source/Environment.ts new file mode 100644 index 0000000..de9a103 --- /dev/null +++ b/source/Environment.ts @@ -0,0 +1,34 @@ +import dotenv from "dotenv"; +import path from "node:path"; + +type DiscordEnvironment = { + token: string; + guildId: string; + clientId: string; +} + +export type DatabaseEnvironment = { + path: string; +} + +export class Environment { + get discord(): DiscordEnvironment { + return { + token: process.env.DISCORD_API_KEY ?? '', + guildId: process.env.DISCORD_GUILD_ID ?? '', + clientId: process.env.DISCORD_CLIENT_ID ?? '', + } + } + + get database(): DatabaseEnvironment { + return { + path: path.resolve(process.env.DB_PATH ?? ''), + } + } + + public setup() { + dotenv.config({ + path: path.resolve(__dirname, "../environment/.env"), + }); + } +} \ No newline at end of file diff --git a/source/Models/GroupModel.ts b/source/Models/GroupModel.ts new file mode 100644 index 0000000..e659bff --- /dev/null +++ b/source/Models/GroupModel.ts @@ -0,0 +1,8 @@ +import {Model} from "./Model"; +import {GuildMember, Role} from "../types/DiscordTypes"; + +export interface GroupModel extends Model { + name: string; + leader: GuildMember; + role: Role; +} \ No newline at end of file diff --git a/source/Models/Model.ts b/source/Models/Model.ts new file mode 100644 index 0000000..4029a15 --- /dev/null +++ b/source/Models/Model.ts @@ -0,0 +1,3 @@ +export interface Model { + id: number; +} diff --git a/source/Models/PlaydateModel.ts b/source/Models/PlaydateModel.ts new file mode 100644 index 0000000..76236b2 --- /dev/null +++ b/source/Models/PlaydateModel.ts @@ -0,0 +1,9 @@ +import {Model} from "./Model"; +import {GroupModel} from "./GroupModel"; +import {Nullable} from "../types/Nullable"; + +export interface PlaydateModel extends Model { + group: Nullable + from_time: Date, + to_time: Date, +} \ No newline at end of file diff --git a/source/Repositories/GroupRepository.ts b/source/Repositories/GroupRepository.ts new file mode 100644 index 0000000..6b4c433 --- /dev/null +++ b/source/Repositories/GroupRepository.ts @@ -0,0 +1,82 @@ +import {Repository} from "./Repository"; +import {GroupModel} from "../Models/GroupModel"; +import Groups, {DBGroup} from "../Database/tables/Groups"; +import {DatabaseConnection} from "../Database/DatabaseConnection"; +import {CacheType, CacheTypeReducer, Guild, GuildMember, GuildMemberRoleManager} from "discord.js"; +import {Nullable} from "../types/Nullable"; + +export class GroupRepository extends Repository { + + constructor( + protected readonly database: DatabaseConnection, + ) { + super( + database, + Groups + ); + + } + + public findGroupByName(name: string): Nullable { + const result = this.database.fetch( + `SELECT * FROM groups WHERE name = ? LIMIT 1`, + name + ) + + if (!result) { + return undefined; + } + + return this.convertToModelType(result); + } + + public findGroupsByRoles(server: string, roleIds: string[]): GroupModel[] { + const template = roleIds.map(roleId => '?').join(','); + + const dbResult = this.database.fetchAll(` + SELECT * FROM groups WHERE server = ? AND role IN (${template}) + `, + server, + ...roleIds) + + + return dbResult.map((result) => this.convertToModelType(result)); + } + + public findGroupsByMember(member: GuildMember) { + if (!member) { + throw new Error("Can't find member for guild: none given"); + } + + return this.findGroupsByRoles(member.guild.id, [...member.roles.cache.keys()]) + } + + protected convertToModelType(intermediateModel: DBGroup | undefined): GroupModel { + if (!intermediateModel) { + throw new Error("No intermediate model provided"); + } + + return { + id: intermediateModel.id, + name: intermediateModel.name, + leader: { + server: intermediateModel.server, + memberid: intermediateModel.leader + }, + role: { + server: intermediateModel.server, + roleid: intermediateModel.role + } + } + } + + protected convertToCreateObject(instance: Partial): object { + return { + name: instance.name ?? '', + server: instance.role?.server ?? null, + leader: instance.leader?.memberid ?? null, + role: instance.role?.roleid ?? null, + } + } + +} \ No newline at end of file diff --git a/source/Repositories/PlaydateRepository.ts b/source/Repositories/PlaydateRepository.ts new file mode 100644 index 0000000..f0290fb --- /dev/null +++ b/source/Repositories/PlaydateRepository.ts @@ -0,0 +1,53 @@ +import {Repository} from "./Repository"; +import {PlaydateModel} from "../Models/PlaydateModel"; +import Playdate, {DBPlaydate} from "../Database/tables/Playdate"; +import {DatabaseConnection} from "../Database/DatabaseConnection"; +import {GroupRepository} from "./GroupRepository"; +import {GroupModel} from "../Models/GroupModel"; +import {Nullable} from "../types/Nullable"; +import playdate from "../Database/tables/Playdate"; + +export class PlaydateRepository extends Repository { + + constructor( + protected readonly database: DatabaseConnection, + private readonly groupRepository: GroupRepository, + ) { + super( + database, + Playdate + ); + } + + findFromGroup(group: GroupModel) { + const finds = this.database.fetchAll( + `SELECT * FROM ${this.schema.name} WHERE groupid = ? AND time_from > ?`, + group.id, + new Date().getTime() + ); + + return finds.map((playdate) => this.convertToModelType(playdate, group)); + } + + protected convertToModelType(intermediateModel: DBPlaydate | undefined, fixedGroup: Nullable = null): PlaydateModel { + if (!intermediateModel) { + throw new Error("Unable to convert the playdate model"); + } + const result: PlaydateModel = { + id: intermediateModel.id, + group: fixedGroup ?? this.groupRepository.getById(intermediateModel.groupid), + from_time: new Date(intermediateModel.time_from), + to_time: new Date(intermediateModel.time_to), + } + + return result; + } + + protected convertToCreateObject(instance: Partial): object { + return { + groupid: instance.group?.id ?? null, + time_from: instance.from_time?.getTime() ?? 0, + time_to: instance.to_time?.getTime() ?? 0, + } + } +} \ No newline at end of file diff --git a/source/Repositories/Repository.ts b/source/Repositories/Repository.ts new file mode 100644 index 0000000..bbd1dd8 --- /dev/null +++ b/source/Repositories/Repository.ts @@ -0,0 +1,52 @@ +import {DatabaseConnection} from "../Database/DatabaseConnection"; +import {Model} from "../Models/Model"; +import { Nullable } from "../types/Nullable"; +import {DatabaseDefinition} from "../Database/DatabaseDefinition"; +import {debug} from "node:util"; + +export class Repository { + constructor( + protected readonly database: DatabaseConnection, + public readonly schema: DatabaseDefinition, + ) {} + + public create(instance: Partial): number|bigint { + const columnNames = this.schema.columns.filter((column) => { + return !column.primaryKey + }).map((column) => { + return column.name; + }); + + const createObject = this.convertToCreateObject(instance); + const keys = Object.keys(createObject); + const missingColumns = columnNames.filter((columnName) => { + return !keys.includes(columnName); + }) + + if (missingColumns.length > 0) { + throw new Error("Can't create instance, due to missing column values: " + missingColumns); + } + + const sql = `INSERT INTO ${this.schema.name}(${Object.keys(createObject).join(',')}) + VALUES (${Object.keys(createObject).map(() => "?").join(',')})`; + const result = this.database.execute(sql, ...Object.values(createObject)); + return result.lastInsertRowid; + } + + public getById(id: number): Nullable { + const sql = `SELECT * FROM ${this.schema.name} WHERE id = ? LIMIT 1`; + return this.convertToModelType(this.database.fetch(sql, id)); + } + + public delete(id: number) { + const sql = `DELETE FROM ${this.schema.name} WHERE id = ?`; + return this.database.execute(sql, id); + } + + protected convertToModelType(intermediateModel: IntermediateModelType | undefined): ModelType { + return intermediateModel as unknown as ModelType; + } + protected convertToCreateObject(instance: Partial): object { + return instance; + } +} \ No newline at end of file diff --git a/source/deploy.ts b/source/deploy.ts new file mode 100644 index 0000000..cbf4a2d --- /dev/null +++ b/source/deploy.ts @@ -0,0 +1,46 @@ +import Commands from "./Discord/Commands/Commands"; +import {Environment} from "./Environment"; +import {DatabaseConnection} from "./Database/DatabaseConnection"; +import {DatabaseUpdater} from "./Database/DatabaseUpdater"; +import Definitions from "./Database/definitions"; +import {Container} from "./Container/Container"; +import {ServiceHint, Services} from "./Container/Services"; +import {Logger} from "log4js"; + +const { REST, Routes } = require('discord.js'); +const container = Container.getInstance(); +Services.setup(container, ServiceHint.Deploy) + +const commands = new Commands().allCommands; + +const environment = container.get(Environment.name); +const logger = container.get("logger"); +// Construct and prepare an instance of the REST module +const rest = new REST().setToken(environment.discord.token); + +// and deploy your commands! +(async () => { + try { + const commandInfos = []; + commands.forEach((command) => { + commandInfos.push(command.definition().toJSON()) + }) + + logger.log(`Started refreshing ${commandInfos.length} application (/) commands.`); + + // The put method is used to fully refresh all commands in the guild with the current set + const data = await rest.put( + Routes.applicationGuildCommands(environment.discord.clientId, environment.discord.guildId), + { body: commandInfos }, + ); + + logger.log(`Successfully reloaded ${commandInfos.length} application (/) commands.`); + } catch (error) { + // And of course, make sure you catch and log any errors! + logger.error(error); + } +})(); + +logger.log("Ensuring Database..."); +const updater = new DatabaseUpdater(container.get(DatabaseConnection.name)); +updater.ensureAvaliablity(Definitions); \ No newline at end of file diff --git a/source/main.ts b/source/main.ts new file mode 100644 index 0000000..b7ea733 --- /dev/null +++ b/source/main.ts @@ -0,0 +1,12 @@ +import {DiscordClient} from "./Discord/DiscordClient"; +import {Environment} from "./Environment"; +import {Container} from "./Container/Container"; +import {DatabaseConnection} from "./Database/DatabaseConnection"; +import {ServiceHint, Services} from "./Container/Services"; + +const container = Container.getInstance(); +Services.setup(container, ServiceHint.App); + +const client = new DiscordClient() +client.applyEvents() +client.connect(container.get(Environment.name).discord.token) \ No newline at end of file diff --git a/source/types/DiscordTypes.ts b/source/types/DiscordTypes.ts new file mode 100644 index 0000000..85cfab8 --- /dev/null +++ b/source/types/DiscordTypes.ts @@ -0,0 +1,9 @@ +export type GuildMember = { + server: string; + memberid: string; +} + +export type Role = { + server: string; + roleid: string; +}; \ No newline at end of file diff --git a/source/types/Nullable.ts b/source/types/Nullable.ts new file mode 100644 index 0000000..7b5ecba --- /dev/null +++ b/source/types/Nullable.ts @@ -0,0 +1 @@ +export type Nullable = T | null | undefined; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..54e62ee --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "_version": "20.1.0", + + "compilerOptions": { + "lib": ["es2023"], + "module": "node16", + "target": "es2022", + + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "node16" + } +} \ No newline at end of file