zumito-framework 1.19.0 → 1.21.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.
@@ -22,6 +22,7 @@ import { SlashCommandRefresher } from './services/SlashCommandRefresher.js';
22
22
  import { ErrorHandler } from './services/handlers/ErrorHandler.js';
23
23
  import { ErrorType } from './definitions/ErrorType.js';
24
24
  import { MongoService } from './services/MongoService.js';
25
+ import { registerDefaultExecutionRules } from './modules/core/baseModule/defaultRules.js';
25
26
  // import better-logging
26
27
  betterLogging(console);
27
28
  /**
@@ -165,6 +166,7 @@ export class ZumitoFramework {
165
166
  this.eventManager.addEventEmitter('discord', this.client);
166
167
  this.eventManager.addEventEmitter('framework', this.eventEmitter);
167
168
  await this.registerModules();
169
+ registerDefaultExecutionRules();
168
170
  await this.refreshSlashCommands();
169
171
  this.startApiServer();
170
172
  if (this.settings.statusOptions) {
@@ -0,0 +1,25 @@
1
+ import { Command } from "./commands/Command.js";
2
+ import { ZumitoFramework } from "../ZumitoFramework.js";
3
+ import { Client, Guild, GuildMember, Message, CommandInteraction, ButtonInteraction, StringSelectMenuInteraction, ModalSubmitInteraction } from "discord.js";
4
+ export type CommandExecutionType = 'prefix' | 'slash' | 'button' | 'selectMenu' | 'modal';
5
+ export interface CommandExecutionContext {
6
+ command: Command;
7
+ type: CommandExecutionType;
8
+ framework: ZumitoFramework;
9
+ client: Client;
10
+ guild?: Guild;
11
+ member?: GuildMember;
12
+ guildSettings?: any;
13
+ message?: Message;
14
+ interaction?: CommandInteraction | ButtonInteraction | StringSelectMenuInteraction | ModalSubmitInteraction;
15
+ args?: Map<string, any>;
16
+ }
17
+ export interface CommandExecutionRule {
18
+ canRun: (context: CommandExecutionContext) => boolean | Promise<boolean>;
19
+ errorMessage?: string | ((context: CommandExecutionContext) => string);
20
+ }
21
+ export interface CommandExecutionCheck {
22
+ passed: boolean;
23
+ ruleName?: string;
24
+ message?: string;
25
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,6 +1,7 @@
1
1
  import { CommandArgDefinition } from './CommandArgDefinition.js';
2
2
  import { CommandParameters, PrefixCommandParameters, SlashCommandParameters } from './CommandParameters.js';
3
3
  import { CommandBinds } from './CommandBinds.js';
4
+ import type { CommandExecutionRule } from '../CommandExecutionRule.js';
4
5
  /**
5
6
  * @name Command
6
7
  * @description Base class for all commands
@@ -201,6 +202,24 @@ export declare abstract class Command {
201
202
  * @see {@link CommandType}
202
203
  */
203
204
  type: string;
205
+ /**
206
+ * @name rules
207
+ * @description Array of {@link CommandExecutionRule} that are checked before the command is executed.
208
+ * Each rule can block execution with a custom error message.
209
+ * @type {CommandExecutionRule[]}
210
+ * @default undefined
211
+ * @example
212
+ * ```ts
213
+ * export class PremiumCommand extends Command {
214
+ * rules = [{
215
+ * canRun: (ctx) => ctx.guildSettings?.premium === true,
216
+ * errorMessage: 'This command requires a premium server.',
217
+ * }];
218
+ * }
219
+ * ```
220
+ * @see {@link CommandExecutionRule}
221
+ */
222
+ rules?: CommandExecutionRule[];
204
223
  parent?: string;
205
224
  binds?: CommandBinds;
206
225
  /**
@@ -199,6 +199,24 @@ export class Command {
199
199
  * @see {@link CommandType}
200
200
  */
