A powerful and extendable Discord bot, with it's own module system :3 thevoid.cafe/projects/voidy

🎉 Initialize v3

Jo b447f3c9 08a04c23

+2 -1
.env.example
··· 1 1 BOT_TOKEN=#Get this from https://discord.com/developers - REQUIRED 2 2 BOT_CLIENT_ID=#Get this from https://discord.com/developers - REQUIRED 3 - DB_URI=#Your MongoDB connection URI - REQUIRED 3 + BOT_ADMINS=#Discord Ids of bot administrators, comma separated 4 + DB_URI=#Your MongoDB connection URI - REQUIRED
+30 -8
.gitignore
··· 1 - ### Jetbrains 2 - .idea 3 - .vscode 1 + # dependencies (bun install) 2 + node_modules 3 + 4 + # output 5 + out 6 + dist 7 + *.tgz 4 8 5 - ### Node/Deno/Bun 6 - node_modules 9 + # code coverage 10 + coverage 11 + *.lcov 7 12 8 - ### Secrets 13 + # logs 14 + logs 15 + _.log 16 + report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 17 + 18 + # dotenv environment variable files 9 19 .env 20 + .env.development.local 21 + .env.test.local 22 + .env.production.local 23 + .env.local 10 24 11 - ### Docker 12 - docker-compose.yml 25 + # caches 26 + .eslintcache 27 + .cache 28 + *.tsbuildinfo 29 + 30 + # IntelliJ based IDEs 31 + .idea 32 + 33 + # Finder (MacOS) folder config 34 + .DS_Store
-31
.zed/settings.json
··· 1 - { 2 - "lsp": { 3 - "deno": { 4 - "settings": { 5 - "deno": { 6 - "enable": true 7 - } 8 - } 9 - } 10 - }, 11 - "languages": { 12 - "TypeScript": { 13 - "language_servers": [ 14 - "deno", 15 - "!typescript-language-server", 16 - "!vtsls", 17 - "!eslint" 18 - ], 19 - "formatter": "language_server" 20 - }, 21 - "TSX": { 22 - "language_servers": [ 23 - "deno", 24 - "!typescript-language-server", 25 - "!vtsls", 26 - "!eslint" 27 - ], 28 - "formatter": "language_server" 29 - } 30 - } 31 - }
+78
bun.lock
··· 1 + { 2 + "lockfileVersion": 1, 3 + "workspaces": { 4 + "": { 5 + "name": "voidydiscord", 6 + "dependencies": { 7 + "discord.js": "^14.21.0", 8 + }, 9 + "devDependencies": { 10 + "@types/bun": "latest", 11 + }, 12 + "peerDependencies": { 13 + "typescript": "^5", 14 + }, 15 + }, 16 + }, 17 + "packages": { 18 + "@discordjs/builders": ["@discordjs/builders@1.11.2", "", { "dependencies": { "@discordjs/formatters": "^0.6.1", "@discordjs/util": "^1.1.1", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.1", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-F1WTABdd8/R9D1icJzajC4IuLyyS8f3rTOz66JsSI3pKvpCAtsMBweu8cyNYsIyvcrKAVn9EPK+Psoymq+XC0A=="], 19 + 20 + "@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="], 21 + 22 + "@discordjs/formatters": ["@discordjs/formatters@0.6.1", "", { "dependencies": { "discord-api-types": "^0.38.1" } }, "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg=="], 23 + 24 + "@discordjs/rest": ["@discordjs/rest@2.5.1", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-Tg9840IneBcbrAjcGaQzHUJWFNq1MMWZjTdjJ0WS/89IffaNKc++iOvffucPxQTF/gviO9+9r8kEPea1X5J2Dw=="], 25 + 26 + "@discordjs/util": ["@discordjs/util@1.1.1", "", {}, "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g=="], 27 + 28 + "@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="], 29 + 30 + "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="], 31 + 32 + "@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="], 33 + 34 + "@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="], 35 + 36 + "@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="], 37 + 38 + "@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="], 39 + 40 + "@types/react": ["@types/react@19.1.9", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA=="], 41 + 42 + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], 43 + 44 + "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.6", "", {}, "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA=="], 45 + 46 + "bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="], 47 + 48 + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], 49 + 50 + "discord-api-types": ["discord-api-types@0.38.18", "", {}, "sha512-ygenySjZKUaBf5JT8BNhZSxLzwpwdp41O0wVroOTu/N2DxFH7dxYTZUSnFJ6v+/2F3BMcnD47PC47u4aLOLxrQ=="], 51 + 52 + "discord.js": ["discord.js@14.21.0", "", { "dependencies": { "@discordjs/builders": "^1.11.2", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.1", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.1", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.1", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-U5w41cEmcnSfwKYlLv5RJjB8Joa+QJyRwIJz5i/eg+v2Qvv6EYpCRhN9I2Rlf0900LuqSDg8edakUATrDZQncQ=="], 53 + 54 + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], 55 + 56 + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], 57 + 58 + "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="], 59 + 60 + "magic-bytes.js": ["magic-bytes.js@1.12.1", "", {}, "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA=="], 61 + 62 + "ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="], 63 + 64 + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 65 + 66 + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], 67 + 68 + "undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="], 69 + 70 + "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], 71 + 72 + "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], 73 + 74 + "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], 75 + 76 + "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], 77 + } 78 + }
-10
deno.json
··· 1 - { 2 - "tasks": { 3 - "dev": "deno run --watch --env-file --allow-env --allow-read --allow-net --unstable-cron src/main.ts" 4 - }, 5 - "imports": { 6 - "@std/fs": "jsr:@std/fs", 7 - "@std/path": "jsr:@std/path", 8 - "discord.js": "npm:discord.js" 9 - } 10 - }
-160
deno.lock
··· 1 - { 2 - "version": "4", 3 - "specifiers": { 4 - "jsr:@std/fs@*": "1.0.17", 5 - "jsr:@std/path@*": "1.0.9", 6 - "jsr:@std/path@^1.0.9": "1.0.9", 7 - "npm:discord.js@*": "14.19.2" 8 - }, 9 - "jsr": { 10 - "@std/fs@1.0.17": { 11 - "integrity": "1c00c632677c1158988ef7a004cb16137f870aafdb8163b9dce86ec652f3952b", 12 - "dependencies": [ 13 - "jsr:@std/path@^1.0.9" 14 - ] 15 - }, 16 - "@std/path@1.0.9": { 17 - "integrity": "260a49f11edd3db93dd38350bf9cd1b4d1366afa98e81b86167b4e3dd750129e" 18 - } 19 - }, 20 - "npm": { 21 - "@discordjs/builders@1.11.1": { 22 - "integrity": "sha512-2zDAVuoeAkdv0YQzYKO8vZfaDfB+1KZ60ymBKtD7QDpsh6lzAnQSUBLqeRkhlons6BT9+yRctOh9fPy94w6kDA==", 23 - "dependencies": [ 24 - "@discordjs/formatters", 25 - "@discordjs/util", 26 - "@sapphire/shapeshift", 27 - "discord-api-types", 28 - "fast-deep-equal", 29 - "ts-mixer", 30 - "tslib" 31 - ] 32 - }, 33 - "@discordjs/collection@1.5.3": { 34 - "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==" 35 - }, 36 - "@discordjs/collection@2.1.1": { 37 - "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==" 38 - }, 39 - "@discordjs/formatters@0.6.1": { 40 - "integrity": "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==", 41 - "dependencies": [ 42 - "discord-api-types" 43 - ] 44 - }, 45 - "@discordjs/rest@2.5.0": { 46 - "integrity": "sha512-PWhchxTzpn9EV3vvPRpwS0EE2rNYB9pvzDU/eLLW3mByJl0ZHZjHI2/wA8EbH2gRMQV7nu+0FoDF84oiPl8VAQ==", 47 - "dependencies": [ 48 - "@discordjs/collection@2.1.1", 49 - "@discordjs/util", 50 - "@sapphire/async-queue", 51 - "@sapphire/snowflake", 52 - "@vladfrangu/async_event_emitter", 53 - "discord-api-types", 54 - "magic-bytes.js", 55 - "tslib", 56 - "undici" 57 - ] 58 - }, 59 - "@discordjs/util@1.1.1": { 60 - "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==" 61 - }, 62 - "@discordjs/ws@1.2.2": { 63 - "integrity": "sha512-dyfq7yn0wO0IYeYOs3z79I6/HumhmKISzFL0Z+007zQJMtAFGtt3AEoq1nuLXtcunUE5YYYQqgKvybXukAK8/w==", 64 - "dependencies": [ 65 - "@discordjs/collection@2.1.1", 66 - "@discordjs/rest", 67 - "@discordjs/util", 68 - "@sapphire/async-queue", 69 - "@types/ws", 70 - "@vladfrangu/async_event_emitter", 71 - "discord-api-types", 72 - "tslib", 73 - "ws" 74 - ] 75 - }, 76 - "@sapphire/async-queue@1.5.5": { 77 - "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==" 78 - }, 79 - "@sapphire/shapeshift@4.0.0": { 80 - "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", 81 - "dependencies": [ 82 - "fast-deep-equal", 83 - "lodash" 84 - ] 85 - }, 86 - "@sapphire/snowflake@3.5.3": { 87 - "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==" 88 - }, 89 - "@types/node@22.12.0": { 90 - "integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==", 91 - "dependencies": [ 92 - "undici-types" 93 - ] 94 - }, 95 - "@types/ws@8.18.1": { 96 - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", 97 - "dependencies": [ 98 - "@types/node" 99 - ] 100 - }, 101 - "@vladfrangu/async_event_emitter@2.4.6": { 102 - "integrity": "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==" 103 - }, 104 - "discord-api-types@0.38.1": { 105 - "integrity": "sha512-vsjsqjAuxsPhiwbPjTBeGQaDPlizFmSkU0mTzFGMgRxqCDIRBR7iTY74HacpzrDV0QtERHRKQEk1tq7drZUtHg==" 106 - }, 107 - "discord.js@14.19.2": { 108 - "integrity": "sha512-L/ivhVefzzRcChHJSaGYsgA4Uqx6or2sst5JZ/ft9OBwrj8OJIzrrcutlkHnm/hlI0Hrm3es62TRVksU8VUqrg==", 109 - "dependencies": [ 110 - "@discordjs/builders", 111 - "@discordjs/collection@1.5.3", 112 - "@discordjs/formatters", 113 - "@discordjs/rest", 114 - "@discordjs/util", 115 - "@discordjs/ws", 116 - "@sapphire/snowflake", 117 - "discord-api-types", 118 - "fast-deep-equal", 119 - "lodash.snakecase", 120 - "magic-bytes.js", 121 - "tslib", 122 - "undici" 123 - ] 124 - }, 125 - "fast-deep-equal@3.1.3": { 126 - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" 127 - }, 128 - "lodash.snakecase@4.1.1": { 129 - "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" 130 - }, 131 - "lodash@4.17.21": { 132 - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 133 - }, 134 - "magic-bytes.js@1.12.1": { 135 - "integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==" 136 - }, 137 - "ts-mixer@6.0.4": { 138 - "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==" 139 - }, 140 - "tslib@2.8.1": { 141 - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" 142 - }, 143 - "undici-types@6.20.0": { 144 - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" 145 - }, 146 - "undici@6.21.1": { 147 - "integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==" 148 - }, 149 - "ws@8.18.1": { 150 - "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==" 151 - } 152 - }, 153 - "workspace": { 154 - "dependencies": [ 155 - "jsr:@std/fs@*", 156 - "jsr:@std/path@*", 157 - "npm:discord.js@*" 158 - ] 159 - } 160 - }
+10
docs/architecture.md
··· 1 + # Voidy Architecture Overview 2 + The Voidy architecture is a complete re-imagination of my previous bot's command and event organization architecture. 3 + 4 + Instead of relying on loose commands and events in respective top-level directories, the new approach groups all sorts of handlers into a single "module". 5 + 6 + And to allow even better handling of data, modules are managed by an even higher entity, "registries". 7 + 8 + Registries have a standalone database, used to store data of included modules. 9 + 10 + A more detailed explainer of each system can be found in the related markdown files.
+35
docs/architecture/top-level.md
··· 1 + # Top level architectural overview 2 + Anything happening before, or required by, our registries, is considered top-level architecture. 3 + 4 + This includes utilities such as the command and event handlers, which are consumed by lower levels of the system. 5 + 6 + We'll go through each of the top-level components below. 7 + 8 + ## Loaders 9 + Loaders are static classes, which provide utility methods for recursive loading of data from a data source, usually a directory. 10 + 11 + The constructor of a loader always takes one parameter, a string/path pointer to the desired data-source. 12 + 13 + Each loader additionally implements an asynchronous `collect` method for initial data collection. 14 + 15 + Finally, loaders provide various means of exporting data in supported formats, through methods like `getJSON`, `getCSV` and more... 16 + 17 + ### Event loader 18 + The event loader walks a directory and stores data from any file exporting an object that follows the Event type structure. 19 + 20 + ### Command loader 21 + The command loader walks a directory and stores data from any file exporting an object that follows the Command type structure. 22 + 23 + ## Handlers 24 + Handlers are static classes, which receive exported data from Loaders, though not directly, as Loader data is usually fetched by a Registry, and the Registry invokes a Handler to get data into our queue system, more on that later. 25 + 26 + Each handler has an `invoke` method, which takes JSON data, though it must always use the common exported JSON structure provided by Loaders. 27 + 28 + Any data filtering or mapping is then run in the background by the invoked Handler, which ultimately pushes data to the queue, and triggers a "handler::postInvoke" event afterward. 29 + 30 + @Todo: document Handler lifecycle events 31 + 32 + ## Lifecycle manager 33 + The Lifecycle manager is a simple event manager with a fancy name, it stores subscribers of events in a map, with the event name as the key. 34 + 35 + Additionally, it implements two very simple methods, notify - which fires lifecycle events, and subscribe - which subscribes a callback function to an event.
+19
package.json
··· 1 + { 2 + "name": "voidydiscord", 3 + "version": "3.0.0-alpha1", 4 + "module": "src/index.ts", 5 + "type": "module", 6 + "private": true, 7 + "scripts": { 8 + "dev": "bun --watch ." 9 + }, 10 + "devDependencies": { 11 + "@types/bun": "latest" 12 + }, 13 + "peerDependencies": { 14 + "typescript": "^5" 15 + }, 16 + "dependencies": { 17 + "discord.js": "^14.21.0" 18 + } 19 + }
+18
src/core/Loader.ts
··· 1 + export interface ILoader { 2 + store: object[]; 3 + 4 + collect: () => Promise<ThisType<this>>, 5 + getJSON: () => object, 6 + } 7 + 8 + export class Loader implements ILoader { 9 + public store = []; 10 + 11 + public async collect() { 12 + return this; 13 + } 14 + 15 + public getJSON() { 16 + return {}; 17 + } 18 + };
+3
src/core/Registry.ts
··· 1 + export interface IRegistry { } 2 + 3 + export class Registry { }
+16
src/core/VoidyClient.ts
··· 1 + import { Client, type ClientOptions } from "discord.js"; 2 + import { Registry } from "./Registry"; 3 + 4 + export class VoidyClient extends Client { 5 + public registries: Registry[]; 6 + 7 + public constructor(options: ClientOptions) { 8 + super(options); 9 + 10 + this.registries = [new Registry()]; 11 + } 12 + 13 + public start(token: string) { 14 + this.login(token); 15 + } 16 + }
-25
src/core/client.ts
··· 1 - import { Client, ClientOptions } from "discord.js"; 2 - import { FeatureRegistry } from "./registry.ts"; 3 - 4 - export class VoidyClient extends Client { 5 - public registry: FeatureRegistry; 6 - 7 - constructor(options: ClientOptions) { 8 - super(options); 9 - 10 - this.registry = new FeatureRegistry(this); 11 - } 12 - 13 - async start(token: string) { 14 - await this.registry.loadFeaturesFromDirectory("src/features"); 15 - 16 - this.once("ready", async () => { 17 - console.log(`✅ Logged in as ${this.user?.tag}`); 18 - 19 - await this.registry.deployCommands(); 20 - await this.registry.notifyReady(); 21 - }); 22 - 23 - await this.login(token); 24 - } 25 - }
-132
src/core/registry.ts
··· 1 - import { walk } from "@std/fs"; 2 - import { resolve } from "@std/path"; 3 - import { Interaction } from "discord.js"; 4 - import { Feature } from "./types.ts"; 5 - import { VoidyClient } from "./client.ts"; 6 - 7 - export class FeatureRegistry { 8 - private client: VoidyClient; 9 - private features = new Map<string, Feature>(); 10 - 11 - constructor(client: VoidyClient) { 12 - this.client = client; 13 - 14 - // Global interaction handler 15 - this.client.on("interactionCreate", async (interaction: Interaction) => { 16 - if (interaction.isChatInputCommand()) { 17 - for (const feature of this.features.values()) { 18 - for (const cmd of feature.commands ?? []) { 19 - if (cmd.data.name === interaction.commandName) { 20 - const context = { 21 - client: this.client, 22 - createCustomId: (id: string) => `${feature.id}:${id}`, 23 - }; 24 - 25 - return await cmd.execute(interaction, context); 26 - } 27 - } 28 - } 29 - } 30 - 31 - if (interaction.isButton()) { 32 - const [featureId, buttonId] = interaction.customId.split(":"); 33 - const feature = this.features.get(featureId); 34 - const buttonHandler = feature?.buttonHandlers?.get(buttonId); 35 - 36 - if (feature && buttonHandler) { 37 - const context = { 38 - client: this.client, 39 - createCustomId: (id: string) => `${feature.id}:${id}`, 40 - }; 41 - 42 - return await buttonHandler(interaction, context); 43 - } 44 - } 45 - }); 46 - 47 - // Global event handler 48 - for (const feature of this.features.values()) { 49 - for (const event of feature.events ?? []) { 50 - const context = { 51 - client: this.client, 52 - createCustomId: (id: string) => `${feature.id}:${id}`, 53 - }; 54 - 55 - if (event.once) { 56 - this.client.once(event.name, async (data) => { 57 - await event.execute(data, context); 58 - }); 59 - } else { 60 - this.client.on(event.name, async (data) => { 61 - await event.execute(data, context); 62 - }); 63 - } 64 - } 65 - } 66 - } 67 - 68 - async loadFeaturesFromDirectory(directory: string) { 69 - const root = resolve(Deno.cwd(), directory); 70 - 71 - for await (const entry of walk(root, { includeDirs: false })) { 72 - if (entry.name === "index.ts") { 73 - const module = await import("file://" + entry.path); 74 - const feature: Feature = module.default; 75 - 76 - if (!feature || !feature.id) { 77 - console.warn(`❌ Invalid feature at ${entry.path}`); 78 - continue; 79 - } 80 - 81 - if (this.features.has(feature.id)) { 82 - console.warn(`⚠ Feature ID conflict: ${feature.id}`); 83 - continue; 84 - } 85 - 86 - this.features.set(feature.id, feature); 87 - await feature.setup?.(); 88 - console.log(`🔹 Loaded feature: ${feature.name}`); 89 - } 90 - } 91 - 92 - // Log some statistics 93 - const featureCount = this.features.size; 94 - const eventsCount = Array.from(this.features.values()) 95 - .flatMap((f) => f.events ?? []) 96 - .length; 97 - const commandsCount = Array.from(this.features.values()) 98 - .flatMap((f) => f.commands ?? []) 99 - .length; 100 - 101 - console.log( 102 - `✅ Loaded ${featureCount} features, with ${eventsCount} events and ${commandsCount} commands.`, 103 - ); 104 - } 105 - 106 - async deployCommands() { 107 - const commands = Array.from(this.features.values()) 108 - .flatMap((f) => f.commands ?? []) 109 - .map((c) => c.data.toJSON()); 110 - 111 - await this.client.application?.commands.set(commands); 112 - console.log(`🚀 Deployed ${commands.length} slash commands.`); 113 - } 114 - 115 - async notifyReady() { 116 - for (const feature of this.features.values()) { 117 - await feature.onReady?.(); 118 - } 119 - } 120 - 121 - async cleanupAll() { 122 - for (const feature of this.features.values()) { 123 - try { 124 - await feature.cleanup?.(); 125 - } catch (err) { 126 - console.warn(`⚠ Error during cleanup of ${feature.id}:`, err); 127 - } 128 - } 129 - 130 - this.features.clear(); 131 - } 132 - }
-49
src/core/types.ts
··· 1 - import { 2 - ButtonInteraction, 3 - ChatInputCommandInteraction, 4 - SlashCommandBuilder, 5 - SlashCommandOptionsOnlyBuilder, 6 - } from "discord.js"; 7 - import { VoidyClient } from "./client.ts"; 8 - 9 - export interface Command { 10 - data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder; 11 - execute: ( 12 - interaction: ChatInputCommandInteraction, 13 - context: FeatureContext, 14 - ) => Promise<void>; 15 - } 16 - 17 - export interface Event { 18 - name: string; 19 - once?: boolean; 20 - execute: ( 21 - data: object, 22 - context: FeatureContext, 23 - ) => Promise<void> | void; 24 - } 25 - 26 - export type ButtonHandler = ( 27 - interaction: ButtonInteraction, 28 - context: FeatureContext, 29 - ) => Promise<void>; 30 - 31 - export interface Feature { 32 - id: string; 33 - name: string; 34 - description?: string; 35 - 36 - commands?: Command[]; 37 - events?: Event[]; 38 - buttonHandlers?: Map<string, ButtonHandler>; 39 - 40 - // Optional lifecycle hooks 41 - setup?: () => Promise<void>; 42 - onReady?: () => Promise<void>; 43 - cleanup?: () => Promise<void>; 44 - } 45 - 46 - export interface FeatureContext { 47 - client: VoidyClient; 48 - createCustomId: (id: string) => string; 49 - }
-125
src/features/maintenance/commands.ts
··· 1 - import { 2 - CommandInteraction, 3 - ContainerBuilder, 4 - MediaGalleryBuilder, 5 - MediaGalleryItemBuilder, 6 - MessageFlags, 7 - PermissionFlagsBits, 8 - SlashCommandBuilder, 9 - TextDisplayBuilder, 10 - } from "discord.js"; 11 - import type { Command, FeatureContext } from "../../core/types.ts"; 12 - import { inspect } from "node:util"; 13 - 14 - export const reloadCommand: Command = { 15 - data: new SlashCommandBuilder() 16 - .setName("reload") 17 - .setDescription("Reload all features (admin only)") 18 - .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), 19 - 20 - execute: async (interaction: CommandInteraction, context: FeatureContext) => { 21 - await interaction.deferReply({ ephemeral: true }); 22 - 23 - try { 24 - const registry = context.client.registry; 25 - 26 - await interaction.editReply("🧹 Cleaning up features..."); 27 - await registry.cleanupAll(); 28 - 29 - console.log("🔍 Reloading features..."); 30 - await interaction.editReply("🔍 Reloading features..."); 31 - await registry.loadFeaturesFromDirectory("src/features"); 32 - 33 - await interaction.editReply("📡 Re-deploying commands..."); 34 - await registry.deployCommands(); 35 - 36 - await interaction.editReply("✅ Notifying features..."); 37 - await registry.notifyReady(); 38 - 39 - await interaction.editReply("✅ Features reloaded successfully."); 40 - } catch (err) { 41 - console.error("❌ Reload failed:", err); 42 - await interaction.editReply("❌ Reload failed. Check logs."); 43 - } 44 - }, 45 - }; 46 - 47 - const OWNER_IDS = 48 - Deno.env.get("BOT_ADMINS")?.split(",").map((id) => id.trim()) ?? []; 49 - 50 - export const evalCommand: Command = { 51 - data: new SlashCommandBuilder() 52 - .setName("eval") 53 - .setDescription("Execute JavaScript code (owner only)") 54 - .addStringOption((opt) => 55 - opt.setName("code").setDescription("JS code to execute").setRequired(true) 56 - ) 57 - .addBooleanOption((opt) => 58 - opt.setName("is_image").setDescription( 59 - "Whether to expect an image", 60 - ) 61 - ) as SlashCommandBuilder, 62 - 63 - execute: async ( 64 - interaction: CommandInteraction, 65 - _context: FeatureContext, 66 - ) => { 67 - if (!interaction.isChatInputCommand()) return; 68 - const userId = interaction.user.id; 69 - 70 - if (!OWNER_IDS.includes(userId)) { 71 - await interaction.reply({ 72 - content: "❌ You are not authorized to use this.", 73 - flags: [MessageFlags.Ephemeral], 74 - }); 75 - 76 - return; 77 - } 78 - 79 - const code = interaction.options.getString("code", true); 80 - 81 - try { 82 - const result = await eval(`(async () => { ${code} })()`); 83 - const output = inspect(result, { depth: 1 }); 84 - 85 - const headerText = new TextDisplayBuilder() 86 - .setContent("✅ Eval Success"); 87 - 88 - const contentText = new TextDisplayBuilder() 89 - .setContent("```js\n" + output + "\n```"); 90 - 91 - const container = new ContainerBuilder() 92 - .addTextDisplayComponents([headerText, contentText]); 93 - 94 - if (interaction.options.getBoolean("is_image", false)) { 95 - const galleryItemComponent = new MediaGalleryItemBuilder().setURL( 96 - output.split("'")[1], 97 - ); 98 - const galleryComponent = new MediaGalleryBuilder().addItems( 99 - galleryItemComponent, 100 - ); 101 - 102 - container.addMediaGalleryComponents(galleryComponent); 103 - } 104 - 105 - await interaction.reply({ 106 - components: [container], 107 - flags: [MessageFlags.IsComponentsV2, MessageFlags.Ephemeral], 108 - }); 109 - } catch (err) { 110 - const headerText = new TextDisplayBuilder() 111 - .setContent("❌ Eval Error"); 112 - 113 - const contentText = new TextDisplayBuilder() 114 - .setContent("```js\n" + err?.toString() + "\n```"); 115 - 116 - const container = new ContainerBuilder() 117 - .addTextDisplayComponents([headerText, contentText]); 118 - 119 - await interaction.reply({ 120 - components: [container], 121 - flags: [MessageFlags.IsComponentsV2, MessageFlags.Ephemeral], 122 - }); 123 - } 124 - }, 125 - };
-11
src/features/maintenance/index.ts
··· 1 - import type { Feature } from "../../core/types.ts"; 2 - import { evalCommand, reloadCommand } from "./commands.ts"; 3 - 4 - const MaintenanceFeature: Feature = { 5 - id: "maintenance", 6 - name: "Maintenance Tools", 7 - 8 - commands: [reloadCommand, evalCommand], 9 - }; 10 - 11 - export default MaintenanceFeature;
-21
src/features/statistics/index.ts
··· 1 - import type { Feature, FeatureContext } from "../../core/types.ts"; 2 - 3 - const messageCreateEvent = { 4 - name: "messageCreate", 5 - 6 - execute(_data: object, context: FeatureContext) { 7 - console.log( 8 - `messageCreate event executed in statistics feature, by ${context.client?.user?.tag}`, 9 - ); 10 - }, 11 - }; 12 - 13 - const StatisticsFeature: Feature = { 14 - id: "statistics", 15 - name: "Statistics", 16 - 17 - commands: [], 18 - events: [messageCreateEvent], 19 - }; 20 - 21 - export default StatisticsFeature;
-129
src/features/utility/commands.ts
··· 1 - import { 2 - ButtonBuilder, 3 - ButtonStyle, 4 - ChatInputCommandInteraction, 5 - CommandInteraction, 6 - ContainerBuilder, 7 - MessageFlags, 8 - SectionBuilder, 9 - SlashCommandBuilder, 10 - TextDisplayBuilder, 11 - } from "discord.js"; 12 - import type { Command, FeatureContext } from "../../core/types.ts"; 13 - 14 - export const pingCommand: Command = { 15 - data: new SlashCommandBuilder() 16 - .setName("ping") 17 - .setDescription("Replies with Pong and a Refresh button"), 18 - 19 - execute: async (interaction: CommandInteraction, context: FeatureContext) => { 20 - const button = new ButtonBuilder() 21 - .setCustomId(context.createCustomId("refresh")) 22 - .setLabel("🔁 Refresh") 23 - .setStyle(ButtonStyle.Primary); 24 - 25 - const headerTitle = new TextDisplayBuilder() 26 - .setContent(`🏓 Pong! ${context.client.ws.ping}ms`); 27 - 28 - const headerSection = new SectionBuilder() 29 - .addTextDisplayComponents([headerTitle]) 30 - .setButtonAccessory(button); 31 - 32 - const container = new ContainerBuilder() 33 - .addSectionComponents([headerSection]); 34 - 35 - await interaction.reply({ 36 - components: [container], 37 - flags: [MessageFlags.IsComponentsV2], 38 - }); 39 - }, 40 - }; 41 - 42 - export const uploadCommand: Command = { 43 - data: new SlashCommandBuilder() 44 - .setName("upload") 45 - .setDescription("Uploads an image to the contest API") 46 - .addAttachmentOption((option) => 47 - option 48 - .setName("image") 49 - .setDescription("The image to upload") 50 - .setRequired(true) 51 - ) 52 - .addIntegerOption((option) => 53 - option 54 - .setName("contest_id") 55 - .setDescription("The contest ID") 56 - .setRequired(true) 57 - ) 58 - .addStringOption((option) => 59 - option 60 - .setName("participant_name") 61 - .setDescription("The name of the participant") 62 - .setRequired(true) 63 - ) 64 - .addStringOption((option) => 65 - option 66 - .setName("participant_email") 67 - .setDescription("The email of the participant") 68 - .setRequired(true) 69 - ), 70 - 71 - execute: async ( 72 - interaction: ChatInputCommandInteraction, 73 - _context: FeatureContext, 74 - ) => { 75 - const attachment = interaction.options.getAttachment("image", true); 76 - const contestId = interaction.options.getInteger("contest_id", true); 77 - const participantName = interaction.options.getString( 78 - "participant_name", 79 - true, 80 - ); 81 - const participantEmail = interaction.options.getString( 82 - "participant_email", 83 - true, 84 - ); 85 - 86 - await interaction.deferReply({ ephemeral: true }); 87 - 88 - try { 89 - const fileResponse = await fetch(attachment.url); 90 - const fileBuffer = await fileResponse.arrayBuffer(); 91 - 92 - const form = new FormData(); 93 - form.append("image", new Blob([fileBuffer])); 94 - form.append("contest_id", contestId.toString()); 95 - form.append("participant_name", participantName); 96 - form.append("participant_email", participantEmail); 97 - 98 - const res = await fetch("http://localhost:8000/api/upload-drawing", { 99 - method: "POST", 100 - body: form, 101 - }); 102 - 103 - if (!res.ok) { 104 - const error = await res.text(); 105 - console.log(error); 106 - 107 - await interaction.editReply({ 108 - content: `❌ Upload failed, check console for details.`, 109 - }); 110 - 111 - return; 112 - } 113 - 114 - const result = await res.json(); 115 - await interaction.editReply({ 116 - content: `✅ Image uploaded successfully! 117 - Participant ID: ${result.participant_id ?? "(not returned)"} 118 - Submission ID: ${result.submission_id ?? "(not returned)"}`, 119 - }); 120 - } catch (err) { 121 - console.error(err); 122 - await interaction.editReply({ 123 - content: `❌ An error occurred during upload.`, 124 - }); 125 - 126 - return; 127 - } 128 - }, 129 - };
-15
src/features/utility/index.ts
··· 1 - import type { Feature } from "../../core/types.ts"; 2 - import { pingCommand, uploadCommand } from "./commands.ts"; 3 - import { refreshButton } from "./interactions.ts"; 4 - 5 - const UtilityFeature: Feature = { 6 - id: "utility", 7 - name: "Utility Commands", 8 - 9 - commands: [pingCommand, uploadCommand], 10 - buttonHandlers: new Map([ 11 - ["refresh", refreshButton], 12 - ]), 13 - }; 14 - 15 - export default UtilityFeature;
-35
src/features/utility/interactions.ts
··· 1 - import { 2 - ButtonBuilder, 3 - ButtonInteraction, 4 - ButtonStyle, 5 - ContainerBuilder, 6 - MessageFlags, 7 - SectionBuilder, 8 - TextDisplayBuilder, 9 - } from "discord.js"; 10 - import { FeatureContext } from "../../core/types.ts"; 11 - 12 - export const refreshButton = async ( 13 - interaction: ButtonInteraction, 14 - context: FeatureContext, 15 - ): Promise<void> => { 16 - const button = new ButtonBuilder() 17 - .setCustomId(context.createCustomId("refresh")) 18 - .setLabel("🔁 Refresh") 19 - .setStyle(ButtonStyle.Primary); 20 - 21 - const headerTitle = new TextDisplayBuilder() 22 - .setContent(`🏓 Pong! ${context.client.ws.ping}ms`); 23 - 24 - const headerSection = new SectionBuilder() 25 - .addTextDisplayComponents([headerTitle]) 26 - .setButtonAccessory(button); 27 - 28 - const container = new ContainerBuilder() 29 - .addSectionComponents([headerSection]); 30 - 31 - await interaction.update({ 32 - components: [container], 33 - flags: [MessageFlags.IsComponentsV2], 34 - }); 35 - };
+14
src/index.ts
··· 1 + import { GatewayIntentBits } from "discord.js" 2 + import { VoidyClient } from "./core/VoidyClient" 3 + 4 + // Client initialization with intents and stuff... 5 + const client = new VoidyClient({ 6 + intents: [GatewayIntentBits.Guilds], 7 + }) 8 + 9 + // Token validation and client start 10 + if (!Bun.env.BOT_TOKEN) throw new Error("[Voidy] Missing bot token"); 11 + client.start(Bun.env.BOT_TOKEN); 12 + 13 + // @Todo: Remove after core registry implementation is complete 14 + console.log(client.registries[0]);
-8
src/main.ts
··· 1 - import { GatewayIntentBits } from "discord.js"; 2 - import { VoidyClient } from "./core/client.ts"; 3 - 4 - const client = new VoidyClient({ 5 - intents: [GatewayIntentBits.Guilds], 6 - }); 7 - 8 - await client.start(Deno.env.get("BOT_TOKEN")!);
+28
tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + // Environment setup & latest features 4 + "lib": [ 5 + "ESNext" 6 + ], 7 + "target": "ESNext", 8 + "module": "Preserve", 9 + "moduleDetection": "force", 10 + "jsx": "react-jsx", 11 + "allowJs": true, 12 + // Bundler mode 13 + "moduleResolution": "bundler", 14 + "allowImportingTsExtensions": true, 15 + "verbatimModuleSyntax": true, 16 + "noEmit": true, 17 + // Best practices 18 + "strict": true, 19 + "skipLibCheck": true, 20 + "noFallthroughCasesInSwitch": true, 21 + "noUncheckedIndexedAccess": true, 22 + "noImplicitOverride": true, 23 + // Some stricter flags (disabled by default) 24 + "noUnusedLocals": false, 25 + "noUnusedParameters": false, 26 + "noPropertyAccessFromIndexSignature": false 27 + } 28 + }