djs-next 1.0.0-dev.1 → 1.0.0-dev.3

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/src/client.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Client, Collection, Interaction } from 'discord.js';
1
+ import { Client, Collection, Interaction, Message } from 'discord.js';
2
2
  import { DJSNextClientOptions, FileCommand, FileComponent, DJSNextConfig } from './types.js';
3
3
  import { loadAndDeployCommands } from './handlers/commandHandler.js';
4
4
  import { loadEvents } from './handlers/eventHandler.js';
@@ -28,7 +28,12 @@ export class DJSNextClient<DB = any> extends Client {
28
28
  private _clientId?: string;
29
29
  private _guildId?: string;
30
30
  private _developers: string[];
31
- private _middleware?: (interaction: Interaction, client: Client) => Promise<boolean> | boolean;
31
+ private _middleware?: (interaction: Interaction | Message, client: Client) => Promise<boolean> | boolean;
32
+ private _prefixes: string[];
33
+ private _enableSlashCommands: boolean;
34
+ private _enableTextCommands: boolean;
35
+ private _enableMentionPrefix: boolean | string[];
36
+ private _enableNoPrefix: boolean | string[];
32
37
 
33
38
  constructor(options: DJSNextClientOptions) {
34
39
  super(options);
@@ -43,7 +48,17 @@ export class DJSNextClient<DB = any> extends Client {
43
48
  this._clientId = options.clientId;
44
49
  this._guildId = options.guildId;
45
50
  this._developers = options.developers || [];
46
- this._middleware = options.middleware;
51
+ this._middleware = options.middleware as any;
52
+
53
+ const prefs = options.prefixes || [];
54
+ this._prefixes = Array.isArray(prefs) ? prefs : [prefs];
55
+ this._prefixes = this._prefixes.filter(p => p !== ''); // Remove empty strings, handle explicitly via enableNoPrefix
56
+
57
+ this._enableSlashCommands = options.enableSlashCommands ?? true;
58
+ this._enableTextCommands = options.enableTextCommands ?? true;
59
+ this._enableMentionPrefix = options.enableMentionPrefix ?? true;
60
+ this._enableNoPrefix = options.enableNoPrefix ?? false;
61
+ if (options.db) this.db = options.db as any;
47
62
 
48
63
  this.attachCoreListeners();
49
64
  }
@@ -63,6 +78,8 @@ export class DJSNextClient<DB = any> extends Client {
63
78
 
64
79
  // 2. Chat Input Commands Execution
65
80
  if (interaction.isChatInputCommand()) {
81
+ if (!this._enableSlashCommands) return;
82
+
66
83
  let commandKey = interaction.commandName;
67
84
  const group = interaction.options.getSubcommandGroup(false);
68
85
  const sub = interaction.options.getSubcommand(false);
@@ -73,56 +90,12 @@ export class DJSNextClient<DB = any> extends Client {
73
90
  const command = this.commands.get(commandKey);
74
91
  if (!command || !command.execute) return;
75
92
 
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
- }
93
+ if (!(await this.handlePreconditions(command, interaction, commandKey))) return;
118
94
 
119
95
  try {
120
96
  await command.execute(interaction, this);
121
97
  } 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);
98
+ await this.handleCommandError(error, commandKey, interaction);
126
99
  }
127
100
  }
128
101
 
@@ -169,17 +142,87 @@ export class DJSNextClient<DB = any> extends Client {
169
142
  }
170
143
  }
171
144
  if (component) {
145
+ if (!(await this.handlePreconditions(component, interaction, interaction.customId))) return;
146
+
172
147
  try {
173
148
  await component.execute(interaction, this, params);
174
149
  } 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);
150
+ await this.handleCommandError(error, interaction.customId, interaction);
179
151
  }
180
152
  }
181
153
  }
182
154
  });
