djs-next 1.0.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of djs-next might be problematic. Click here for more details.
- package/README.md +219 -0
- package/assets/djs-next.png +0 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +344 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.mjs +321 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.d.mts +109 -0
- package/dist/index.d.ts +109 -0
- package/dist/index.js +919 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +885 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +45 -0
- package/src/cli.ts +203 -0
- package/src/client.ts +283 -0
- package/src/handlers/commandHandler.ts +139 -0
- package/src/handlers/componentHandler.ts +31 -0
- package/src/handlers/eventHandler.ts +37 -0
- package/src/handlers/taskHandler.ts +38 -0
- package/src/handlers/utils.ts +25 -0
- package/src/index.ts +7 -0
- package/src/plugins/dnxt.ts +379 -0
- package/src/templates/cjs.ts +49 -0
- package/src/templates/esm.ts +43 -0
- package/src/templates/ts.ts +47 -0
- package/src/test/client.test.ts +8 -0
- package/src/types.ts +69 -0
- package/src/utils/configLoader.ts +27 -0
- package/src/utils/i18n.ts +57 -0
- package/src/utils/paginate.ts +71 -0
- package/tsconfig.json +16 -0
- package/tsup.config.ts +10 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { Client, Collection, Interaction } from 'discord.js';
|
|
2
|
+
import { DJSNextClientOptions, FileCommand, FileComponent, DJSNextConfig } from './types.js';
|
|
3
|
+
import { loadAndDeployCommands } from './handlers/commandHandler.js';
|
|
4
|
+
import { loadEvents } from './handlers/eventHandler.js';
|
|
5
|
+
import { loadComponents } from './handlers/componentHandler.js';
|
|
6
|
+
import { loadTasks } from './handlers/taskHandler.js';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import { pathToFileURL } from 'url';
|
|
10
|
+
|
|
11
|
+
import { loadConfig } from './utils/configLoader.js';
|
|
12
|
+
import { loadLocales, translate } from './utils/i18n.js';
|
|
13
|
+
import { handleDNXT } from './plugins/dnxt.js';
|
|
14
|
+
|
|
15
|
+
export class DJSNextClient<DB = any> extends Client {
|
|
16
|
+
public commands: Collection<string, FileCommand>;
|
|
17
|
+
public components: Collection<string, FileComponent>;
|
|
18
|
+
public cooldowns: Collection<string, Collection<string, number>>;
|
|
19
|
+
public config: DJSNextConfig = {};
|
|
20
|
+
public t = translate;
|
|
21
|
+
public db!: DB;
|
|
22
|
+
|
|
23
|
+
private _commandsDir?: string;
|
|
24
|
+
private _eventsDir?: string;
|
|
25
|
+
private _componentsDir?: string;
|
|
26
|
+
private _tasksDir?: string;
|
|
27
|
+
private _localesDir?: string;
|
|
28
|
+
private _clientId?: string;
|
|
29
|
+
private _guildId?: string;
|
|
30
|
+
private _developers: string[];
|
|
31
|
+
private _middleware?: (interaction: Interaction, client: Client) => Promise<boolean> | boolean;
|
|
32
|
+
|
|
33
|
+
constructor(options: DJSNextClientOptions) {
|
|
34
|
+
super(options);
|
|
35
|
+
this.commands = new Collection();
|
|
36
|
+
this.components = new Collection();
|
|
37
|
+
this.cooldowns = new Collection();
|
|
38
|
+
|
|
39
|
+
this._commandsDir = options.commandsDir ? path.resolve(process.cwd(), options.commandsDir) : undefined;
|
|
40
|
+
this._eventsDir = options.eventsDir ? path.resolve(process.cwd(), options.eventsDir) : undefined;
|
|
41
|
+
this._componentsDir = options.componentsDir ? path.resolve(process.cwd(), options.componentsDir) : undefined;
|
|
42
|
+
this._tasksDir = options.tasksDir ? path.resolve(process.cwd(), options.tasksDir) : undefined;
|
|
43
|
+
this._clientId = options.clientId;
|
|
44
|
+
this._guildId = options.guildId;
|
|
45
|
+
this._developers = options.developers || [];
|
|
46
|
+
this._middleware = options.middleware;
|
|
47
|
+
|
|
48
|
+
this.attachCoreListeners();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private attachCoreListeners() {
|
|
52
|
+
this.on('interactionCreate', async (interaction: Interaction) => {
|
|
53
|
+
// 1. Global Middleware execution
|
|
54
|
+
if (this._middleware) {
|
|
55
|
+
try {
|
|
56
|
+
const shouldContinue = await this._middleware(interaction, this);
|
|
57
|
+
if (!shouldContinue) return; // Middleware halted execution
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error(`[djs-next] Middleware error:`, error);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 2. Chat Input Commands Execution
|
|
65
|
+
if (interaction.isChatInputCommand()) {
|
|
66
|
+
let commandKey = interaction.commandName;
|
|
67
|
+
const group = interaction.options.getSubcommandGroup(false);
|
|
68
|
+
const sub = interaction.options.getSubcommand(false);
|
|
69
|
+
|
|
70
|
+
if (group) commandKey += ` ${group}`;
|
|
71
|
+
if (sub) commandKey += ` ${sub}`;
|
|
72
|
+
|
|
73
|
+
const command = this.commands.get(commandKey);
|
|
74
|
+
if (!command || !command.execute) return;
|
|
75
|
+
|
|
76
|
+
// --- Permissions & Validation Checks ---
|
|
77
|
+
if (command.developerOnly && !this._developers.includes(interaction.user.id)) {
|
|
78
|
+
return interaction.reply({ content: 'Only developers can use this command.', ephemeral: true }) as never;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (command.guildOnly && !interaction.inGuild()) {
|
|
82
|
+
return interaction.reply({ content: 'This command can only be used in a server.', ephemeral: true }) as never;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (command.userPermissions && interaction.memberPermissions) {
|
|
86
|
+
const missing = interaction.memberPermissions.missing(command.userPermissions);
|
|
87
|
+
if (missing.length > 0) {
|
|
88
|
+
return interaction.reply({ content: `You are missing permissions: \`${missing.join(', ')}\``, ephemeral: true }) as never;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (command.botPermissions && interaction.guild?.members.me?.permissions) {
|
|
93
|
+
const missing = interaction.guild.members.me.permissions.missing(command.botPermissions);
|
|
94
|
+
if (missing.length > 0) {
|
|
95
|
+
return interaction.reply({ content: `I am missing permissions to run this: \`${missing.join(', ')}\``, ephemeral: true }) as never;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// --- Built-in Cooldowns ---
|
|
100
|
+
if (command.cooldown) {
|
|
101
|
+
if (!this.cooldowns.has(commandKey)) this.cooldowns.set(commandKey, new Collection());
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
const timestamps = this.cooldowns.get(commandKey)!;
|
|
104
|
+
const cooldownAmount = command.cooldown * 1000;
|
|
105
|
+
|
|
106
|
+
if (timestamps.has(interaction.user.id)) {
|
|
107
|
+
const expirationTime = timestamps.get(interaction.user.id)! + cooldownAmount;
|
|
108
|
+
if (now < expirationTime) {
|
|
109
|
+
return interaction.reply({
|
|
110
|
+
content: `Please wait, you are on a cooldown. You can use it again <t:${Math.round(expirationTime / 1000)}:R>.`,
|
|
111
|
+
ephemeral: true
|
|
112
|
+
}) as never;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
timestamps.set(interaction.user.id, now);
|
|
116
|
+
setTimeout(() => timestamps.delete(interaction.user.id), cooldownAmount);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
await command.execute(interaction, this);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error(`[djs-next] Error executing command: "${commandKey}"`, error);
|
|
123
|
+
const msg = { content: 'We ran into an internal error executing this command. The developers have been notified.', ephemeral: true };
|
|
124
|
+
if (interaction.replied || interaction.deferred) await interaction.followUp(msg).catch(()=>null);
|
|
125
|
+
else await interaction.reply(msg).catch(()=>null);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 3. Autocomplete Routing
|
|
130
|
+
if (interaction.isAutocomplete()) {
|
|
131
|
+
let commandKey = interaction.commandName;
|
|
132
|
+
const group = interaction.options.getSubcommandGroup(false);
|
|
133
|
+
const sub = interaction.options.getSubcommand(false);
|
|
134
|
+
|
|
135
|
+
if (group) commandKey += ` ${group}`;
|
|
136
|
+
if (sub) commandKey += ` ${sub}`;
|
|
137
|
+
|
|
138
|
+
const command = this.commands.get(commandKey);
|
|
139
|
+
if (command && command.autocomplete) {
|
|
140
|
+
try {
|
|
141
|
+
await command.autocomplete(interaction, this);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error(`[djs-next] Error executing autocomplete for: "${commandKey}"`, error);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 4. Component Routing (Buttons, Modals, Menus)
|
|
149
|
+
if (interaction.isMessageComponent() || interaction.isModalSubmit()) {
|
|
150
|
+
// Skip pagination built-in buttons
|
|
151
|
+
if (interaction.customId === 'djs_prev' || interaction.customId === 'djs_next') return;
|
|
152
|
+
|
|
153
|
+
let component = this.components.get(interaction.customId);
|
|
154
|
+
let params: Record<string, string> = {};
|
|
155
|
+
|
|
156
|
+
if (!component) {
|
|
157
|
+
for (const [key, comp] of this.components) {
|
|
158
|
+
if (!key.includes('[')) continue;
|
|
159
|
+
// Escape regex chars except brackets
|
|
160
|
+
const escapedKey = key.replace(/[.*+?^${}()|\\-]/g, '\\$&');
|
|
161
|
+
// Transform \[id\] into (?<id>.+)
|
|
162
|
+
const regexStr = '^' + escapedKey.replace(/\\\[([^\]]+)\\\]/g, '(?<$1>.+)') + '$';
|
|
163
|
+
const match = interaction.customId.match(new RegExp(regexStr));
|
|
164
|
+
if (match) {
|
|
165
|
+
component = comp;
|
|
166
|
+
if (match.groups) params = match.groups;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (component) {
|
|
172
|
+
try {
|
|
173
|
+
await component.execute(interaction, this, params);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.error(`[djs-next] Error executing component: "${interaction.customId}"`, error);
|
|
176
|
+
const msg = { content: 'We ran into an internal error executing this component.', ephemeral: true };
|
|
177
|
+
if (interaction.replied || interaction.deferred) await interaction.followUp(msg).catch(()=>null);
|
|
178
|
+
else await interaction.reply(msg).catch(()=>null);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
public async start(token: string): Promise<void> {
|
|
186
|
+
if (!token) throw new Error("[djs-next] A token must be provided to start the bot.");
|
|
187
|
+
|
|
188
|
+
this.config = await loadConfig();
|
|
189
|
+
|
|
190
|
+
// Fallback options to config
|
|
191
|
+
this._guildId = this._guildId || this.config.devGuildId;
|
|
192
|
+
if (!this._commandsDir && this.config.directories?.commands) this._commandsDir = path.resolve(process.cwd(), this.config.directories.commands);
|
|
193
|
+
if (!this._eventsDir && this.config.directories?.events) this._eventsDir = path.resolve(process.cwd(), this.config.directories.events);
|
|
194
|
+
if (!this._componentsDir && this.config.directories?.components) this._componentsDir = path.resolve(process.cwd(), this.config.directories.components);
|
|
195
|
+
if (!this._tasksDir && this.config.directories?.tasks) this._tasksDir = path.resolve(process.cwd(), this.config.directories.tasks);
|
|
196
|
+
if (!this._localesDir && this.config.directories?.locales) this._localesDir = path.resolve(process.cwd(), this.config.directories.locales);
|
|
197
|
+
|
|
198
|
+
if (this._localesDir) loadLocales(this._localesDir, this.config.defaultLocale);
|
|
199
|
+
|
|
200
|
+
// Load middleware from root
|
|
201
|
+
if (!this._middleware) {
|
|
202
|
+
const exts = ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts'];
|
|
203
|
+
const cwd = process.cwd();
|
|
204
|
+
for (const ext of exts) {
|
|
205
|
+
const mwPath = path.join(cwd, `middleware${ext}`);
|
|
206
|
+
if (fs.existsSync(mwPath)) {
|
|
207
|
+
try {
|
|
208
|
+
const mwModule = await import(pathToFileURL(mwPath).href);
|
|
209
|
+
this._middleware = mwModule.default?.middleware || mwModule.middleware || mwModule.default || mwModule;
|
|
210
|
+
if (this._middleware) {
|
|
211
|
+
console.log(`[djs-next] Loaded global middleware.`);
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
} catch (err) {
|
|
215
|
+
console.error(`[djs-next] Error loading middleware file ${mwPath}:`, err);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (this._eventsDir) await loadEvents(this, this._eventsDir);
|
|
222
|
+
if (this._componentsDir) this.components = await loadComponents(this._componentsDir);
|
|
223
|
+
if (this._tasksDir) await loadTasks(this, this._tasksDir);
|
|
224
|
+
|
|
225
|
+
if (this._commandsDir) {
|
|
226
|
+
if (!this._clientId) throw new Error("[djs-next] You must provide a clientId to deploy commands.");
|
|
227
|
+
this.commands = await loadAndDeployCommands(this._commandsDir, token, this._clientId, this._guildId);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
await this.login(token);
|
|
231
|
+
console.log(`[djs-next] Bot is ready and logged in as ${this.user?.tag}!`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
public enableDevTools(prefix: 'dnxt' | 'nxt' = 'dnxt'): void {
|
|
235
|
+
if (prefix !== 'dnxt' && prefix !== 'nxt') {
|
|
236
|
+
throw new Error(`[djs-next] Developer Tools prefix must be either 'dnxt' or 'nxt'. Received: ${prefix}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
this.on('messageCreate', async (message) => {
|
|
240
|
+
await handleDNXT(message, this, prefix);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
console.log(`[djs-next] 🛠️ Developer Tools enabled. Use "${prefix}" in chat. Ensure MessageContent intent is enabled!`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
public async enableHMR(): Promise<void> {
|
|
247
|
+
try {
|
|
248
|
+
const chokidar = await import('chokidar');
|
|
249
|
+
console.log(`[djs-next] 🔄 HMR Enabled. Watching for file changes...`);
|
|
250
|
+
|
|
251
|
+
const watcher = chokidar.watch([
|
|
252
|
+
this._commandsDir,
|
|
253
|
+
this._eventsDir,
|
|
254
|
+
this._componentsDir,
|
|
255
|
+
this._localesDir
|
|
256
|
+
].filter(Boolean) as string[], { ignoreInitial: true });
|
|
257
|
+
|
|
258
|
+
watcher.on('change', async (filePath) => {
|
|
259
|
+
console.log(`[djs-next] ♻️ File changed: ${filePath}. Reloading...`);
|
|
260
|
+
// We will just naively re-run the loaders.
|
|
261
|
+
// For events, we need to remove all listeners first to avoid memory leaks.
|
|
262
|
+
this.removeAllListeners();
|
|
263
|
+
this.commands.clear();
|
|
264
|
+
this.components.clear();
|
|
265
|
+
|
|
266
|
+
// Re-attach core listener
|
|
267
|
+
this.attachCoreListeners();
|
|
268
|
+
|
|
269
|
+
// Reload
|
|
270
|
+
if (this._eventsDir) await loadEvents(this, this._eventsDir);
|
|
271
|
+
if (this._componentsDir) this.components = await loadComponents(this._componentsDir);
|
|
272
|
+
if (this._commandsDir && this._clientId) {
|
|
273
|
+
this.commands = await loadAndDeployCommands(this._commandsDir, this.token!, this._clientId, this._guildId);
|
|
274
|
+
}
|
|
275
|
+
if (this._localesDir) loadLocales(this._localesDir, this.config.defaultLocale);
|
|
276
|
+
|
|
277
|
+
console.log(`[djs-next] ✅ Hot Reload complete.`);
|
|
278
|
+
});
|
|
279
|
+
} catch (e) {
|
|
280
|
+
console.warn(`[djs-next] chokidar not installed. HMR is disabled. Please run "npm install chokidar" to use this feature.`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Collection, REST, Routes } from 'discord.js';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { pathToFileURL } from 'url';
|
|
5
|
+
import { FileCommand } from '../types.js';
|
|
6
|
+
import { getAllFiles } from './utils.js';
|
|
7
|
+
|
|
8
|
+
interface CommandNode {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
options: any[];
|
|
12
|
+
execute?: Function;
|
|
13
|
+
children: Map<string, CommandNode>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function loadAndDeployCommands(
|
|
17
|
+
commandsDir: string,
|
|
18
|
+
token: string,
|
|
19
|
+
clientId: string,
|
|
20
|
+
guildId?: string
|
|
21
|
+
): Promise<Collection<string, FileCommand>> {
|
|
22
|
+
const flatCommands = new Collection<string, FileCommand>();
|
|
23
|
+
|
|
24
|
+
if (!fs.existsSync(commandsDir)) {
|
|
25
|
+
console.warn(`[djs-next] Commands directory "${commandsDir}" does not exist.`);
|
|
26
|
+
return flatCommands;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const commandFiles = getAllFiles(commandsDir);
|
|
30
|
+
const rootNodes = new Map<string, CommandNode>();
|
|
31
|
+
|
|
32
|
+
function getOrCreateNode(pathParts: string[]): CommandNode {
|
|
33
|
+
let currentMap = rootNodes;
|
|
34
|
+
let currentNode: CommandNode | undefined;
|
|
35
|
+
|
|
36
|
+
for (const part of pathParts) {
|
|
37
|
+
if (!currentMap.has(part)) {
|
|
38
|
+
currentMap.set(part, {
|
|
39
|
+
name: part,
|
|
40
|
+
description: `${part} command`, // Fallback description
|
|
41
|
+
options: [],
|
|
42
|
+
children: new Map()
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
currentNode = currentMap.get(part)!;
|
|
46
|
+
currentMap = currentNode.children;
|
|
47
|
+
}
|
|
48
|
+
return currentNode!;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const file of commandFiles) {
|
|
52
|
+
const relativePath = path.relative(commandsDir, file);
|
|
53
|
+
const parsed = path.parse(relativePath);
|
|
54
|
+
|
|
55
|
+
// Normalize path separators
|
|
56
|
+
const dirParts = parsed.dir ? parsed.dir.split(path.sep) : [];
|
|
57
|
+
const name = parsed.name;
|
|
58
|
+
|
|
59
|
+
const pathParts = [...dirParts];
|
|
60
|
+
if (name !== 'index') {
|
|
61
|
+
pathParts.push(name);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (pathParts.length === 0) continue;
|
|
65
|
+
if (pathParts.length > 3) {
|
|
66
|
+
console.warn(`[djs-next] Command path too deep (Discord allows max 3 levels): ${relativePath}`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const module = await import(pathToFileURL(file).href);
|
|
71
|
+
const commandData: FileCommand = module.default || module.command || module;
|
|
72
|
+
if (commandData) commandData.filepath = file;
|
|
73
|
+
|
|
74
|
+
const node = getOrCreateNode(pathParts);
|
|
75
|
+
if (commandData.description) node.description = commandData.description;
|
|
76
|
+
if (commandData.options) node.options = commandData.options;
|
|
77
|
+
if (commandData.execute) {
|
|
78
|
+
node.execute = commandData.execute;
|
|
79
|
+
// Map entire FileCommand object to a space-separated string (e.g. "economy balance")
|
|
80
|
+
flatCommands.set(pathParts.join(' '), commandData);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Build JSON payloads for Discord
|
|
85
|
+
function buildCommandJSON(node: CommandNode, depth: number): any {
|
|
86
|
+
const json: any = {
|
|
87
|
+
name: node.name,
|
|
88
|
+
description: node.description,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
if (node.children.size > 0) {
|
|
92
|
+
json.options = [];
|
|
93
|
+
for (const [_, childNode] of node.children) {
|
|
94
|
+
const childJson = buildCommandJSON(childNode, depth + 1);
|
|
95
|
+
|
|
96
|
+
// 1 = SUB_COMMAND, 2 = SUB_COMMAND_GROUP
|
|
97
|
+
if (depth === 0) {
|
|
98
|
+
childJson.type = childNode.children.size > 0 ? 2 : 1;
|
|
99
|
+
} else if (depth === 1) {
|
|
100
|
+
childJson.type = 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
json.options.push(childJson);
|
|
104
|
+
}
|
|
105
|
+
} else if (node.options && node.options.length > 0) {
|
|
106
|
+
json.options = node.options;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return json;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const commandsData = Array.from(rootNodes.values()).map(node => buildCommandJSON(node, 0));
|
|
113
|
+
|
|
114
|
+
// Deploy commands
|
|
115
|
+
const rest = new REST({ version: '10' }).setToken(token);
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
console.log(`[djs-next] Started refreshing ${commandsData.length} File-System application (/) commands.`);
|
|
119
|
+
|
|
120
|
+
let data: any;
|
|
121
|
+
if (guildId) {
|
|
122
|
+
data = await rest.put(
|
|
123
|
+
Routes.applicationGuildCommands(clientId, guildId),
|
|
124
|
+
{ body: commandsData },
|
|
125
|
+
);
|
|
126
|
+
} else {
|
|
127
|
+
data = await rest.put(
|
|
128
|
+
Routes.applicationCommands(clientId),
|
|
129
|
+
{ body: commandsData },
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log(`[djs-next] Successfully reloaded ${data.length} File-System application (/) commands.`);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error(`[djs-next] Failed to deploy commands:`, error);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return flatCommands;
|
|
139
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Collection } from 'discord.js';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { pathToFileURL } from 'url';
|
|
5
|
+
import { FileComponent } from '../types.js';
|
|
6
|
+
import { getAllFiles } from './utils.js';
|
|
7
|
+
|
|
8
|
+
export async function loadComponents(componentsDir: string): Promise<Collection<string, FileComponent>> {
|
|
9
|
+
const components = new Collection<string, FileComponent>();
|
|
10
|
+
|
|
11
|
+
if (!fs.existsSync(componentsDir)) return components;
|
|
12
|
+
|
|
13
|
+
const files = getAllFiles(componentsDir);
|
|
14
|
+
|
|
15
|
+
for (const file of files) {
|
|
16
|
+
const module = await import(pathToFileURL(file).href);
|
|
17
|
+
const componentData: FileComponent = module.default || module.component || module;
|
|
18
|
+
if (componentData) componentData.filepath = file;
|
|
19
|
+
|
|
20
|
+
if (!componentData || !componentData.execute) continue;
|
|
21
|
+
|
|
22
|
+
// Infer customId from filename if not explicitly provided
|
|
23
|
+
const parsed = path.parse(file);
|
|
24
|
+
const customId = componentData.customId || parsed.name;
|
|
25
|
+
|
|
26
|
+
components.set(customId, componentData);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log(`[djs-next] Successfully loaded ${components.size} components.`);
|
|
30
|
+
return components;
|
|
31
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Client } from 'discord.js';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
import { Event } from '../types.js';
|
|
5
|
+
import { getAllFiles } from './utils.js';
|
|
6
|
+
|
|
7
|
+
export async function loadEvents(client: Client, eventsDir: string) {
|
|
8
|
+
if (!fs.existsSync(eventsDir)) {
|
|
9
|
+
console.warn(`[djs-next] Events directory "${eventsDir}" does not exist.`);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const eventFiles = getAllFiles(eventsDir);
|
|
14
|
+
let loadedEvents = 0;
|
|
15
|
+
|
|
16
|
+
for (const file of eventFiles) {
|
|
17
|
+
const eventModule = await import(pathToFileURL(file).href);
|
|
18
|
+
const eventData: Event<any> = eventModule.default?.event || eventModule.event || eventModule.default || eventModule;
|
|
19
|
+
if (eventData) eventData.filepath = file;
|
|
20
|
+
|
|
21
|
+
if (!eventData || !eventData.name || !eventData.execute) {
|
|
22
|
+
console.warn(`[djs-next] The event at ${file} is missing a required "name" or "execute" property.`);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (eventData.once) {
|
|
27
|
+
client.once(eventData.name, (...args) => eventData.execute(client, ...args));
|
|
28
|
+
} else {
|
|
29
|
+
client.on(eventData.name, (...args) => eventData.execute(client, ...args));
|
|
30
|
+
}
|
|
31
|
+
loadedEvents++;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (loadedEvents > 0) {
|
|
35
|
+
console.log(`[djs-next] Successfully loaded ${loadedEvents} events.`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
import { Client } from 'discord.js';
|
|
5
|
+
import { FileTask } from '../types.js';
|
|
6
|
+
import { getAllFiles } from './utils.js';
|
|
7
|
+
|
|
8
|
+
export async function loadTasks(client: Client, tasksDir: string) {
|
|
9
|
+
if (!fs.existsSync(tasksDir)) return;
|
|
10
|
+
|
|
11
|
+
const files = getAllFiles(tasksDir);
|
|
12
|
+
let loaded = 0;
|
|
13
|
+
|
|
14
|
+
for (const file of files) {
|
|
15
|
+
const module = await import(pathToFileURL(file).href);
|
|
16
|
+
const taskData: FileTask = module.default || module.task || module;
|
|
17
|
+
if (taskData) taskData.filepath = file;
|
|
18
|
+
|
|
19
|
+
if (!taskData || !taskData.interval || !taskData.execute) continue;
|
|
20
|
+
|
|
21
|
+
const intervalId = setInterval(async () => {
|
|
22
|
+
try {
|
|
23
|
+
await taskData.execute(client);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error(`[djs-next] Background Task error at ${file}:`, error);
|
|
26
|
+
}
|
|
27
|
+
}, taskData.interval);
|
|
28
|
+
|
|
29
|
+
if (!(client as any)._activeTasks) (client as any)._activeTasks = new Map();
|
|
30
|
+
(client as any)._activeTasks.set(file, intervalId);
|
|
31
|
+
|
|
32
|
+
loaded++;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (loaded > 0) {
|
|
36
|
+
console.log(`[djs-next] Successfully scheduled ${loaded} background tasks.`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Recursively gets all files in a directory
|
|
6
|
+
*/
|
|
7
|
+
export function getAllFiles(dirPath: string, arrayOfFiles: string[] = []): string[] {
|
|
8
|
+
const files = fs.readdirSync(dirPath);
|
|
9
|
+
|
|
10
|
+
files.forEach(file => {
|
|
11
|
+
const fullPath = path.join(dirPath, file);
|
|
12
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
13
|
+
arrayOfFiles = getAllFiles(fullPath, arrayOfFiles);
|
|
14
|
+
} else {
|
|
15
|
+
if (file.endsWith('.js') || file.endsWith('.ts') || file.endsWith('.mjs') || file.endsWith('.cjs')) {
|
|
16
|
+
// Skip TypeScript declaration files
|
|
17
|
+
if (!file.endsWith('.d.ts')) {
|
|
18
|
+
arrayOfFiles.push(fullPath);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return arrayOfFiles;
|
|
25
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from './client.js';
|
|
2
|
+
export { DJSNextClientOptions, FileCommand, FileComponent, FileTask, Event as DJSNextEvent, DJSNextConfig } from './types.js';
|
|
3
|
+
export * from './utils/paginate.js';
|
|
4
|
+
export * from './utils/configLoader.js';
|
|
5
|
+
export * from './utils/i18n.js';
|
|
6
|
+
// Re-export everything from discord.js so users don't need to install it explicitly
|
|
7
|
+
export * from 'discord.js';
|