+5
.env.example
+5
.env.example
···
1
+
BOT_TOKEN: #Get this from https://discord.com/developers - REQUIRED
2
+
BOT_CLIENT_ID: #Get this from https://discord.com/developers - REQUIRED
3
+
BOT_ADMINS: #Discord Ids of bot administrators, comma separated
4
+
DB_URI: #Your MongoDB connection URI - REQUIRED
5
+
ACCESS_TOKEN: # Your API Bearer token
+23
-2
bun.lock
+23
-2
bun.lock
···
8
8
"oxfmt": "^0.17.0",
9
9
},
10
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
+
},
11
26
"packages/bot": {
12
27
"name": "@voidy/bot",
13
28
"version": "0.1.0",
14
29
"dependencies": {
30
+
"@voidy/api": "workspace:*",
15
31
"@voidy/framework": "workspace:*",
16
32
"discord.js": "^14.25.1",
33
+
"hono": "^4.11.1",
17
34
"mongoose": "^9.0.1",
18
35
},
19
36
"devDependencies": {
···
74
91
75
92
"@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="],
76
93
77
-
"@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="],
94
+
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
78
95
79
96
"@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="],
80
97
···
85
102
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
86
103
87
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"],
88
107
89
108
"@voidy/bot": ["@voidy/bot@workspace:packages/bot"],
90
109
···
92
111
93
112
"bson": ["bson@7.0.0", "", {}, "sha512-Kwc6Wh4lQ5OmkqqKhYGKIuELXl+EPYSCObVE6bWsp1T/cGkOCBN0I8wF/T44BiuhHyNi1mmKVPXk60d41xZ7kw=="],
94
113
95
-
"bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
114
+
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
96
115
97
116
"discord-api-types": ["discord-api-types@0.38.37", "", {}, "sha512-Cv47jzY1jkGkh5sv0bfHYqGgKOWO1peOrGMkDFM4UmaGMOTgOW8QSexhvixa9sVOiz8MnVOBryWYyw/CEVhj7w=="],
98
117
99
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=="],
100
119
101
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=="],
102
123
103
124
"kareem": ["kareem@3.0.0", "", {}, "sha512-RKhaOBSPN8L7y4yAgNhDT2602G5FD6QbOIISbjN9D6mjHPeqeg7K+EB5IGSU5o81/X2Gzm3ICnAvQW3x3OP8HA=="],
104
125
+40
docker-compose.yml
+40
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
+11
packages/bot/Dockerfile
+11
packages/bot/Dockerfile
+8
packages/bot/package.json
+8
packages/bot/package.json
···
4
4
"module": "src/index.ts",
5
5
"type": "module",
6
6
"private": true,
7
+
"exports": {
8
+
"./db": {
9
+
"import": "./src/modules/index.ts",
10
+
"require": "./src/modules/index.cjs"
11
+
}
12
+
},
7
13
"scripts": {
8
14
"dev": "bun --watch ."
9
15
},
···
15
21
},
16
22
"dependencies": {
17
23
"@voidy/framework": "workspace:*",
24
+
"@voidy/api": "workspace:*",
18
25
"discord.js": "^14.25.1",
26
+
"hono": "^4.11.1",
19
27
"mongoose": "^9.0.1"
20
28
}
21
29
}
+8
packages/bot/src/index.ts
+8
packages/bot/src/index.ts
···
2
2
import { VoidyClient } from "@voidy/framework";
3
3
import { connect } from "mongoose";
4
4
5
+
//===============================================
6
+
// Discord client initialization
7
+
//===============================================
8
+
5
9
// Client initialization with intents and stuff...
6
10
const client = new VoidyClient({
7
11
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages],
···
22
26
// Token validation and client start
23
27
if (!Bun.env.BOT_TOKEN) throw new Error("[Voidy] Missing bot token");
24
28
await client.start(Bun.env.BOT_TOKEN, `${import.meta.dirname}/modules`);
29
+
30
+
//===============================================
31
+
// API initialization
32
+
//===============================================
+1
packages/bot/src/modules/economy/schemas/index.ts
+1
packages/bot/src/modules/economy/schemas/index.ts
···
1
+
export * from "./UserCurrency";
+2
packages/bot/src/modules/index.ts
+2
packages/bot/src/modules/index.ts
+65
packages/bot/src/modules/user/commands/integration/update.ts
+65
packages/bot/src/modules/user/commands/integration/update.ts
···
1
+
import { MessageFlags, SlashCommandBuilder } 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 SlashCommandBuilder()
8
+
.setName("Update or link integration")
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/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
+
);