155
+
156
+ this.on('messageCreate', async (message: Message) => {
157
+ if (message.author.bot) return;
158
+ if (!this._enableTextCommands) return;
159
+
160
+ // Global Middleware execution for Messages
161
+ if (this._middleware) {
162
+ try {
163
+ const shouldContinue = await this._middleware(message, this);
164
+ if (!shouldContinue) return;
165
+ } catch (error) {
166
+ console.error(`[djs-next] Middleware error (Message):`, error);
167
+ return;
168
+ }
169
+ }
170
+
171
+ let content = message.content.trim();
172
+ let matchedPrefix = '';
173
+ let isCommand = false;
174
+
175
+ // 1. Check Mention Prefix
176
+ const mentionRegex = new RegExp(`^<@!?${this.user?.id}>\\s*`);
177
+ const canUseMention = this._enableMentionPrefix === true || (Array.isArray(this._enableMentionPrefix) && this._enableMentionPrefix.includes(message.author.id));
178
+
179
+ if (canUseMention && mentionRegex.test(content)) {
180
+ matchedPrefix = content.match(mentionRegex)![0];
181
+ isCommand = true;
182
+ } else {
183
+ // 2. Check Standard Prefixes
184
+ const sortedPrefs = [...this._prefixes].sort((a, b) => b.length - a.length);
185
+
186
+ for (const p of sortedPrefs) {
187
+ if (content.startsWith(p)) {
188
+ matchedPrefix = p;
189
+ isCommand = true;
190
+ break;
191
+ }
192
+ }
193
+
194
+ // 3. Check No Prefix
195
+ const canUseNoPrefix = this._enableNoPrefix === true || (Array.isArray(this._enableNoPrefix) && this._enableNoPrefix.includes(message.author.id));
196
+ if (!isCommand && canUseNoPrefix) {
197
+ isCommand = true;
198
+ matchedPrefix = '';
199
+ }
200
+ }
201
+
202
+ if (!isCommand) return;
203
+
204
+ const args = content.slice(matchedPrefix.length).trim().split(/ +/g);
205
+ const commandName = args.shift()?.toLowerCase();
206
+ if (!commandName) return;
207
+
208
+ // Find the command
209
+ let command = this.commands.get(commandName);
210
+ if (!command) {
211
+ // Check aliases
212
+ command = this.commands.find(c => c.aliases?.includes(commandName) || false);
213
+ }
214
+
215
+ if (!command || !command.executeText) return;
216
+
217
+ if (!(await this.handlePreconditions(command, message, commandName))) return;
218
+
219
+ // Execute Text Command
220
+ try {
221
+ await command.executeText(message, args, this);
222
+ } catch (error) {
223
+ await this.handleCommandError(error, commandName, message);
224
+ }
225
+ });
183
226
  }
184
227
 
185
228
  public async start(token: string): Promise<void> {
@@ -187,15 +230,17 @@ export class DJSNextClient<DB = any> extends Client {
187
230
 
188
231
  this.config = await loadConfig();
189
232
 
190
- // Fallback options to config
233
+ // Fallback options to config, then to default 'src' folders
191
234
  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);
235
+
236
+ if (!this._commandsDir) this._commandsDir = path.resolve(process.cwd(), this.config.directories?.commands || 'src/commands');
237
+ if (!this._eventsDir) this._eventsDir = path.resolve(process.cwd(), this.config.directories?.events || 'src/events');
238
+ if (!this._componentsDir) this._componentsDir = path.resolve(process.cwd(), this.config.directories?.components || 'src/components');
239
+ if (!this._tasksDir) this._tasksDir = path.resolve(process.cwd(), this.config.directories?.tasks || 'src/tasks');
240
+ if (!this._localesDir) this._localesDir = path.resolve(process.cwd(), this.config.directories?.locales || 'src/locales');
197
241
 
198
- if (this._localesDir) loadLocales(this._localesDir, this.config.defaultLocale);
242
+ // Only load if the directories actually exist
243
+ if (fs.existsSync(this._localesDir)) loadLocales(this._localesDir, this.config.defaultLocale);
199
244
 
200
245
  // Load middleware from root
201
246
  if (!this._middleware) {
@@ -218,17 +263,18 @@ export class DJSNextClient<DB = any> extends Client {
218
263
  }
219
264
  }
220
265
 
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
- }
266
+ if (fs.existsSync(this._eventsDir)) await loadEvents(this, this._eventsDir);
267
+ if (fs.existsSync(this._componentsDir)) this.components = await loadComponents(this._componentsDir);
268
+ if (fs.existsSync(this._tasksDir)) await loadTasks(this, this._tasksDir);
229
269
 
230
270
  await this.login(token);
231
271
  console.log(`[djs-next] Bot is ready and logged in as ${this.user?.tag}!`);
272
+
273
+ // Load and deploy commands AFTER login so we can automatically use this.user.id
274
+ if (fs.existsSync(this._commandsDir)) {
275
+ this._clientId = this._clientId || this.user!.id;
276
+ this.commands = await loadAndDeployCommands(this._commandsDir, token, this._clientId, this._guildId);
277
+ }
232
278
  }
233
279
 
234
280
  public enableDevTools(prefix: 'dnxt' | 'nxt' = 'dnxt'): void {
@@ -280,4 +326,167 @@ export class DJSNextClient<DB = any> extends Client {
280
326
  console.warn(`[djs-next] chokidar not installed. HMR is disabled. Please run "npm install chokidar" to use this feature.`);
281
327
  }
282
328
  }
