zumito-framework 1.4.2 → 1.6.0

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.
@@ -21,6 +21,8 @@ import { RecursiveObjectMerger } from './services/utilities/RecursiveObjectMerge
21
21
  import { MemberPermissionChecker } from './services/utilities/MemberPermissionChecker.js';
22
22
  import { CommandParser } from './services/CommandParser.js';
23
23
  import { SlashCommandRefresher } from './services/SlashCommandRefresher.js';
24
+ import { ErrorHandler } from '@services/handlers/ErrorHandler.js';
25
+ import { ErrorType } from '@definitions/ErrorType.js';
24
26
  // import better-logging
25
27
  betterLogging(console);
26
28
  /**
@@ -211,6 +213,21 @@ export class ZumitoFramework {
211
213
  this.app.use(express.json());
212
214
  this.app.use(express.urlencoded({ extended: false }));
213
215
  this.app.use(cookieParser());
216
+ //Error handler
217
+ this.app.use(function (err, req, res, next) {
218
+ const errorHandler = ServiceContainer.getService(ErrorHandler);
219
+ errorHandler.handleError(err, {
220
+ type: ErrorType.Api,
221
+ endpoint: req.originalUrl,
222
+ method: req.method,
223
+ });
224
+ if (!res.headersSent) {
225
+ return res.status(500).json({
226
+ error: 'Internal Server Error',
227
+ message: 'An unexpected error occurred. Please try again later.',
228
+ });
229
+ }
230
+ });
214
231
  //this.app.use(express.static(path.join(__dirname, "public")));
215
232
  //To allow cross-origin requests
216
233
  this.app.use(cors());
@@ -291,16 +308,13 @@ export class ZumitoFramework {
291
308
  module = await this.modules.loadModuleFile(path.join(modulesFolder, moduleName));
292
309
  }
293
310
  // Create module instance
294
- const moduleInstance = await this.modules.instanceModule(module, path.join(modulesFolder, moduleName), moduleName);
295
- // Register module in the framework
296
- this.modules.registerModule(moduleInstance);
311
+ await this.modules.instanceModule(module, path.join(modulesFolder, moduleName), moduleName);
297
312
  }
298
313
  async registerBundle(bundlePath, bundleOptions) {
299
314
  console.log(bundlePath);
300
315
  const bundle = await this.modules.loadModuleFile(bundlePath);
301
316
  const bundleName = path.basename(bundlePath);
302
- const moduleInstance = await this.modules.instanceModule(bundle, bundlePath, bundleName, bundleOptions);
303
- this.modules.registerModule(moduleInstance);
317
+ await this.modules.instanceModule(bundle, bundlePath, bundleName, bundleOptions);
304
318
  }
305
319
  /**
306
320
  * Initializes the Discord client using the Discord.js library.
@@ -2,5 +2,6 @@ export declare enum ErrorType {
2
2
  CommandInstance = 1,
3
3
  CommandLoad = 2,
4
4
  CommandRun = 3,
5
- Other = 4
5
+ Api = 4,
6
+ Other = 5
6
7
  }
@@ -3,5 +3,6 @@ export var ErrorType;
3
3
  ErrorType[ErrorType["CommandInstance"] = 1] = "CommandInstance";
4
4
  ErrorType[ErrorType["CommandLoad"] = 2] = "CommandLoad";
5
5
  ErrorType[ErrorType["CommandRun"] = 3] = "CommandRun";
6
- ErrorType[ErrorType["Other"] = 4] = "Other";
6
+ ErrorType[ErrorType["Api"] = 4] = "Api";
7
+ ErrorType[ErrorType["Other"] = 5] = "Other";
7
8
  })(ErrorType || (ErrorType = {}));
@@ -4,6 +4,11 @@ import { FrameworkEvent } from './FrameworkEvent.js';
4
4
  import { DatabaseModel } from './DatabaseModel.js';
5
5
  import { CommandManager } from '../services/managers/CommandManager.js';
6
6
  import { ModuleParameters } from './parameters/ModuleParameters.js';
7
+ export declare type ModuleRequeriments = {
8
+ modules: Array<string>;
9
+ services: Array<string>;
10
+ custom: Array<() => Promise<boolean>>;
11
+ };
7
12
  export declare abstract class Module {
8
13
  protected path: string;
9
14
  protected parameters: ModuleParameters;
@@ -11,6 +16,7 @@ export declare abstract class Module {
11
16
  protected commands: CommandManager;
12
17
  protected events: Map<string, FrameworkEvent>;
13
18
  protected models: Array<DatabaseModel>;
19
+ static requeriments: ModuleRequeriments;
14
20
  protected commandManager: CommandManager;
15
21
  constructor(path: any, parameters?: ModuleParameters);
16
22
  initialize(): Promise<void>;
@@ -11,6 +11,7 @@ export class Module {
11
11
  commands;
12
12
  events = new Map();
13
13
  models = [];
14
+ static requeriments;
14
15
  commandManager;
15
16
  constructor(path, parameters) {
16
17
  this.path = path;
@@ -1,4 +1,3 @@
1
- import { ActionRowBuilder, EmbedBuilder } from 'discord.js';
2
1
  import { EventParameters } from '../../../../../definitions/parameters/EventParameters.js';
3
2
  import { FrameworkEvent } from '../../../../../definitions/FrameworkEvent.js';
4
3
  import { MemberPermissionChecker } from '../../../../../services/utilities/MemberPermissionChecker.js';
@@ -13,12 +12,4 @@ export declare class MessageCreate extends FrameworkEvent {
13
12
  constructor();
14
13
  execute({ message, framework }: EventParameters): Promise<import("discord.js").Message<boolean>>;
15
14
  autocorrect(str: string, words: string[]): any;
16
- getErrorEmbed(error: any, parse: any): {
17
- embeds: EmbedBuilder[];
18
- components: ActionRowBuilder<import("@discordjs/builders").AnyComponentBuilder>[];
19
- allowedMentions: {
20
- repliedUser: boolean;
21
- };
22
- };
23
- parseError(error: any): any;
24
15
  }
@@ -1,13 +1,13 @@
1
- import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, EmbedBuilder, PermissionsBitField, } from 'discord.js';
2
- import ErrorStackParser from 'error-stack-parser';
1
+ import { PermissionsBitField, } from 'discord.js';
3
2
  import { FrameworkEvent } from '../../../../../definitions/FrameworkEvent.js';
4
3
  import leven from 'leven';
5
- import path from 'path';
6
4
  import { ServiceContainer } from '../../../../../services/ServiceContainer.js';
7
5
  import { CommandParser } from '../../../../../services/CommandParser.js';
8
6
  import { MemberPermissionChecker } from '../../../../../services/utilities/MemberPermissionChecker.js';
9
7
  import { ZumitoFramework } from '../../../../../ZumitoFramework.js';
10
8
  import { GuildDataGetter } from '../../../../../services/utilities/GuildDataGetter.js';
9
+ import { ErrorHandler } from '../../../../../services/handlers/ErrorHandler.js';
10
+ import { ErrorType } from '../../../../../definitions/ErrorType.js';
11
11
  export class MessageCreate extends FrameworkEvent {
12
12
  once = false;
13
13
  source = 'discord';
@@ -114,6 +114,15 @@ export class MessageCreate extends FrameworkEvent {
114
114
  return framework.translations.get('command.' + commandInstance.name + '.' + key, guildSettings.lang, params);
115
115
  }
116
116
  },
117
+ }).catch((error) => {
118
+ const errorHandler = ServiceContainer.getService(ErrorHandler);
119
+ errorHandler.handleError(error, {
120
+ command: commandInstance,
121
+ type: ErrorType.CommandRun,
122
+ });
123
+ message.reply({
124
+ content: "An error ocurred while running this command.",
125
+ });
117
126
  });
118
127
  if (!message.channel.isDMBased && !message.deletable) {
119
128
  return; // TODO: test if this works
@@ -129,21 +138,14 @@ export class MessageCreate extends FrameworkEvent {
129
138
  }
130
139
  }
131
140
  catch (error) {
132
- const content = await this.getErrorEmbed({
133
- name: error.name,
134
- message: error.message,
141
+ const errorHandler = ServiceContainer.getService(ErrorHandler);
142
+ errorHandler.handleError(error, {
135
143
  command: commandInstance,
136
- args: args,
137
- stack: error.stack,
138
- }, true);
139
- try {
140
- message.reply(content);
141
- }
142
- catch (e) {
143
- if (channel.type !== ChannelType.GuildStageVoice) {
144
- channel.send(content);
145
- }
146
- }
144
+ type: ErrorType.CommandRun,
145
+ });
146
+ message.reply({
147
+ content: "An error ocurred while running this command.",
148
+ });
147
149
  }
148
150
  }
149
151
  }
@@ -164,106 +166,4 @@ export class MessageCreate extends FrameworkEvent {
164
166
  }
165
167
  return bestWord;
166
168
  }
167
- getErrorEmbed(error, parse) {
168
- let parsedError;
169
- if (parse) {
170
- parsedError = this.parseError(error);
171
- }
172
- else {
173
- parsedError = error;
174
- }
175
- const embed = new EmbedBuilder()
176
- .setTitle('Error')
177
- .setDescription('An error has occured while executing this command.')
178
- .setTimestamp()
179
- .addFields([
180
- {
181
- name: 'Command:',
182
- value: error.command.name || 'Not defined',
183
- },
184
- ])
185
- .addFields([
186
- {
187
- name: 'Arguments:',
188
- value: error.args.toString() || 'None',
189
- },
190
- ])
191
- .addFields([
192
- {
193
- name: 'Error name:',
194
- value: error.name || 'Not defined',
195
- },
196
- ])
197
- .addFields([
198
- {
199
- name: 'Error message:',
200
- value: error.message || 'Not defined',
201
- },
202
- ]);
203
- if (error.possibleSolutions !== undefined) {
204
- error.possibleSolutions.forEach((solution) => {
205
- embed.addFields([
206
- {
207
- name: 'Posible solution:',
208
- value: solution,
209
- },
210
- ]);
211
- });
212
- }
213
- const stackFrames = ErrorStackParser.parse(error).filter((e) => !e.fileName.includes('node_modules') &&
214
- !e.fileName.includes('node:internal'));
215
- let stack = '';
216
- const path1 = path.resolve('./');
217
- const path2 = path1.replaceAll('\\', '/');
218
- stackFrames.forEach((frame) => {
219
- stack += `[${frame.fileName
220
- .replace(path1, '')
221
- .replace(path2, '')
222
- .replace('file://', '')}:${frame.lineNumber}](https://zumito.ga/redirect?url=vscode://file/${frame.fileName.replace('file://', '')}:${frame.lineNumber}) ${frame.functionName}()\n`;
223
- });
224
- if (error.stack !== undefined) {
225
- embed.addFields([
226
- {
227
- name: 'Call stack:',
228
- value: stack || error.stack || error.stack.toString(),
229
- },
230
- ]);
231
- }
232
- if (error.details !== undefined) {
233
- error.details.forEach((detail) => {
234
- embed.addFields([
235
- {
236
- name: 'Detail:',
237
- value: detail,
238
- },
239
- ]);
240
- });
241
- }
242
- const body = `\n\n\n---\nComand:\`\`\`${error.command.name || 'not defined'}\`\`\`\nArguments:\`\`\`${error.args.toString() || 'none'}\`\`\`\nError:\`\`\`${error.name || 'not defined'}\`\`\`\nError message:\`\`\`${error.message || 'not defined'}\`\`\`\n`;
243
- const requestUrl = `https://github.com/ZumitoTeam/Zumito/issues/new?body=${encodeURIComponent(body)}`;
244
- const row = new ActionRowBuilder().addComponents(new ButtonBuilder()
245
- .setStyle(ButtonStyle.Link)
246
- .setLabel('Report error')
247
- .setEmoji('975645505302437978')
248
- .setURL(requestUrl));
249
- return {
250
- embeds: [embed],
251
- components: [row],
252
- allowedMentions: {
253
- repliedUser: false,
254
- },
255
- };
256
- }
257
- parseError(error) {
258
- error.possibleSolutions = [];
259
- if (/(?:^|(?<= ))(EmbedBuilder|Discord|ActionRowBuilder|ButtonBuilder|MessageSelectMenu)(?:(?= )|$) is not defined/gm.test(error.message)) {
260
- error.possibleSolutions.push('const { ' +
261
- error.message.split(' ')[0] +
262
- " } = require('discord.js');");
263
- }
264
- else if (error.message.includes('A custom id and url cannot both be specified')) {
265
- error.possibleSolutions.push('Remove .setCustomId(...) or .setURL(...)');
266
- }
267
- return error;
268
- }
269
169
  }
@@ -4,6 +4,7 @@ declare class ServiceContainerManager {
4
4
  getService<T>(serviceClass: new (...args: any[]) => T | string): T;
5
5
  getServiceByName<T>(serviceName: string): T;
6
6
  addInstance(serviceClass: any, instance: any): void;
7
+ hasService(serviceClass: any): boolean;
7
8
  }
8
9
  export declare const ServiceContainer: ServiceContainerManager;
9
10
  export {};
@@ -39,6 +39,10 @@ class ServiceContainerManager {
39
39
  return;
40
40
  this.services.get(serviceName).instance = instance;
41
41
  }
42
+ hasService(serviceClass) {
43
+ const serviceName = typeof serviceClass == 'string' ? serviceClass : serviceClass.name;
44
+ return this.services.has(serviceName);
45
+ }
42
46
  }
43
47
  if (!global.ServiceContainer)
44
48
  global.ServiceContainer = new ServiceContainerManager();
@@ -9,10 +9,19 @@ declare type CommandErrorOptions = BaseErrorOptions & {
9
9
  type: ErrorType.CommandInstance | ErrorType.CommandLoad | ErrorType.CommandRun;
10
10
  command: Command;
11
11
  };
12
+ declare type ApiErrorOptions = BaseErrorOptions & {
13
+ type: ErrorType.Api;
14
+ endpoint: string;
15
+ method: string;
16
+ };
17
+ declare type OtherErrorOptions = BaseErrorOptions & {
18
+ type: ErrorType.Other;
19
+ };
20
+ declare type ErrorOptions = CommandErrorOptions | ApiErrorOptions | OtherErrorOptions;
12
21
  export declare class ErrorHandler {
13
22
  framework: ZumitoFramework;
14
23
  constructor(framework: ZumitoFramework);
15
- handleError(error: any, options: BaseErrorOptions | CommandErrorOptions): void;
24
+ handleError(error: any, options: ErrorOptions): void;
16
25
  handleCommandError(error: Error, options: CommandErrorOptions): void;
17
26
  handleShapeShiftErrors(error: any): void;
18
27
  printErrorStack(error: Error): void;
@@ -22,6 +22,13 @@ export class ErrorHandler {
22
22
  console.error(`Validation error: ${error.validator} received invalid input: ${error.given}`);
23
23
  console.line('');
24
24
  }
25
+ else if (options?.type == ErrorType.Api) {
26
+ console.group(`[❌] Error in API endpoint ${options.endpoint} (${options.method})`);
27
+ console.line(chalk.red('Error:'));
28
+ console.line(error.toString());
29
+ console.line('');
30
+ console.groupEnd();
31
+ }
25
32
  else {
26
33
  console.error(error.toString());
27
34
  console.line('');
@@ -197,13 +197,27 @@ export class InteractionHandler {
197
197
  if (!guildSettings && interaction.guildId) {
198
198
  guildSettings = await ServiceContainer.getService(GuildDataGetter).getGuildSettings(interaction.guildId);
199
199
  }
200
- commandInstance.modalSubmit({
201
- client: this.client,
202
- path,
203
- interaction,
204
- framework,
205
- guildSettings,
206
- });
200
+ const trans = this.translationManager.getShortHandMethod('command.' + commandInstance.name, guildSettings?.lang);
201
+ if (commandInstance.binds?.modalSubmit) {
202
+ commandInstance.binds?.modalSubmit({
203
+ path,
204
+ interaction,
205
+ client: this.client,
206
+ framework,
207
+ guildSettings,
208
+ trans,
209
+ });
210
+ }
211
+ else if ( // Deprecated
212
+ commandInstance.constructor.prototype.hasOwnProperty('modalSubmit')) {
213
+ commandInstance.modalSubmit({
214
+ client: this.client,
215
+ path,
216
+ interaction,
217
+ framework,
218
+ guildSettings,
219
+ });
220
+ }
207
221
  }
208
222
  this.eventManager.emitEvent('modalSubmit', 'framework', {
209
223
  client: this.client,
@@ -3,6 +3,12 @@ import { Module } from "../../definitions/Module.js";
3
3
  import { ModuleParameters } from "../../definitions/parameters/ModuleParameters.js";
4
4
  export declare class ModuleManager {
5
5
  protected modules: Map<string, Module>;
6
+ protected pendingInstancePool: Array<{
7
+ module: any;
8
+ rootPath: string;
9
+ name?: string;
10
+ options?: ModuleParameters;
11
+ }>;
6
12
  protected framework: ZumitoFramework;
7
13
  constructor(framework: ZumitoFramework);
8
14
  set(name: string, module: Module): void;
@@ -14,5 +20,24 @@ export declare class ModuleManager {
14
20
  get size(): number;
15
21
  loadModuleFile(folderPath: string): Promise<unknown>;
16
22
  registerModule(module: InstanceType<typeof Module>): void;
17
- instanceModule(module: any, rootPath: string, name?: string, options?: ModuleParameters): Promise<Module>;
23
+ instanceModule(module: any, rootPath: string, name?: string, options?: ModuleParameters): Promise<Module | {
24
+ modules: string[];
25
+ services: string[];
26
+ custom: boolean;
27
+ }>;
28
+ /**
29
+ * Comprueba si los requisitos de un módulo están satisfechos antes de instanciarlo.
30
+ * Retorna un array con los requisitos incumplidos.
31
+ * @param moduleClass Clase del módulo (no instancia)
32
+ */
33
+ checkModuleRequeriments(moduleClass: typeof Module): Promise<{
34
+ modules: string[];
35
+ services: string[];
36
+ custom: boolean;
37
+ }>;
38
+ /**
39
+ * Intenta inicializar todos los módulos pendientes en la pool, iterando hasta que no queden o no se pueda avanzar más.
40
+ * Evita bucles infinitos si ningún módulo puede ser inicializado en una iteración.
41
+ */
42
+ initializePendingModules(): Promise<void>;
18
43
  }
