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.
- package/dist/ZumitoFramework.js +2 -0
- package/dist/definitions/CommandExecutionRule.d.ts +25 -0
- package/dist/definitions/CommandExecutionRule.js +1 -0
- package/dist/definitions/commands/Command.d.ts +19 -0
- package/dist/definitions/commands/Command.js +18 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/launcher.js +9 -19
- package/dist/modules/core/baseModule/defaultRules.d.ts +1 -0
- package/dist/modules/core/baseModule/defaultRules.js +82 -0
- package/dist/modules/core/baseModule/events/discord/MessageCreate.js +19 -42
- package/dist/services/CommandExecutionChecker.d.ts +10 -0
- package/dist/services/CommandExecutionChecker.js +53 -0
- package/dist/services/handlers/InteractionHandler.js +75 -1
- package/dist/services/utilities/BotReadyLogger.d.ts +7 -0
- package/dist/services/utilities/BotReadyLogger.js +12 -0
- package/dist/services/utilities/EnvValidator.d.ts +9 -0
- package/dist/services/utilities/EnvValidator.js +50 -0
- package/package.json +1 -1
package/dist/ZumitoFramework.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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,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
|
+
}
|