329
+
330
+ private async handleCommandError(error: any, commandKey: string, context: Interaction | Message) {
331
+ console.error(`[djs-next] Error executing command/component: "${commandKey}"`, error);
332
+ const djs = require('discord.js');
333
+ if (this.config.responses?.errorBoundary === null) {
334
+ // Developer explicitly opted out of error boundary user messages
335
+ } else {
336
+ try {
337
+ const errorContent = this.config.responses?.errorBoundary || `### ⚠️ Execution Error\n> The framework encountered a fatal error while executing \`${commandKey}\`.`;
338
+ const codeBlock = `\`\`\`js\n${String(error.stack || error.message).substring(0, 1500)}\n\`\`\``;
339
+
340
+ if ('commandName' in context || 'customId' in context) {
341
+ const container = new djs.ContainerBuilder()
342
+ .setAccentColor(0xED4245)
343
+ .addTextDisplayComponents(new djs.TextDisplayBuilder().setContent(errorContent))
344
+ .addSeparatorComponents(new djs.SeparatorBuilder())
345
+ .addTextDisplayComponents(new djs.TextDisplayBuilder().setContent(codeBlock));
346
+
347
+ const payload = { components: [container], flags: 32768, ephemeral: true };
348
+ const i = context as Interaction;
349
+ if (i.isRepliable()) {
350
+ if (i.replied || i.deferred) await i.followUp(payload);
351
+ else await i.reply(payload);
352
+ }
353
+ } else {
354
+ const embed = new djs.EmbedBuilder()
355
+ .setColor(0xED4245)
356
+ .setDescription(`${errorContent}\n\n${codeBlock}`);
357
+
358
+ const m = context as Message;
359
+ await m.reply({ embeds: [embed] });
360
+ }
361
+ } catch (e) {
362
+ // Fallback ignore if channel is dead
363
+ }
364
+ }
365
+
366
+ if (this.config.errorLogChannelId) {
367
+ try {
368
+ const channel = await this.channels.fetch(this.config.errorLogChannelId);
369
+ if (channel && channel.isTextBased()) {
370
+ const userId = 'user' in context ? context.user.id : context.author.id;
371
+ await (channel as any).send(`🚨 **Global Error Boundary Caught Exception**\n**Context:** \`${commandKey}\`\n**User:** <@${userId}>\n\`\`\`js\n${String(error.stack || error.message).substring(0, 1900)}\n\`\`\``);
372
+ }
373
+ } catch (e) {
374
+ console.error(`[djs-next] Failed to send error to log channel:`, e);
375
+ }
376
+ }
377
+ }
378
+
379
+ private async handlePreconditions(command: FileCommand | FileComponent, context: Interaction | Message, commandKey: string): Promise<boolean> {
380
+ const userId = 'user' in context ? context.user.id : context.author.id;
381
+ const isGuild = 'guild' in context && context.guild !== null;
382
+ const member = context.member as any;
383
+
384
+ const sendErr = async (msg: string | null) => {
385
+ if (msg === null) return;
386
+ if ('reply' in context && typeof context.reply === 'function') {
387
+ try {
388
+ if ('replied' in context && (context as any).isRepliable() && ((context as any).replied || (context as any).deferred)) {
389
+ await (context as any).followUp({ content: msg, ephemeral: true });
390
+ } else {
391
+ await (context as any).reply({ content: msg, ephemeral: true, allowedMentions: { repliedUser: false } });
392
+ }
393
+ } catch(e) {}
394
+ }
395
+ };
396
+
397
+ if ('developerOnly' in command && command.developerOnly && !this._developers.includes(userId)) {
398
+ await sendErr(this.config.responses?.developerOnly !== undefined ? this.config.responses.developerOnly : 'Only developers can use this command.');
399
+ return false;
400
+ }
401
+ if ('guildOnly' in command && command.guildOnly && !isGuild) {
402
+ await sendErr(this.config.responses?.guildOnly !== undefined ? this.config.responses.guildOnly : 'This command can only be used in a server.');
403
+ return false;
404
+ }
405
+ if ('userPermissions' in command && command.userPermissions && member?.permissions) {
406
+ const missing = member.permissions.missing(command.userPermissions);
407
+ if (missing.length > 0) {
408
+ await sendErr(this.config.responses?.missingPerms !== undefined ? (this.config.responses.missingPerms === null ? null : this.config.responses.missingPerms.replace('{perms}', missing.join(', '))) : `You are missing permissions: \`${missing.join(', ')}\``);
409
+ return false;
410
+ }
411
+ }
412
+ if ('botPermissions' in command && command.botPermissions && context.guild?.members.me?.permissions) {
413
+ const missing = context.guild.members.me.permissions.missing(command.botPermissions);
414
+ if (missing.length > 0) {
415
+ await sendErr(this.config.responses?.missingPerms !== undefined ? (this.config.responses.missingPerms === null ? null : this.config.responses.missingPerms.replace('{perms}', missing.join(', '))) : `I am missing permissions to run this: \`${missing.join(', ')}\``);
416
+ return false;
417
+ }
418
+ }
419
+
420
+ let totalCooldown = 'cooldown' in command && command.cooldown ? command.cooldown * 1000 : 0;
421
+
422
+ if (command.preconditions) {
423
+ for (const pre of command.preconditions) {
424
+ const parts = pre.split(':');
425
+ const type = parts[0].toLowerCase();
426
+ const val = parts.slice(1).join(':');
427
+
428
+ if (type === 'owneronly' || type === 'developeronly') {
429
+ if (!this._developers.includes(userId)) {
430
+ await sendErr(this.config.responses?.developerOnly !== undefined ? this.config.responses.developerOnly : 'Only developers can use this command.');
431
+ return false;
432
+ }
433
+ } else if (type === 'guildonly') {
434
+ if (!isGuild) {
435
+ await sendErr(this.config.responses?.guildOnly !== undefined ? this.config.responses.guildOnly : 'This command can only be used in a server.');
436
+ return false;
437
+ }
438
+ } else if (type === 'requireperms') {
439
+ const perms = val.split(',').map(p => p.trim());
440
+ if (member?.permissions) {
441
+ const missing = member.permissions.missing(perms);
442
+ if (missing.length > 0) {
443
+ await sendErr(this.config.responses?.missingPerms !== undefined ? (this.config.responses.missingPerms === null ? null : this.config.responses.missingPerms.replace('{perms}', missing.join(', '))) : `You are missing permissions: \`${missing.join(', ')}\``);
444
+ return false;
445
+ }
446
+ }
447
+ } else if (type === 'cooldown') {
448
+ const match = val.match(/(\d+)(s|m|h)/);
449
+ if (match) {
450
+ let amount = parseInt(match[1]) * 1000;
451
+ if (match[2] === 'm') amount *= 60;
452
+ if (match[2] === 'h') amount *= 3600;
453
+ totalCooldown = Math.max(totalCooldown, amount);
454
+ }
455
+ }
456
+ }
457
+ }
458
+
459
+ if (totalCooldown > 0) {
460
+ const now = Date.now();
461
+
462
+ if (this.config.cooldownAdapter) {
463
+ const lastUsed = await this.config.cooldownAdapter.get(commandKey, userId);
464
+ if (lastUsed) {
465
+ const expirationTime = lastUsed + totalCooldown;
466
+ if (now < expirationTime) {
467
+ const timeStr = `<t:${Math.round(expirationTime / 1000)}:R>`;
468
+ await sendErr(this.config.responses?.cooldown !== undefined ? (this.config.responses.cooldown === null ? null : this.config.responses.cooldown.replace('{time}', timeStr)) : `Please wait, you are on a cooldown. You can use it again ${timeStr}.`);
469
+ return false;
470
+ }
471
+ }
472
+ await this.config.cooldownAdapter.set(commandKey, userId, now);
473
+ } else {
474
+ if (!this.cooldowns.has(commandKey)) this.cooldowns.set(commandKey, new Collection());
475
+ const timestamps = this.cooldowns.get(commandKey)!;
476
+
477
+ if (timestamps.has(userId)) {
478
+ const expirationTime = timestamps.get(userId)! + totalCooldown;
479
+ if (now < expirationTime) {
480
+ const timeStr = `<t:${Math.round(expirationTime / 1000)}:R>`;
481
+ await sendErr(this.config.responses?.cooldown !== undefined ? (this.config.responses.cooldown === null ? null : this.config.responses.cooldown.replace('{time}', timeStr)) : `Please wait, you are on a cooldown. You can use it again ${timeStr}.`);
482
+ return false;
483
+ }
484
+ }
485
+ timestamps.set(userId, now);
486
+ setTimeout(() => timestamps.delete(userId), totalCooldown);
487
+ }
488
+ }
489
+
490
+ return true;
491
+ }
283
492
  }
package/src/index.ts CHANGED
@@ -1,7 +1,10 @@
1
+ import 'dotenv/config';
1
2
  export * from './client.js';
2
- export { DJSNextClientOptions, FileCommand, FileComponent, FileTask, Event as DJSNextEvent, DJSNextConfig } from './types.js';
3
+ export { DJSNextClientOptions, FileCommand, FileComponent, FileTask, Event as DJSNextEvent, DJSNextConfig, CooldownAdapter } from './types.js';
3
4
  export * from './utils/paginate.js';
5
+ export * from './utils/prompts.js';
4
6
  export * from './utils/configLoader.js';
5
7
  export * from './utils/i18n.js';
8
+ export * from './utils/PaginationBuilder.js';
6
9
  // Re-export everything from discord.js so users don't need to install it explicitly
7
10
  export * from 'discord.js';