+2
-1
.env.example
+2
-1
.env.example
···
1
1
BOT_TOKEN=#Get this from https://discord.com/developers - REQUIRED
2
2
BOT_CLIENT_ID=#Get this from https://discord.com/developers - REQUIRED
3
-
DB_URI=#Your MongoDB connection URI - REQUIRED
3
+
BOT_ADMINS=#Discord Ids of bot administrators, comma separated
4
+
DB_URI=#Your MongoDB connection URI - REQUIRED
+30
-8
.gitignore
+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
-
}
+78
bun.lock
+78
bun.lock
···
1
+
{
2
+
"lockfileVersion": 1,
3
+
"workspaces": {
4
+
"": {
5
+
"name": "voidydiscord",
6
+
"dependencies": {
7
+
"discord.js": "^14.21.0",
8
+
},
9
+
"devDependencies": {
10
+
"@types/bun": "latest",
11
+
},
12
+
"peerDependencies": {
13
+
"typescript": "^5",
14
+
},
15
+
},
16
+
},
17
+
"packages": {
18
+
"@discordjs/builders": ["@discordjs/builders@1.11.2", "", { "dependencies": { "@discordjs/formatters": "^0.6.1", "@discordjs/util": "^1.1.1", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.1", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-F1WTABdd8/R9D1icJzajC4IuLyyS8f3rTOz66JsSI3pKvpCAtsMBweu8cyNYsIyvcrKAVn9EPK+Psoymq+XC0A=="],
19
+
20
+
"@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="],
21
+
22
+
"@discordjs/formatters": ["@discordjs/formatters@0.6.1", "", { "dependencies": { "discord-api-types": "^0.38.1" } }, "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg=="],
23
+
24
+
"@discordjs/rest": ["@discordjs/rest@2.5.1", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-Tg9840IneBcbrAjcGaQzHUJWFNq1MMWZjTdjJ0WS/89IffaNKc++iOvffucPxQTF/gviO9+9r8kEPea1X5J2Dw=="],
25
+
26
+
"@discordjs/util": ["@discordjs/util@1.1.1", "", {}, "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g=="],
27
+
28
+
"@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="],
29
+
30
+
"@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="],
31
+
32
+
"@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="],
33
+
34
+
"@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="],
35
+
36
+
"@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="],
37
+
38
+
"@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="],
39
+
40
+
"@types/react": ["@types/react@19.1.9", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA=="],
41
+
42
+
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
43
+
44
+
"@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.6", "", {}, "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA=="],
45
+
46
+
"bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
47
+
48
+
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
49
+
50
+
"discord-api-types": ["discord-api-types@0.38.18", "", {}, "sha512-ygenySjZKUaBf5JT8BNhZSxLzwpwdp41O0wVroOTu/N2DxFH7dxYTZUSnFJ6v+/2F3BMcnD47PC47u4aLOLxrQ=="],
51
+
52
+
"discord.js": ["discord.js@14.21.0", "", { "dependencies": { "@discordjs/builders": "^1.11.2", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.1", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.1", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.1", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-U5w41cEmcnSfwKYlLv5RJjB8Joa+QJyRwIJz5i/eg+v2Qvv6EYpCRhN9I2Rlf0900LuqSDg8edakUATrDZQncQ=="],
53
+
54
+
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
55
+
56
+
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
57
+
58
+
"lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="],
59
+
60
+
"magic-bytes.js": ["magic-bytes.js@1.12.1", "", {}, "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA=="],
61
+
62
+
"ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="],
63
+
64
+
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
65
+
66
+
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
67
+
68
+
"undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="],
69
+
70
+
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
71
+
72
+
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
73
+
74
+
"@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
75
+
76
+
"@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
77
+
}
78
+
}
-10
deno.json
-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
-
}
+10
docs/architecture.md
+10
docs/architecture.md
···
1
+
# Voidy Architecture Overview
2
+
The Voidy architecture is a complete re-imagination of my previous bot's command and event organization architecture.
3
+
4
+
Instead of relying on loose commands and events in respective top-level directories, the new approach groups all sorts of handlers into a single "module".
5
+
6
+
And to allow even better handling of data, modules are managed by an even higher entity, "registries".
7
+
8
+
Registries have a standalone database, used to store data of included modules.
9
+
10
+
A more detailed explainer of each system can be found in the related markdown files.
+35
docs/architecture/top-level.md
+35
docs/architecture/top-level.md
···
1
+
# Top level architectural overview
2
+
Anything happening before, or required by, our registries, is considered top-level architecture.
3
+
4
+
This includes utilities such as the command and event handlers, which are consumed by lower levels of the system.
5
+
6
+
We'll go through each of the top-level components below.
7
+
8
+
## Loaders
9
+
Loaders are static classes, which provide utility methods for recursive loading of data from a data source, usually a directory.
10
+
11
+
The constructor of a loader always takes one parameter, a string/path pointer to the desired data-source.
12
+
13
+
Each loader additionally implements an asynchronous `collect` method for initial data collection.
14
+
15
+
Finally, loaders provide various means of exporting data in supported formats, through methods like `getJSON`, `getCSV` and more...
16
+
17
+
### Event loader
18
+
The event loader walks a directory and stores data from any file exporting an object that follows the Event type structure.
19
+
20
+
### Command loader
21
+
The command loader walks a directory and stores data from any file exporting an object that follows the Command type structure.
22
+
23
+
## Handlers
24
+
Handlers are static classes, which receive exported data from Loaders, though not directly, as Loader data is usually fetched by a Registry, and the Registry invokes a Handler to get data into our queue system, more on that later.
25
+
26
+
Each handler has an `invoke` method, which takes JSON data, though it must always use the common exported JSON structure provided by Loaders.
27
+
28
+
Any data filtering or mapping is then run in the background by the invoked Handler, which ultimately pushes data to the queue, and triggers a "handler::postInvoke" event afterward.
29
+
30
+
@Todo: document Handler lifecycle events
31
+
32
+
## Lifecycle manager
33
+
The Lifecycle manager is a simple event manager with a fancy name, it stores subscribers of events in a map, with the event name as the key.
34
+
35
+
Additionally, it implements two very simple methods, notify - which fires lifecycle events, and subscribe - which subscribes a callback function to an event.
+19
package.json
+19
package.json
···
1
+
{
2
+
"name": "voidydiscord",
3
+
"version": "3.0.0-alpha1",
4
+
"module": "src/index.ts",
5
+
"type": "module",
6
+
"private": true,
7
+
"scripts": {
8
+
"dev": "bun --watch ."
9
+
},
10
+
"devDependencies": {
11
+
"@types/bun": "latest"
12
+
},
13
+
"peerDependencies": {
14
+
"typescript": "^5"
15
+
},
16
+
"dependencies": {
17
+
"discord.js": "^14.21.0"
18
+
}
19
+
}
+18
src/core/Loader.ts
+18
src/core/Loader.ts
···
1
+
export interface ILoader {
2
+
store: object[];
3
+
4
+
collect: () => Promise<ThisType<this>>,
5
+
getJSON: () => object,
6
+
}
7
+
8
+
export class Loader implements ILoader {
9
+
public store = [];
10
+
11
+
public async collect() {
12
+
return this;
13
+
}
14
+
15
+
public getJSON() {
16
+
return {};
17
+
}
18
+
};
+16
src/core/VoidyClient.ts
+16
src/core/VoidyClient.ts
···
1
+
import { Client, type ClientOptions } from "discord.js";
2
+
import { Registry } from "./Registry";
3
+
4
+
export class VoidyClient extends Client {
5
+
public registries: Registry[];
6
+
7
+
public constructor(options: ClientOptions) {
8
+
super(options);
9
+
10
+
this.registries = [new Registry()];
11
+
}
12
+
13
+
public start(token: string) {
14
+
this.login(token);
15
+
}
16
+
}
-25
src/core/client.ts
-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
-
};
+14
src/index.ts
+14
src/index.ts
···
1
+
import { GatewayIntentBits } from "discord.js"
2
+
import { VoidyClient } from "./core/VoidyClient"
3
+
4
+
// Client initialization with intents and stuff...
5
+
const client = new VoidyClient({
6
+
intents: [GatewayIntentBits.Guilds],
7
+
})
8
+
9
+
// Token validation and client start
10
+
if (!Bun.env.BOT_TOKEN) throw new Error("[Voidy] Missing bot token");
11
+
client.start(Bun.env.BOT_TOKEN);
12
+
13
+
// @Todo: Remove after core registry implementation is complete
14
+
console.log(client.registries[0]);
-8
src/main.ts
-8
src/main.ts
+28
tsconfig.json
+28
tsconfig.json
···
1
+
{
2
+
"compilerOptions": {
3
+
// Environment setup & latest features
4
+
"lib": [
5
+
"ESNext"
6
+
],
7
+
"target": "ESNext",
8
+
"module": "Preserve",
9
+
"moduleDetection": "force",
10
+
"jsx": "react-jsx",
11
+
"allowJs": true,
12
+
// Bundler mode
13
+
"moduleResolution": "bundler",
14
+
"allowImportingTsExtensions": true,
15
+
"verbatimModuleSyntax": true,
16
+
"noEmit": true,
17
+
// Best practices
18
+
"strict": true,
19
+
"skipLibCheck": true,
20
+
"noFallthroughCasesInSwitch": true,
21
+
"noUncheckedIndexedAccess": true,
22
+
"noImplicitOverride": true,
23
+
// Some stricter flags (disabled by default)
24
+
"noUnusedLocals": false,
25
+
"noUnusedParameters": false,
26
+
"noPropertyAccessFromIndexSignature": false
27
+
}
28
+
}