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

✨🗃️ ️Add user module and UserConfig Schema

+3
.oxfmtrc.jsonc
··· 1 + { 2 + "$schema": "./node_modules/oxfmt/configuration_schema.json", 3 + }
+2
README.md
··· 4 4 <br> 5 5 6 6 ## 🚀 Deployment 7 + 7 8 Deploying this project is extremely simple. First, clone the repository: 8 9 9 10 ```sh ··· 24 25 ``` 25 26 26 27 ## 🎨 Credits 28 + 27 29 Certain features of this bot were the result 28 30 of conversations with: 29 31
+76 -12
bun.lock
··· 4 4 "workspaces": { 5 5 "": { 6 6 "name": "voidydiscord", 7 + "devDependencies": { 8 + "oxfmt": "^0.17.0", 9 + }, 7 10 }, 8 11 "packages/bot": { 9 12 "name": "@voidy/bot", 10 13 "version": "0.1.0", 11 14 "dependencies": { 12 15 "@voidy/framework": "workspace:*", 13 - "discord.js": "^14.21.0", 16 + "discord.js": "^14.25.1", 17 + "mongoose": "^9.0.1", 14 18 }, 15 19 "devDependencies": { 16 20 "@types/bun": "latest", 17 21 }, 18 22 "peerDependencies": { 19 - "typescript": "^5", 23 + "typescript": "^5.9.2", 20 24 }, 21 25 }, 22 26 "packages/framework": { 23 27 "name": "@voidy/framework", 24 28 "version": "0.1.0", 25 29 "dependencies": { 26 - "discord.js": "^14.21.0", 30 + "discord.js": "^14.25.1", 27 31 }, 28 32 "devDependencies": { 29 33 "@types/bun": "latest", 30 34 }, 31 35 "peerDependencies": { 32 - "typescript": "^5", 36 + "typescript": "^5.9.2", 33 37 }, 34 38 }, 35 39 }, 36 40 "packages": { 37 - "@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=="], 41 + "@discordjs/builders": ["@discordjs/builders@1.13.1", "", { "dependencies": { "@discordjs/formatters": "^0.6.2", "@discordjs/util": "^1.2.0", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.33", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w=="], 38 42 39 43 "@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="], 40 44 41 - "@discordjs/formatters": ["@discordjs/formatters@0.6.1", "", { "dependencies": { "discord-api-types": "^0.38.1" } }, "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg=="], 45 + "@discordjs/formatters": ["@discordjs/formatters@0.6.2", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ=="], 42 46 43 - "@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=="], 47 + "@discordjs/rest": ["@discordjs/rest@2.6.0", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.16", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w=="], 44 48 45 - "@discordjs/util": ["@discordjs/util@1.1.1", "", {}, "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g=="], 49 + "@discordjs/util": ["@discordjs/util@1.2.0", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg=="], 46 50 47 51 "@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=="], 48 52 53 + "@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.4.0", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-ZHzx7Z3rdlWL1mECydvpryWN/ETXJiCxdgQKTAH+djzIPe77HdnSizKBDi1TVDXZjXyOj2IqEG/vPw71ULF06w=="], 54 + 55 + "@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.17.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-OMv0tOb+xiwSZKjYbM6TwMSP5QwFJlBGQmEsk98QJ30sHhdyC//0UvGKuR0KZuzZW4E0+k0rHDmos1Z5DmBEkA=="], 56 + 57 + "@oxfmt/darwin-x64": ["@oxfmt/darwin-x64@0.17.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-trzidyzryKIdL/cLCYU9IwprgJegVBUrz1rqzOMe5is+qdgH/RxTCvhYUNFzxRHpil3g4QUYd2Ja831tc5Nehg=="], 58 + 59 + "@oxfmt/linux-arm64-gnu": ["@oxfmt/linux-arm64-gnu@0.17.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-KlwzidgvHznbUaaglZT1goTS30osTV553pfbKve9B1PyTDkluNDfm/polOaf3SVLN7wL/NNLFZRMupvJ1eJXAw=="], 60 + 61 + "@oxfmt/linux-arm64-musl": ["@oxfmt/linux-arm64-musl@0.17.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-+tbYJTocF4BNLaQQbc/xrBWTNgiU6zmYeF4NvRDxuuQjDOnmUZPn0EED3PZBRJyg4/YllhplHDo8x+gfcb9G3A=="], 62 + 63 + "@oxfmt/linux-x64-gnu": ["@oxfmt/linux-x64-gnu@0.17.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pEmv7zJIw2HpnA4Tn1xrfJNGi2wOH2+usT14Pkvf/c5DdB+pOir6k/5jzfe70+V3nEtmtV9Lm+spndN/y6+X7A=="], 64 + 65 + "@oxfmt/linux-x64-musl": ["@oxfmt/linux-x64-musl@0.17.0", "", { "os": "linux", "cpu": "x64" }, "sha512-+DrFSCZWyFdtEAWR5xIBTV8GX0RA9iB+y7ZlJPRAXrNG8TdBY9vc7/MIGolIgrkMPK4mGMn07YG/qEyPY+iKaw=="], 66 + 67 + "@oxfmt/win32-arm64": ["@oxfmt/win32-arm64@0.17.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-FoUZRR7mVpTYIaY/qz2BYwzqMnL+HsUxmMWAIy6nl29UEkDgxNygULJ4rIGY4/Axne41fhtldLrSGBOpwNm3jA=="], 68 + 69 + "@oxfmt/win32-x64": ["@oxfmt/win32-x64@0.17.0", "", { "os": "win32", "cpu": "x64" }, "sha512-fBIcUpHmCwf3leWlo0cYwLb9Pd2mzxQlZYJX9dD9nylPvsxOnsy9fmsaflpj34O0JbQJN3Y0SRkoaCcHHlxFww=="], 70 + 49 71 "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="], 50 72 51 73 "@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="], 52 74 53 75 "@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="], 54 76 55 - "@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="], 77 + "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], 56 78 57 79 "@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="], 80 + 81 + "@types/webidl-conversions": ["@types/webidl-conversions@7.0.3", "", {}, "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="], 82 + 83 + "@types/whatwg-url": ["@types/whatwg-url@13.0.0", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q=="], 58 84 59 85 "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], 60 86 ··· 64 90 65 91 "@voidy/framework": ["@voidy/framework@workspace:packages/framework"], 66 92 67 - "bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], 93 + "bson": ["bson@7.0.0", "", {}, "sha512-Kwc6Wh4lQ5OmkqqKhYGKIuELXl+EPYSCObVE6bWsp1T/cGkOCBN0I8wF/T44BiuhHyNi1mmKVPXk60d41xZ7kw=="], 68 94 69 - "discord-api-types": ["discord-api-types@0.38.18", "", {}, "sha512-ygenySjZKUaBf5JT8BNhZSxLzwpwdp41O0wVroOTu/N2DxFH7dxYTZUSnFJ6v+/2F3BMcnD47PC47u4aLOLxrQ=="], 95 + "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], 96 + 97 + "discord-api-types": ["discord-api-types@0.38.37", "", {}, "sha512-Cv47jzY1jkGkh5sv0bfHYqGgKOWO1peOrGMkDFM4UmaGMOTgOW8QSexhvixa9sVOiz8MnVOBryWYyw/CEVhj7w=="], 70 98 71 - "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=="], 99 + "discord.js": ["discord.js@14.25.1", "", { "dependencies": { "@discordjs/builders": "^1.13.0", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.2", "@discordjs/rest": "^2.6.0", "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.33", "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-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g=="], 72 100 73 101 "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], 102 + 103 + "kareem": ["kareem@3.0.0", "", {}, "sha512-RKhaOBSPN8L7y4yAgNhDT2602G5FD6QbOIISbjN9D6mjHPeqeg7K+EB5IGSU5o81/X2Gzm3ICnAvQW3x3OP8HA=="], 74 104 75 105 "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], 76 106 ··· 78 108 79 109 "magic-bytes.js": ["magic-bytes.js@1.12.1", "", {}, "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA=="], 80 110 111 + "memory-pager": ["memory-pager@1.5.0", "", {}, "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="], 112 + 113 + "mongodb": ["mongodb@7.0.0", "", { "dependencies": { "@mongodb-js/saslprep": "^1.3.0", "bson": "^7.0.0", "mongodb-connection-string-url": "^7.0.0" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.806.0", "@mongodb-js/zstd": "^7.0.0", "gcp-metadata": "^7.0.1", "kerberos": "^7.0.0", "mongodb-client-encryption": ">=7.0.0 <7.1.0", "snappy": "^7.3.2", "socks": "^2.8.6" }, "optionalPeers": ["@aws-sdk/credential-providers", "@mongodb-js/zstd", "gcp-metadata", "kerberos", "mongodb-client-encryption", "snappy", "socks"] }, "sha512-vG/A5cQrvGGvZm2mTnCSz1LUcbOPl83hfB6bxULKQ8oFZauyox/2xbZOoGNl+64m8VBrETkdGCDBdOsCr3F3jg=="], 114 + 115 + "mongodb-connection-string-url": ["mongodb-connection-string-url@7.0.0", "", { "dependencies": { "@types/whatwg-url": "^13.0.0", "whatwg-url": "^14.1.0" } }, "sha512-irhhjRVLE20hbkRl4zpAYLnDMM+zIZnp0IDB9akAFFUZp/3XdOfwwddc7y6cNvF2WCEtfTYRwYbIfYa2kVY0og=="], 116 + 117 + "mongoose": ["mongoose@9.0.1", "", { "dependencies": { "kareem": "3.0.0", "mongodb": "~7.0", "mpath": "0.9.0", "mquery": "6.0.0", "ms": "2.1.3", "sift": "17.1.3" } }, "sha512-aHPfQx2YX5UwAmMVud7OD4lIz9AEO4jI+oDnRh3lPZq9lrKTiHmOzszVffDMyQHXvrf4NXsJ34kpmAhyYAZGbw=="], 118 + 119 + "mpath": ["mpath@0.9.0", "", {}, "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew=="], 120 + 121 + "mquery": ["mquery@6.0.0", "", {}, "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw=="], 122 + 123 + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 124 + 125 + "oxfmt": ["oxfmt@0.17.0", "", { "optionalDependencies": { "@oxfmt/darwin-arm64": "0.17.0", "@oxfmt/darwin-x64": "0.17.0", "@oxfmt/linux-arm64-gnu": "0.17.0", "@oxfmt/linux-arm64-musl": "0.17.0", "@oxfmt/linux-x64-gnu": "0.17.0", "@oxfmt/linux-x64-musl": "0.17.0", "@oxfmt/win32-arm64": "0.17.0", "@oxfmt/win32-x64": "0.17.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-12Rmq2ub61rUZ3Pqnsvmo99rRQ6hQJwQsjnFnbvXYLMrlIsWT6SFVsrjAkBBrkXXSHv8ePIpKQ0nZph5KDrOqw=="], 126 + 127 + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], 128 + 129 + "sift": ["sift@17.1.3", "", {}, "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ=="], 130 + 131 + "sparse-bitfield": ["sparse-bitfield@3.0.3", "", { "dependencies": { "memory-pager": "^1.0.2" } }, "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ=="], 132 + 133 + "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], 134 + 81 135 "ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="], 82 136 83 137 "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], ··· 88 142 89 143 "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], 90 144 145 + "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], 146 + 147 + "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], 148 + 91 149 "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=="], 92 150 93 151 "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], 94 152 95 153 "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], 154 + 155 + "@discordjs/ws/@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=="], 156 + 157 + "@discordjs/ws/@discordjs/util": ["@discordjs/util@1.1.1", "", {}, "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g=="], 158 + 159 + "@discordjs/ws/discord-api-types": ["discord-api-types@0.38.18", "", {}, "sha512-ygenySjZKUaBf5JT8BNhZSxLzwpwdp41O0wVroOTu/N2DxFH7dxYTZUSnFJ6v+/2F3BMcnD47PC47u4aLOLxrQ=="], 96 160 } 97 161 }
+3
package.json
··· 5 5 ], 6 6 "scripts": { 7 7 "dev": "cd packages/bot && bun dev" 8 + }, 9 + "devDependencies": { 10 + "oxfmt": "^0.17.0" 8 11 } 9 12 }
+10
packages/bot/docker-compose.yml
··· 1 + # Don't change these development defaults, use a docker-compose.override.yml file instead :) 2 + services: 3 + mongo: 4 + image: mongo:latest 5 + container_name: voidy_db 6 + ports: 7 + - "27017:27017" 8 + environment: 9 + MONGO_INITDB_ROOT_USERNAME: voidy 10 + MONGO_INITDB_ROOT_PASSWORD: voidy
+19 -18
packages/bot/package.json
··· 1 1 { 2 - "name": "@voidy/bot", 3 - "version": "0.1.0", 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 - "@voidy/framework": "workspace:*", 18 - "discord.js": "^14.21.0" 19 - } 2 + "name": "@voidy/bot", 3 + "version": "0.1.0", 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.9.2" 15 + }, 16 + "dependencies": { 17 + "@voidy/framework": "workspace:*", 18 + "discord.js": "^14.25.1", 19 + "mongoose": "^9.0.1" 20 + } 20 21 }
+12 -3
packages/bot/src/index.ts
··· 1 - import { GatewayIntentBits } from "discord.js" 1 + import { GatewayIntentBits } from "discord.js"; 2 2 import { VoidyClient } from "@voidy/framework"; 3 + import { connect } from "mongoose"; 3 4 4 5 // Client initialization with intents and stuff... 5 6 const client = new VoidyClient({ 6 - intents: [GatewayIntentBits.Guilds], 7 - }) 7 + intents: [GatewayIntentBits.Guilds], 8 + }); 9 + 10 + // Database URI validation and connection check 11 + if (!Bun.env.DB_URI) throw new Error("[Voidy] Missing database URI"); 12 + await connect(Bun.env.DB_URI).then(() => { 13 + console.log("Connected to database"); 14 + }).catch((error) => { 15 + console.error("Failed to connect to database:", error); 16 + }); 8 17 9 18 // Token validation and client start 10 19 if (!Bun.env.BOT_TOKEN) throw new Error("[Voidy] Missing bot token");
+20 -23
packages/bot/src/modules/core/commands/api/httpcat.ts
··· 2 2 import type { Command } from "@voidy/framework"; 3 3 4 4 export default { 5 - id: "api.httpcat", 6 - data: new SlashCommandSubcommandBuilder() 7 - .setName("httpcat") 8 - .setDescription("Display a cat from the https://http.cat API.") 9 - .addStringOption(option => option 10 - .setName("code") 11 - .setDescription("The desired HTTP status code.") 12 - .setRequired(true) 13 - ) 14 - .addBooleanOption(option => option 15 - .setName("ephemeral") 16 - .setDescription("Whether to publicly share the bot response") 17 - ), 5 + id: "api.httpcat", 6 + data: new SlashCommandSubcommandBuilder() 7 + .setName("httpcat") 8 + .setDescription("Display a cat from the https://http.cat API.") 9 + .addStringOption((option) => 10 + option.setName("code").setDescription("The desired HTTP status code.").setRequired(true), 11 + ) 12 + .addBooleanOption((option) => 13 + option.setName("ephemeral").setDescription("Whether to publicly share the bot response"), 14 + ), 18 15 19 - execute: async (interaction, _client) => { 20 - const { options } = interaction; 16 + execute: async (interaction, _client) => { 17 + const { options } = interaction; 21 18 22 - const httpCode = options.getString("code"); 23 - const ephemeral = options.getBoolean("ephemeral") ?? true; 19 + const httpCode = options.getString("code"); 20 + const ephemeral = options.getBoolean("ephemeral") ?? true; 24 21 25 - await interaction.reply({ 26 - files: [`https://http.cat/${httpCode}.jpg`], 27 - flags: ephemeral ? [MessageFlags.Ephemeral] : [] 28 - }); 29 - } 30 - } as Command 22 + await interaction.reply({ 23 + files: [`https://http.cat/${httpCode}.jpg`], 24 + flags: ephemeral ? [MessageFlags.Ephemeral] : [], 25 + }); 26 + }, 27 + } as Command;
+11 -11
packages/bot/src/modules/core/commands/ping.ts
··· 2 2 import type { Command } from "@voidy/framework"; 3 3 4 4 export default { 5 - id: "ping", 6 - data: new SlashCommandBuilder() 7 - .setName("ping") 8 - .setDescription("View the websocket ping between Discord and the Bot."), 5 + id: "ping", 6 + data: new SlashCommandBuilder() 7 + .setName("ping") 8 + .setDescription("View the websocket ping between Discord and the Bot."), 9 9 10 - execute: async (interaction, client) => { 11 - await interaction.reply({ 12 - content: `The current websocket ping is at ${client.ws.ping}`, 13 - flags: [MessageFlags.Ephemeral] 14 - }); 15 - } 16 - } as Command 10 + execute: async (interaction, client) => { 11 + await interaction.reply({ 12 + content: `The current websocket ping is at ${client.ws.ping}`, 13 + flags: [MessageFlags.Ephemeral], 14 + }); 15 + }, 16 + } as Command;
+46 -44
packages/bot/src/modules/core/events/interactionCreate.ts
··· 1 1 import { Events, MessageFlags, type Interaction } from "discord.js"; 2 2 import { 3 - ChatInputCommandHandler, 4 - ButtonHandler, 5 - type VoidyClient, 6 - type Event 3 + ChatInputCommandHandler, 4 + ButtonHandler, 5 + type VoidyClient, 6 + type Event, 7 7 } from "@voidy/framework"; 8 8 9 9 export default { 10 - id: "interactionCreate", 11 - name: Events.InteractionCreate, 12 - execute: async (client: VoidyClient, interaction: Interaction) => { 13 - if (interaction.isChatInputCommand() && interaction.isCommand()) { 14 - // Set the top-level command name 15 - let commandId = interaction.commandName; 10 + id: "interactionCreate", 11 + name: Events.InteractionCreate, 12 + execute: async (client: VoidyClient, interaction: Interaction) => { 13 + if (interaction.isChatInputCommand() && interaction.isCommand()) { 14 + // Set the top-level command name 15 + let commandId = interaction.commandName; 16 16 17 - // Try to get a subgroup first 18 - const subgroup = interaction.options.getSubcommandGroup(false); 19 - if (subgroup) commandId += `.${subgroup}`; 17 + // Try to get a subgroup first 18 + const subgroup = interaction.options.getSubcommandGroup(false); 19 + if (subgroup) commandId += `.${subgroup}`; 20 20 21 - // Then subcommand (or subcommand in a group) 22 - const subcommand = interaction.options.getSubcommand(false); 23 - if (subcommand) commandId += `.${subcommand}`; 21 + // Then subcommand (or subcommand in a group) 22 + const subcommand = interaction.options.getSubcommand(false); 23 + if (subcommand) commandId += `.${subcommand}`; 24 24 25 - const command = client.commands.get(commandId); 25 + const command = client.commands.get(commandId); 26 26 27 - if (!command) return interaction.reply({ 28 - content: `Sorry, but the command ${interaction.commandName} could not be located in my command cache >:3`, 29 - flags: [MessageFlags.Ephemeral] 30 - }); 27 + if (!command) 28 + return interaction.reply({ 29 + content: `Sorry, but the command ${interaction.commandName} could not be located in my command cache >:3`, 30 + flags: [MessageFlags.Ephemeral], 31 + }); 31 32 32 - ChatInputCommandHandler.invoke(interaction, command, client); 33 - } else if (interaction.isButton()) { 34 - // Filter the client button cache to locate the invoked button 35 - const button = client.buttons.get(interaction.customId); 33 + ChatInputCommandHandler.invoke(interaction, command, client); 34 + } else if (interaction.isButton()) { 35 + // Filter the client button cache to locate the invoked button 36 + const button = client.buttons.get(interaction.customId); 36 37 37 - if (!button) return interaction.reply({ 38 - content: `Sorry, but the button ${interaction.customId} could not be located in my button cache >:3`, 39 - flags: [MessageFlags.Ephemeral] 40 - }); 38 + if (!button) 39 + return interaction.reply({ 40 + content: `Sorry, but the button ${interaction.customId} could not be located in my button cache >:3`, 41 + flags: [MessageFlags.Ephemeral], 42 + }); 41 43 42 - ButtonHandler.invoke(interaction, button, client); 43 - } else { 44 - let dmChannel = interaction.user.dmChannel; 44 + ButtonHandler.invoke(interaction, button, client); 45 + } else { 46 + let dmChannel = interaction.user.dmChannel; 45 47 46 - // Attempt DM channel creation, if not found. 47 - if (!dmChannel) { 48 - dmChannel = await interaction.user.createDM(); 49 - } 48 + // Attempt DM channel creation, if not found. 49 + if (!dmChannel) { 50 + dmChannel = await interaction.user.createDM(); 51 + } 50 52 51 - // If the DM channel is still not available, give up. 52 - if (!dmChannel || !dmChannel.isSendable()) return; 53 + // If the DM channel is still not available, give up. 54 + if (!dmChannel || !dmChannel.isSendable()) return; 53 55 54 - dmChannel.send({ 55 - content: `Sorry, but your last interaction wasn't successful and has been logged as an error case, for debugging purposes.\n\nIf you have any additional information to share with us, please communicate with the bot within this DM channel, and we will get in contact.\n\nThank you for understanding, and have a great day :3`, 56 - }) 57 - } 58 - } 59 - } as Event 56 + dmChannel.send({ 57 + content: `Sorry, but your last interaction wasn't successful and has been logged as an error case, for debugging purposes.\n\nIf you have any additional information to share with us, please communicate with the bot within this DM channel, and we will get in contact.\n\nThank you for understanding, and have a great day :3`, 58 + }); 59 + } 60 + }, 61 + } as Event;
+10 -10
packages/bot/src/modules/core/events/ready.ts
··· 2 2 import { ActivityType, Events } from "discord.js"; 3 3 4 4 export default { 5 - id: "ready", 6 - name: Events.ClientReady, 7 - once: true, 8 - execute: async (client: VoidyClient) => { 9 - console.log("THE BOT IS READY >:3"); 5 + id: "clientReady", 6 + name: Events.ClientReady, 7 + once: true, 8 + execute: async (client: VoidyClient) => { 9 + console.log("THE BOT IS READY >:3"); 10 10 11 - client.user?.setActivity({ 12 - name: `${client.guilds.cache.size} guilds :3`, 13 - type: ActivityType.Watching 14 - }); 15 - } 11 + client.user?.setActivity({ 12 + name: `${client.guilds.cache.size} guilds :3`, 13 + type: ActivityType.Watching, 14 + }); 15 + }, 16 16 } as Event;
+15 -20
packages/bot/src/modules/core/module.ts
··· 1 - import { 2 - ButtonLoader, 3 - CommandLoader, 4 - EventLoader, 5 - type Module 6 - } from "@voidy/framework"; 1 + import { CommandLoader, EventLoader, type Module } from "@voidy/framework"; 7 2 8 3 export default { 9 - id: "core", 10 - name: "Core", 11 - description: "The core feature set of the bot, required for command handling to work.", 12 - author: "jokiller230", 4 + id: "core", 5 + name: "Core", 6 + description: "Initializes the bot and registers all commands.", 7 + author: "jokiller230", 13 8 14 - exports: [ 15 - { 16 - source: `${import.meta.dir}/events`, 17 - loader: EventLoader, 18 - }, 19 - { 20 - source: `${import.meta.dir}/commands`, 21 - loader: CommandLoader, 22 - } 23 - ] 9 + exports: [ 10 + { 11 + source: `${import.meta.dir}/events`, 12 + loader: EventLoader, 13 + }, 14 + { 15 + source: `${import.meta.dir}/commands`, 16 + loader: CommandLoader, 17 + }, 18 + ], 24 19 } as Module;
+54
packages/bot/src/modules/user/commands/consent/grant.ts
··· 1 + import { MessageFlags, SlashCommandBuilder } from "discord.js"; 2 + import type { Command } from "@voidy/framework"; 3 + import { UserConfig } from "../../schemas/UserConfig"; 4 + 5 + export default { 6 + id: "user.consent.update", 7 + data: new SlashCommandBuilder() 8 + .setName("Update Consent") 9 + .setDescription("Update your user consent choices.") 10 + .addStringOption((option) => 11 + option 12 + .setName("category") 13 + .setDescription("Choose which category of consent to update.") 14 + .setRequired(true) 15 + .addChoices( 16 + { name: "Storage", value: "storage" }, 17 + { name: "Statistics", value: "statistics" }, 18 + ), 19 + ) 20 + .addBooleanOption((option) => 21 + option 22 + .setName("consent") 23 + .setDescription("Choose whether to grant or revoke consent.") 24 + .setRequired(true), 25 + ), 26 + 27 + execute: async (interaction, _client) => { 28 + const { options } = interaction; 29 + const category = options.getString("category", true); 30 + const consent = options.getBoolean("consent", true); 31 + 32 + let userConfig = await UserConfig.findById(interaction.user.id); 33 + if (!userConfig) { 34 + userConfig = new UserConfig({ id: interaction.user.id }); 35 + } 36 + 37 + switch (category) { 38 + case "storage": 39 + userConfig.consent.storage = consent; 40 + break; 41 + case "statistics": 42 + userConfig.consent.statistics = consent; 43 + break; 44 + default: 45 + console.error(`Invalid category: ${category}`); 46 + } 47 + 48 + await userConfig.save(); 49 + await interaction.reply({ 50 + content: `Updated consent for \`${category}\` to \`${consent ? "granted" : "revoked"}\`.`, 51 + flags: [MessageFlags.Ephemeral], 52 + }); 53 + }, 54 + } as Command;
+14
packages/bot/src/modules/user/module.ts
··· 1 + import { CommandLoader } from "@voidy/framework"; 2 + 3 + export default { 4 + id: "user", 5 + name: "User Management Services", 6 + description: 7 + "Provides various resources for working with users, like global preferences and consent statements.", 8 + exports: [ 9 + { 10 + src: `${import.meta.dir}/commands`, 11 + loader: CommandLoader, 12 + }, 13 + ], 14 + };
+12
packages/bot/src/modules/user/schemas/UserConfig.ts
··· 1 + import { Schema, model } from "mongoose"; 2 + 3 + export const UserConfig = model( 4 + "UserConfig", 5 + new Schema({ 6 + id: { type: String, required: true, primary: true }, 7 + consent: { type: Object, required: true, default: { 8 + storage: { type: Boolean, required: true, default: true }, 9 + statistics: { type: Boolean, required: true, default: true }, 10 + } }, 11 + }), 12 + );
+2 -4
packages/bot/tsconfig.json
··· 1 1 { 2 - "extends": "../../tsconfig.json", 3 - "include": [ 4 - "src" 5 - ] 2 + "extends": "../../tsconfig.json", 3 + "include": ["./src"], 6 4 }
+20 -20
packages/framework/package.json
··· 1 1 { 2 - "name": "@voidy/framework", 3 - "version": "0.1.0", 4 - "module": "src/index.ts", 5 - "type": "module", 6 - "private": true, 7 - "exports": { 8 - ".": { 9 - "import": "./src/index.ts", 10 - "require": "./src/index.cjs" 11 - } 12 - }, 13 - "devDependencies": { 14 - "@types/bun": "latest" 15 - }, 16 - "peerDependencies": { 17 - "typescript": "^5" 18 - }, 19 - "dependencies": { 20 - "discord.js": "^14.21.0" 21 - } 2 + "name": "@voidy/framework", 3 + "version": "0.1.0", 4 + "module": "src/index.ts", 5 + "type": "module", 6 + "private": true, 7 + "exports": { 8 + ".": { 9 + "import": "./src/index.ts", 10 + "require": "./src/index.cjs" 11 + } 12 + }, 13 + "devDependencies": { 14 + "@types/bun": "latest" 15 + }, 16 + "peerDependencies": { 17 + "typescript": "^5.9.2" 18 + }, 19 + "dependencies": { 20 + "discord.js": "^14.25.1" 21 + } 22 22 }
+53 -50
packages/framework/src/core/Loader.ts
··· 7 7 // Loader Definition 8 8 //=============================================== 9 9 interface ILoader<T extends object> { 10 - id: string 11 - cache: T[] 12 - source: string 10 + id: string; 11 + cache: T[]; 12 + source: string; 13 13 14 - collect: () => Promise<ThisType<this>> 15 - validate: (data: Partial<T>) => Promise<T | null> 16 - getJSON: () => T[] 14 + collect: () => Promise<ThisType<this>>; 15 + validate: (data: Partial<T>) => Promise<T | null>; 16 + getJSON: () => T[]; 17 17 } 18 18 19 19 //=============================================== 20 20 // Loader Implementation 21 21 //=============================================== 22 22 export abstract class Loader<T extends object> implements ILoader<T> { 23 - public abstract id: string; 24 - public cache: T[] = []; 25 - public source: string; 23 + public abstract id: string; 24 + public cache: T[] = []; 25 + public source: string; 26 26 27 - public constructor(source: string) { 28 - if (!source) throw new Error("Class of type Loader was initialized without the *required* source parameter."); 27 + public constructor(source: string) { 28 + if (!source) 29 + throw new Error( 30 + "Class of type Loader was initialized without the *required* source parameter.", 31 + ); 29 32 30 - this.source = source; 31 - } 33 + this.source = source; 34 + } 32 35 33 - /** 34 - * Recursively collects data from a directory based on the path specificed in dataSource property. 35 - */ 36 - public async collect() { 37 - const glob = new Glob(`**/**.ts`); 38 - const iterator = glob.scan(this.source); 36 + /** 37 + * Recursively collects data from a directory based on the path specificed in dataSource property. 38 + */ 39 + public async collect() { 40 + const glob = new Glob(`**/**.ts`); 41 + const iterator = glob.scan(this.source); 39 42 40 - try { 41 - for await (const path of iterator) { 42 - let moduleDefault: T | null; 43 + try { 44 + for await (const path of iterator) { 45 + let moduleDefault: T | null; 43 46 44 - try { 45 - const module = (await import(`${this.source}/${path}`)); 46 - moduleDefault = module.default; 47 + try { 48 + const module = await import(`${this.source}/${path}`); 49 + moduleDefault = module.default; 47 50 48 - if (!moduleDefault) continue; 49 - } catch { 50 - continue; 51 - } 51 + if (!moduleDefault) continue; 52 + } catch { 53 + continue; 54 + } 52 55 53 - const final = await this.validate(moduleDefault); 54 - if (!final) continue; 56 + const final = await this.validate(moduleDefault); 57 + if (!final) continue; 55 58 56 - this.cache.push(final); 57 - } 58 - } catch { 59 - console.error(`[Voidy] Specified loader target ${this.source} doesn't exist. Skipping...`); 60 - return this; 61 - } 59 + this.cache.push(final); 60 + } 61 + } catch { 62 + console.error(`[Voidy] Specified loader target ${this.source} doesn't exist. Skipping...`); 63 + return this; 64 + } 62 65 63 - return this; 64 - } 66 + return this; 67 + } 65 68 66 - /** 67 - * Validates a singular element during data collection, and returns whatever should be written to the cache. 68 - */ 69 - public abstract validate(data: Partial<T>): Promise<T | null>; 69 + /** 70 + * Validates a singular element during data collection, and returns whatever should be written to the cache. 71 + */ 72 + public abstract validate(data: Partial<T>): Promise<T | null>; 70 73 71 - /** 72 - * Returns the JSON-ified contents of the loader cache 73 - */ 74 - public getJSON() { 75 - return this.cache; 76 - } 77 - }; 74 + /** 75 + * Returns the JSON-ified contents of the loader cache 76 + */ 77 + public getJSON() { 78 + return this.cache; 79 + } 80 + }
+45 -45
packages/framework/src/core/ModuleManager.ts
··· 13 13 // ModuleManager Implementation 14 14 //=============================================== 15 15 export class ModuleManager { 16 - private cache = new Map<string, Map<string, unknown>>(); 16 + private cache = new Map<string, Map<string, unknown>>(); 17 17 18 - // Module Loading 19 - //============================== 20 - async loadModules(path: string) { 21 - const moduleLoader = new ModuleLoader(path); 22 - const modules = (await moduleLoader.collect()).getJSON(); 18 + // Module Loading 19 + //============================== 20 + async loadModules(path: string) { 21 + const moduleLoader = new ModuleLoader(path); 22 + const modules = (await moduleLoader.collect()).getJSON(); 23 23 24 - for (const module of modules) { 25 - await this.prepareModule(module); 26 - } 27 - } 24 + for (const module of modules) { 25 + await this.prepareModule(module); 26 + } 27 + } 28 28 29 - async prepareModule(module: Module) { 30 - for (const exp of module.exports) { 31 - const loader = new exp.loader(exp.source); 32 - const data = (await loader.collect()).getJSON(); 29 + async prepareModule(module: Module) { 30 + for (const exp of module.exports) { 31 + const loader = new exp.loader(exp.source); 32 + const data = (await loader.collect()).getJSON(); 33 33 34 - for (const item of data) { 35 - this.set(loader.id, (item as any).id, item); 36 - } 37 - } 38 - } 34 + for (const item of data) { 35 + this.set(loader.id, (item as any).id, item); 36 + } 37 + } 38 + } 39 39 40 - // Core API 41 - //============================== 42 - set<T>(type: string, id: string, value: T) { 43 - if (!this.cache.has(type)) this.cache.set(type, new Map()); 44 - (this.cache.get(type) as CacheMap<T>).set(id, value); 45 - } 40 + // Core API 41 + //============================== 42 + set<T>(type: string, id: string, value: T) { 43 + if (!this.cache.has(type)) this.cache.set(type, new Map()); 44 + (this.cache.get(type) as CacheMap<T>).set(id, value); 45 + } 46 46 47 - get<T>(type: string, id: string): T | undefined { 48 - return (this.cache.get(type) as CacheMap<T>)?.get(id); 49 - } 47 + get<T>(type: string, id: string): T | undefined { 48 + return (this.cache.get(type) as CacheMap<T>)?.get(id); 49 + } 50 50 51 - getAll<T>(type: string): CacheMap<T> { 52 - return (this.cache.get(type) as CacheMap<T>) ?? new Map(); 53 - } 51 + getAll<T>(type: string): CacheMap<T> { 52 + return (this.cache.get(type) as CacheMap<T>) ?? new Map(); 53 + } 54 54 55 - // Typed Accessors 56 - //============================== 57 - get modules(): CacheMap<Module> { 58 - return this.getAll<Module>("module"); 59 - } 55 + // Typed Accessors 56 + //============================== 57 + get modules(): CacheMap<Module> { 58 + return this.getAll<Module>("module"); 59 + } 60 60 61 - get commands(): CacheMap<Command> { 62 - return this.getAll<Command>("command"); 63 - } 61 + get commands(): CacheMap<Command> { 62 + return this.getAll<Command>("command"); 63 + } 64 64 65 - get buttons(): CacheMap<Button> { 66 - return this.getAll<Button>("button"); 67 - } 65 + get buttons(): CacheMap<Button> { 66 + return this.getAll<Button>("button"); 67 + } 68 68 69 - get events(): CacheMap<Event> { 70 - return this.getAll<Event>("event"); 71 - } 69 + get events(): CacheMap<Event> { 70 + return this.getAll<Event>("event"); 71 + } 72 72 }
+95 -91
packages/framework/src/core/VoidyClient.ts
··· 2 2 // Imports 3 3 //=============================================== 4 4 import { 5 - type ClientOptions, 6 - SlashCommandSubcommandGroupBuilder, 7 - SlashCommandSubcommandBuilder, 8 - SlashCommandBuilder, 9 - Client, 5 + type ClientOptions, 6 + SlashCommandSubcommandGroupBuilder, 7 + SlashCommandSubcommandBuilder, 8 + SlashCommandBuilder, 9 + Client, 10 + Events, 10 11 } from "discord.js"; 11 12 import { ModuleManager, type CacheMap } from "./ModuleManager"; 12 13 import type { Command } from "./types/Command"; ··· 17 18 // VoidyClient Implementation 18 19 //=============================================== 19 20 export class VoidyClient extends Client { 20 - public moduleManager = new ModuleManager(); 21 + public moduleManager = new ModuleManager(); 21 22 22 - public constructor(options: ClientOptions) { 23 - super(options); 24 - } 23 + public constructor(options: ClientOptions) { 24 + super(options); 25 + } 25 26 26 - /** 27 - * Launches the bot 28 - * @param token - The Discord application bot token. 29 - * @param modulesPath - Where the bot should search for modules. 30 - */ 31 - public async start(token: string, modulesPath: string) { 32 - // Load modules and register events 33 - await this.moduleManager.loadModules(modulesPath); 34 - await this.registerEvents(); 27 + /** 28 + * Launches the bot 29 + * @param token - The Discord application bot token. 30 + * @param modulesPath - Where the bot should search for modules. 31 + */ 32 + public async start(token: string, modulesPath: string) { 33 + // Load modules and register events 34 + await this.moduleManager.loadModules(modulesPath); 35 + await this.registerEvents(); 35 36 36 - // Register commands on ready event 37 - this.on("ready", this.registerCommands); 37 + // Register commands on ready event 38 + this.on(Events.ClientReady, this.registerCommands); 38 39 39 - // Login using the bot token 40 - await this.login(token); 41 - } 40 + // Login using the bot token 41 + await this.login(token); 42 + } 42 43 43 - /** 44 - * Registers all cached events 45 - * @param events 46 - */ 47 - private async registerEvents() { 48 - const events = this.moduleManager.events; 44 + /** 45 + * Registers all cached events 46 + * @param events 47 + */ 48 + private async registerEvents() { 49 + const events = this.moduleManager.events; 49 50 50 - for (const [_id, event] of events) { 51 - const execute = (...args: unknown[]) => event.execute(this, ...args); 51 + for (const [_id, event] of events) { 52 + const execute = (...args: unknown[]) => event.execute(this, ...args); 52 53 53 - if (event.once) this.once(event.name, execute); 54 - else this.on(event.name, execute); 55 - } 56 - } 54 + if (event.once) this.once(event.name, execute); 55 + else this.on(event.name, execute); 56 + } 57 + } 57 58 58 - /** 59 - * Registers all provided commands globally 60 - * @param commands 61 - */ 62 - private async registerCommands(): Promise<void> { 63 - const topLevelCommands = new Map<string, SlashCommandBuilder>(); 59 + /** 60 + * Registers all provided commands globally 61 + * @param commands 62 + */ 63 + private async registerCommands(): Promise<void> { 64 + const topLevelCommands = new Map<string, SlashCommandBuilder>(); 64 65 65 - for (const cmd of this.moduleManager.commands.values()) { 66 - const parts = cmd.id.split("."); // ["music", "set", "channel"] 67 - const command = parts[0]; 68 - const subcommand = parts[1]; 69 - const subgroupcommand = parts[2]; 66 + for (const cmd of this.moduleManager.commands.values()) { 67 + const parts = cmd.id.split("."); // ["music", "set", "channel"] 68 + const command = parts[0]; 69 + const subcommand = parts[1]; 70 + const subgroupcommand = parts[2]; 70 71 71 - if (!command) continue; 72 + if (!command) continue; 72 73 73 - // Ensure top-level builder exists 74 - if (!topLevelCommands.has(command)) { 75 - const topCommand = ( 76 - this.moduleManager.commands.get(command)?.data as SlashCommandBuilder 77 - ) ?? new SlashCommandBuilder().setName(command).setDescription("..."); 74 + // Ensure top-level builder exists 75 + if (!topLevelCommands.has(command)) { 76 + const topCommand = 77 + (this.moduleManager.commands.get(command)?.data as SlashCommandBuilder) ?? 78 + new SlashCommandBuilder().setName(command).setDescription("..."); 78 79 79 - topLevelCommands.set(command, topCommand); 80 - } 80 + topLevelCommands.set(command, topCommand); 81 + } 81 82 82 - const parent = topLevelCommands.get(command)!; 83 + const parent = topLevelCommands.get(command)!; 83 84 84 - if (subcommand && !subgroupcommand) { 85 - // It's a subcommand 86 - parent.addSubcommand(cmd.data as SlashCommandSubcommandBuilder); 87 - } else if (subcommand && subgroupcommand) { 88 - // It's a subgroup command 89 - let group = parent.options.find( 90 - (o): o is SlashCommandSubcommandGroupBuilder => o instanceof SlashCommandSubcommandGroupBuilder && o.name === subcommand 91 - ); 85 + if (subcommand && !subgroupcommand) { 86 + // It's a subcommand 87 + parent.addSubcommand(cmd.data as SlashCommandSubcommandBuilder); 88 + } else if (subcommand && subgroupcommand) { 89 + // It's a subgroup command 90 + let group = parent.options.find( 91 + (o): o is SlashCommandSubcommandGroupBuilder => 92 + o instanceof SlashCommandSubcommandGroupBuilder && o.name === subcommand, 93 + ); 92 94 93 - if (!group) { 94 - group = new SlashCommandSubcommandGroupBuilder().setName(subcommand).setDescription("..."); 95 - parent.addSubcommandGroup(group); 96 - } 95 + if (!group) { 96 + group = new SlashCommandSubcommandGroupBuilder() 97 + .setName(subcommand) 98 + .setDescription("..."); 99 + parent.addSubcommandGroup(group); 100 + } 97 101 98 - group.addSubcommand(cmd.data as SlashCommandSubcommandBuilder); 99 - } 100 - } 102 + group.addSubcommand(cmd.data as SlashCommandSubcommandBuilder); 103 + } 104 + } 101 105 102 - // Finally convert assembled top-level commands to JSON and register them 103 - await this.application?.commands.set([...topLevelCommands.values()].map(c => c.toJSON())); 104 - } 106 + // Finally convert assembled top-level commands to JSON and register them 107 + await this.application?.commands.set([...topLevelCommands.values()].map((c) => c.toJSON())); 108 + } 105 109 106 - /** 107 - * Returns all cached commands 108 - */ 109 - get commands(): CacheMap<Command> { 110 - return this.moduleManager.commands; 111 - } 110 + /** 111 + * Returns all cached commands 112 + */ 113 + get commands(): CacheMap<Command> { 114 + return this.moduleManager.commands; 115 + } 112 116 113 - /** 114 - * Returns all cached events 115 - */ 116 - get events(): CacheMap<Event> { 117 - return this.moduleManager.events; 118 - } 117 + /** 118 + * Returns all cached events 119 + */ 120 + get events(): CacheMap<Event> { 121 + return this.moduleManager.events; 122 + } 119 123 120 - /** 121 - * Returns all cached buttons 122 - */ 123 - get buttons(): CacheMap<Button> { 124 - return this.moduleManager.buttons; 125 - } 124 + /** 125 + * Returns all cached buttons 126 + */ 127 + get buttons(): CacheMap<Button> { 128 + return this.moduleManager.buttons; 129 + } 126 130 }
+1 -3
packages/framework/src/core/types/Button.ts
··· 9 9 // Button Definition 10 10 //=============================================== 11 11 export interface Button extends Resource { 12 - execute: ( 13 - interaction: ButtonInteraction, client: VoidyClient 14 - ) => Promise<void> 12 + execute: (interaction: ButtonInteraction, client: VoidyClient) => Promise<void>; 15 13 }
+6 -8
packages/framework/src/core/types/Command.ts
··· 2 2 // Imports 3 3 //=============================================== 4 4 import type { 5 - SlashCommandSubcommandGroupBuilder, 6 - SlashCommandSubcommandBuilder, 7 - ChatInputCommandInteraction, 8 - SlashCommandBuilder, 5 + SlashCommandSubcommandGroupBuilder, 6 + SlashCommandSubcommandBuilder, 7 + ChatInputCommandInteraction, 8 + SlashCommandBuilder, 9 9 } from "discord.js"; 10 10 import type { VoidyClient } from "../VoidyClient"; 11 11 import type { Resource } from "./Resource"; ··· 14 14 // Command Definition 15 15 //=============================================== 16 16 export interface Command extends Resource { 17 - data: SlashCommandBuilder | SlashCommandSubcommandBuilder | SlashCommandSubcommandGroupBuilder, 18 - execute: ( 19 - interaction: ChatInputCommandInteraction, client: VoidyClient 20 - ) => Promise<void> 17 + data: SlashCommandBuilder | SlashCommandSubcommandBuilder | SlashCommandSubcommandGroupBuilder; 18 + execute: (interaction: ChatInputCommandInteraction, client: VoidyClient) => Promise<void>; 21 19 }
+3 -3
packages/framework/src/core/types/Event.ts
··· 9 9 // Event Definition 10 10 //=============================================== 11 11 export interface Event extends Resource { 12 - name: keyof ClientEvents, 13 - once?: boolean, 14 - execute: (client: VoidyClient, ...args: unknown[]) => void, 12 + name: keyof ClientEvents; 13 + once?: boolean; 14 + execute: (client: VoidyClient, ...args: unknown[]) => void; 15 15 }
+6 -6
packages/framework/src/core/types/Module.ts
··· 8 8 // ModuleExportsItem Definition 9 9 //=============================================== 10 10 export interface ModuleExportsItem<T extends object> { 11 - source: string 12 - loader: new (...args: ConstructorParameters<typeof Loader<T>>) => Loader<T> 11 + source: string; 12 + loader: new (...args: ConstructorParameters<typeof Loader<T>>) => Loader<T>; 13 13 } 14 14 15 15 //=============================================== 16 16 // Module Definition 17 17 //=============================================== 18 18 export interface Module extends Resource { 19 - name: string 20 - description: string 21 - author: string 22 - exports: ModuleExportsItem<object>[] 19 + name: string; 20 + description: string; 21 + author: string; 22 + exports: ModuleExportsItem<object>[]; 23 23 }
+1 -1
packages/framework/src/core/types/Resource.ts
··· 2 2 // Resource Definition 3 3 //=============================================== 4 4 export interface Resource { 5 - id: string 5 + id: string; 6 6 }
+3 -3
packages/framework/src/handlers/ButtonHandler.ts
··· 3 3 import type { VoidyClient } from "../core/VoidyClient"; 4 4 5 5 export class ButtonHandler { 6 - public static invoke(interaction: ButtonInteraction, payload: Button, client: VoidyClient): void { 7 - payload.execute(interaction, client); 8 - } 6 + public static invoke(interaction: ButtonInteraction, payload: Button, client: VoidyClient): void { 7 + payload.execute(interaction, client); 8 + } 9 9 }
+7 -3
packages/framework/src/handlers/CommandHandler.ts
··· 3 3 import type { VoidyClient } from "../core/VoidyClient"; 4 4 5 5 export class ChatInputCommandHandler { 6 - public static invoke(interaction: ChatInputCommandInteraction, payload: Command, client: VoidyClient): void { 7 - payload.execute(interaction, client); 8 - } 6 + public static invoke( 7 + interaction: ChatInputCommandInteraction, 8 + payload: Command, 9 + client: VoidyClient, 10 + ): void { 11 + payload.execute(interaction, client); 12 + } 9 13 }
+5 -5
packages/framework/src/loaders/ButtonLoader.ts
··· 8 8 // ButtonLoader Implementation 9 9 //=============================================== 10 10 export class ButtonLoader extends Loader<Button> { 11 - public id = "button"; 12 - public async validate(data: Partial<Button>) { 13 - if (!data.id || !data.execute) return null; 14 - return data as Button; 15 - } 11 + public id = "button"; 12 + public async validate(data: Partial<Button>) { 13 + if (!data.id || !data.execute) return null; 14 + return data as Button; 15 + } 16 16 }
+5 -5
packages/framework/src/loaders/CommandLoader.ts
··· 8 8 // CommandLoader Implementation 9 9 //=============================================== 10 10 export class CommandLoader extends Loader<Command> { 11 - public id = "command"; 12 - public async validate(data: Partial<Command>) { 13 - if (!data.id || !data.data || !data.execute) return null; 14 - return data as Command; 15 - } 11 + public id = "command"; 12 + public async validate(data: Partial<Command>) { 13 + if (!data.id || !data.data || !data.execute) return null; 14 + return data as Command; 15 + } 16 16 }
+5 -5
packages/framework/src/loaders/EventLoader.ts
··· 8 8 // EventLoader Implemenation 9 9 //=============================================== 10 10 export class EventLoader extends Loader<Event> { 11 - public id = "event"; 12 - public async validate(data: Partial<Event>) { 13 - if (!data.id || !data.name || !data.execute) return null; 14 - return data as Event; 15 - } 11 + public id = "event"; 12 + public async validate(data: Partial<Event>) { 13 + if (!data.id || !data.name || !data.execute) return null; 14 + return data as Event; 15 + } 16 16 }
+6 -12
packages/framework/src/loaders/ModuleLoader.ts
··· 2 2 // Imports 3 3 //=============================================== 4 4 import type { Module } from "../core/types/Module"; 5 - import { Loader } from "../core/Loader" 5 + import { Loader } from "../core/Loader"; 6 6 7 7 //=============================================== 8 8 // ModuleLoader Implementation 9 9 //=============================================== 10 10 export class ModuleLoader extends Loader<Module> { 11 - public id = "module"; 12 - public async validate(data: Partial<Module>) { 13 - if ( 14 - !data.id || 15 - !data.name || 16 - !data.description || 17 - !data.author || 18 - !data.exports 19 - ) return null; 11 + public id = "module"; 12 + public async validate(data: Partial<Module>) { 13 + if (!data.id || !data.name || !data.description || !data.author || !data.exports) return null; 20 14 21 - return data as Module; 22 - } 15 + return data as Module; 16 + } 23 17 }
+10 -12
packages/framework/tsconfig.json
··· 1 1 { 2 - "extends": "../../tsconfig.json", 3 - "compilerOptions": { 4 - "declaration": true, 5 - "declarationDir": "dist/types", 6 - "outDir": "dist", 7 - "strict": true, 8 - "esModuleInterop": true, 9 - "composite": true 10 - }, 11 - "include": [ 12 - "src" 13 - ] 2 + "extends": "../../tsconfig.json", 3 + "compilerOptions": { 4 + "declaration": true, 5 + "declarationDir": "dist/types", 6 + "outDir": "dist", 7 + "strict": true, 8 + "esModuleInterop": true, 9 + "composite": true, 10 + }, 11 + "include": ["src"], 14 12 }
+2 -2
tsconfig.json
··· 16 16 "noImplicitOverride": true, 17 17 "noUnusedLocals": false, 18 18 "noUnusedParameters": false, 19 - "noPropertyAccessFromIndexSignature": false 20 - } 19 + "noPropertyAccessFromIndexSignature": false, 20 + }, 21 21 }