+5
.env.example
+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
.github/assets/cyn-hi-chat-cyn-murder-drones.gif
This is a binary file and will not be displayed.
+18
-6
README.md
+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
+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
+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
-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
-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
-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
-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
+10
-4
package.json
+2
packages/api/.env.example
+2
packages/api/.env.example
+12
packages/api/Dockerfile
+12
packages/api/Dockerfile
+25
packages/api/index.ts
+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
+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
+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
+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
+8
packages/api/routes/api/v1/index.ts
+98
packages/api/routes/api/v1/shop.ts
+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
+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
+11
packages/bot/Dockerfile
+10
packages/bot/docker-compose.yml
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+3
packages/bot/src/modules/economy/schemas/index.ts
+2
packages/bot/src/modules/index.ts
+2
packages/bot/src/modules/index.ts
+29
packages/bot/src/modules/toys/commands/api/httpcat.ts
+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
+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
+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
+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
+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
+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
+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
+2
packages/bot/src/modules/user/schemas/index.ts
+2
-12
packages/bot/tsconfig.json
+2
-12
packages/bot/tsconfig.json
+20
-14
packages/framework/package.json
+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
+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
+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
+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
+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
+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
+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
+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
+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
+1
-1
packages/framework/src/core/types/Resource.ts
+4
-4
packages/framework/src/handlers/ButtonHandler.ts
+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
+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
+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
+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
+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
+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
+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
+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
}