@@ -1,8 +1,10 @@
1
1
  import { Module } from "../../definitions/Module.js";
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
+ import { ServiceContainer } from "../ServiceContainer.js";
4
5
  export class ModuleManager {
5
6
  modules;
7
+ pendingInstancePool = [];
6
8
  framework;
7
9
  constructor(framework) {
8
10
  this.modules = new Map();
@@ -58,6 +60,15 @@ export class ModuleManager {
58
60
  */
59
61
  }
60
62
  async instanceModule(module, rootPath, name, options) {
63
+ // Comprobar requerimientos del módulo
64
+ const unmeetRequeriments = await this.checkModuleRequeriments(module);
65
+ // If there are failed requeriments, put the class into a pool for later retry
66
+ if (unmeetRequeriments.modules.length > 0 || unmeetRequeriments.services.length > 0 || unmeetRequeriments.custom === false) {
67
+ this.pendingInstancePool.push({
68
+ module, rootPath, name, options
69
+ });
70
+ return unmeetRequeriments;
71
+ }
61
72
  let moduleInstance;
62
73
  if (module.constructor) {
63
74
  try {
@@ -73,6 +84,76 @@ export class ModuleManager {
73
84
  else {
74
85
  //moduleInstance = new Module();
75
86
  }
87
+ this.registerModule(moduleInstance);
76
88
  return moduleInstance;
77
89
  }
90
+ /**
91
+ * Comprueba si los requisitos de un módulo están satisfechos antes de instanciarlo.
92
+ * Retorna un array con los requisitos incumplidos.
93
+ * @param moduleClass Clase del módulo (no instancia)
94
+ */
95
+ async checkModuleRequeriments(moduleClass) {
96
+ const requeriments = moduleClass.requeriments;
97
+ const failed = {
98
+ modules: [],
99
+ services: [],
100
+ custom: true
101
+ };
102
+ if (!requeriments) {
103
+ // Si no hay requerimientos definidos, se considera válido
104
+ return failed;
105
+ }
106
+ // Comprobar módulos requeridos
107
+ if (requeriments.modules && requeriments.modules.length > 0) {
108
+ for (const mod of requeriments.modules) {
109
+ if (!this.modules.get(mod))
110
+ failed.modules.push(mod);
111
+ }
112
+ }
113
+ // Comprobar servicios requeridos
114
+ if (requeriments.services && requeriments.services.length > 0) {
115
+ for (const service of requeriments.services) {
116
+ if (!ServiceContainer.hasService(service))
117
+ failed.services.push(service);
118
+ }
119
+ }
120
+ // Solo probar custom si los anteriores se cumplieron
121
+ if (failed.services.length === 0 && failed.modules.length == 0 && requeriments.custom && requeriments.custom.length > 0) {
122
+ for (const custom of requeriments.custom) {
123
+ if (!(await custom()))
124
+ failed.custom = false;
125
+ }
126
+ }
127
+ return failed;
128
+ }
129
+ /**
130
+ * Intenta inicializar todos los módulos pendientes en la pool, iterando hasta que no queden o no se pueda avanzar más.
131
+ * Evita bucles infinitos si ningún módulo puede ser inicializado en una iteración.
132
+ */
133
+ async initializePendingModules() {
134
+ let initializedInLastIteration;
135
+ do {
136
+ initializedInLastIteration = 0;
137
+ // Copia actual de la pool para iterar
138
+ const poolCopy = [...this.pendingInstancePool];
139
+ this.pendingInstancePool = [];
140
+ for (const pending of poolCopy) {
141
+ const result = await this.instanceModule(pending.module, pending.rootPath, pending.name, pending.options);
142
+ // Si no se pudo inicializar, vuelve a ponerlo en la pool
143
+ if (typeof result !== 'object' || result instanceof Module) {
144
+ initializedInLastIteration++;
145
+ }
146
+ else {
147
+ this.pendingInstancePool.push(pending);
148
+ }
149
+ }
150
+ } while (this.pendingInstancePool.length > 0 && initializedInLastIteration > 0);
151
+ // Si quedan módulos en la pool, no se pudieron inicializar por dependencias incumplidas
152
+ if (this.pendingInstancePool.length > 0) {
153
+ console.warn(`[📦⚠️] No se pudieron inicializar los siguientes módulos por requerimientos incumplidos:`);
154
+ for (const pending of this.pendingInstancePool) {
155
+ console.warn(`- ${pending.name || pending.module.name}`);
156
+ }
157
+ }
158
+ }
78
159
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zumito-framework",
3
- "version": "1.4.2",
3
+ "version": "1.6.0",
4
4
  "description": "Discord.js bot framework",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",