+5
-3
.env.example
+5
-3
.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
-
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
+
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.
+30
-8
.gitignore
+30
-8
.gitignore
···
1
-
### Jetbrains
2
-
.idea
3
-
.vscode
1
+
# dependencies (bun install)
2
+
node_modules
3
+
4
+
# output
5
+
out
6
+
dist
7
+
*.tgz
4
8
5
-
### Node/Deno/Bun
6
-
node_modules
9
+
# code coverage
10
+
coverage
11
+
*.lcov
7
12
8
-
### Secrets
13
+
# logs
14
+
logs
15
+
_.log
16
+
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
17
+
18
+
# dotenv environment variable files
9
19
.env
20
+
.env.development.local
21
+
.env.test.local
22
+
.env.production.local
23
+
.env.local
10
24
11
-
### Docker
12
-
docker-compose.yml
25
+
# caches
26
+
.eslintcache
27
+
.cache
28
+
*.tsbuildinfo
29
+
30
+
# IntelliJ based IDEs
31
+
.idea
32
+
33
+
# Finder (MacOS) folder config
34
+
.DS_Store
-31
.zed/settings.json
-31
.zed/settings.json
···
1
-
{
2
-
"lsp": {
3
-
"deno": {
4
-
"settings": {
5
-
"deno": {
6
-
"enable": true
7
-
}
8
-
}
9
-
}
10
-
},
11
-
"languages": {
12
-
"TypeScript": {
13
-
"language_servers": [
14
-
"deno",
15
-
"!typescript-language-server",
16
-
"!vtsls",
17
-
"!eslint"
18
-
],
19
-
"formatter": "language_server"
20
-
},
21
-
"TSX": {
22
-
"language_servers": [
23
-
"deno",
24
-
"!typescript-language-server",
25
-
"!vtsls",
26
-
"!eslint"
27
-
],
28
-
"formatter": "language_server"
29
-
}
30
-
}
31
-
}
+18
-9
README.md
+18
-9
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
-
deno task dev
11
+
git clone git@tangled.org:thevoid.cafe/voidy
12
+
```
13
+
14
+
Then, navigate to the project directory and install all dependencies:
15
+
16
+
```sh
17
+
cd voidy
18
+
bun install
11
19
```
12
20
13
-
## ๐ Goals
14
-
- Implement a robust combined event and command handling system, with proper namespacing, titled "feature manager"
15
-
- Built-in statistics system, which records monthly channel/user activity, command executions per guild/user, and more (all recorded data is viewable by the user or guild admins)
16
-
- Built-in relationship management system, which allows sharing secret messages and giving other users access to your statistics data
17
-
- Integration of Snowfall API, with configurable instance URL and token, for synchronizing statistics, in a last.fm like fashion, but more open and cool
21
+
Finally, run the development server:
22
+
23
+
```sh
24
+
bun dev
25
+
```
18
26
19
27
## ๐จ Credits
28
+
20
29
Certain features of this bot were the result
21
30
of conversations with:
22
31
+182
bun.lock
+182
bun.lock
···
1
+
{
2
+
"lockfileVersion": 1,
3
+
"configVersion": 0,
4
+
"workspaces": {
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
+
},
25
+
},
26
+
"packages/bot": {
27
+
"name": "@voidy/bot",
28
+
"version": "0.1.0",
29
+
"dependencies": {
30
+
"@voidy/api": "workspace:*",
31
+
"@voidy/framework": "workspace:*",
32
+
"discord.js": "^14.25.1",
33
+
"hono": "^4.11.1",
34
+
"mongoose": "^9.0.1",
35
+
},
36
+
"devDependencies": {
37
+
"@types/bun": "latest",
38
+
},
39
+
"peerDependencies": {
40
+
"typescript": "^5.9.2",
41
+
},
42
+
},
43
+
"packages/framework": {
44
+
"name": "@voidy/framework",
45
+
"version": "0.1.0",
46
+
"dependencies": {
47
+
"discord.js": "^14.25.1",
48
+
},
49
+
"devDependencies": {
50
+
"@types/bun": "latest",
51
+
},
52
+
"peerDependencies": {
53
+
"typescript": "^5.9.2",
54
+
},
55
+
},
56
+
},
57
+
"packages": {
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=="],
59
+
60
+
"@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="],
61
+
62
+
"@discordjs/formatters": ["@discordjs/formatters@0.6.2", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ=="],
63
+
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=="],
65
+
66
+
"@discordjs/util": ["@discordjs/util@1.2.0", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg=="],
67
+
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=="],
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
+
88
+
"@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="],
89
+
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=="],
91
+
92
+
"@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="],
93
+
94
+
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
95
+
96
+
"@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="],
97
+
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=="],
101
+
102
+
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
103
+
104
+
"@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.6", "", {}, "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA=="],
105
+
106
+
"@voidy/api": ["@voidy/api@workspace:packages/api"],
107
+
108
+
"@voidy/bot": ["@voidy/bot@workspace:packages/bot"],
109
+
110
+
"@voidy/framework": ["@voidy/framework@workspace:packages/framework"],
111
+
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=="],
119
+
120
+
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
121
+
122
+
"hono": ["hono@4.11.1", "", {}, "sha512-KsFcH0xxHes0J4zaQgWbYwmz3UPOOskdqZmItstUG93+Wk1ePBLkLGwbP9zlmh1BFUiL8Qp+Xfu9P7feJWpGNg=="],
123
+
124
+
"kareem": ["kareem@3.0.0", "", {}, "sha512-RKhaOBSPN8L7y4yAgNhDT2602G5FD6QbOIISbjN9D6mjHPeqeg7K+EB5IGSU5o81/X2Gzm3ICnAvQW3x3OP8HA=="],
125
+
126
+
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
127
+
128
+
"lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="],
129
+
130
+
"magic-bytes.js": ["magic-bytes.js@1.12.1", "", {}, "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA=="],
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
+
156
+
"ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="],
157
+
158
+
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
159
+
160
+
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
161
+
162
+
"undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="],
163
+
164
+
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
165
+
166
+
"webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="],
167
+
168
+
"whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="],
169
+
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=="],
171
+
172
+
"@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
173
+
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=="],
181
+
}
182
+
}
-10
deno.json
-10
deno.json
-160
deno.lock
-160
deno.lock
···
1
-
{
2
-
"version": "4",
3
-
"specifiers": {
4
-
"jsr:@std/fs@*": "1.0.17",
5
-
"jsr:@std/path@*": "1.0.9",
6
-
"jsr:@std/path@^1.0.9": "1.0.9",
7
-
"npm:discord.js@*": "14.19.2"
8
-
},
9
-
"jsr": {
10
-
"@std/fs@1.0.17": {
11
-
"integrity": "1c00c632677c1158988ef7a004cb16137f870aafdb8163b9dce86ec652f3952b",
12
-
"dependencies": [
13
-
"jsr:@std/path@^1.0.9"
14
-
]
15
-
},
16
-
"@std/path@1.0.9": {
17
-
"integrity": "260a49f11edd3db93dd38350bf9cd1b4d1366afa98e81b86167b4e3dd750129e"
18
-
}
19
-
},
20
-
"npm": {
21
-
"@discordjs/builders@1.11.1": {
22
-
"integrity": "sha512-2zDAVuoeAkdv0YQzYKO8vZfaDfB+1KZ60ymBKtD7QDpsh6lzAnQSUBLqeRkhlons6BT9+yRctOh9fPy94w6kDA==",
23
-
"dependencies": [
24
-
"@discordjs/formatters",
25
-
"@discordjs/util",
26
-
"@sapphire/shapeshift",
27
-
"discord-api-types",
28
-
"fast-deep-equal",
29
-
"ts-mixer",
30
-
"tslib"
31
-
]
32
-
},
33
-
"@discordjs/collection@1.5.3": {
34
-
"integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="
35
-
},
36
-
"@discordjs/collection@2.1.1": {
37
-
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="
38
-
},
39
-
"@discordjs/formatters@0.6.1": {
40
-
"integrity": "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==",
41
-
"dependencies": [
42
-
"discord-api-types"
43
-
]
44
-
},
45
-
"@discordjs/rest@2.5.0": {
46
-
"integrity": "sha512-PWhchxTzpn9EV3vvPRpwS0EE2rNYB9pvzDU/eLLW3mByJl0ZHZjHI2/wA8EbH2gRMQV7nu+0FoDF84oiPl8VAQ==",
47
-
"dependencies": [
48
-
"@discordjs/collection@2.1.1",
49
-
"@discordjs/util",
50
-
"@sapphire/async-queue",
51
-
"@sapphire/snowflake",
52
-
"@vladfrangu/async_event_emitter",
53
-
"discord-api-types",
54
-
"magic-bytes.js",
55
-
"tslib",
56
-
"undici"
57
-
]
58
-
},
59
-
"@discordjs/util@1.1.1": {
60
-
"integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g=="
61
-
},
62
-
"@discordjs/ws@1.2.2": {
63
-
"integrity": "sha512-dyfq7yn0wO0IYeYOs3z79I6/HumhmKISzFL0Z+007zQJMtAFGtt3AEoq1nuLXtcunUE5YYYQqgKvybXukAK8/w==",
64
-
"dependencies": [
65
-
"@discordjs/collection@2.1.1",
66
-
"@discordjs/rest",
67
-
"@discordjs/util",
68
-
"@sapphire/async-queue",
69
-
"@types/ws",
70
-
"@vladfrangu/async_event_emitter",
71
-
"discord-api-types",
72
-
"tslib",
73
-
"ws"
74
-
]
75
-
},
76
-
"@sapphire/async-queue@1.5.5": {
77
-
"integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="
78
-
},
79
-
"@sapphire/shapeshift@4.0.0": {
80
-
"integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==",
81
-
"dependencies": [
82
-
"fast-deep-equal",
83
-
"lodash"
84
-
]
85
-
},
86
-
"@sapphire/snowflake@3.5.3": {
87
-
"integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="
88
-
},
89
-
"@types/node@22.12.0": {
90
-
"integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==",
91
-
"dependencies": [
92
-
"undici-types"
93
-
]
94
-
},
95
-
"@types/ws@8.18.1": {
96
-
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
97
-
"dependencies": [
98
-
"@types/node"
99
-
]
100
-
},
101
-
"@vladfrangu/async_event_emitter@2.4.6": {
102
-
"integrity": "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA=="
103
-
},
104
-
"discord-api-types@0.38.1": {
105
-
"integrity": "sha512-vsjsqjAuxsPhiwbPjTBeGQaDPlizFmSkU0mTzFGMgRxqCDIRBR7iTY74HacpzrDV0QtERHRKQEk1tq7drZUtHg=="
106
-
},
107
-
"discord.js@14.19.2": {
108
-
"integrity": "sha512-L/ivhVefzzRcChHJSaGYsgA4Uqx6or2sst5JZ/ft9OBwrj8OJIzrrcutlkHnm/hlI0Hrm3es62TRVksU8VUqrg==",
109
-
"dependencies": [
110
-
"@discordjs/builders",
111
-
"@discordjs/collection@1.5.3",
112
-
"@discordjs/formatters",
113
-
"@discordjs/rest",
114
-
"@discordjs/util",
115
-
"@discordjs/ws",
116
-
"@sapphire/snowflake",
117
-
"discord-api-types",
118
-
"fast-deep-equal",
119
-
"lodash.snakecase",
120
-
"magic-bytes.js",
121
-
"tslib",
122
-
"undici"
123
-
]
124
-
},
125
-
"fast-deep-equal@3.1.3": {
126
-
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
127
-
},
128
-
"lodash.snakecase@4.1.1": {
129
-
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="
130
-
},
131
-
"lodash@4.17.21": {
132
-
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
133
-
},
134
-
"magic-bytes.js@1.12.1": {
135
-
"integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA=="
136
-
},
137
-
"ts-mixer@6.0.4": {
138
-
"integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="
139
-
},
140
-
"tslib@2.8.1": {
141
-
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
142
-
},
143
-
"undici-types@6.20.0": {
144
-
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="
145
-
},
146
-
"undici@6.21.1": {
147
-
"integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ=="
148
-
},
149
-
"ws@8.18.1": {
150
-
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w=="
151
-
}
152
-
},
153
-
"workspace": {
154
-
"dependencies": [
155
-
"jsr:@std/fs@*",
156
-
"jsr:@std/path@*",
157
-
"npm:discord.js@*"
158
-
]
159
-
}
160
-
}
+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
+12
package.json
+12
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
packages/bot/.env.example
+5
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
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
+29
packages/bot/package.json
+29
packages/bot/package.json
···
1
+
{
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
+
}
29
+
}
+28
packages/bot/src/index.ts
+28
packages/bot/src/index.ts
···
1
+
import { GatewayIntentBits } from "discord.js";
2
+
import { VoidyClient } from "@voidy/framework";
3
+
import { connect } from "mongoose";
4
+
5
+
//===============================================
6
+
// Discord client initialization
7
+
//===============================================
8
+
9
+
// Client initialization with intents and stuff...
10
+
const client = new VoidyClient({
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
+
});
25
+
26
+
// Token validation and client start
27
+
if (!Bun.env.BOT_TOKEN) throw new Error("[Voidy] Missing bot token");
28
+
await client.start(Bun.env.BOT_TOKEN, `${import.meta.dirname}/modules`);
+16
packages/bot/src/modules/core/commands/ping.ts
+16
packages/bot/src/modules/core/commands/ping.ts
···
1
+
import { MessageFlags, SlashCommandBuilder } from "discord.js";
2
+
import type { Command } from "@voidy/framework";
3
+
4
+
export default {
5
+
id: "ping",
6
+
data: new SlashCommandBuilder()
7
+
.setName("ping")
8
+
.setDescription("View the websocket ping between Discord and the Bot."),
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;
+61
packages/bot/src/modules/core/events/interactionCreate.ts
+61
packages/bot/src/modules/core/events/interactionCreate.ts
···
1
+
import { Events, MessageFlags, type Interaction } from "discord.js";
2
+
import {
3
+
ChatInputCommandHandler,
4
+
ButtonHandler,
5
+
type VoidyClient,
6
+
type Event,
7
+
} from "@voidy/framework";
8
+
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;
16
+
17
+
// Try to get a subgroup first
18
+
const subgroup = interaction.options.getSubcommandGroup(false);
19
+
if (subgroup) commandId += `.${subgroup}`;
20
+
21
+
// Then subcommand (or subcommand in a group)
22
+
const subcommand = interaction.options.getSubcommand(false);
23
+
if (subcommand) commandId += `.${subcommand}`;
24
+
25
+
const command = client.commands.get(commandId);
26
+
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
+
});
32
+
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);
37
+
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
+
});
43
+
44
+
ButtonHandler.invoke(interaction, button, client);
45
+
} else {
46
+
let dmChannel = interaction.user.dmChannel;
47
+
48
+
// Attempt DM channel creation, if not found.
49
+
if (!dmChannel) {
50
+
dmChannel = await interaction.user.createDM();
51
+
}
52
+
53
+
// If the DM channel is still not available, give up.
54
+
if (!dmChannel || !dmChannel.isSendable()) return;
55
+
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;
+16
packages/bot/src/modules/core/events/ready.ts
+16
packages/bot/src/modules/core/events/ready.ts
···
1
+
import type { Event, VoidyClient } from "@voidy/framework";
2
+
import { ActivityType, Events } from "discord.js";
3
+
4
+
export default {
5
+
id: "clientReady",
6
+
name: Events.ClientReady,
7
+
once: true,
8
+
execute: async (client: VoidyClient) => {
9
+
console.log("THE BOT IS READY >:3");
10
+
11
+
client.user?.setActivity({
12
+
name: `${client.guilds.cache.size} guilds :3`,
13
+
type: ActivityType.Watching,
14
+
});
15
+
},
16
+
} as Event;
+19
packages/bot/src/modules/core/module.ts
+19
packages/bot/src/modules/core/module.ts
···
1
+
import { CommandLoader, EventLoader, type Module } from "@voidy/framework";
2
+
3
+
export default {
4
+
id: "core",
5
+
name: "Core",
6
+
description: "Initializes the bot and registers all commands.",
7
+
author: "thevoid.cafe",
8
+
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
+
],
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
+4
packages/bot/tsconfig.json
+4
packages/bot/tsconfig.json
+22
packages/framework/package.json
+22
packages/framework/package.json
···
1
+
{
2
+
"name": "@voidy/framework",
3
+
"version": "0.1.0",
4
+
"module": "src/index.ts",
5
+
"type": "module",
6
+
"private": true,
7
+
"exports": {
8
+
".": {
9
+
"import": "./src/index.ts",
10
+
"require": "./src/index.cjs"
11
+
}
12
+
},
13
+
"devDependencies": {
14
+
"@types/bun": "latest"
15
+
},
16
+
"peerDependencies": {
17
+
"typescript": "^5.9.2"
18
+
},
19
+
"dependencies": {
20
+
"discord.js": "^14.25.1"
21
+
}
22
+
}
+80
packages/framework/src/core/Loader.ts
+80
packages/framework/src/core/Loader.ts
···
1
+
//===============================================
2
+
// Imports
3
+
//===============================================
4
+
import { Glob } from "bun";
5
+
6
+
//===============================================
7
+
// Loader Definition
8
+
//===============================================
9
+
interface ILoader<T extends object> {
10
+
id: string;
11
+
cache: T[];
12
+
source: string;
13
+
14
+
collect: () => Promise<ThisType<this>>;
15
+
validate: (data: Partial<T>) => Promise<T | null>;
16
+
getJSON: () => T[];
17
+
}
18
+
19
+
//===============================================
20
+
// Loader Implementation
21
+
//===============================================
22
+
export abstract class Loader<T extends object> implements ILoader<T> {
23
+
public abstract id: string;
24
+
public cache: T[] = [];
25
+
public source: string;
26
+
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
+
);
32
+
33
+
this.source = source;
34
+
}
35
+
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);
42
+
43
+
try {
44
+
for await (const path of iterator) {
45
+
let moduleDefault: T | null;
46
+
47
+
try {
48
+
const module = await import(`${this.source}/${path}`);
49
+
moduleDefault = module.default;
50
+
51
+
if (!moduleDefault) continue;
52
+
} catch {
53
+
continue;
54
+
}
55
+
56
+
const final = await this.validate(moduleDefault);
57
+
if (!final) continue;
58
+
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
+
}
65
+
66
+
return this;
67
+
}
68
+
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>;
73
+
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
+
}
+72
packages/framework/src/core/ModuleManager.ts
+72
packages/framework/src/core/ModuleManager.ts
···
1
+
//===============================================
2
+
// Imports
3
+
//===============================================
4
+
import { ModuleLoader } from "../loaders/ModuleLoader";
5
+
import type { Command } from "./types/Command";
6
+
import type { Button } from "./types/Button";
7
+
import type { Module } from "./types/Module";
8
+
import type { Event } from "./types/Event";
9
+
10
+
export type CacheMap<T = unknown> = Map<string, T>;
11
+
12
+
//===============================================
13
+
// ModuleManager Implementation
14
+
//===============================================
15
+
export class ModuleManager {
16
+
private cache = new Map<string, Map<string, unknown>>();
17
+
18
+
// Module Loading
19
+
//==============================
20
+
async loadModules(path: string) {
21
+
const moduleLoader = new ModuleLoader(path);
22
+
const modules = (await moduleLoader.collect()).getJSON();
23
+
24
+
for (const module of modules) {
25
+
await this.prepareModule(module);
26
+
}
27
+
}
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();
33
+
34
+
for (const item of data) {
35
+
this.set(loader.id, (item as any).id, item);
36
+
}
37
+
}
38
+
}
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
+
}
46
+
47
+
get<T>(type: string, id: string): T | undefined {
48
+
return (this.cache.get(type) as CacheMap<T>)?.get(id);
49
+
}
50
+
51
+
getAll<T>(type: string): CacheMap<T> {
52
+
return (this.cache.get(type) as CacheMap<T>) ?? new Map();
53
+
}
54
+
55
+
// Typed Accessors
56
+
//==============================
57
+
get modules(): CacheMap<Module> {
58
+
return this.getAll<Module>("module");
59
+
}
60
+
61
+
get commands(): CacheMap<Command> {
62
+
return this.getAll<Command>("command");
63
+
}
64
+
65
+
get buttons(): CacheMap<Button> {
66
+
return this.getAll<Button>("button");
67
+
}
68
+
69
+
get events(): CacheMap<Event> {
70
+
return this.getAll<Event>("event");
71
+
}
72
+
}
+151
packages/framework/src/core/VoidyClient.ts
+151
packages/framework/src/core/VoidyClient.ts
···
1
+
//===============================================
2
+
// Imports
3
+
//===============================================
4
+
import {
5
+
type ClientOptions,
6
+
SlashCommandSubcommandGroupBuilder,
7
+
SlashCommandSubcommandBuilder,
8
+
SlashCommandBuilder,
9
+
Client,
10
+
Events,
11
+
} from "discord.js";
12
+
import { ModuleManager, type CacheMap } from "./ModuleManager";
13
+
import { Logger } from "./Logger";
14
+
import type { Command } from "./types/Command";
15
+
import type { Button } from "./types/Button";
16
+
import type { Event } from "./types/Event";
17
+
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
+
//===============================================
27
+
// VoidyClient Implementation
28
+
//===============================================
29
+
export class VoidyClient extends Client {
30
+
public moduleManager = new ModuleManager();
31
+
public developers: string[] = [];
32
+
public logger: Logger = new Logger(this);
33
+
34
+
public constructor(options: VoidyClientOptions) {
35
+
super(options);
36
+
37
+
// Set developers, if provided.
38
+
if (options.developers) {
39
+
this.developers = options.developers;
40
+
}
41
+
42
+
// Inject channel ID into logger, if provided.
43
+
if (options.logChannelId) {
44
+
this.logger.setChannelId(options.logChannelId);
45
+
}
46
+
}
47
+
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();
57
+
58
+
// Register commands on ready event
59
+
this.on(Events.ClientReady, this.registerCommands);
60
+
61
+
// Login using the bot token
62
+
await this.login(token);
63
+
}
64
+
65
+
/**
66
+
* Registers all cached events
67
+
* @param events
68
+
*/
69
+
private async registerEvents() {
70
+
const events = this.moduleManager.events;
71
+
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>();
86
+
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];
92
+
93
+
if (!command) continue;
94
+
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("...");
100
+
101
+
topLevelCommands.set(command, topCommand);
102
+
}
103
+
104
+
const parent = topLevelCommands.get(command)!;
105
+
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
+
);
115
+
116
+
if (!group) {
117
+
group = new SlashCommandSubcommandGroupBuilder()
118
+
.setName(subcommand)
119
+
.setDescription("...");
120
+
parent.addSubcommandGroup(group);
121
+
}
122
+
123
+
group.addSubcommand(cmd.data as SlashCommandSubcommandBuilder);
124
+
}
125
+
}
126
+
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
+
}
130
+
131
+
/**
132
+
* Returns all cached commands
133
+
*/
134
+
get commands(): CacheMap<Command> {
135
+
return this.moduleManager.commands;
136
+
}
137
+
138
+
/**
139
+
* Returns all cached events
140
+
*/
141
+
get events(): CacheMap<Event> {
142
+
return this.moduleManager.events;
143
+
}
144
+
145
+
/**
146
+
* Returns all cached buttons
147
+
*/
148
+
get buttons(): CacheMap<Button> {
149
+
return this.moduleManager.buttons;
150
+
}
151
+
}
+13
packages/framework/src/core/types/Button.ts
+13
packages/framework/src/core/types/Button.ts
···
1
+
//===============================================
2
+
// Imports
3
+
//===============================================
4
+
import type { ButtonInteraction } from "discord.js";
5
+
import type { VoidyClient } from "../VoidyClient";
6
+
import type { Resource } from "./Resource";
7
+
8
+
//===============================================
9
+
// Button Definition
10
+
//===============================================
11
+
export interface Button extends Resource {
12
+
execute: (interaction: ButtonInteraction, client: VoidyClient) => Promise<void>;
13
+
}
+20
packages/framework/src/core/types/Command.ts
+20
packages/framework/src/core/types/Command.ts
···
1
+
//===============================================
2
+
// Imports
3
+
//===============================================
4
+
import type {
5
+
SlashCommandSubcommandGroupBuilder,
6
+
SlashCommandSubcommandBuilder,
7
+
ChatInputCommandInteraction,
8
+
SlashCommandBuilder,
9
+
} from "discord.js";
10
+
import type { VoidyClient } from "../VoidyClient";
11
+
import type { Resource } from "./Resource";
12
+
13
+
//===============================================
14
+
// Command Definition
15
+
//===============================================
16
+
export interface Command extends Resource {
17
+
data: SlashCommandBuilder | SlashCommandSubcommandBuilder | SlashCommandSubcommandGroupBuilder;
18
+
devOnly: boolean | null;
19
+
execute: (interaction: ChatInputCommandInteraction, client: VoidyClient) => Promise<void>;
20
+
}
+15
packages/framework/src/core/types/Event.ts
+15
packages/framework/src/core/types/Event.ts
···
1
+
//===============================================
2
+
// Imports
3
+
//===============================================
4
+
import type { VoidyClient } from "../VoidyClient";
5
+
import type { ClientEvents } from "discord.js";
6
+
import type { Resource } from "./Resource";
7
+
8
+
//===============================================
9
+
// Event Definition
10
+
//===============================================
11
+
export interface Event extends Resource {
12
+
name: keyof ClientEvents;
13
+
once?: boolean;
14
+
execute: (client: VoidyClient, ...args: unknown[]) => void;
15
+
}
+23
packages/framework/src/core/types/Module.ts
+23
packages/framework/src/core/types/Module.ts
···
1
+
//===============================================
2
+
// Imports
3
+
//===============================================
4
+
import type { Resource } from "./Resource";
5
+
import type { Loader } from "../Loader";
6
+
7
+
//===============================================
8
+
// ModuleExportsItem Definition
9
+
//===============================================
10
+
export interface ModuleExportsItem<T extends object> {
11
+
source: string;
12
+
loader: new (...args: ConstructorParameters<typeof Loader<T>>) => Loader<T>;
13
+
}
14
+
15
+
//===============================================
16
+
// Module Definition
17
+
//===============================================
18
+
export interface Module extends Resource {
19
+
name: string;
20
+
description: string;
21
+
author: string;
22
+
exports: ModuleExportsItem<object>[];
23
+
}
+6
packages/framework/src/core/types/Resource.ts
+6
packages/framework/src/core/types/Resource.ts
+9
packages/framework/src/handlers/ButtonHandler.ts
+9
packages/framework/src/handlers/ButtonHandler.ts
···
1
+
import type { ButtonInteraction } from "discord.js";
2
+
import type { Button } from "../core/types/Button";
3
+
import type { VoidyClient } from "../core/VoidyClient";
4
+
5
+
export class ButtonHandler {
6
+
public static invoke(interaction: ButtonInteraction, payload: Button, client: VoidyClient): void {
7
+
payload.execute(interaction, client);
8
+
}
9
+
}
+24
packages/framework/src/handlers/CommandHandler.ts
+24
packages/framework/src/handlers/CommandHandler.ts
···
1
+
import { MessageFlags, type ChatInputCommandInteraction } from "discord.js";
2
+
import type { Command } from "../core/types/Command";
3
+
import type { VoidyClient } from "../core/VoidyClient";
4
+
5
+
export class ChatInputCommandHandler {
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
+
}
24
+
}
+21
packages/framework/src/index.ts
+21
packages/framework/src/index.ts
···
1
+
// Core
2
+
export * from "./core/Loader";
3
+
export * from "./core/ModuleManager";
4
+
export * from "./core/VoidyClient";
5
+
6
+
// Types
7
+
export * from "./core/types/Button";
8
+
export * from "./core/types/Command";
9
+
export * from "./core/types/Event";
10
+
export * from "./core/types/Module";
11
+
export * from "./core/types/Resource";
12
+
13
+
// Handlers
14
+
export * from "./handlers/ButtonHandler";
15
+
export * from "./handlers/CommandHandler";
16
+
17
+
// Loaders
18
+
export * from "./loaders/ButtonLoader";
19
+
export * from "./loaders/CommandLoader";
20
+
export * from "./loaders/EventLoader";
21
+
export * from "./loaders/ModuleLoader";
+16
packages/framework/src/loaders/ButtonLoader.ts
+16
packages/framework/src/loaders/ButtonLoader.ts
···
1
+
//===============================================
2
+
// Imports
3
+
//===============================================
4
+
import type { Button } from "../core/types/Button";
5
+
import { Loader } from "../core/Loader";
6
+
7
+
//===============================================
8
+
// ButtonLoader Implementation
9
+
//===============================================
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
+
}
16
+
}
+16
packages/framework/src/loaders/CommandLoader.ts
+16
packages/framework/src/loaders/CommandLoader.ts
···
1
+
//===============================================
2
+
// Imports
3
+
//===============================================
4
+
import type { Command } from "../core/types/Command";
5
+
import { Loader } from "../core/Loader";
6
+
7
+
//===============================================
8
+
// CommandLoader Implementation
9
+
//===============================================
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
+
}
16
+
}
+16
packages/framework/src/loaders/EventLoader.ts
+16
packages/framework/src/loaders/EventLoader.ts
···
1
+
//===============================================
2
+
// Imports
3
+
//===============================================
4
+
import type { Event } from "../core/types/Event";
5
+
import { Loader } from "../core/Loader";
6
+
7
+
//===============================================
8
+
// EventLoader Implemenation
9
+
//===============================================
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
+
}
16
+
}
+17
packages/framework/src/loaders/ModuleLoader.ts
+17
packages/framework/src/loaders/ModuleLoader.ts
···
1
+
//===============================================
2
+
// Imports
3
+
//===============================================
4
+
import type { Module } from "../core/types/Module";
5
+
import { Loader } from "../core/Loader";
6
+
7
+
//===============================================
8
+
// ModuleLoader Implementation
9
+
//===============================================
10
+
export class ModuleLoader extends Loader<Module> {
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;
14
+
15
+
return data as Module;
16
+
}
17
+
}
+12
packages/framework/tsconfig.json
+12
packages/framework/tsconfig.json
-25
src/core/client.ts
-25
src/core/client.ts
···
1
-
import { Client, ClientOptions } from "discord.js";
2
-
import { FeatureRegistry } from "./registry.ts";
3
-
4
-
export class VoidyClient extends Client {
5
-
public registry: FeatureRegistry;
6
-
7
-
constructor(options: ClientOptions) {
8
-
super(options);
9
-
10
-
this.registry = new FeatureRegistry(this);
11
-
}
12
-
13
-
async start(token: string) {
14
-
await this.registry.loadFeaturesFromDirectory("src/features");
15
-
16
-
this.once("ready", async () => {
17
-
console.log(`โ
Logged in as ${this.user?.tag}`);
18
-
19
-
await this.registry.deployCommands();
20
-
await this.registry.notifyReady();
21
-
});
22
-
23
-
await this.login(token);
24
-
}
25
-
}
-132
src/core/registry.ts
-132
src/core/registry.ts
···
1
-
import { walk } from "@std/fs";
2
-
import { resolve } from "@std/path";
3
-
import { Interaction } from "discord.js";
4
-
import { Feature } from "./types.ts";
5
-
import { VoidyClient } from "./client.ts";
6
-
7
-
export class FeatureRegistry {
8
-
private client: VoidyClient;
9
-
private features = new Map<string, Feature>();
10
-
11
-
constructor(client: VoidyClient) {
12
-
this.client = client;
13
-
14
-
// Global interaction handler
15
-
this.client.on("interactionCreate", async (interaction: Interaction) => {
16
-
if (interaction.isChatInputCommand()) {
17
-
for (const feature of this.features.values()) {
18
-
for (const cmd of feature.commands ?? []) {
19
-
if (cmd.data.name === interaction.commandName) {
20
-
const context = {
21
-
client: this.client,
22
-
createCustomId: (id: string) => `${feature.id}:${id}`,
23
-
};
24
-
25
-
return await cmd.execute(interaction, context);
26
-
}
27
-
}
28
-
}
29
-
}
30
-
31
-
if (interaction.isButton()) {
32
-
const [featureId, buttonId] = interaction.customId.split(":");
33
-
const feature = this.features.get(featureId);
34
-
const buttonHandler = feature?.buttonHandlers?.get(buttonId);
35
-
36
-
if (feature && buttonHandler) {
37
-
const context = {
38
-
client: this.client,
39
-
createCustomId: (id: string) => `${feature.id}:${id}`,
40
-
};
41
-
42
-
return await buttonHandler(interaction, context);
43
-
}
44
-
}
45
-
});
46
-
47
-
// Global event handler
48
-
for (const feature of this.features.values()) {
49
-
for (const event of feature.events ?? []) {
50
-
const context = {
51
-
client: this.client,
52
-
createCustomId: (id: string) => `${feature.id}:${id}`,
53
-
};
54
-
55
-
if (event.once) {
56
-
this.client.once(event.name, async (data) => {
57
-
await event.execute(data, context);
58
-
});
59
-
} else {
60
-
this.client.on(event.name, async (data) => {
61
-
await event.execute(data, context);
62
-
});
63
-
}
64
-
}
65
-
}
66
-
}
67
-
68
-
async loadFeaturesFromDirectory(directory: string) {
69
-
const root = resolve(Deno.cwd(), directory);
70
-
71
-
for await (const entry of walk(root, { includeDirs: false })) {
72
-
if (entry.name === "index.ts") {
73
-
const module = await import("file://" + entry.path);
74
-
const feature: Feature = module.default;
75
-
76
-
if (!feature || !feature.id) {
77
-
console.warn(`โ Invalid feature at ${entry.path}`);
78
-
continue;
79
-
}
80
-
81
-
if (this.features.has(feature.id)) {
82
-
console.warn(`โ Feature ID conflict: ${feature.id}`);
83
-
continue;
84
-
}
85
-
86
-
this.features.set(feature.id, feature);
87
-
await feature.setup?.();
88
-
console.log(`๐น Loaded feature: ${feature.name}`);
89
-
}
90
-
}
91
-
92
-
// Log some statistics
93
-
const featureCount = this.features.size;
94
-
const eventsCount = Array.from(this.features.values())
95
-
.flatMap((f) => f.events ?? [])
96
-
.length;
97
-
const commandsCount = Array.from(this.features.values())
98
-
.flatMap((f) => f.commands ?? [])
99
-
.length;
100
-
101
-
console.log(
102
-
`โ
Loaded ${featureCount} features, with ${eventsCount} events and ${commandsCount} commands.`,
103
-
);
104
-
}
105
-
106
-
async deployCommands() {
107
-
const commands = Array.from(this.features.values())
108
-
.flatMap((f) => f.commands ?? [])
109
-
.map((c) => c.data.toJSON());
110
-
111
-
await this.client.application?.commands.set(commands);
112
-
console.log(`๐ Deployed ${commands.length} slash commands.`);
113
-
}
114
-
115
-
async notifyReady() {
116
-
for (const feature of this.features.values()) {
117
-
await feature.onReady?.();
118
-
}
119
-
}
120
-
121
-
async cleanupAll() {
122
-
for (const feature of this.features.values()) {
123
-
try {
124
-
await feature.cleanup?.();
125
-
} catch (err) {
126
-
console.warn(`โ Error during cleanup of ${feature.id}:`, err);
127
-
}
128
-
}
129
-
130
-
this.features.clear();
131
-
}
132
-
}
-49
src/core/types.ts
-49
src/core/types.ts
···
1
-
import {
2
-
ButtonInteraction,
3
-
ChatInputCommandInteraction,
4
-
SlashCommandBuilder,
5
-
SlashCommandOptionsOnlyBuilder,
6
-
} from "discord.js";
7
-
import { VoidyClient } from "./client.ts";
8
-
9
-
export interface Command {
10
-
data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder;
11
-
execute: (
12
-
interaction: ChatInputCommandInteraction,
13
-
context: FeatureContext,
14
-
) => Promise<void>;
15
-
}
16
-
17
-
export interface Event {
18
-
name: string;
19
-
once?: boolean;
20
-
execute: (
21
-
data: object,
22
-
context: FeatureContext,
23
-
) => Promise<void> | void;
24
-
}
25
-
26
-
export type ButtonHandler = (
27
-
interaction: ButtonInteraction,
28
-
context: FeatureContext,
29
-
) => Promise<void>;
30
-
31
-
export interface Feature {
32
-
id: string;
33
-
name: string;
34
-
description?: string;
35
-
36
-
commands?: Command[];
37
-
events?: Event[];
38
-
buttonHandlers?: Map<string, ButtonHandler>;
39
-
40
-
// Optional lifecycle hooks
41
-
setup?: () => Promise<void>;
42
-
onReady?: () => Promise<void>;
43
-
cleanup?: () => Promise<void>;
44
-
}
45
-
46
-
export interface FeatureContext {
47
-
client: VoidyClient;
48
-
createCustomId: (id: string) => string;
49
-
}
-125
src/features/maintenance/commands.ts
-125
src/features/maintenance/commands.ts
···
1
-
import {
2
-
CommandInteraction,
3
-
ContainerBuilder,
4
-
MediaGalleryBuilder,
5
-
MediaGalleryItemBuilder,
6
-
MessageFlags,
7
-
PermissionFlagsBits,
8
-
SlashCommandBuilder,
9
-
TextDisplayBuilder,
10
-
} from "discord.js";
11
-
import type { Command, FeatureContext } from "../../core/types.ts";
12
-
import { inspect } from "node:util";
13
-
14
-
export const reloadCommand: Command = {
15
-
data: new SlashCommandBuilder()
16
-
.setName("reload")
17
-
.setDescription("Reload all features (admin only)")
18
-
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
19
-
20
-
execute: async (interaction: CommandInteraction, context: FeatureContext) => {
21
-
await interaction.deferReply({ ephemeral: true });
22
-
23
-
try {
24
-
const registry = context.client.registry;
25
-
26
-
await interaction.editReply("๐งน Cleaning up features...");
27
-
await registry.cleanupAll();
28
-
29
-
console.log("๐ Reloading features...");
30
-
await interaction.editReply("๐ Reloading features...");
31
-
await registry.loadFeaturesFromDirectory("src/features");
32
-
33
-
await interaction.editReply("๐ก Re-deploying commands...");
34
-
await registry.deployCommands();
35
-
36
-
await interaction.editReply("โ
Notifying features...");
37
-
await registry.notifyReady();
38
-
39
-
await interaction.editReply("โ
Features reloaded successfully.");
40
-
} catch (err) {
41
-
console.error("โ Reload failed:", err);
42
-
await interaction.editReply("โ Reload failed. Check logs.");
43
-
}
44
-
},
45
-
};
46
-
47
-
const OWNER_IDS =
48
-
Deno.env.get("BOT_ADMINS")?.split(",").map((id) => id.trim()) ?? [];
49
-
50
-
export const evalCommand: Command = {
51
-
data: new SlashCommandBuilder()
52
-
.setName("eval")
53
-
.setDescription("Execute JavaScript code (owner only)")
54
-
.addStringOption((opt) =>
55
-
opt.setName("code").setDescription("JS code to execute").setRequired(true)
56
-
)
57
-
.addBooleanOption((opt) =>
58
-
opt.setName("is_image").setDescription(
59
-
"Whether to expect an image",
60
-
)
61
-
) as SlashCommandBuilder,
62
-
63
-
execute: async (
64
-
interaction: CommandInteraction,
65
-
_context: FeatureContext,
66
-
) => {
67
-
if (!interaction.isChatInputCommand()) return;
68
-
const userId = interaction.user.id;
69
-
70
-
if (!OWNER_IDS.includes(userId)) {
71
-
await interaction.reply({
72
-
content: "โ You are not authorized to use this.",
73
-
flags: [MessageFlags.Ephemeral],
74
-
});
75
-
76
-
return;
77
-
}
78
-
79
-
const code = interaction.options.getString("code", true);
80
-
81
-
try {
82
-
const result = await eval(`(async () => { ${code} })()`);
83
-
const output = inspect(result, { depth: 1 });
84
-
85
-
const headerText = new TextDisplayBuilder()
86
-
.setContent("โ
Eval Success");
87
-
88
-
const contentText = new TextDisplayBuilder()
89
-
.setContent("```js\n" + output + "\n```");
90
-
91
-
const container = new ContainerBuilder()
92
-
.addTextDisplayComponents([headerText, contentText]);
93
-
94
-
if (interaction.options.getBoolean("is_image", false)) {
95
-
const galleryItemComponent = new MediaGalleryItemBuilder().setURL(
96
-
output.split("'")[1],
97
-
);
98
-
const galleryComponent = new MediaGalleryBuilder().addItems(
99
-
galleryItemComponent,
100
-
);
101
-
102
-
container.addMediaGalleryComponents(galleryComponent);
103
-
}
104
-
105
-
await interaction.reply({
106
-
components: [container],
107
-
flags: [MessageFlags.IsComponentsV2, MessageFlags.Ephemeral],
108
-
});
109
-
} catch (err) {
110
-
const headerText = new TextDisplayBuilder()
111
-
.setContent("โ Eval Error");
112
-
113
-
const contentText = new TextDisplayBuilder()
114
-
.setContent("```js\n" + err?.toString() + "\n```");
115
-
116
-
const container = new ContainerBuilder()
117
-
.addTextDisplayComponents([headerText, contentText]);
118
-
119
-
await interaction.reply({
120
-
components: [container],
121
-
flags: [MessageFlags.IsComponentsV2, MessageFlags.Ephemeral],
122
-
});
123
-
}
124
-
},
125
-
};
-11
src/features/maintenance/index.ts
-11
src/features/maintenance/index.ts
···
1
-
import type { Feature } from "../../core/types.ts";
2
-
import { evalCommand, reloadCommand } from "./commands.ts";
3
-
4
-
const MaintenanceFeature: Feature = {
5
-
id: "maintenance",
6
-
name: "Maintenance Tools",
7
-
8
-
commands: [reloadCommand, evalCommand],
9
-
};
10
-
11
-
export default MaintenanceFeature;
-21
src/features/statistics/index.ts
-21
src/features/statistics/index.ts
···
1
-
import type { Feature, FeatureContext } from "../../core/types.ts";
2
-
3
-
const messageCreateEvent = {
4
-
name: "messageCreate",
5
-
6
-
execute(_data: object, context: FeatureContext) {
7
-
console.log(
8
-
`messageCreate event executed in statistics feature, by ${context.client?.user?.tag}`,
9
-
);
10
-
},
11
-
};
12
-
13
-
const StatisticsFeature: Feature = {
14
-
id: "statistics",
15
-
name: "Statistics",
16
-
17
-
commands: [],
18
-
events: [messageCreateEvent],
19
-
};
20
-
21
-
export default StatisticsFeature;
-129
src/features/utility/commands.ts
-129
src/features/utility/commands.ts
···
1
-
import {
2
-
ButtonBuilder,
3
-
ButtonStyle,
4
-
ChatInputCommandInteraction,
5
-
CommandInteraction,
6
-
ContainerBuilder,
7
-
MessageFlags,
8
-
SectionBuilder,
9
-
SlashCommandBuilder,
10
-
TextDisplayBuilder,
11
-
} from "discord.js";
12
-
import type { Command, FeatureContext } from "../../core/types.ts";
13
-
14
-
export const pingCommand: Command = {
15
-
data: new SlashCommandBuilder()
16
-
.setName("ping")
17
-
.setDescription("Replies with Pong and a Refresh button"),
18
-
19
-
execute: async (interaction: CommandInteraction, context: FeatureContext) => {
20
-
const button = new ButtonBuilder()
21
-
.setCustomId(context.createCustomId("refresh"))
22
-
.setLabel("๐ Refresh")
23
-
.setStyle(ButtonStyle.Primary);
24
-
25
-
const headerTitle = new TextDisplayBuilder()
26
-
.setContent(`๐ Pong! ${context.client.ws.ping}ms`);
27
-
28
-
const headerSection = new SectionBuilder()
29
-
.addTextDisplayComponents([headerTitle])
30
-
.setButtonAccessory(button);
31
-
32
-
const container = new ContainerBuilder()
33
-
.addSectionComponents([headerSection]);
34
-
35
-
await interaction.reply({
36
-
components: [container],
37
-
flags: [MessageFlags.IsComponentsV2],
38
-
});
39
-
},
40
-
};
41
-
42
-
export const uploadCommand: Command = {
43
-
data: new SlashCommandBuilder()
44
-
.setName("upload")
45
-
.setDescription("Uploads an image to the contest API")
46
-
.addAttachmentOption((option) =>
47
-
option
48
-
.setName("image")
49
-
.setDescription("The image to upload")
50
-
.setRequired(true)
51
-
)
52
-
.addIntegerOption((option) =>
53
-
option
54
-
.setName("contest_id")
55
-
.setDescription("The contest ID")
56
-
.setRequired(true)
57
-
)
58
-
.addStringOption((option) =>
59
-
option
60
-
.setName("participant_name")
61
-
.setDescription("The name of the participant")
62
-
.setRequired(true)
63
-
)
64
-
.addStringOption((option) =>
65
-
option
66
-
.setName("participant_email")
67
-
.setDescription("The email of the participant")
68
-
.setRequired(true)
69
-
),
70
-
71
-
execute: async (
72
-
interaction: ChatInputCommandInteraction,
73
-
_context: FeatureContext,
74
-
) => {
75
-
const attachment = interaction.options.getAttachment("image", true);
76
-
const contestId = interaction.options.getInteger("contest_id", true);
77
-
const participantName = interaction.options.getString(
78
-
"participant_name",
79
-
true,
80
-
);
81
-
const participantEmail = interaction.options.getString(
82
-
"participant_email",
83
-
true,
84
-
);
85
-
86
-
await interaction.deferReply({ ephemeral: true });
87
-
88
-
try {
89
-
const fileResponse = await fetch(attachment.url);
90
-
const fileBuffer = await fileResponse.arrayBuffer();
91
-
92
-
const form = new FormData();
93
-
form.append("image", new Blob([fileBuffer]));
94
-
form.append("contest_id", contestId.toString());
95
-
form.append("participant_name", participantName);
96
-
form.append("participant_email", participantEmail);
97
-
98
-
const res = await fetch("http://localhost:8000/api/upload-drawing", {
99
-
method: "POST",
100
-
body: form,
101
-
});
102
-
103
-
if (!res.ok) {
104
-
const error = await res.text();
105
-
console.log(error);
106
-
107
-
await interaction.editReply({
108
-
content: `โ Upload failed, check console for details.`,
109
-
});
110
-
111
-
return;
112
-
}
113
-
114
-
const result = await res.json();
115
-
await interaction.editReply({
116
-
content: `โ
Image uploaded successfully!
117
-
Participant ID: ${result.participant_id ?? "(not returned)"}
118
-
Submission ID: ${result.submission_id ?? "(not returned)"}`,
119
-
});
120
-
} catch (err) {
121
-
console.error(err);
122
-
await interaction.editReply({
123
-
content: `โ An error occurred during upload.`,
124
-
});
125
-
126
-
return;
127
-
}
128
-
},
129
-
};
-15
src/features/utility/index.ts
-15
src/features/utility/index.ts
···
1
-
import type { Feature } from "../../core/types.ts";
2
-
import { pingCommand, uploadCommand } from "./commands.ts";
3
-
import { refreshButton } from "./interactions.ts";
4
-
5
-
const UtilityFeature: Feature = {
6
-
id: "utility",
7
-
name: "Utility Commands",
8
-
9
-
commands: [pingCommand, uploadCommand],
10
-
buttonHandlers: new Map([
11
-
["refresh", refreshButton],
12
-
]),
13
-
};
14
-
15
-
export default UtilityFeature;
-35
src/features/utility/interactions.ts
-35
src/features/utility/interactions.ts
···
1
-
import {
2
-
ButtonBuilder,
3
-
ButtonInteraction,
4
-
ButtonStyle,
5
-
ContainerBuilder,
6
-
MessageFlags,
7
-
SectionBuilder,
8
-
TextDisplayBuilder,
9
-
} from "discord.js";
10
-
import { FeatureContext } from "../../core/types.ts";
11
-
12
-
export const refreshButton = async (
13
-
interaction: ButtonInteraction,
14
-
context: FeatureContext,
15
-
): Promise<void> => {
16
-
const button = new ButtonBuilder()
17
-
.setCustomId(context.createCustomId("refresh"))
18
-
.setLabel("๐ Refresh")
19
-
.setStyle(ButtonStyle.Primary);
20
-
21
-
const headerTitle = new TextDisplayBuilder()
22
-
.setContent(`๐ Pong! ${context.client.ws.ping}ms`);
23
-
24
-
const headerSection = new SectionBuilder()
25
-
.addTextDisplayComponents([headerTitle])
26
-
.setButtonAccessory(button);
27
-
28
-
const container = new ContainerBuilder()
29
-
.addSectionComponents([headerSection]);
30
-
31
-
await interaction.update({
32
-
components: [container],
33
-
flags: [MessageFlags.IsComponentsV2],
34
-
});
35
-
};
-8
src/main.ts
-8
src/main.ts
+21
tsconfig.json
+21
tsconfig.json
···
1
+
{
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
+
},
21
+
}