.env.example
packages/bot/.env.example
.env.example
packages/bot/.env.example
+24
-7
bun.lock
+24
-7
bun.lock
···
3
3
"workspaces": {
4
4
"": {
5
5
"name": "voidydiscord",
6
+
},
7
+
"packages/bot": {
8
+
"name": "voidy-bot",
9
+
"version": "0.1.0",
6
10
"dependencies": {
7
-
"ascii-table3": "^1.0.1",
11
+
"discord.js": "^14.21.0",
12
+
"voidy-framework": "workspace:*",
13
+
},
14
+
"devDependencies": {
15
+
"@types/bun": "latest",
16
+
},
17
+
"peerDependencies": {
18
+
"typescript": "^5",
19
+
},
20
+
},
21
+
"packages/framework": {
22
+
"name": "voidy-framework",
23
+
"version": "0.1.0",
24
+
"dependencies": {
8
25
"discord.js": "^14.21.0",
9
26
},
10
27
"devDependencies": {
···
34
51
35
52
"@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="],
36
53
37
-
"@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="],
54
+
"@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="],
38
55
39
56
"@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="],
40
57
···
44
61
45
62
"@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.6", "", {}, "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA=="],
46
63
47
-
"ascii-table3": ["ascii-table3@1.0.1", "", { "dependencies": { "printable-characters": "^1.0.42" } }, "sha512-xOCMZC8S375W4JajrAxFWPyI1VddfbscW9G5zMfhCySSt2Rvi/rs21jAjopzldTPOaFrOocjyGKibQiGExmLrg=="],
48
-
49
-
"bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
64
+
"bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="],
50
65
51
66
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
52
67
···
62
77
63
78
"magic-bytes.js": ["magic-bytes.js@1.12.1", "", {}, "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA=="],
64
79
65
-
"printable-characters": ["printable-characters@1.0.42", "", {}, "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ=="],
66
-
67
80
"ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="],
68
81
69
82
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
···
73
86
"undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="],
74
87
75
88
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
89
+
90
+
"voidy-bot": ["voidy-bot@workspace:packages/bot"],
91
+
92
+
"voidy-framework": ["voidy-framework@workspace:packages/framework"],
76
93
77
94
"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=="],
78
95
+4
-18
package.json
+4
-18
package.json
···
1
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
-
"ascii-table3": "^1.0.1",
18
-
"discord.js": "^14.21.0"
19
-
}
2
+
"name": "voidy",
3
+
"workspaces": [
4
+
"packages/*"
5
+
]
20
6
}
+20
packages/bot/package.json
+20
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
+
"scripts": {
8
+
"dev": "bun --watch ."
9
+
},
10
+
"devDependencies": {
11
+
"@types/bun": "latest"
12
+
},
13
+
"peerDependencies": {
14
+
"typescript": "^5"
15
+
},
16
+
"dependencies": {
17
+
"voidy-framework": "workspace:*",
18
+
"discord.js": "^14.21.0"
19
+
}
20
+
}
+28
packages/bot/src/modules/core/module.ts
+28
packages/bot/src/modules/core/module.ts
···
1
+
import {
2
+
ButtonLoader,
3
+
CommandLoader,
4
+
EventLoader,
5
+
type Module
6
+
} from "voidy-framework";
7
+
8
+
export default {
9
+
id: "core",
10
+
name: "Core",
11
+
description: "The core feature set of the bot, required for command handling to work.",
12
+
author: "jokiller230",
13
+
14
+
exports: [
15
+
{
16
+
source: `${import.meta.dir}/events`,
17
+
loader: EventLoader,
18
+
},
19
+
{
20
+
source: `${import.meta.dir}/commands`,
21
+
loader: CommandLoader,
22
+
},
23
+
{
24
+
source: `${import.meta.dir}/buttons`,
25
+
loader: ButtonLoader,
26
+
}
27
+
]
28
+
} as Module;
+14
packages/bot/tsconfig.json
+14
packages/bot/tsconfig.json
+16
packages/framework/package.json
+16
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
+
"devDependencies": {
8
+
"@types/bun": "latest"
9
+
},
10
+
"peerDependencies": {
11
+
"typescript": "^5"
12
+
},
13
+
"dependencies": {
14
+
"discord.js": "^14.21.0"
15
+
}
16
+
}
+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
+
}
+126
packages/framework/src/core/VoidyClient.ts
+126
packages/framework/src/core/VoidyClient.ts
···
1
+
//===============================================
2
+
// Imports
3
+
//===============================================
4
+
import {
5
+
type ClientOptions,
6
+
SlashCommandSubcommandGroupBuilder,
7
+
SlashCommandSubcommandBuilder,
8
+
SlashCommandBuilder,
9
+
Client,
10
+
} from "discord.js";
11
+
import { ModuleManager, type CacheMap } from "./ModuleManager";
12
+
import type { Command } from "./types/Command";
13
+
import type { Button } from "./types/Button";
14
+
import type { Event } from "./types/Event";
15
+
16
+
//===============================================
17
+
// VoidyClient Implementation
18
+
//===============================================
19
+
export class VoidyClient extends Client {
20
+
public moduleManager = new ModuleManager();
21
+
22
+
public constructor(options: ClientOptions) {
23
+
super(options);
24
+
}
25
+
26
+
/**
27
+
* Launches the bot
28
+
* @param token - The Discord application bot token.
29
+
* @param modulesPath - Where the bot should search for modules.
30
+
*/
31
+
public async start(token: string, modulesPath: string) {
32
+
// Load modules and register events
33
+
await this.moduleManager.loadModules(modulesPath);
34
+
await this.registerEvents();
35
+
36
+
// Register commands on ready event
37
+
this.on("ready", this.registerCommands);
38
+
39
+
// Login using the bot token
40
+
await this.login(token);
41
+
}
42
+
43
+
/**
44
+
* Registers all cached events
45
+
* @param events
46
+
*/
47
+
private async registerEvents() {
48
+
const events = this.moduleManager.events;
49
+
50
+
for (const [_id, event] of events) {
51
+
const execute = (...args: unknown[]) => event.execute(this, ...args);
52
+
53
+
if (event.once) this.once(event.name, execute);
54
+
else this.on(event.name, execute);
55
+
}
56
+
}
57
+
58
+
/**
59
+
* Registers all provided commands globally
60
+
* @param commands
61
+
*/
62
+
private async registerCommands(): Promise<void> {
63
+
const topLevelCommands = new Map<string, SlashCommandBuilder>();
64
+
65
+
for (const cmd of this.moduleManager.commands.values()) {
66
+
const parts = cmd.id.split("."); // ["music", "set", "channel"]
67
+
const command = parts[0];
68
+
const subcommand = parts[1];
69
+
const subgroupcommand = parts[2];
70
+
71
+
if (!command) continue;
72
+
73
+
// Ensure top-level builder exists
74
+
if (!topLevelCommands.has(command)) {
75
+
const topCommand = (
76
+
this.moduleManager.commands.get(command)?.data as SlashCommandBuilder
77
+
) ?? new SlashCommandBuilder().setName(command).setDescription("...");
78
+
79
+
topLevelCommands.set(command, topCommand);
80
+
}
81
+
82
+
const parent = topLevelCommands.get(command)!;
83
+
84
+
if (subcommand && !subgroupcommand) {
85
+
// It's a subcommand
86
+
parent.addSubcommand(cmd.data as SlashCommandSubcommandBuilder);
87
+
} else if (subcommand && subgroupcommand) {
88
+
// It's a subgroup command
89
+
let group = parent.options.find(
90
+
(o): o is SlashCommandSubcommandGroupBuilder => o instanceof SlashCommandSubcommandGroupBuilder && o.name === subcommand
91
+
);
92
+
93
+
if (!group) {
94
+
group = new SlashCommandSubcommandGroupBuilder().setName(subcommand).setDescription("...");
95
+
parent.addSubcommandGroup(group);
96
+
}
97
+
98
+
group.addSubcommand(cmd.data as SlashCommandSubcommandBuilder);
99
+
}
100
+
}
101
+
102
+
// Finally convert assembled top-level commands to JSON and register them
103
+
await this.application?.commands.set([...topLevelCommands.values()].map(c => c.toJSON()));
104
+
}
105
+
106
+
/**
107
+
* Returns all cached commands
108
+
*/
109
+
get commands(): CacheMap<Command> {
110
+
return this.moduleManager.commands;
111
+
}
112
+
113
+
/**
114
+
* Returns all cached events
115
+
*/
116
+
get events(): CacheMap<Event> {
117
+
return this.moduleManager.events;
118
+
}
119
+
120
+
/**
121
+
* Returns all cached buttons
122
+
*/
123
+
get buttons(): CacheMap<Button> {
124
+
return this.moduleManager.buttons;
125
+
}
126
+
}
+15
packages/framework/src/core/types/Button.ts
+15
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: (
13
+
interaction: ButtonInteraction, client: VoidyClient
14
+
) => Promise<void>
15
+
}
+21
packages/framework/src/core/types/Command.ts
+21
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
+
execute: (
19
+
interaction: ChatInputCommandInteraction, client: VoidyClient
20
+
) => Promise<void>
21
+
}
+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
+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
+
}
+23
packages/framework/src/loaders/ModuleLoader.ts
+23
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 (
14
+
!data.id ||
15
+
!data.name ||
16
+
!data.description ||
17
+
!data.author ||
18
+
!data.exports
19
+
) return null;
20
+
21
+
return data as Module;
22
+
}
23
+
}
+14
packages/framework/tsconfig.json
+14
packages/framework/tsconfig.json
-33
src/core/Lifecycle.ts
-33
src/core/Lifecycle.ts
···
1
-
export enum LifecycleEvents {
2
-
// Registries
3
-
RegistryPreCollect = "registry::preCollect",
4
-
RegistryPostCollect = "registry::postCollect",
5
-
6
-
// Client
7
-
ClientLoop = "client::loop",
8
-
}
9
-
10
-
type LifecycleEventCallback = () => void;
11
-
12
-
export class Lifecycle {
13
-
public static subscribers = new Map<string, Array<LifecycleEventCallback>>();
14
-
15
-
public static subscribe(event: LifecycleEvents, callback: LifecycleEventCallback): void {
16
-
const subscribers = this.subscribers.get(event);
17
-
if (!subscribers) {
18
-
this.subscribers.set(event, [callback]);
19
-
return;
20
-
}
21
-
22
-
this.subscribers.set(event, subscribers.concat([callback]));
23
-
}
24
-
25
-
public static notify(event: LifecycleEvents) {
26
-
const subscribers = this.subscribers.get(event);
27
-
if (!subscribers) return;
28
-
29
-
for (const subscriber of subscribers) {
30
-
subscriber();
31
-
}
32
-
}
33
-
}
+14
-7
src/core/Loader.ts
packages/framework/src/core/Loader.ts
+14
-7
src/core/Loader.ts
packages/framework/src/core/Loader.ts
···
1
+
//===============================================
2
+
// Imports
3
+
//===============================================
1
4
import { Glob } from "bun";
2
5
3
-
interface ILoader<T> {
6
+
//===============================================
7
+
// Loader Definition
8
+
//===============================================
9
+
interface ILoader<T extends object> {
4
10
id: string
5
11
cache: T[]
6
12
source: string
···
10
16
getJSON: () => T[]
11
17
}
12
18
13
-
export class Loader<T extends object> implements ILoader<T> {
14
-
public id = "loader";
19
+
//===============================================
20
+
// Loader Implementation
21
+
//===============================================
22
+
export abstract class Loader<T extends object> implements ILoader<T> {
23
+
public abstract id: string;
15
24
public cache: T[] = [];
16
-
public source;
25
+
public source: string;
17
26
18
27
public constructor(source: string) {
19
28
if (!source) throw new Error("Class of type Loader was initialized without the *required* source parameter.");
···
57
66
/**
58
67
* Validates a singular element during data collection, and returns whatever should be written to the cache.
59
68
*/
60
-
public async validate(data: Partial<T>): Promise<T | null> {
61
-
return null;
62
-
}
69
+
public abstract validate(data: Partial<T>): Promise<T | null>;
63
70
64
71
/**
65
72
* Returns the JSON-ified contents of the loader cache
-83
src/core/Registry.ts
-83
src/core/Registry.ts
···
1
-
import { ButtonLoader, type Button } from "../loaders/ButtonLoader"
2
-
import { CommandLoader, type Command } from "../loaders/CommandLoader"
3
-
import { EventLoader, type Event } from "../loaders/EventLoader"
4
-
import { ModuleLoader, type Module } from "../loaders/ModuleLoader"
5
-
6
-
export enum RegistryCacheKey {
7
-
Events = "events",
8
-
Modules = "modules",
9
-
Buttons = "buttons",
10
-
Commands = "commands",
11
-
}
12
-
13
-
export type RegistryCache = {
14
-
events: Event[],
15
-
modules: Module[],
16
-
buttons: Button[],
17
-
commands: Command[],
18
-
[x: string]: object[],
19
-
};
20
-
21
-
export interface IRegistry {
22
-
id: string
23
-
active: boolean
24
-
dataSource: string
25
-
26
-
cache: RegistryCache;
27
-
28
-
collectModules: () => Promise<void>
29
-
processModules: () => Promise<void>
30
-
31
-
activate: () => Promise<void>
32
-
deactivate: () => Promise<void>
33
-
}
34
-
35
-
export class Registry implements IRegistry {
36
-
public id: string;
37
-
public active = false;
38
-
public dataSource: string;
39
-
40
-
// Initialize cache stores
41
-
public cache: RegistryCache = {
42
-
events: [],
43
-
modules: [],
44
-
buttons: [],
45
-
commands: [],
46
-
}
47
-
48
-
public constructor(id: string, dataSource: string) {
49
-
this.id = id;
50
-
this.dataSource = dataSource;
51
-
}
52
-
53
-
/** Collect modules from specified dataSource directory */
54
-
public async collectModules() {
55
-
// Collect modules and bundle their JSON contents into an array.
56
-
const moduleLoader = new ModuleLoader(this.dataSource);
57
-
const modules = (await moduleLoader.collect()).getJSON();
58
-
59
-
// Merge all modules into the store.
60
-
this.cache.modules = modules;
61
-
}
62
-
63
-
/** Process exports of all collected modules */
64
-
public async processModules() {
65
-
for (const module of this.cache.modules) {
66
-
for (const item of module.exports) {
67
-
const loader = new item.loader(item.source);
68
-
await loader.collect();
69
-
70
-
// Mape loader output to correct cache key
71
-
this.cache[`${loader.id}s`] = [...loader.getJSON()];
72
-
}
73
-
}
74
-
}
75
-
76
-
public async activate() {
77
-
this.active = true;
78
-
}
79
-
80
-
public async deactivate() {
81
-
this.active = false;
82
-
}
83
-
}
-62
src/core/RegistryManager.ts
-62
src/core/RegistryManager.ts
···
1
-
import { Registry, type RegistryCache } from "./Registry"
2
-
3
-
interface IRegistryManager {
4
-
addRegistry: (newRegistry: Registry) => boolean
5
-
getRegistry: (registryID: string) => Registry | null
6
-
7
-
prepareRegistries: () => void
8
-
getCache: () => RegistryCache
9
-
}
10
-
11
-
export class RegistryManager implements IRegistryManager {
12
-
private registries = new Map<string, Registry>();
13
-
14
-
public addRegistry(newRegistry: Registry) {
15
-
// Append registry to registries array if ID is unique,
16
-
// else return false.
17
-
if (this.registries.get(newRegistry.id)) return false;
18
-
this.registries.set(newRegistry.id, newRegistry);
19
-
20
-
// The registry was added successfully, therefore return true.
21
-
return true;
22
-
}
23
-
24
-
public getRegistry(registryID: string) {
25
-
const registry = this.registries.get(registryID);
26
-
27
-
if (!registry) return null;
28
-
return registry;
29
-
}
30
-
31
-
public async prepareRegistries() {
32
-
for (const registry of this.registries.values()) {
33
-
// 1. Collecting required registry data
34
-
console.info(`[Voidy] Collecting registry data: ${registry.dataSource}`);
35
-
await registry.collectModules();
36
-
37
-
// 2. Processing collected registry modules and their exports
38
-
console.info(`[Voidy] Processing registry data: ${registry.dataSource}`);
39
-
await registry.processModules();
40
-
41
-
// 3. Activating registry
42
-
console.info(`[Voidy] Activating registry: ${registry.dataSource}`);
43
-
await registry.activate();
44
-
}
45
-
}
46
-
47
-
public getCache() {
48
-
let cache: RegistryCache = {
49
-
events: [],
50
-
modules: [],
51
-
commands: [],
52
-
buttons: [],
53
-
};
54
-
55
-
// Combine all registry caches
56
-
for (const registry of this.registries.values()) {
57
-
if (registry.active) cache = { ...cache, ...registry.cache };
58
-
}
59
-
60
-
return cache;
61
-
}
62
-
}
-114
src/core/VoidyClient.ts
-114
src/core/VoidyClient.ts
···
1
-
import {
2
-
type RESTPostAPIChatInputApplicationCommandsJSONBody,
3
-
type APIApplicationCommandSubcommandGroupOption,
4
-
type APIApplicationCommandSubcommandOption,
5
-
type ApplicationCommandDataResolvable,
6
-
type ClientOptions,
7
-
Client,
8
-
} from "discord.js";
9
-
import { Registry } from "./Registry";
10
-
import { RegistryManager } from "./RegistryManager";
11
-
import type { Event } from "../loaders/EventLoader";
12
-
import { Lifecycle, LifecycleEvents } from "./Lifecycle";
13
-
14
-
export class VoidyClient extends Client {
15
-
public registryManager = new RegistryManager();
16
-
private intervalID?: NodeJS.Timeout | NodeJS.Timer;
17
-
18
-
public constructor(options: ClientOptions) {
19
-
super(options);
20
-
21
-
// Add the core registry to our registry manager
22
-
this.registryManager.addRegistry(
23
-
new Registry('core', `${process.cwd()}/src/modules`)
24
-
);
25
-
}
26
-
27
-
/**
28
-
* Register all provided events
29
-
* @param events
30
-
*/
31
-
private async registerEventHandlers(events: Event[]) {
32
-
console.log(`[Voidy] Registering ${events.length} event listeners: ${events.map(event => event.name).join(", ")}`);
33
-
34
-
for (const event of events) {
35
-
const execute = (...args: unknown[]) => event.execute(this, ...args);
36
-
37
-
if (event.once) this.once(event.name, execute);
38
-
else this.on(event.name, execute);
39
-
}
40
-
}
41
-
42
-
/**
43
-
* Register all provided commands to the global discord context
44
-
* @param commands
45
-
* @todo Fix this type mess, if possible
46
-
*/
47
-
private async registerCommands(commands: (RESTPostAPIChatInputApplicationCommandsJSONBody | APIApplicationCommandSubcommandOption | APIApplicationCommandSubcommandGroupOption)[]): Promise<void> {
48
-
console.info(`[Voidy] Registering ${commands.length} commands: ${commands.map(command => command.name).join(", ")}`);
49
-
50
-
await this.application?.commands.set(commands as ApplicationCommandDataResolvable[]);
51
-
}
52
-
53
-
/**
54
-
* Refresh the registry manager and re-register relevant data
55
-
* @param token
56
-
*/
57
-
private async refresh() {
58
-
// 1. Prepare and fetch registry manager cache
59
-
await this.registryManager.prepareRegistries();
60
-
const cache = this.registryManager.getCache();
61
-
62
-
let cacheSize = 0;
63
-
for (const cacheValue of Object.values(cache)) {
64
-
cacheSize += cacheValue.length;
65
-
}
66
-
67
-
// 2. Showcase number of loaded cache entities
68
-
console.log(`[Voidy] Refreshed RegistryManager cache, with a total of ${cacheSize} items.`);
69
-
70
-
// 3. Clear and re-register events
71
-
const events = cache.events;
72
-
this.removeAllListeners();
73
-
this.registerEventHandlers(events);
74
-
75
-
// 4. Register all active commands
76
-
const commands = cache.commands.flatMap(command => command.data.toJSON());
77
-
this.registerCommands(commands);
78
-
}
79
-
80
-
/**
81
-
* Runs reccurring tasks, doesn't loop by itself, though
82
-
*/
83
-
private async loop() {
84
-
// Notifies the "client_loop" lifecycle event
85
-
Lifecycle.notify(LifecycleEvents.ClientLoop);
86
-
}
87
-
88
-
/**
89
-
* Starts the client loop, with a customizable interval
90
-
* @param interval
91
-
*/
92
-
public startLoop(interval: number = 60 * 1000) {
93
-
this.intervalID = setInterval(this.loop.bind(this), interval);
94
-
}
95
-
96
-
/**
97
-
* Stops the client loop
98
-
*/
99
-
public stopLoop() {
100
-
if (!this.intervalID) return;
101
-
clearInterval(this.intervalID);
102
-
}
103
-
104
-
/**
105
-
* Launch the bot, additionally starts the client loop
106
-
* @param token
107
-
*/
108
-
public async start(token: string) {
109
-
await this.refresh();
110
-
await this.login(token);
111
-
112
-
this.startLoop();
113
-
}
114
-
}
src/handlers/ButtonHandler.ts
packages/framework/src/handlers/ButtonHandler.ts
src/handlers/ButtonHandler.ts
packages/framework/src/handlers/ButtonHandler.ts
src/handlers/CommandHandler.ts
packages/framework/src/handlers/CommandHandler.ts
src/handlers/CommandHandler.ts
packages/framework/src/handlers/CommandHandler.ts
+2
-2
src/index.ts
packages/bot/src/index.ts
+2
-2
src/index.ts
packages/bot/src/index.ts
···
1
1
import { GatewayIntentBits } from "discord.js"
2
-
import { VoidyClient } from "./core/VoidyClient"
2
+
import { VoidyClient } from "voidy-framework";
3
3
4
4
// Client initialization with intents and stuff...
5
5
const client = new VoidyClient({
···
8
8
9
9
// Token validation and client start
10
10
if (!Bun.env.BOT_TOKEN) throw new Error("[Voidy] Missing bot token");
11
-
await client.start(Bun.env.BOT_TOKEN);
11
+
await client.start(Bun.env.BOT_TOKEN, `${import.meta.dirname}/modules`);
-18
src/loaders/ButtonLoader.ts
-18
src/loaders/ButtonLoader.ts
···
1
-
import type { ButtonInteraction } from "discord.js";
2
-
import { Loader } from "../core/Loader";
3
-
import type { VoidyClient } from "../core/VoidyClient";
4
-
5
-
export interface Button {
6
-
id: string,
7
-
execute: (
8
-
interaction: ButtonInteraction, client: VoidyClient
9
-
) => Promise<void>
10
-
}
11
-
12
-
export class ButtonLoader extends Loader<Button> {
13
-
public override id = "button";
14
-
public override async validate(data: Partial<Button>) {
15
-
if (!data.id || !data.execute) return null;
16
-
return data as Button;
17
-
}
18
-
}
-21
src/loaders/CommandLoader.ts
-21
src/loaders/CommandLoader.ts
···
1
-
import type {
2
-
ChatInputCommandInteraction,
3
-
SlashCommandBuilder, SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder
4
-
} from "discord.js";
5
-
import { Loader } from "../core/Loader";
6
-
import type { VoidyClient } from "../core/VoidyClient";
7
-
8
-
export interface Command {
9
-
data: SlashCommandBuilder | SlashCommandSubcommandBuilder | SlashCommandSubcommandGroupBuilder,
10
-
execute: (
11
-
interaction: ChatInputCommandInteraction, client: VoidyClient
12
-
) => Promise<void>
13
-
}
14
-
15
-
export class CommandLoader extends Loader<Command> {
16
-
public override id = "command";
17
-
public override async validate(data: Partial<Command>) {
18
-
if (!data.data || !data.execute) return null;
19
-
return data as Command;
20
-
}
21
-
}
-17
src/loaders/EventLoader.ts
-17
src/loaders/EventLoader.ts
···
1
-
import { type ClientEvents } from "discord.js";
2
-
import { Loader } from "../core/Loader";
3
-
import type { VoidyClient } from "../core/VoidyClient";
4
-
5
-
export interface Event {
6
-
name: keyof ClientEvents,
7
-
once?: boolean,
8
-
execute: (client: VoidyClient, ...args: unknown[]) => void,
9
-
}
10
-
11
-
export class EventLoader extends Loader<Event> {
12
-
public override id = "event";
13
-
public override async validate(data: Partial<Event>) {
14
-
if (!data.name || !data.execute) return null;
15
-
return data as Event;
16
-
}
17
-
}
-27
src/loaders/ModuleLoader.ts
-27
src/loaders/ModuleLoader.ts
···
1
-
import { Loader } from "../core/Loader"
2
-
3
-
export interface ModuleExportsItem<T extends object> {
4
-
source: string
5
-
loader: typeof Loader<T>
6
-
}
7
-
8
-
export interface Module {
9
-
name: string
10
-
description: string
11
-
author: string
12
-
exports: ModuleExportsItem<object>[]
13
-
}
14
-
15
-
export class ModuleLoader extends Loader<Module> {
16
-
public override id = "module";
17
-
public override async validate(data: Partial<Module>) {
18
-
if (
19
-
!data.name ||
20
-
!data.description ||
21
-
!data.author ||
22
-
!data.exports
23
-
) return null;
24
-
25
-
return data as Module;
26
-
}
27
-
}
+2
-1
src/modules/core/commands/ping.ts
packages/bot/src/modules/core/commands/ping.ts
+2
-1
src/modules/core/commands/ping.ts
packages/bot/src/modules/core/commands/ping.ts
···
1
1
import { MessageFlags, SlashCommandBuilder } from "discord.js";
2
-
import type { Command } from "../../../loaders/CommandLoader";
2
+
import type { Command } from "voidy-framework";
3
3
4
4
export default {
5
+
id: "ping",
5
6
data: new SlashCommandBuilder()
6
7
.setName("ping")
7
8
.setDescription("View the websocket ping between Discord and the Bot."),
+24
-11
src/modules/core/events/interactionCreate.ts
packages/bot/src/modules/core/events/interactionCreate.ts
+24
-11
src/modules/core/events/interactionCreate.ts
packages/bot/src/modules/core/events/interactionCreate.ts
···
1
-
import type { Event } from "../../../loaders/EventLoader";
2
-
import type { VoidyClient } from "../../../core/VoidyClient";
3
-
import { ButtonHandler } from "../../../handlers/ButtonHandler";
4
1
import { Events, MessageFlags, type Interaction } from "discord.js";
5
-
import { ChatInputCommandHandler } from "../../../handlers/CommandHandler";
2
+
import {
3
+
ChatInputCommandHandler,
4
+
ButtonHandler,
5
+
type VoidyClient,
6
+
type Event
7
+
} from "voidy-framework";
6
8
7
9
export default {
10
+
id: "interactionCreate",
8
11
name: Events.InteractionCreate,
9
12
execute: async (client: VoidyClient, interaction: Interaction) => {
10
13
if (interaction.isChatInputCommand() && interaction.isCommand()) {
11
-
// Filter the client command cache to locate the invoked command
12
-
const payload = client.registryManager.getCache().commands.filter(commands => commands.data.name === interaction.commandName)[0];
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);
13
26
14
-
if (!payload) return interaction.reply({
27
+
if (!command) return interaction.reply({
15
28
content: `Sorry, but the command ${interaction.commandName} could not be located in my command cache >:3`,
16
29
flags: [MessageFlags.Ephemeral]
17
30
});
18
31
19
-
ChatInputCommandHandler.invoke(interaction, payload, client);
32
+
ChatInputCommandHandler.invoke(interaction, command, client);
20
33
} else if (interaction.isButton()) {
21
34
// Filter the client button cache to locate the invoked button
22
-
const payload = client.registryManager.getCache().buttons.filter(buttons => buttons.id === interaction.customId)[0];
35
+
const button = client.buttons.get(interaction.customId);
23
36
24
-
if (!payload) return interaction.reply({
37
+
if (!button) return interaction.reply({
25
38
content: `Sorry, but the button ${interaction.customId} could not be located in my button cache >:3`,
26
39
flags: [MessageFlags.Ephemeral]
27
40
});
28
41
29
-
ButtonHandler.invoke(interaction, payload, client);
42
+
ButtonHandler.invoke(interaction, button, client);
30
43
} else {
31
44
let dmChannel = interaction.user.dmChannel;
32
45
+2
-2
src/modules/core/events/ready.ts
packages/bot/src/modules/core/events/ready.ts
+2
-2
src/modules/core/events/ready.ts
packages/bot/src/modules/core/events/ready.ts
···
1
+
import type { Event, VoidyClient } from "voidy-framework";
1
2
import { ActivityType, Events } from "discord.js";
2
-
import type { Event } from "../../../loaders/EventLoader";
3
-
import type { VoidyClient } from "../../../core/VoidyClient";
4
3
5
4
export default {
5
+
id: "ready",
6
6
name: Events.ClientReady,
7
7
once: true,
8
8
execute: async (client: VoidyClient) => {
-30
src/modules/core/module.ts
-30
src/modules/core/module.ts
···
1
-
import { Lifecycle, LifecycleEvents } from "../../core/Lifecycle";
2
-
import { ButtonLoader } from "../../loaders/ButtonLoader";
3
-
import { CommandLoader } from "../../loaders/CommandLoader";
4
-
import { EventLoader } from "../../loaders/EventLoader";
5
-
import type { Module } from "../../loaders/ModuleLoader";
6
-
7
-
Lifecycle.subscribe(LifecycleEvents.ClientLoop, () => {
8
-
console.log("Wait what, wait what...");
9
-
})
10
-
11
-
export default {
12
-
name: "core",
13
-
description: "The core feature set of the bot, required for command handling to work.",
14
-
author: "jokiller230",
15
-
16
-
exports: [
17
-
{
18
-
source: `${import.meta.dir}/events`,
19
-
loader: EventLoader,
20
-
},
21
-
{
22
-
source: `${import.meta.dir}/commands`,
23
-
loader: CommandLoader,
24
-
},
25
-
{
26
-
source: `${import.meta.dir}/buttons`,
27
-
loader: ButtonLoader,
28
-
}
29
-
]
30
-
} as Module;
-5
tsconfig.json
-5
tsconfig.json
···
1
1
{
2
2
"compilerOptions": {
3
-
// Environment setup & latest features
4
3
"lib": [
5
4
"ESNext"
6
5
],
7
6
"target": "ESNext",
8
7
"module": "Preserve",
9
8
"moduleDetection": "force",
10
-
"jsx": "react-jsx",
11
9
"allowJs": true,
12
-
// Bundler mode
13
10
"moduleResolution": "bundler",
14
11
"allowImportingTsExtensions": true,
15
12
"verbatimModuleSyntax": true,
16
13
"noEmit": true,
17
-
// Best practices
18
14
"strict": true,
19
15
"skipLibCheck": true,
20
16
"noFallthroughCasesInSwitch": true,
21
17
"noUncheckedIndexedAccess": true,
22
18
"noImplicitOverride": true,
23
-
// Some stricter flags (disabled by default)
24
19
"noUnusedLocals": false,
25
20
"noUnusedParameters": false,
26
21
"noPropertyAccessFromIndexSignature": false