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

Compare changes

Choose any two refs to compare.

+5
.env.example
··· 1 + BOT_TOKEN: #Get this from https://discord.com/developers - REQUIRED 2 + BOT_CLIENT_ID: #Get this from https://discord.com/developers - REQUIRED 3 + BOT_ADMINS: #Discord Ids of bot administrators, comma separated 4 + DB_URI: #Your MongoDB connection URI - REQUIRED 5 + ACCESS_TOKEN: # Your API Bearer token
.github/assets/cyn-hi-chat-cyn-murder-drones.gif

This is a binary file and will not be displayed.

+3
.oxfmtrc.jsonc
··· 1 + { 2 + "$schema": "./node_modules/oxfmt/configuration_schema.json", 3 + }
+18 -6
README.md
··· 1 1 <br> 2 - <h1 align="center">๏ธโœจ Voidy โœจ<br></h1> 3 - <div align="center">My powerful discord bot :3</div> 2 + <h1 align="center">๏ธVoidy<br></h1> 3 + <div align="center">A powerful and extendable Discord bot, with it's own module system :3</div> 4 4 <br> 5 5 6 6 ## ๐Ÿš€ Deployment 7 - To deploy this project, in development mode, run the following command in your terminal of choice. 7 + 8 + Deploying this project is extremely simple. First, clone the repository: 8 9 9 10 ```sh 10 - bun dev 11 + git clone git@tangled.org:thevoid.cafe/voidy 11 12 ``` 12 13 13 - ## ๐Ÿ“ Goals 14 - See docs folder for planned implementation goals. 14 + Then, navigate to the project directory and install all dependencies: 15 + 16 + ```sh 17 + cd voidy 18 + bun install 19 + ``` 20 + 21 + Finally, run the development server: 22 + 23 + ```sh 24 + bun dev 25 + ``` 15 26 16 27 ## ๐ŸŽจ Credits 28 + 17 29 Certain features of this bot were the result 18 30 of conversations with: 19 31
+101 -19
bun.lock
··· 1 1 { 2 2 "lockfileVersion": 1, 3 + "configVersion": 0, 3 4 "workspaces": { 4 5 "": { 5 6 "name": "voidydiscord", 7 + "devDependencies": { 8 + "oxfmt": "^0.17.0", 9 + }, 10 + }, 11 + "packages/api": { 12 + "name": "@voidy/api", 13 + "version": "0.1.0", 14 + "dependencies": { 15 + "@voidy/bot": "workspace:*", 16 + "hono": "^4.11.1", 17 + "mongoose": "^9.0.1", 18 + }, 19 + "devDependencies": { 20 + "@types/bun": "latest", 21 + }, 22 + "peerDependencies": { 23 + "typescript": "^5.9.2", 24 + }, 6 25 }, 7 26 "packages/bot": { 8 - "name": "voidy-bot", 27 + "name": "@voidy/bot", 9 28 "version": "0.1.0", 10 29 "dependencies": { 11 - "discord.js": "^14.21.0", 12 - "voidy-framework": "workspace:*", 30 + "@voidy/api": "workspace:*", 31 + "@voidy/framework": "workspace:*", 32 + "discord.js": "^14.25.1", 33 + "hono": "^4.11.1", 34 + "mongoose": "^9.0.1", 13 35 }, 14 36 "devDependencies": { 15 37 "@types/bun": "latest", 16 38 }, 17 39 "peerDependencies": { 18 - "typescript": "^5", 40 + "typescript": "^5.9.2", 19 41 }, 20 42 }, 21 43 "packages/framework": { 22 - "name": "voidy-framework", 44 + "name": "@voidy/framework", 23 45 "version": "0.1.0", 24 46 "dependencies": { 25 - "discord.js": "^14.21.0", 47 + "discord.js": "^14.25.1", 26 48 }, 27 49 "devDependencies": { 28 50 "@types/bun": "latest", 29 51 }, 30 52 "peerDependencies": { 31 - "typescript": "^5", 53 + "typescript": "^5.9.2", 32 54 }, 33 55 }, 34 56 }, 35 57 "packages": { 36 - "@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=="], 58 + "@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=="], 37 59 38 60 "@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="], 39 61 40 - "@discordjs/formatters": ["@discordjs/formatters@0.6.1", "", { "dependencies": { "discord-api-types": "^0.38.1" } }, "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg=="], 62 + "@discordjs/formatters": ["@discordjs/formatters@0.6.2", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ=="], 41 63 42 - "@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=="], 64 + "@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=="], 43 65 44 - "@discordjs/util": ["@discordjs/util@1.1.1", "", {}, "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g=="], 66 + "@discordjs/util": ["@discordjs/util@1.2.0", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg=="], 45 67 46 68 "@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=="], 47 69 70 + "@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.4.0", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-ZHzx7Z3rdlWL1mECydvpryWN/ETXJiCxdgQKTAH+djzIPe77HdnSizKBDi1TVDXZjXyOj2IqEG/vPw71ULF06w=="], 71 + 72 + "@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.17.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-OMv0tOb+xiwSZKjYbM6TwMSP5QwFJlBGQmEsk98QJ30sHhdyC//0UvGKuR0KZuzZW4E0+k0rHDmos1Z5DmBEkA=="], 73 + 74 + "@oxfmt/darwin-x64": ["@oxfmt/darwin-x64@0.17.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-trzidyzryKIdL/cLCYU9IwprgJegVBUrz1rqzOMe5is+qdgH/RxTCvhYUNFzxRHpil3g4QUYd2Ja831tc5Nehg=="], 75 + 76 + "@oxfmt/linux-arm64-gnu": ["@oxfmt/linux-arm64-gnu@0.17.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-KlwzidgvHznbUaaglZT1goTS30osTV553pfbKve9B1PyTDkluNDfm/polOaf3SVLN7wL/NNLFZRMupvJ1eJXAw=="], 77 + 78 + "@oxfmt/linux-arm64-musl": ["@oxfmt/linux-arm64-musl@0.17.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-+tbYJTocF4BNLaQQbc/xrBWTNgiU6zmYeF4NvRDxuuQjDOnmUZPn0EED3PZBRJyg4/YllhplHDo8x+gfcb9G3A=="], 79 + 80 + "@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=="], 81 + 82 + "@oxfmt/linux-x64-musl": ["@oxfmt/linux-x64-musl@0.17.0", "", { "os": "linux", "cpu": "x64" }, "sha512-+DrFSCZWyFdtEAWR5xIBTV8GX0RA9iB+y7ZlJPRAXrNG8TdBY9vc7/MIGolIgrkMPK4mGMn07YG/qEyPY+iKaw=="], 83 + 84 + "@oxfmt/win32-arm64": ["@oxfmt/win32-arm64@0.17.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-FoUZRR7mVpTYIaY/qz2BYwzqMnL+HsUxmMWAIy6nl29UEkDgxNygULJ4rIGY4/Axne41fhtldLrSGBOpwNm3jA=="], 85 + 86 + "@oxfmt/win32-x64": ["@oxfmt/win32-x64@0.17.0", "", { "os": "win32", "cpu": "x64" }, "sha512-fBIcUpHmCwf3leWlo0cYwLb9Pd2mzxQlZYJX9dD9nylPvsxOnsy9fmsaflpj34O0JbQJN3Y0SRkoaCcHHlxFww=="], 87 + 48 88 "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="], 49 89 50 90 "@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="], 51 91 52 92 "@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="], 53 93 54 - "@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="], 94 + "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], 55 95 56 96 "@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="], 57 97 58 - "@types/react": ["@types/react@19.1.9", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA=="], 98 + "@types/webidl-conversions": ["@types/webidl-conversions@7.0.3", "", {}, "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="], 99 + 100 + "@types/whatwg-url": ["@types/whatwg-url@13.0.0", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q=="], 59 101 60 102 "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], 61 103 62 104 "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.6", "", {}, "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA=="], 63 105 64 - "bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="], 106 + "@voidy/api": ["@voidy/api@workspace:packages/api"], 65 107 66 - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], 108 + "@voidy/bot": ["@voidy/bot@workspace:packages/bot"], 67 109 68 - "discord-api-types": ["discord-api-types@0.38.18", "", {}, "sha512-ygenySjZKUaBf5JT8BNhZSxLzwpwdp41O0wVroOTu/N2DxFH7dxYTZUSnFJ6v+/2F3BMcnD47PC47u4aLOLxrQ=="], 110 + "@voidy/framework": ["@voidy/framework@workspace:packages/framework"], 69 111 70 - "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=="], 112 + "bson": ["bson@7.0.0", "", {}, "sha512-Kwc6Wh4lQ5OmkqqKhYGKIuELXl+EPYSCObVE6bWsp1T/cGkOCBN0I8wF/T44BiuhHyNi1mmKVPXk60d41xZ7kw=="], 113 + 114 + "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], 115 + 116 + "discord-api-types": ["discord-api-types@0.38.37", "", {}, "sha512-Cv47jzY1jkGkh5sv0bfHYqGgKOWO1peOrGMkDFM4UmaGMOTgOW8QSexhvixa9sVOiz8MnVOBryWYyw/CEVhj7w=="], 117 + 118 + "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=="], 71 119 72 120 "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], 73 121 122 + "hono": ["hono@4.11.1", "", {}, "sha512-KsFcH0xxHes0J4zaQgWbYwmz3UPOOskdqZmItstUG93+Wk1ePBLkLGwbP9zlmh1BFUiL8Qp+Xfu9P7feJWpGNg=="], 123 + 124 + "kareem": ["kareem@3.0.0", "", {}, "sha512-RKhaOBSPN8L7y4yAgNhDT2602G5FD6QbOIISbjN9D6mjHPeqeg7K+EB5IGSU5o81/X2Gzm3ICnAvQW3x3OP8HA=="], 125 + 74 126 "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], 75 127 76 128 "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="], 77 129 78 130 "magic-bytes.js": ["magic-bytes.js@1.12.1", "", {}, "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA=="], 79 131 132 + "memory-pager": ["memory-pager@1.5.0", "", {}, "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="], 133 + 134 + "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=="], 135 + 136 + "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=="], 137 + 138 + "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=="], 139 + 140 + "mpath": ["mpath@0.9.0", "", {}, "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew=="], 141 + 142 + "mquery": ["mquery@6.0.0", "", {}, "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw=="], 143 + 144 + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 145 + 146 + "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=="], 147 + 148 + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], 149 + 150 + "sift": ["sift@17.1.3", "", {}, "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ=="], 151 + 152 + "sparse-bitfield": ["sparse-bitfield@3.0.3", "", { "dependencies": { "memory-pager": "^1.0.2" } }, "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ=="], 153 + 154 + "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], 155 + 80 156 "ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="], 81 157 82 158 "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], ··· 87 163 88 164 "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], 89 165 90 - "voidy-bot": ["voidy-bot@workspace:packages/bot"], 166 + "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], 91 167 92 - "voidy-framework": ["voidy-framework@workspace:packages/framework"], 168 + "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], 93 169 94 170 "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=="], 95 171 96 172 "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], 97 173 98 174 "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], 175 + 176 + "@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=="], 177 + 178 + "@discordjs/ws/@discordjs/util": ["@discordjs/util@1.1.1", "", {}, "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g=="], 179 + 180 + "@discordjs/ws/discord-api-types": ["discord-api-types@0.38.18", "", {}, "sha512-ygenySjZKUaBf5JT8BNhZSxLzwpwdp41O0wVroOTu/N2DxFH7dxYTZUSnFJ6v+/2F3BMcnD47PC47u4aLOLxrQ=="], 99 181 } 100 182 }
+44
docker-compose.yml
··· 1 + # Don't change these development defaults, use a docker-compose.override.yml file instead :) 2 + services: 3 + api: 4 + build: 5 + context: . 6 + dockerfile: packages/api/Dockerfile 7 + env_file: .env 8 + ports: 9 + - "4300:4300" 10 + labels: 11 + - "traefik.enable=true" 12 + - "traefik.http.routers.voidy-api.entrypoints=websecure" 13 + - "traefik.http.routers.voidy-api.rule=Host(`voidy.thevoid.cafe`)" 14 + - "traefik.http.services.voidy-api.loadbalancer.server.port=4300" 15 + networks: 16 + - default 17 + - proxy 18 + restart: unless-stopped 19 + 20 + bot: 21 + build: 22 + context: . 23 + dockerfile: packages/bot/Dockerfile 24 + env_file: .env 25 + depends_on: 26 + - api 27 + networks: 28 + - default 29 + restart: unless-stopped 30 + 31 + mongo: 32 + image: mongo:latest 33 + container_name: voidy_db 34 + ports: 35 + - "27017:27017" 36 + environment: 37 + MONGO_INITDB_ROOT_USERNAME: voidy 38 + MONGO_INITDB_ROOT_PASSWORD: voidy 39 + networks: 40 + - default 41 + 42 + networks: 43 + proxy: 44 + external: true
-14
docs/architecture/module-level.md
··· 1 - # Module level architectural overview 2 - Modules are groups of commands, events and more. 3 - They export those, and provide a combined title, description and the discord name of the user who created them, using the author field. 4 - 5 - ## Modules 6 - Each module *can* contain a set of commands, events or subscribers. 7 - 8 - Subsribers are similar to events, but are related to the internal Lifecycle management of the bot, not Discord. 9 - 10 - These are the properties a module is required to provide: 11 - - name (a small and concise name for the module, e.g. "statistics") 12 - - description (a small text, which describes the use-cases of the module) 13 - - author (discord username of the module's author) 14 - - exports (an array of ModuleExportItem's, each containing a `source` and uninstantiated `loader` property)
-24
docs/architecture/registry-level.md
··· 1 - # Registry level architectural overview 2 - The Registry level of our architecture mainly focuses on Registries, exported Module data structure, Lifecycle events, and first database interactions, to track Registry/Module states. 3 - 4 - ## Registries 5 - All Registries will follow a common Registry structure. 6 - 7 - The following properties are required: 8 - - store (Where the raw structure of imported Modules is stored) 9 - 10 - The following methods are required: 11 - - collect (uses the ModuleLoader to collect the raw JSON output of all registry modules) [registry::preCollect, registry::postCollect] 12 - - prepare (uses various loaders to prepare Module contents, based on the Module's exports property, which exports an array of ModuleExportItem's.) [registry::prePrepare, registry::postPrepare] 13 - - activate (activates the registry and all contained features) [registry::preActivate, registry::postActivate] 14 - - unload (deactivates all modules stored in the registry and the registry itself) [registry::preUnload, registry::postUnload] 15 - 16 - 17 - ### ModuleExportItem 18 - Each Module provides a public `exports` property, which is an array of ModuleExportItem's, each ModuleFetchItem provides a `source` and a `loader` property. 19 - 20 - The `source` property is a simple path, pointing to a directory or file. 21 - The `loader` property takes an uninitialized loader class, which is then instantiated by the Registry, while loading the Module. 22 - 23 - 24 - @Todo: document registry error-notify feature, which uses a Module's author field, to notify the user of an error, directly within Discord.
-39
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 - Additionally, each loader implements their own asynchronous `validate` method, which is invoked within `collect`, to validate the contents of a file, before adding it to the Loader store. 16 - 17 - Finally, loaders provide various means of exporting data in supported formats, through methods like `getJSON`, `getCSV` and more... 18 - 19 - ### Event loader 20 - The event loader walks a directory and stores data from any file exporting an object that follows the Event type structure. 21 - 22 - ### Command loader 23 - The command loader walks a directory and stores data from any file exporting an object that follows the Command type structure. 24 - 25 - ## Handlers 26 - Handlers are static classes, which are invoked, usually on discord events or interactions. 27 - 28 - Each handler has an `invoke` method, which is the one I mentioned calling above. 29 - 30 - The handler then queries the core registry and all other registries afterward, to find a fitting execution target. 31 - 32 - It's important to note that only repositories marked as active are taken into account. 33 - 34 - @Todo: document Handler lifecycle events 35 - 36 - ## Lifecycle manager 37 - 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. 38 - 39 - Additionally, it implements two very simple methods, notify - which fires lifecycle events, and subscribe - which subscribes a callback function to an event.
-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.
+10 -4
package.json
··· 1 1 { 2 - "name": "voidy", 3 - "workspaces": [ 4 - "packages/*" 5 - ] 2 + "name": "voidy", 3 + "workspaces": [ 4 + "packages/*" 5 + ], 6 + "scripts": { 7 + "dev": "cd packages/bot && bun dev" 8 + }, 9 + "devDependencies": { 10 + "oxfmt": "^0.17.0" 11 + } 6 12 }
+2
packages/api/.env.example
··· 1 + DB_URI: #Your MongoDB connection URI - REQUIRED 2 + ACCESS_TOKEN: # Your Bearer token
+12
packages/api/Dockerfile
··· 1 + FROM oven/bun:1.1 2 + 3 + WORKDIR /app 4 + 5 + COPY . . 6 + 7 + RUN bun install 8 + 9 + WORKDIR /app/packages/api 10 + 11 + EXPOSE 3000 12 + CMD ["bun", "run", "dev"]
+25
packages/api/index.ts
··· 1 + import { Hono } from "hono"; 2 + import { v1 } from "./routes/api/v1"; 3 + import { connect } from "mongoose"; 4 + 5 + // Instantiate Hono 6 + const app = new Hono(); 7 + 8 + // Database URI validation and connection check 9 + if (!Bun.env.DB_URI) throw new Error("[Voidy] Missing database URI"); 10 + await connect(Bun.env.DB_URI) 11 + .then(() => { 12 + console.log("Connected to database"); 13 + }) 14 + .catch((error) => { 15 + console.error("Failed to connect to database:", error); 16 + }); 17 + 18 + // Define routes 19 + app.route("/api/v1", v1); 20 + 21 + // Export app configuration 22 + export default { 23 + fetch: app.fetch, 24 + port: Bun.env.PORT || 4300, 25 + }
+22
packages/api/middlewares/isAuthenticated.ts
··· 1 + import type { MiddlewareHandler } from "hono"; 2 + 3 + export const isAuthenticated: MiddlewareHandler = async (c, next) => { 4 + const auth = c.req.header("authorization"); 5 + 6 + if (!auth || !auth.startsWith("Bearer ")) { 7 + return c.json({ error: "Unauthorized" }, 401); 8 + } 9 + 10 + const token = auth.slice("Bearer ".length); 11 + 12 + if (!Bun.env.ACCESS_TOKEN) { 13 + console.error("ACCESS_TOKEN environment variable is not set"); 14 + return c.json({ error: "Internal server error" }, 500); 15 + } 16 + 17 + if (token !== Bun.env.ACCESS_TOKEN) { 18 + return c.json({ error: "Invalid token" }, 401); 19 + } 20 + 21 + await next(); 22 + };
+21
packages/api/package.json
··· 1 + { 2 + "name": "@voidy/api", 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/bot": "workspace:*", 18 + "hono": "^4.11.1", 19 + "mongoose": "^9.0.1" 20 + } 21 + }
+71
packages/api/routes/api/v1/currency.ts
··· 1 + import { Hono } from "hono"; 2 + import { UserCurrency, UserCurrencyType, UserIntegration } from "@voidy/bot/db"; 3 + import { isAuthenticated } from "../../../middlewares/isAuthenticated"; 4 + 5 + export const currency = new Hono(); 6 + 7 + async function findUserId(serviceType: string, serviceId: string) { 8 + const integration = await UserIntegration.findOne({ 9 + service: { 10 + type: serviceType, 11 + id: serviceId, 12 + } 13 + }); 14 + 15 + if (!integration) return null; 16 + 17 + return integration.userId; 18 + } 19 + 20 + currency.get("/", isAuthenticated, async (c) => { 21 + const serviceType = c.req.query("serviceType"); 22 + const serviceId = c.req.query("serviceId"); 23 + 24 + if (!serviceId || !serviceType) return c.json({ error: "Missing serviceId or serviceType" }, 400); 25 + 26 + const userId = await findUserId(serviceType, serviceId); 27 + if (!userId) return c.json({ error: "User not found" }, 404); 28 + 29 + const userCurrency = await UserCurrency.findOne({ userId, type: UserCurrencyType.BITS }); 30 + if (!userCurrency) return c.json({ error: "User currency not found" }, 404); 31 + 32 + return c.json(userCurrency); 33 + }); 34 + 35 + currency.post("/deposit", isAuthenticated, async (c) => { 36 + const { serviceType, serviceId, amount } = await c.req.json(); 37 + 38 + if (!serviceId || !serviceType) return c.json({ error: "Missing serviceId or serviceType" }, 400); 39 + if (!amount) return c.json({ error: "Missing amount" }, 400); 40 + 41 + const userId = await findUserId(serviceType, serviceId); 42 + if (!userId) return c.json({ error: "User not found" }, 404); 43 + 44 + const userCurrency = await UserCurrency.findOne({ userId, type: UserCurrencyType.BITS }); 45 + if (!userCurrency) return c.json({ error: "User currency not found" }, 404); 46 + 47 + userCurrency.amount += parseInt(amount); 48 + await userCurrency.save(); 49 + 50 + return c.json(userCurrency); 51 + }); 52 + 53 + currency.post("/withdraw", isAuthenticated, async (c) => { 54 + const { serviceType, serviceId, amount } = await c.req.json(); 55 + 56 + if (!serviceId || !serviceType) return c.json({ error: "Missing serviceId or serviceType" }, 400); 57 + if (!amount) return c.json({ error: "Missing amount" }, 400); 58 + 59 + const userId = await findUserId(serviceType, serviceId); 60 + if (!userId) return c.json({ error: "User not found" }, 404); 61 + 62 + const userCurrency = await UserCurrency.findOne({ userId, type: UserCurrencyType.BITS }); 63 + if (!userCurrency) return c.json({ error: "User currency not found" }, 404); 64 + 65 + if (userCurrency.amount < parseInt(amount)) return c.json({ error: "Insufficient balance" }, 400); 66 + 67 + userCurrency.amount -= parseInt(amount); 68 + await userCurrency.save(); 69 + 70 + return c.json(userCurrency); 71 + });
+8
packages/api/routes/api/v1/index.ts
··· 1 + import { Hono } from "hono"; 2 + import { currency } from "./currency"; 3 + import { shop } from "./shop"; 4 + 5 + export const v1 = new Hono(); 6 + 7 + v1.route("/currency", currency); 8 + v1.route("/shop", shop);
+98
packages/api/routes/api/v1/shop.ts
··· 1 + import { Hono } from "hono"; 2 + import { MinecraftShopItem, MinecraftShop } from "@voidy/bot/db"; 3 + import { isAuthenticated } from "../../../middlewares/isAuthenticated"; 4 + 5 + export const shop = new Hono(); 6 + 7 + shop.get("/", async (c) => { 8 + const latestShop = await MinecraftShop.findOne().sort({ createdAt: -1 }); 9 + 10 + if (!latestShop) { 11 + return c.json({ error: "No shop found" }, 404); 12 + } 13 + 14 + return c.json(latestShop); 15 + }); 16 + 17 + shop.get("/items", async (c) => { 18 + const item = c.req.query("item"); 19 + 20 + // Return all items if no item is specified 21 + if (!item) { 22 + const locatedItems = await MinecraftShopItem.find(); 23 + return c.json(locatedItems); 24 + } 25 + 26 + const locatedItem = await MinecraftShopItem.findOne({ item }); 27 + 28 + if (!locatedItem) { 29 + return c.json({ error: "No item with that ID found" }, 404); 30 + } 31 + 32 + return c.json(locatedItem); 33 + }); 34 + 35 + shop.post("/generate", isAuthenticated, async (c) => { 36 + const body = await c.req.json().catch(() => ({})); 37 + const { highlight } = body as { highlight?: string }; 38 + 39 + const allItems = await MinecraftShopItem.find(); 40 + 41 + if (!allItems.length) { 42 + return c.json( 43 + { error: "No shop items available to generate from" }, 44 + 400, 45 + ); 46 + } 47 + 48 + // Config 49 + const MIN_ITEMS = 4; 50 + const MAX_ITEMS = 8; 51 + 52 + const selected: typeof allItems = []; 53 + 54 + // --- Force highlighted item --- 55 + if (highlight) { 56 + const highlightedItem = allItems.find( 57 + (item) => item.item === highlight, 58 + ); 59 + 60 + if (!highlightedItem) { 61 + return c.json( 62 + { error: `Highlighted item '${highlight}' not found` }, 63 + 400, 64 + ); 65 + } 66 + 67 + selected.push(highlightedItem); 68 + } 69 + 70 + // --- Random fill --- 71 + const pool = allItems.filter( 72 + (item) => !selected.some((s) => s.id === item.id), 73 + ); 74 + 75 + const targetCount = 76 + Math.floor(Math.random() * (MAX_ITEMS - MIN_ITEMS + 1)) + MIN_ITEMS; 77 + 78 + while (selected.length < targetCount && pool.length > 0) { 79 + const index = Math.floor(Math.random() * pool.length); 80 + selected.push(pool.splice(index, 1)[0]); 81 + } 82 + 83 + // --- Snapshot normalization --- 84 + const items = selected.map((item) => ({ 85 + label: item.label, 86 + icon: item.icon, 87 + item: item.item, 88 + price: item.price, 89 + defaultOptions: { 90 + quantity: item.defaultOptions.quantity, 91 + stackLimit: item.defaultOptions.stackLimit, 92 + }, 93 + })); 94 + 95 + const newShop = await MinecraftShop.create({ items }); 96 + 97 + return c.json(newShop, 201); 98 + });
+5 -4
packages/bot/.env.example
··· 1 - BOT_TOKEN=#Get this from https://discord.com/developers - REQUIRED 2 - BOT_CLIENT_ID=#Get this from https://discord.com/developers - REQUIRED 3 - BOT_ADMINS=#Discord Ids of bot administrators, comma separated 4 - DB_URI=#Your MongoDB connection URI - REQUIRED 1 + BOT_TOKEN: #Get this from https://discord.com/developers - REQUIRED 2 + BOT_CLIENT_ID: #Get this from https://discord.com/developers - REQUIRED 3 + BOT_ADMINS: #Discord Ids of bot administrators, comma separated 4 + DB_URI: #Your MongoDB connection URI - REQUIRED 5 + VOIDY_API_TOKEN: # 123456789
+11
packages/bot/Dockerfile
··· 1 + FROM oven/bun:1.1 2 + 3 + WORKDIR /app 4 + 5 + COPY . . 6 + 7 + RUN bun install 8 + 9 + WORKDIR /app/packages/bot 10 + 11 + CMD ["bun", "run", "dev"]
+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
+27 -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 + "exports": { 8 + "./db": { 9 + "import": "./src/modules/index.ts", 10 + "require": "./src/modules/index.cjs" 11 + } 12 + }, 13 + "scripts": { 14 + "dev": "bun --watch ." 15 + }, 16 + "devDependencies": { 17 + "@types/bun": "latest" 18 + }, 19 + "peerDependencies": { 20 + "typescript": "^5.9.2" 21 + }, 22 + "dependencies": { 23 + "@voidy/framework": "workspace:*", 24 + "@voidy/api": "workspace:*", 25 + "discord.js": "^14.25.1", 26 + "hono": "^4.11.1", 27 + "mongoose": "^9.0.1" 28 + } 20 29 }
+21 -4
packages/bot/src/index.ts
··· 1 - import { GatewayIntentBits } from "discord.js" 2 - import { VoidyClient } from "voidy-framework"; 1 + import { GatewayIntentBits } from "discord.js"; 2 + import { VoidyClient } from "@voidy/framework"; 3 + import { connect } from "mongoose"; 4 + 5 + //=============================================== 6 + // Discord client initialization 7 + //=============================================== 3 8 4 9 // Client initialization with intents and stuff... 5 10 const client = new VoidyClient({ 6 - intents: [GatewayIntentBits.Guilds], 7 - }) 11 + intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages], 12 + developers: Bun.env.BOT_ADMINS?.split(",") ?? ["423520077246103563"], 13 + logChannelId: "1451025628206731459" 14 + }); 15 + 16 + // Database URI validation and connection check 17 + if (!Bun.env.DB_URI) throw new Error("[Voidy] Missing database URI"); 18 + await connect(Bun.env.DB_URI) 19 + .then(() => { 20 + console.log("Connected to database"); 21 + }) 22 + .catch((error) => { 23 + console.error("Failed to connect to database:", error); 24 + }); 8 25 9 26 // Token validation and client start 10 27 if (!Bun.env.BOT_TOKEN) throw new Error("[Voidy] Missing bot token");
+12 -12
packages/bot/src/modules/core/commands/ping.ts
··· 1 1 import { MessageFlags, SlashCommandBuilder } from "discord.js"; 2 - import type { Command } from "voidy-framework"; 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;
+47 -45
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 7 - } from "voidy-framework"; 3 + ChatInputCommandHandler, 4 + ButtonHandler, 5 + type VoidyClient, 6 + type Event, 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;
+11 -11
packages/bot/src/modules/core/events/ready.ts
··· 1 - import type { Event, VoidyClient } from "voidy-framework"; 1 + import type { Event, VoidyClient } from "@voidy/framework"; 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 -24
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: "thevoid.cafe", 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 - { 24 - source: `${import.meta.dir}/buttons`, 25 - loader: ButtonLoader, 26 - } 27 - ] 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 + ], 28 19 } as Module;
+86
packages/bot/src/modules/economy/commands/currency/set.ts
··· 1 + import { MessageFlags, SlashCommandSubcommandBuilder } from "discord.js"; 2 + import { UserCurrencyType, UserCurrency } from "../../schemas/UserCurrency"; 3 + import type { Command } from "@voidy/framework"; 4 + 5 + export default { 6 + id: "currency.set", 7 + devOnly: true, 8 + data: new SlashCommandSubcommandBuilder() 9 + .setName("set") 10 + .setDescription("Override a users current balance.") 11 + .addUserOption((option) => 12 + option.setName("user").setDescription("The user whose balance to override.").setRequired(true) 13 + ) 14 + .addStringOption((option) => 15 + option 16 + .setName("currency") 17 + .setDescription("The currency to override.") 18 + .setRequired(true) 19 + .setChoices( 20 + { name: "BiTS", value: UserCurrencyType.BITS }, 21 + { name: "Gems", value: UserCurrencyType.GEMS } 22 + ) 23 + ) 24 + .addIntegerOption((option) => 25 + option 26 + .setName("new_balance") 27 + .setDescription("The new balance to set.") 28 + .setRequired(true) 29 + ), 30 + 31 + execute: async (interaction, _client) => { 32 + const { options } = interaction; 33 + 34 + // Retrieve the requested user from our interaction options 35 + const user = options.getUser("user"); 36 + if (!user) { 37 + await interaction.reply({ 38 + content: "Please provide a valid user.", 39 + flags: [MessageFlags.Ephemeral], 40 + }); 41 + 42 + return; 43 + }; 44 + 45 + // Retrieve the selected currency from our interaction options 46 + const selectedCurrency = options.getString("currency"); 47 + if (!selectedCurrency) { 48 + await interaction.reply({ 49 + content: "Please provide a valid currency.", 50 + flags: [MessageFlags.Ephemeral], 51 + }); 52 + 53 + return; 54 + } 55 + 56 + // Retrieve the new balance from our interaction options 57 + const newBalance = options.getInteger("new_balance"); 58 + if (!newBalance) { 59 + await interaction.reply({ 60 + content: "Please provide a valid new balance.", 61 + flags: [MessageFlags.Ephemeral], 62 + }); 63 + 64 + return; 65 + } 66 + 67 + // Retrieve the requested user's balance from our database 68 + let userBalance = await UserCurrency.findOne({ userId: user.id, type: selectedCurrency }); 69 + if (!userBalance) { 70 + userBalance = await UserCurrency.create({ 71 + userId: user.id, 72 + type: selectedCurrency, 73 + amount: newBalance, 74 + }); 75 + } else { 76 + userBalance.amount = newBalance; 77 + await userBalance.save(); 78 + } 79 + 80 + // Reply with the requested user's current balance 81 + await interaction.reply({ 82 + content: `Balance of user \`${user.username}\` has been set to \`${userBalance.amount} ${selectedCurrency.toUpperCase()}\`.`, 83 + flags: [MessageFlags.Ephemeral], 84 + }); 85 + }, 86 + } as Command;
+66
packages/bot/src/modules/economy/commands/currency/view.ts
··· 1 + import { MessageFlags, SlashCommandSubcommandBuilder } from "discord.js"; 2 + import { UserCurrencyType, UserCurrency } from "../../schemas/UserCurrency"; 3 + import type { Command } from "@voidy/framework"; 4 + 5 + export default { 6 + id: "currency.view", 7 + data: new SlashCommandSubcommandBuilder() 8 + .setName("view") 9 + .setDescription("Retrieve a users current balance.") 10 + .addUserOption((option) => 11 + option.setName("user").setDescription("The user whose balance to view.").setRequired(true) 12 + ) 13 + .addStringOption((option) => 14 + option 15 + .setName("currency") 16 + .setDescription("The currency to view.") 17 + .setRequired(true) 18 + .setChoices( 19 + { name: "BiTS", value: UserCurrencyType.BITS }, 20 + { name: "Gems", value: UserCurrencyType.GEMS } 21 + ) 22 + ), 23 + 24 + execute: async (interaction, _client) => { 25 + const { options } = interaction; 26 + 27 + // Retrieve the requested user from our interaction options 28 + const user = options.getUser("user"); 29 + if (!user) { 30 + await interaction.reply({ 31 + content: "Please provide a valid user.", 32 + flags: [MessageFlags.Ephemeral], 33 + }); 34 + 35 + return; 36 + }; 37 + 38 + // Retrieve the selected currency from our interaction options 39 + const selectedCurrency = options.getString("currency"); 40 + if (!selectedCurrency) { 41 + await interaction.reply({ 42 + content: "Please provide a valid currency.", 43 + flags: [MessageFlags.Ephemeral], 44 + }); 45 + 46 + return; 47 + } 48 + 49 + // Retrieve the requested user's balance from our database 50 + const userBalance = await UserCurrency.findOne({ userId: user.id, type: selectedCurrency }); 51 + if (!userBalance || UserCurrencyType === undefined || userBalance.amount === 0) { 52 + await interaction.reply({ 53 + content: `The requested user hasn't earned any \`${selectedCurrency.toUpperCase()}\` yet.`, 54 + flags: [MessageFlags.Ephemeral], 55 + }); 56 + 57 + return; 58 + } 59 + 60 + // Reply with the requested user's current balance 61 + await interaction.reply({ 62 + content: `User \`${user.username}\` has \`${userBalance.amount} ${selectedCurrency.toUpperCase()}\`.`, 63 + flags: [MessageFlags.Ephemeral], 64 + }); 65 + }, 66 + } as Command;
+91
packages/bot/src/modules/economy/commands/minecraft/shop/add-item.ts
··· 1 + import { MessageFlags, SlashCommandSubcommandBuilder } from "discord.js"; 2 + import type { Command } from "@voidy/framework"; 3 + import { MinecraftShopItem } from "../../../schemas"; 4 + 5 + export default { 6 + id: "minecraft.shop.add-item", 7 + devOnly: true, 8 + data: new SlashCommandSubcommandBuilder() 9 + .setName("add-item") 10 + .setDescription("Add a new Minecraft shop item to the database.") 11 + .addStringOption((option) => 12 + option 13 + .setName("item") 14 + .setDescription("Minecraft item id (e.g. minecraft:diamond)") 15 + .setRequired(true), 16 + ) 17 + .addStringOption((option) => 18 + option 19 + .setName("label") 20 + .setDescription("Display label for the shop item") 21 + .setRequired(true), 22 + ) 23 + .addStringOption((option) => 24 + option 25 + .setName("icon") 26 + .setDescription("Texture path for the item icon") 27 + .setRequired(true), 28 + ) 29 + .addIntegerOption((option) => 30 + option 31 + .setName("price") 32 + .setDescription("Base price of the item") 33 + .setRequired(true), 34 + ) 35 + .addIntegerOption((option) => 36 + option 37 + .setName("quantity") 38 + .setDescription("Default quantity per purchase (default: 16)"), 39 + ) 40 + .addIntegerOption((option) => 41 + option 42 + .setName("stack-limit") 43 + .setDescription("Maximum stack size (default: 64)"), 44 + ), 45 + 46 + execute: async (interaction, client) => { 47 + await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); 48 + 49 + const item = interaction.options.getString("item", true); 50 + const label = interaction.options.getString("label", true); 51 + const icon = interaction.options.getString("icon", true); 52 + const price = interaction.options.getInteger("price", true); 53 + const quantity = interaction.options.getInteger("quantity") ?? 16; 54 + const stackLimit = interaction.options.getInteger("stack-limit") ?? 64; 55 + 56 + try { 57 + // Prevent duplicates 58 + const existing = await MinecraftShopItem.findOne({ item }); 59 + if (existing) { 60 + await interaction.followUp({ 61 + content: `โš ๏ธ Item \`${item}\` already exists in the shop database.`, 62 + flags: [MessageFlags.Ephemeral], 63 + }); 64 + return; 65 + } 66 + 67 + await MinecraftShopItem.create({ 68 + item, 69 + label, 70 + icon, 71 + price, 72 + defaultOptions: { 73 + quantity, 74 + stackLimit, 75 + }, 76 + }); 77 + 78 + await interaction.followUp({ 79 + content: `โœ… Added shop item **${label}** (\`${item}\`) for ${price} coins.`, 80 + flags: [MessageFlags.Ephemeral], 81 + }); 82 + } catch (err) { 83 + client.logger.send(`[minecraft.shop.add-item] Failed: \`\`\`${err}\`\`\``); 84 + 85 + await interaction.followUp({ 86 + content: "โŒ Failed to add shop item. Check logs for details.", 87 + flags: [MessageFlags.Ephemeral], 88 + }); 89 + } 90 + }, 91 + } as Command;
+63
packages/bot/src/modules/economy/commands/minecraft/shop/generate.ts
··· 1 + import { MessageFlags, SlashCommandSubcommandBuilder } from "discord.js"; 2 + import type { Command } from "@voidy/framework"; 3 + 4 + export default { 5 + id: "minecraft.shop.generate", 6 + devOnly: true, 7 + data: new SlashCommandSubcommandBuilder() 8 + .setName("generate") 9 + .setDescription("Generate a new shop for the minecraft currency integration.") 10 + .addStringOption((option) => 11 + option 12 + .setName("highlight") 13 + .setDescription("Force pick an item to highlight.") 14 + ), 15 + 16 + execute: async (interaction, client) => { 17 + await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); 18 + 19 + const highlight = interaction.options.getString("highlight"); 20 + 21 + // Todo: Replace hardcoded URL with environment variable 22 + try { 23 + const res = await fetch( 24 + "https://voidy.thevoid.cafe/api/v1/shop/generate", 25 + { 26 + method: "POST", 27 + headers: { 28 + "Content-Type": "application/json", 29 + Authorization: `Bearer ${process.env.ACCESS_TOKEN}`, 30 + }, 31 + body: highlight 32 + ? JSON.stringify({ highlight }) 33 + : undefined, 34 + }, 35 + ); 36 + 37 + if (!res.ok) { 38 + const text = await res.text(); 39 + throw new Error(`API error (${res.status}): ${text}`); 40 + } 41 + 42 + if (!highlight) { 43 + await interaction.followUp({ 44 + content: "Generated new shop for the Minecraft currency integration.", 45 + flags: [MessageFlags.Ephemeral], 46 + }); 47 + return; 48 + } 49 + 50 + await interaction.followUp({ 51 + content: `Generated new shop for the Minecraft currency integration, with highlighted item: \`${highlight}\``, 52 + flags: [MessageFlags.Ephemeral], 53 + }); 54 + } catch (err) { 55 + client.logger.send(`[minecraft.shop.generate] Failed: \`\`\`${err}\`\`\``); 56 + 57 + await interaction.followUp({ 58 + content: "โŒ Failed to generate a new shop. Check logs for details.", 59 + flags: [MessageFlags.Ephemeral], 60 + }); 61 + } 62 + }, 63 + } as Command;
+48
packages/bot/src/modules/economy/commands/minecraft/shop/rm-item.ts
··· 1 + import { MessageFlags, SlashCommandSubcommandBuilder } from "discord.js"; 2 + import type { Command } from "@voidy/framework"; 3 + import { MinecraftShopItem } from "../../../schemas"; 4 + 5 + export default { 6 + id: "minecraft.shop.rm-item", 7 + devOnly: true, 8 + data: new SlashCommandSubcommandBuilder() 9 + .setName("rm-item") 10 + .setDescription("Remove a Minecraft shop item from the database.") 11 + .addStringOption((option) => 12 + option 13 + .setName("item") 14 + .setDescription("Minecraft item id (e.g. minecraft:diamond)") 15 + .setRequired(true), 16 + ), 17 + 18 + execute: async (interaction, client) => { 19 + await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); 20 + 21 + const item = interaction.options.getString("item", true); 22 + 23 + try { 24 + // Remove item from database, if it exists 25 + const deleted = await MinecraftShopItem.findOneAndDelete({ item }); 26 + 27 + if (!deleted) { 28 + await interaction.followUp({ 29 + content: `โš ๏ธ Item \`${item}\` does not exist in the shop database.`, 30 + flags: [MessageFlags.Ephemeral], 31 + }); 32 + return; 33 + } 34 + 35 + await interaction.followUp({ 36 + content: `โœ… Removed shop item \`${item}\` from the database.`, 37 + flags: [MessageFlags.Ephemeral], 38 + }); 39 + } catch (err) { 40 + client.logger.send(`[minecraft.shop.rm-item] Failed: \`\`\`${err}\`\`\``); 41 + 42 + await interaction.followUp({ 43 + content: "โŒ Failed to remove shop item. Check logs for details.", 44 + flags: [MessageFlags.Ephemeral], 45 + }); 46 + } 47 + }, 48 + } as Command;
+31
packages/bot/src/modules/economy/events/messageCreate.ts
··· 1 + import type { Event, VoidyClient } from "@voidy/framework"; 2 + import { UserCurrency, UserCurrencyType } from "../schemas/UserCurrency"; 3 + import { Events, Message } from "discord.js"; 4 + 5 + export default { 6 + id: "messageCreate", 7 + name: Events.MessageCreate, 8 + execute: async (client: VoidyClient, message: Message) => { 9 + // Don't collect currencies for bots 10 + if (message.author.bot) return; 11 + 12 + // Retrieve user balance of BITS 13 + let userCurrency = await UserCurrency.findOne({ userId: message.author.id, type: UserCurrencyType.BITS }); 14 + if (!userCurrency) { 15 + userCurrency = await UserCurrency.create({ userId: message.author.id, type: UserCurrencyType.BITS }); 16 + } 17 + 18 + // Increase user balance of BITS at random 19 + if (Math.random() < 0.5) { // 50% chance 20 + // Calculate amount of BITS to give, between 1 and 10 21 + const amount = Math.floor(Math.random() * 10) + 1; 22 + 23 + // Update user balance of BITS and commit changes 24 + userCurrency.amount += amount; 25 + await userCurrency.save(); 26 + 27 + // Log to logging channel 28 + await client.logger.send(`User \`${message.author.tag}\` received \`${amount} BITS\``); 29 + } 30 + } 31 + } as Event;
+20
packages/bot/src/modules/economy/module.ts
··· 1 + import { CommandLoader, EventLoader } from "@voidy/framework"; 2 + 3 + export default { 4 + id: "economy", 5 + name: "Economy Management Services", 6 + description: 7 + "Provides various resources for working with users economies and currencies, including storage, transactions, and economy-related commands.", 8 + author: "thevoid.cafe", 9 + 10 + exports: [ 11 + { 12 + source: `${import.meta.dir}/commands`, 13 + loader: CommandLoader, 14 + }, 15 + { 16 + source: `${import.meta.dir}/events`, 17 + loader: EventLoader, 18 + }, 19 + ], 20 + };
+28
packages/bot/src/modules/economy/schemas/MinecraftShop.ts
··· 1 + import { Schema, model } from "mongoose"; 2 + 3 + export const MinecraftShop = model( 4 + "MinecraftShop", 5 + new Schema({ 6 + createdAt: { 7 + type: Date, 8 + required: true, 9 + default: Date.now, 10 + }, 11 + items: { 12 + type: Array, 13 + required: true, 14 + default: [ 15 + { 16 + label: "Bread", 17 + icon: "textures/items/bread", 18 + item: "minecraft:bread", 19 + price: 12, 20 + defaultOptions: { 21 + quantity: 16, 22 + stackLimit: 64, 23 + }, 24 + }, 25 + ] 26 + } 27 + }), 28 + );
+18
packages/bot/src/modules/economy/schemas/MinecraftShopItem.ts
··· 1 + import { Schema, model } from "mongoose"; 2 + 3 + export const MinecraftShopItem = model( 4 + "MinecraftShopItem", 5 + new Schema({ 6 + label: { type: String, required: true }, 7 + icon: { type: String, required: true }, 8 + item: { type: String, required: true, unique: true }, 9 + price: { type: Number, required: true }, 10 + defaultOptions: { 11 + type: { 12 + quantity: { type: Number, required: true, default: 16 }, 13 + stackLimit: { type: Number, required: true, default: 64 }, 14 + }, 15 + required: true 16 + }, 17 + }), 18 + );
+17
packages/bot/src/modules/economy/schemas/UserCurrency.ts
··· 1 + import { Schema, model } from "mongoose"; 2 + 3 + // Define valid currency types, 4 + // this should be the only source of truth for currency types. 5 + export enum UserCurrencyType { 6 + BITS = "bits", 7 + GEMS = "gems" 8 + } 9 + 10 + export const UserCurrency = model( 11 + "UserCurrency", 12 + new Schema({ 13 + userId: { type: String, required: true }, 14 + type: { type: String, enum: UserCurrencyType, required: true }, 15 + amount: { type: Number, required: true, default: 0 }, 16 + }), 17 + );
+3
packages/bot/src/modules/economy/schemas/index.ts
··· 1 + export * from "./UserCurrency"; 2 + export * from "./MinecraftShop"; 3 + export * from "./MinecraftShopItem";
+2
packages/bot/src/modules/index.ts
··· 1 + export * from "./economy/schemas"; 2 + export * from "./user/schemas";
+29
packages/bot/src/modules/toys/commands/api/httpcat.ts
··· 1 + import { MessageFlags, SlashCommandSubcommandBuilder } from "discord.js"; 2 + import type { Command } from "@voidy/framework"; 3 + 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) => 10 + option.setName("code").setDescription("The desired HTTP status code.").setRequired(true), 11 + ) 12 + .addBooleanOption((option) => 13 + option 14 + .setName("ephemeral") 15 + .setDescription("Whether or not to publicly share the bot response"), 16 + ), 17 + 18 + execute: async (interaction, _client) => { 19 + const { options } = interaction; 20 + 21 + const httpCode = options.getString("code"); 22 + const ephemeral = options.getBoolean("ephemeral") ?? true; 23 + 24 + await interaction.reply({ 25 + files: [`https://http.cat/${httpCode}.jpg`], 26 + flags: ephemeral ? [MessageFlags.Ephemeral] : [], 27 + }); 28 + }, 29 + } as Command;
+15
packages/bot/src/modules/toys/module.ts
··· 1 + import { CommandLoader } from "@voidy/framework"; 2 + 3 + export default { 4 + id: "toys", 5 + name: "Toys", 6 + description: "Provides various fun commands, e.g. coinflip, dice or simple API interactions", 7 + author: "thevoid.cafe", 8 + 9 + exports: [ 10 + { 11 + source: `${import.meta.dir}/commands`, 12 + loader: CommandLoader, 13 + }, 14 + ], 15 + };
+54
packages/bot/src/modules/user/commands/consent/update.ts
··· 1 + import { MessageFlags, SlashCommandSubcommandBuilder } 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 SlashCommandSubcommandBuilder() 8 + .setName("update") 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;
+65
packages/bot/src/modules/user/commands/integration/update.ts
··· 1 + import { MessageFlags, SlashCommandSubcommandBuilder } from "discord.js"; 2 + import type { Command } from "@voidy/framework"; 3 + import { UserIntegration } from "../../schemas/UserIntegration"; 4 + 5 + export default { 6 + id: "user.integration.update", 7 + data: new SlashCommandSubcommandBuilder() 8 + .setName("update") 9 + .setDescription("Update one of your external service integrations.") 10 + .addStringOption((option) => 11 + option 12 + .setName("service") 13 + .setDescription("Choose which service to update/link.") 14 + .setRequired(true) 15 + .addChoices( 16 + { name: "Minecraft", value: "minecraft" }, 17 + ), 18 + ) 19 + .addStringOption((option) => 20 + option 21 + .setName("id") 22 + .setDescription("Provide your external service user ID.") 23 + .setRequired(true), 24 + ), 25 + 26 + execute: async (interaction, client) => { 27 + const { options } = interaction; 28 + const service = options.getString("service", true); 29 + const id = options.getString("id", true); 30 + 31 + let userIntegration = await UserIntegration.findOne({ 32 + userId: interaction.user.id, 33 + service: { 34 + type: service, 35 + id: id, 36 + } }); 37 + 38 + if (!userIntegration) { 39 + userIntegration = new UserIntegration({ 40 + userId: interaction.user.id, 41 + service: { 42 + type: service, 43 + id: id, 44 + }, 45 + }); 46 + } 47 + 48 + switch (service) { 49 + case "minecraft": 50 + userIntegration.service.type = service; 51 + userIntegration.service.id = id; 52 + break; 53 + default: 54 + console.error(`Invalid service: ${service}`); 55 + } 56 + 57 + await userIntegration.save(); 58 + await interaction.reply({ 59 + content: `Updated integration \`${service}\` to ID \`${id}\`.`, 60 + flags: [MessageFlags.Ephemeral], 61 + }); 62 + 63 + await client.logger.send(`Updated integration \`${service}\` to ID \`${id}\` for user \`${interaction.user.tag}\``); 64 + }, 65 + } as Command;
+16
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 + author: "thevoid.cafe", 9 + 10 + exports: [ 11 + { 12 + source: `${import.meta.dir}/commands`, 13 + loader: CommandLoader, 14 + }, 15 + ], 16 + };
+16
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 + userId: { type: String, required: true }, 7 + consent: { 8 + type: Object, 9 + required: true, 10 + default: { 11 + storage: { type: Boolean, required: true, default: true }, 12 + statistics: { type: Boolean, required: true, default: true }, 13 + }, 14 + }, 15 + }), 16 + );
+16
packages/bot/src/modules/user/schemas/UserIntegration.ts
··· 1 + import { Schema, model } from "mongoose"; 2 + 3 + export const UserIntegration = model( 4 + "UserIntegration", 5 + new Schema({ 6 + userId: { type: String, required: true }, 7 + service: { 8 + type: Object, 9 + required: true, 10 + default: { 11 + type: { type: String, required: true }, 12 + id: { type: String, required: true }, 13 + } 14 + } 15 + }), 16 + );
+2
packages/bot/src/modules/user/schemas/index.ts
··· 1 + export * from "./UserConfig"; 2 + export * from "./UserIntegration";
+2 -12
packages/bot/tsconfig.json
··· 1 1 { 2 - "extends": "../../tsconfig.json", 3 - "include": [ 4 - "src" 5 - ], 6 - "compilerOptions": { 7 - "baseUrl": ".", 8 - "paths": { 9 - "voidy-framework": [ 10 - "../framework/src" 11 - ] 12 - } 13 - } 2 + "extends": "../../tsconfig.json", 3 + "include": ["./src"], 14 4 }
+20 -14
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 - "devDependencies": { 8 - "@types/bun": "latest" 9 - }, 10 - "peerDependencies": { 11 - "typescript": "^5" 12 - }, 13 - "dependencies": { 14 - "discord.js": "^14.21.0" 15 - } 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 + } 16 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 + }
+24
packages/framework/src/core/Logger.ts
··· 1 + import type { VoidyClient } from "./VoidyClient"; 2 + 3 + export class Logger { 4 + private logChannelId?: string; 5 + 6 + constructor(private client: VoidyClient) {} 7 + 8 + public async send(message: string) { 9 + if (!this.logChannelId) return; 10 + 11 + try { 12 + const loggingChannel = this.client.channels.cache.get(this.logChannelId); 13 + if (loggingChannel && loggingChannel.isSendable()) { 14 + await loggingChannel.send(message); 15 + } 16 + } catch {} 17 + 18 + console.log(message); 19 + } 20 + 21 + public setChannelId(id: string) { 22 + this.logChannelId = id; 23 + } 24 + }
+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 }
+116 -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"; 13 + import { Logger } from "./Logger"; 12 14 import type { Command } from "./types/Command"; 13 15 import type { Button } from "./types/Button"; 14 16 import type { Event } from "./types/Event"; 15 17 16 18 //=============================================== 19 + // ClientOptions Override 20 + //=============================================== 21 + export interface VoidyClientOptions extends ClientOptions { 22 + developers?: string[]; // List of developer user ids 23 + logChannelId?: string; // ID of the channel to log events to 24 + } 25 + 26 + //=============================================== 17 27 // VoidyClient Implementation 18 28 //=============================================== 19 29 export class VoidyClient extends Client { 20 - public moduleManager = new ModuleManager(); 30 + public moduleManager = new ModuleManager(); 31 + public developers: string[] = []; 32 + public logger: Logger = new Logger(this); 21 33 22 - public constructor(options: ClientOptions) { 23 - super(options); 24 - } 34 + public constructor(options: VoidyClientOptions) { 35 + super(options); 25 36 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(); 37 + // Set developers, if provided. 38 + if (options.developers) { 39 + this.developers = options.developers; 40 + } 35 41 36 - // Register commands on ready event 37 - this.on("ready", this.registerCommands); 42 + // Inject channel ID into logger, if provided. 43 + if (options.logChannelId) { 44 + this.logger.setChannelId(options.logChannelId); 45 + } 46 + } 38 47 39 - // Login using the bot token 40 - await this.login(token); 41 - } 48 + /** 49 + * Launches the bot 50 + * @param token - The Discord application bot token. 51 + * @param modulesPath - Where the bot should search for modules. 52 + */ 53 + public async start(token: string, modulesPath: string) { 54 + // Load modules and register events 55 + await this.moduleManager.loadModules(modulesPath); 56 + await this.registerEvents(); 42 57 43 - /** 44 - * Registers all cached events 45 - * @param events 46 - */ 47 - private async registerEvents() { 48 - const events = this.moduleManager.events; 58 + // Register commands on ready event 59 + this.on(Events.ClientReady, this.registerCommands); 49 60 50 - for (const [_id, event] of events) { 51 - const execute = (...args: unknown[]) => event.execute(this, ...args); 61 + // Login using the bot token 62 + await this.login(token); 63 + } 52 64 53 - if (event.once) this.once(event.name, execute); 54 - else this.on(event.name, execute); 55 - } 56 - } 65 + /** 66 + * Registers all cached events 67 + * @param events 68 + */ 69 + private async registerEvents() { 70 + const events = this.moduleManager.events; 57 71 58 - /** 59 - * Registers all provided commands globally 60 - * @param commands 61 - */ 62 - private async registerCommands(): Promise<void> { 63 - const topLevelCommands = new Map<string, SlashCommandBuilder>(); 72 + for (const [_id, event] of events) { 73 + const execute = (...args: unknown[]) => event.execute(this, ...args); 74 + 75 + if (event.once) this.once(event.name, execute); 76 + else this.on(event.name, execute); 77 + } 78 + } 79 + 80 + /** 81 + * Registers all provided commands globally 82 + * @param commands 83 + */ 84 + private async registerCommands(): Promise<void> { 85 + const topLevelCommands = new Map<string, SlashCommandBuilder>(); 64 86 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]; 87 + for (const cmd of this.moduleManager.commands.values()) { 88 + const parts = cmd.id.split("."); // ["music", "set", "channel"] 89 + const command = parts[0]; 90 + const subcommand = parts[1]; 91 + const subgroupcommand = parts[2]; 70 92 71 - if (!command) continue; 93 + if (!command) continue; 72 94 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("..."); 95 + // Ensure top-level builder exists 96 + if (!topLevelCommands.has(command)) { 97 + const topCommand = 98 + (this.moduleManager.commands.get(command)?.data as SlashCommandBuilder) ?? 99 + new SlashCommandBuilder().setName(command).setDescription("..."); 78 100 79 - topLevelCommands.set(command, topCommand); 80 - } 101 + topLevelCommands.set(command, topCommand); 102 + } 81 103 82 - const parent = topLevelCommands.get(command)!; 104 + const parent = topLevelCommands.get(command)!; 83 105 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 - ); 106 + if (subcommand && !subgroupcommand) { 107 + // It's a subcommand 108 + parent.addSubcommand(cmd.data as SlashCommandSubcommandBuilder); 109 + } else if (subcommand && subgroupcommand) { 110 + // It's a subgroup command 111 + let group = parent.options.find( 112 + (o): o is SlashCommandSubcommandGroupBuilder => 113 + o instanceof SlashCommandSubcommandGroupBuilder && o.name === subcommand, 114 + ); 92 115 93 - if (!group) { 94 - group = new SlashCommandSubcommandGroupBuilder().setName(subcommand).setDescription("..."); 95 - parent.addSubcommandGroup(group); 96 - } 116 + if (!group) { 117 + group = new SlashCommandSubcommandGroupBuilder() 118 + .setName(subcommand) 119 + .setDescription("..."); 120 + parent.addSubcommandGroup(group); 121 + } 97 122 98 - group.addSubcommand(cmd.data as SlashCommandSubcommandBuilder); 99 - } 100 - } 123 + group.addSubcommand(cmd.data as SlashCommandSubcommandBuilder); 124 + } 125 + } 101 126 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 - } 127 + // Finally convert assembled top-level commands to JSON and register them 128 + await this.application?.commands.set([...topLevelCommands.values()].map((c) => c.toJSON())); 129 + } 105 130 106 - /** 107 - * Returns all cached commands 108 - */ 109 - get commands(): CacheMap<Command> { 110 - return this.moduleManager.commands; 111 - } 131 + /** 132 + * Returns all cached commands 133 + */ 134 + get commands(): CacheMap<Command> { 135 + return this.moduleManager.commands; 136 + } 112 137 113 - /** 114 - * Returns all cached events 115 - */ 116 - get events(): CacheMap<Event> { 117 - return this.moduleManager.events; 118 - } 138 + /** 139 + * Returns all cached events 140 + */ 141 + get events(): CacheMap<Event> { 142 + return this.moduleManager.events; 143 + } 119 144 120 - /** 121 - * Returns all cached buttons 122 - */ 123 - get buttons(): CacheMap<Button> { 124 - return this.moduleManager.buttons; 125 - } 145 + /** 146 + * Returns all cached buttons 147 + */ 148 + get buttons(): CacheMap<Button> { 149 + return this.moduleManager.buttons; 150 + } 126 151 }
+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 }
+7 -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 + devOnly: boolean | null; 19 + execute: (interaction: ChatInputCommandInteraction, client: VoidyClient) => Promise<void>; 21 20 }
+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 }
+4 -4
packages/framework/src/handlers/ButtonHandler.ts
··· 1 1 import type { ButtonInteraction } from "discord.js"; 2 - import type { Button } from "../loaders/ButtonLoader"; 2 + import type { Button } from "../core/types/Button"; 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 }
+20 -5
packages/framework/src/handlers/CommandHandler.ts
··· 1 - import type { ChatInputCommandInteraction } from "discord.js"; 2 - import type { Command } from "../loaders/CommandLoader"; 1 + import { MessageFlags, type ChatInputCommandInteraction } from "discord.js"; 2 + import type { Command } from "../core/types/Command"; 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 async invoke( 7 + interaction: ChatInputCommandInteraction, 8 + payload: Command, 9 + client: VoidyClient, 10 + ): Promise<void> { 11 + if (payload.devOnly && !client.developers.includes(interaction.user.id)) { 12 + await interaction.reply({ 13 + content: "You are not authorized to use this command.", 14 + flags: [MessageFlags.Ephemeral] 15 + }); 16 + 17 + return; 18 + } 19 + 20 + client.logger.send(`[ChatInputCommandHandler] \`${payload.id}\` invoked by \`${interaction.user.tag}\``); 21 + 22 + payload.execute(interaction, client); 23 + } 9 24 }
+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 }
+19 -21
tsconfig.json
··· 1 1 { 2 - "compilerOptions": { 3 - "lib": [ 4 - "ESNext" 5 - ], 6 - "target": "ESNext", 7 - "module": "Preserve", 8 - "moduleDetection": "force", 9 - "allowJs": true, 10 - "moduleResolution": "bundler", 11 - "allowImportingTsExtensions": true, 12 - "verbatimModuleSyntax": true, 13 - "noEmit": true, 14 - "strict": true, 15 - "skipLibCheck": true, 16 - "noFallthroughCasesInSwitch": true, 17 - "noUncheckedIndexedAccess": true, 18 - "noImplicitOverride": true, 19 - "noUnusedLocals": false, 20 - "noUnusedParameters": false, 21 - "noPropertyAccessFromIndexSignature": false 22 - } 2 + "compilerOptions": { 3 + "lib": ["ESNext"], 4 + "target": "ESNext", 5 + "module": "Preserve", 6 + "moduleDetection": "force", 7 + "allowJs": true, 8 + "moduleResolution": "bundler", 9 + "allowImportingTsExtensions": true, 10 + "verbatimModuleSyntax": true, 11 + "noEmit": true, 12 + "strict": true, 13 + "skipLibCheck": true, 14 + "noFallthroughCasesInSwitch": true, 15 + "noUncheckedIndexedAccess": true, 16 + "noImplicitOverride": true, 17 + "noUnusedLocals": false, 18 + "noUnusedParameters": false, 19 + "noPropertyAccessFromIndexSignature": false, 20 + }, 23 21 }