201
201
  type = CommandType.any;
202
+ /**
203
+ * @name rules
204
+ * @description Array of {@link CommandExecutionRule} that are checked before the command is executed.
205
+ * Each rule can block execution with a custom error message.
206
+ * @type {CommandExecutionRule[]}
207
+ * @default undefined
208
+ * @example
209
+ * ```ts
210
+ * export class PremiumCommand extends Command {
211
+ * rules = [{
212
+ * canRun: (ctx) => ctx.guildSettings?.premium === true,
213
+ * errorMessage: 'This command requires a premium server.',
214
+ * }];
215
+ * }
216
+ * ```
217
+ * @see {@link CommandExecutionRule}
218
+ */
219
+ rules;
202
220
  parent;
203
221
  binds;
204
222
  async executePrefixCommand({ message, args, client, framework, trans, }) {
package/dist/index.d.ts CHANGED
@@ -31,8 +31,11 @@ import { CommandManager } from './services/managers/CommandManager.js';
31
31
  import { ErrorType } from './definitions/ErrorType.js';
32
32
  import { InviteUrlGenerator } from './services/utilities/InviteUrlGenerator.js';
33
33
  import { PrefixResolver } from './services/utilities/PrefixResolver.js';
34
+ import { CommandExecutionChecker } from './services/CommandExecutionChecker.js';
35
+ export type { CommandExecutionRule, CommandExecutionContext, CommandExecutionCheck, CommandExecutionType } from './definitions/CommandExecutionRule.js';
34
36
  export { ModalSubmitParameters } from './definitions/parameters/ModalSubmitParameters.js';
35
37
  export { CommandBinds } from './definitions/commands/CommandBinds.js';
36
38
  export { Injectable } from './definitions/decorators/Injectable.decorator.js';
37
39
  export { LauncherConfig } from './definitions/config/LauncherConfig.js';
38
- export { ZumitoFramework, FrameworkSettings, Command, Module, CommandParameters, CommandArguments, FrameworkEvent, Translation, TranslationManager, ApiResponse, SelectMenuParameters, CommandType, CommandArgDefinition, CommandChoiceDefinition, ButtonPressed, ButtonPressedParams, TextFormatter, EmojiFallback, DatabaseConfigLoader, PresenceDataRule, RuledPresenceData, StatusManagerOptions, discord, EventParameters, ServiceContainer, GuildDataGetter, SlashCommandRefresher, CommandParser, ErrorHandler, ErrorType, Route, RouteMethod, InteractionHandler, CommandManager, InviteUrlGenerator, PrefixResolver, };
40
+ export { ModuleParameters } from './definitions/parameters/ModuleParameters.js';
41
+ export { ZumitoFramework, FrameworkSettings, Command, Module, CommandParameters, CommandArguments, FrameworkEvent, Translation, TranslationManager, ApiResponse, SelectMenuParameters, CommandType, CommandArgDefinition, CommandChoiceDefinition, ButtonPressed, ButtonPressedParams, TextFormatter, EmojiFallback, DatabaseConfigLoader, PresenceDataRule, RuledPresenceData, StatusManagerOptions, discord, EventParameters, ServiceContainer, GuildDataGetter, SlashCommandRefresher, CommandParser, ErrorHandler, ErrorType, Route, RouteMethod, InteractionHandler, CommandManager, InviteUrlGenerator, PrefixResolver, CommandExecutionChecker, };
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ import { CommandManager } from './services/managers/CommandManager.js';
24
24
  import { ErrorType } from './definitions/ErrorType.js';
25
25
  import { InviteUrlGenerator } from './services/utilities/InviteUrlGenerator.js';
26
26
  import { PrefixResolver } from './services/utilities/PrefixResolver.js';
27
+ import { CommandExecutionChecker } from './services/CommandExecutionChecker.js';
27
28
  export { Injectable } from './definitions/decorators/Injectable.decorator.js';
28
29
  ServiceContainer.addService(TextFormatter, []);
29
30
  ServiceContainer.addService(EmojiFallback, [discord.Client.name, TranslationManager.name]);
@@ -34,5 +35,6 @@ ServiceContainer.addService(SlashCommandRefresher, [ZumitoFramework.name]);
34
35
  ServiceContainer.addService(InteractionHandler, []);
35
36
  ServiceContainer.addService(InviteUrlGenerator, []);
36
37
  ServiceContainer.addService(PrefixResolver, []);
38
+ ServiceContainer.addService(CommandExecutionChecker, [], true);
37
39
  ServiceContainer.addService(ErrorHandler, ['ZumitoFramework']);
38
- export { ZumitoFramework, Command, Module, CommandArguments, FrameworkEvent, Translation, TranslationManager, ApiResponse, CommandType, ButtonPressed, TextFormatter, EmojiFallback, DatabaseConfigLoader, discord, ServiceContainer, GuildDataGetter, SlashCommandRefresher, CommandParser, ErrorHandler, ErrorType, Route, RouteMethod, InteractionHandler, CommandManager, InviteUrlGenerator, PrefixResolver, };
40
+ export { ZumitoFramework, Command, Module, CommandArguments, FrameworkEvent, Translation, TranslationManager, ApiResponse, CommandType, ButtonPressed, TextFormatter, EmojiFallback, DatabaseConfigLoader, discord, ServiceContainer, GuildDataGetter, SlashCommandRefresher, CommandParser, ErrorHandler, ErrorType, Route, RouteMethod, InteractionHandler, CommandManager, InviteUrlGenerator, PrefixResolver, CommandExecutionChecker, };
package/dist/launcher.js CHANGED
@@ -6,16 +6,15 @@ import fs from 'fs';
6
6
  import { pathToFileURL } from 'url';
7
7
  import dotenv from 'dotenv';
8
8
  import { RecursiveObjectMerger } from './services/utilities/RecursiveObjectMerger';
9
+ import { EnvValidator } from './services/utilities/EnvValidator';
10
+ import { BotReadyLogger } from './services/utilities/BotReadyLogger';
9
11
  dotenv.config();
10
- if (!process.env.DISCORD_TOKEN) {
11
- throw new Error("Discord Token not found (DISCORD_TOKEN)");
12
- }
13
- else if (!process.env.DISCORD_CLIENT_ID) {
14
- throw new Error("Discord Client ID not found (DISCORD_CLIENT_ID)");
15
- }
16
- else if (!process.env.MONGO_URI) {
17
- throw new Error("No MongoDB connection string specified in .env file (MONGO_URI)");
18
- }
12
+ const REQUIRED_ENV_VARS = {
13
+ DISCORD_TOKEN: 'Discord Bot Token',
14
+ DISCORD_CLIENT_ID: 'Discord Client ID',
15
+ MONGO_URI: 'MongoDB connection URI',
16
+ };
17
+ EnvValidator.validate(REQUIRED_ENV_VARS);
19
18
  const defaultConfig = {
20
19
  discordClientOptions: {
21
20
  intents: 3276799,
@@ -35,16 +34,7 @@ import(pathToFileURL(configFilePath).href)
35
34
  .then(({ config: userConfig }) => {
36
35
  const config = RecursiveObjectMerger.merge(defaultConfig, userConfig);
37
36
  new ZumitoFramework(config, (bot) => {
38
- // Log number of commands loaded
39
- console.log(`Loaded ${bot.commands.size} commands`);
40
- // Log number of events loaded
41
- console.log(`Loaded ${bot.events.size} events`);
42
- // Log number of modules loaded
43
- console.log(`Loaded ${bot.modules.size} modules`);
44
- // Log number of translations loaded
45
- console.log(`Loaded ${bot.translations.getAll().size} translations`);
46
- // Log number of routes registered
47
- console.log(`Loaded ${bot.routes.length} routes`);
37
+ BotReadyLogger.log(bot);
48
38
  userConfig.callbacks?.load?.(bot);
49
39
  });
50
40
  })
@@ -0,0 +1 @@
1
+ export declare function registerDefaultExecutionRules(): void;
@@ -0,0 +1,82 @@
1
+ import { PermissionsBitField, } from 'discord.js';
2
+ import { CommandExecutionChecker, } from '../../../services/CommandExecutionChecker.js';
3
+ import { ServiceContainer } from '../../../services/ServiceContainer.js';
4
+ import { MemberPermissionChecker } from '../../../services/utilities/MemberPermissionChecker.js';
5
+ async function isAdminOrOwner(ctx) {
6
+ if (!ctx.member || !ctx.guild)
7
+ return false;
8
+ if (ctx.member.id === ctx.guild.ownerId)
9
+ return true;
10
+ const permChecker = ServiceContainer.getService(MemberPermissionChecker);
11
+ return permChecker.hasPermissionOnChannel(ctx.member, ctx.guild.channels?.cache?.first?.() || {}, PermissionsBitField.Flags.Administrator);
12
+ }
13
+ async function checkUserPermissions(ctx) {
14
+ if (!ctx.member || !ctx.guild)
15
+ return false;
16
+ const permChecker = ServiceContainer.getService(MemberPermissionChecker);
17
+ const channel = (ctx.message?.channel ||
18
+ ctx.interaction?.channel);
19
+ for (const permission of ctx.command.userPermissions) {
20
+ if (!(await permChecker.hasPermissionOnChannel(ctx.member, channel, permission))) {
21
+ return false;
22
+ }
23
+ }
24
+ return true;
25
+ }
26
+ export function registerDefaultExecutionRules() {
27
+ const checker = ServiceContainer.getService(CommandExecutionChecker);
28
+ checker.addRule('no-bots', {
29
+ canRun: (ctx) => {
30
+ return !ctx.member?.user?.bot;
31
+ },
32
+ errorMessage: 'Bots cannot use commands.',
33
+ });
34
+ checker.addRule('dm-disabled', {
35
+ canRun: (ctx) => {
36
+ if (ctx.command.dm)
37
+ return true;
38
+ if (ctx.guild === null || ctx.guild === undefined)
39
+ return false;
40
+ return true;
41
+ },
42
+ errorMessage: 'This command cannot be used in DMs.',
43
+ });
44
+ checker.addRule('admin-only', {
45
+ canRun: async (ctx) => {
46
+ if (!ctx.command.adminOnly)
47
+ return true;
48
+ if (ctx.type === 'button' || ctx.type === 'selectMenu' || ctx.type === 'modal')
49
+ return true;
50
+ return isAdminOrOwner(ctx);
51
+ },
52
+ errorMessage: 'This command is restricted to administrators.',
53
+ });
54
+ checker.addRule('user-permissions', {
55
+ canRun: async (ctx) => {
56
+ if (!ctx.command.userPermissions || ctx.command.userPermissions.length === 0)
57
+ return true;
58
+ if (ctx.type === 'button' || ctx.type === 'selectMenu' || ctx.type === 'modal')
59
+ return true;
60
+ if (await isAdminOrOwner(ctx))
61
+ return true;
62
+ return checkUserPermissions(ctx);
63
+ },
64
+ errorMessage: (ctx) => `You do not have permission to use \`${ctx.command.name}\`.`,
65
+ });
66
+ checker.addRule('nsfw-only', {
67
+ canRun: async (ctx) => {
68
+ if (!ctx.command.nsfw)
69
+ return true;
70
+ if (ctx.type === 'button' || ctx.type === 'selectMenu' || ctx.type === 'modal')
71
+ return true;
72
+ if (await isAdminOrOwner(ctx))
73
+ return true;
74
+ const channel = (ctx.message?.channel ||
75
+ ctx.interaction?.channel);
76
+ if (!channel || !('nsfw' in channel))
77
+ return true;
78
+ return channel.nsfw === true;
79
+ },
80
+ errorMessage: 'This command can only be used in NSFW channels.',
81
+ });
82
+ }
@@ -1,4 +1,3 @@
1
- import { PermissionsBitField, } from 'discord.js';
2
1
  import { FrameworkEvent } from '../../../../../definitions/FrameworkEvent.js';
3
2
  import leven from 'leven';
4
3
  import { ServiceContainer } from '../../../../../services/ServiceContainer.js';
@@ -8,6 +7,7 @@ import { ZumitoFramework } from '../../../../../ZumitoFramework.js';
8
7
  import { GuildDataGetter } from '../../../../../services/utilities/GuildDataGetter.js';
9
8
  import { ErrorHandler } from '../../../../../services/handlers/ErrorHandler.js';
10
9
  import { ErrorType } from '../../../../../definitions/ErrorType.js';
10
+ import { CommandExecutionChecker } from '../../../../../services/CommandExecutionChecker.js';
11
11
  export class MessageCreate extends FrameworkEvent {
12
12
  once = false;
13
13
  source = 'discord';
@@ -46,47 +46,6 @@ export class MessageCreate extends FrameworkEvent {
46
46
  commandInstance = Array.from(this.framework.commands.getAll().values()).find((c) => c.name == args.at(0) && c.parent && c.parent == command) || commandInstance;
47
47
  if (!commandInstance)
48
48
  return;
49
- if (message.guild == null && commandInstance.dm == false)
50
- return;
51
- if (commandInstance.adminOnly ||
52
- commandInstance.userPermissions.length > 0) {
53
- let denied = false;
54
- if (this.memberPermissionChecker.hasPermissionOnChannel(message.member, message.channel, PermissionsBitField.Flags.Administrator) ||
55
- message.member.id != message.guild.ownerId) {
56
- if (commandInstance.userPermissions.length > 0) {
57
- commandInstance.userPermissions.forEach((permission) => {
58
- if (!this.memberPermissionChecker.hasPermissionOnChannel(message.member, message.channel, permission)) {
59
- denied = true;
60
- }
61
- });
62
- }
63
- }
64
- if (denied) {
65
- return message.reply({
66
- content: 'You do not have permission to use this command.',
67
- allowedMentions: {
68
- repliedUser: false,
69
- },
70
- });
71
- }
72
- }
73
- if (message.channel.isTextBased) {
74
- const channel = message.channel;
75
- // Check command is nsfw and if channel is allowed
76
- if (commandInstance.nsfw &&
77
- !channel.nsfw &&
78
- !channel
79
- .permissionsFor(message.member)
80
- .has(PermissionsBitField.Flags.Administrator) &&
81
- message.member.id != message.guild.ownerId) {
82
- return message.reply({
83
- content: 'This command is nsfw and this channel is not nsfw.',
84
- allowedMentions: {
85
- repliedUser: false,
86
- },
87
- });
88
- }
89
- }
90
49
  try {
91
50
  const guildSettings = await this.guildDataGetter.getGuildSettings(message.guildId);
92
51
  const parsedArgsResponse = await CommandParser.parseFromSplitedString(args, commandInstance.args, message.guild);
@@ -99,6 +58,24 @@ export class MessageCreate extends FrameworkEvent {
99
58
  });
100
59
  }
101
60
  const parsedArgs = parsedArgsResponse.parsedArgs;
61
+ const executionChecker = ServiceContainer.getService(CommandExecutionChecker);
62
+ const check = await executionChecker.check({
63
+ command: commandInstance,
64
+ type: 'prefix',
65
+ framework: this.framework,
66
+ client: this.framework.client,
67
+ guild: message.guild,
68
+ member: message.member,
69
+ guildSettings,
70
+ message,
71
+ args: parsedArgs,
72
+ });
73
+ if (!check.passed) {
74
+ return message.reply({
75
+ content: check.message || 'You cannot run this command.',
76
+ allowedMentions: { repliedUser: false },
77
+ });
78
+ }
102
79
  await commandInstance.execute({
103
80
  message,
104
81
  args: parsedArgs,
@@ -0,0 +1,10 @@
1
+ import { CommandExecutionContext, CommandExecutionRule, CommandExecutionCheck } from "../definitions/CommandExecutionRule.js";
2
+ export declare class CommandExecutionChecker {
3
+ private rules;
4
+ addRule(name: string, rule: CommandExecutionRule): void;
5
+ getRule(name: string): CommandExecutionRule | undefined;
6
+ removeRule(name: string): boolean;
7
+ getAllRules(): Map<string, CommandExecutionRule>;
8
+ check(context: CommandExecutionContext): Promise<CommandExecutionCheck>;
9
+ private evaluateRules;
10
+ }
@@ -0,0 +1,53 @@
1
+ export class CommandExecutionChecker {
2
+ rules = new Map();
3
+ addRule(name, rule) {
4
+ this.rules.set(name, rule);
5
+ }
6
+ getRule(name) {
7
+ return this.rules.get(name);
8
+ }
9
+ removeRule(name) {
10
+ return this.rules.delete(name);
11
+ }
12
+ getAllRules() {
13
+ return this.rules;
14
+ }
15
+ async check(context) {
16
+ const globalCheck = await this.evaluateRules('global', this.rules, context);
17
+ if (!globalCheck.passed)
18
+ return globalCheck;
19
+ const commandRules = context.command.rules;
20
+ if (commandRules && commandRules.length > 0) {
21
+ const commandRulesMap = new Map(commandRules.map((rule, i) => [`${context.command.name}:${i}`, rule]));
22
+ const commandCheck = await this.evaluateRules('command', commandRulesMap, context);
23
+ if (!commandCheck.passed)
24
+ return commandCheck;
25
+ }
26
+ return { passed: true };
27
+ }
28
+ async evaluateRules(scope, rules, context) {
29
+ for (const [name, rule] of rules) {
30
+ try {
31
+ const allowed = await rule.canRun(context);
32
+ if (!allowed) {
33
+ const message = typeof rule.errorMessage === 'function'
34
+ ? rule.errorMessage(context)
35
+ : rule.errorMessage;
36
+ return {
37
+ passed: false,
38
+ ruleName: `${scope}:${name}`,
39
+ message,
40
+ };
41
+ }
42
+ }
43
+ catch (err) {
44
+ return {
45
+ passed: false,
46
+ ruleName: `${scope}:${name}`,
47
+ message: `Rule "${name}" threw an error: ${err instanceof Error ? err.message : String(err)}`,
48
+ };
49
+ }
50
+ }
51
+ return { passed: true };
52
+ }
53
+ }
@@ -8,6 +8,7 @@ import { TranslationManager } from "../managers/TranslationManager";
8
8
  import { EventManager } from "../managers/EventManager";
9
9
  import { ErrorHandler } from "./ErrorHandler";
10
10
  import { ErrorType } from "../../definitions/ErrorType";
11
+ import { CommandExecutionChecker } from "../CommandExecutionChecker";
11
12
  import 'reflect-metadata';
12
13
  export class InteractionHandler {
13
14
  commandManager;
@@ -91,6 +92,25 @@ export class InteractionHandler {
91
92
  if (!guildSettings && interaction.guildId) {
92
93
  guildSettings = await ServiceContainer.getService(GuildDataGetter).getGuildSettings(interaction.guildId);
93
94
  }
95
+ const executionChecker = ServiceContainer.getService(CommandExecutionChecker);
96
+ const check = await executionChecker.check({
97
+ command: commandInstance,
98
+ type: 'slash',
99
+ framework,
100
+ client: this.client,
101
+ guild: interaction.guild,
102
+ member: interaction.member,
103
+ guildSettings,
104
+ interaction,
105
+ args,
106
+ });
107
+ if (!check.passed) {
108
+ await interaction.reply({
109
+ content: check.message || 'You cannot run this command.',
110
+ ephemeral: true,
111
+ });
112
+ return;
113
+ }
94
114
  const trans = this.translationManager.getShortHandMethod('command.' + commandInstance.name, guildSettings?.lang);
95
115
  if (commandInstance.type === CommandType.separated ||
96
116
  commandInstance.type === CommandType.slash) {
@@ -136,6 +156,24 @@ export class InteractionHandler {
136
156
  if (!guildSettings && interaction.guildId) {
137
157
  guildSettings = await ServiceContainer.getService(GuildDataGetter).getGuildSettings(interaction.guildId);
138
158
  }
159
+ const executionChecker = ServiceContainer.getService(CommandExecutionChecker);
160
+ const check = await executionChecker.check({
161
+ command: commandInstance,
162
+ type: 'button',
163
+ framework,
164
+ client: this.client,
165
+ guild: interaction.guild,
166
+ member: interaction.member,
167
+ guildSettings,
168
+ interaction,
169
+ });
170
+ if (!check.passed) {
171
+ await interaction.reply({
172
+ content: check.message || 'You cannot use this button.',
173
+ ephemeral: true,
174
+ });
175
+ return;
176
+ }
139
177
  // If the command has impements ButtonPress class then execute the method
140
178
  if (commandInstance.constructor.prototype.hasOwnProperty('buttonPressed')) {
141
179
  commandInstance.buttonPressed({
@@ -155,8 +193,26 @@ export class InteractionHandler {
155
193
  if (!guildSettings && interaction.guildId) {
156
194
  guildSettings = await ServiceContainer.getService(GuildDataGetter).getGuildSettings(interaction.guildId);
157
195
  }
158
- const trans = this.translationManager.getShortHandMethod('command.' + commandInstance.name, guildSettings?.lang);
159
196
  const framework = ServiceContainer.getService(ZumitoFramework);
197
+ const executionChecker = ServiceContainer.getService(CommandExecutionChecker);
198
+ const check = await executionChecker.check({
199
+ command: commandInstance,
200
+ type: 'selectMenu',
201
+ framework,
202
+ client: this.client,
203
+ guild: interaction.guild,
204
+ member: interaction.member,
205
+ guildSettings,
206
+ interaction,
207
+ });
208
+ if (!check.passed) {
209
+ await interaction.reply({
210
+ content: check.message || 'You cannot use this menu.',
211
+ ephemeral: true,
212
+ });
213
+ return;
214
+ }
215
+ const trans = this.translationManager.getShortHandMethod('command.' + commandInstance.name, guildSettings?.lang);
160
216
  if (commandInstance.binds?.selectMenu) {
161
217
  commandInstance.binds?.selectMenu({
162
218
  path,
@@ -189,6 +245,24 @@ export class InteractionHandler {
189
245
  guildSettings = await ServiceContainer.getService(GuildDataGetter).getGuildSettings(interaction.guildId);
190
246
  }
191
247
  const framework = ServiceContainer.getService(ZumitoFramework);
248
+ const executionChecker = ServiceContainer.getService(CommandExecutionChecker);
249
+ const check = await executionChecker.check({
250
+ command: commandInstance,
251
+ type: 'modal',
252
+ framework,
253
+ client: this.client,
254
+ guild: interaction.guild,
255
+ member: interaction.member,
256
+ guildSettings,
257
+ interaction,
258
+ });
259
+ if (!check.passed) {
260
+ await interaction.reply({
261
+ content: check.message || 'You cannot submit this form.',
262
+ ephemeral: true,
263
+ });
264
+ return;
265
+ }
192
266
  if (commandInstance.binds?.modalSubmit) {
193
267
  const trans = this.translationManager.getShortHandMethod('command.' + commandInstance.name, guildSettings?.lang);
194
268
  commandInstance.binds.modalSubmit({
@@ -0,0 +1,7 @@
1
+ import { ZumitoFramework } from '../../ZumitoFramework';
2
+ export declare class BotReadyLogger {
3
+ /**
4
+ * Log standard bot startup statistics (commands, events, modules, etc).
5
+ */
6
+ static log(bot: ZumitoFramework): void;
7
+ }
@@ -0,0 +1,12 @@
1
+ export class BotReadyLogger {
2
+ /**
3
+ * Log standard bot startup statistics (commands, events, modules, etc).
4
+ */
5
+ static log(bot) {
6
+ console.log(`Loaded ${bot.commands.size} commands`);
7
+ console.log(`Loaded ${bot.events.size} events`);
8
+ console.log(`Loaded ${bot.modules.size} modules`);
9
+ console.log(`Loaded ${bot.translations.getAll().size} translations`);
10
+ console.log(`Loaded ${bot.routes.length} routes`);
11
+ }
12
+ }
@@ -0,0 +1,9 @@
1
+ export type EnvVarDefinitions = Record<string, string>;
2
+ export declare class EnvValidator {
3
+ /**
4
+ * Validate that all required env vars are present. If any are missing,
5
+ * renders a visual error report and exits the process.
6
+ * @param required - Map of { KEY: 'Human label' }
7
+ */
8
+ static validate(required: EnvVarDefinitions): void;
9
+ }
@@ -0,0 +1,50 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import chalk from 'chalk';
4
+ export class EnvValidator {
5
+ /**
6
+ * Validate that all required env vars are present. If any are missing,
7
+ * renders a visual error report and exits the process.
8
+ * @param required - Map of { KEY: 'Human label' }
9
+ */
10
+ static validate(required) {
11
+ const missing = [];
12
+ const present = [];
13
+ for (const [key] of Object.entries(required)) {
14
+ if (process.env[key]) {
15
+ present.push(key);
16
+ }
17
+ else {
18
+ missing.push(key);
19
+ }
20
+ }
21
+ if (missing.length === 0)
22
+ return;
23
+ const divider = chalk.yellow('━'.repeat(54));
24
+ const envPath = path.join(process.cwd(), '.env');
25
+ const hasEnvFile = fs.existsSync(envPath);
26
+ console.error(`\n${divider}`);
27
+ console.error(` ${chalk.bold.red('CONFIGURATION ERROR')} — Missing environment variables`);
28
+ console.error(divider);
29
+ for (const key of present) {
30
+ const label = required[key];
31
+ const value = process.env[key];
32
+ const display = value.length > 28 ? value.slice(0, 25) + '...' : value;
33
+ console.error(` ${chalk.green('✓')} ${chalk.green(key.padEnd(20))} ${chalk.dim(display)}`);
34
+ }
35
+ for (const key of missing) {
36
+ const label = required[key];
37
+ console.error(` ${chalk.red('✗')} ${chalk.red(key.padEnd(20))} ${chalk.bold(label)}`);
38
+ }
39
+ console.error(divider);
40
+ if (!hasEnvFile) {
41
+ console.error(` ${chalk.yellow('No .env file found in project root.')}`);
42
+ console.error(` ${chalk.dim('Create one from .env.example and fill in the missing values.')}`);
43
+ }
44
+ else {
45
+ console.error(` ${chalk.dim('.env file found but missing required variables.')}`);
46
+ }
47
+ console.error(divider + '\n');
48
+ process.exit(1);
49
+ }
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zumito-framework",
3
- "version": "1.19.0",
3
+ "version": "1.21.0",
4
4
  "description": "Discord.js bot framework",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",