meocord 3.0.0 → 3.1.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.
Files changed (34) hide show
  1. package/AUTHOR.md +2 -3
  2. package/README.md +224 -41
  3. package/dist/cjs/_shared/controller.decorator-MUHA_A3z.cjs +529 -0
  4. package/dist/cjs/core/index.cjs +246 -86
  5. package/dist/cjs/decorator/index.cjs +3 -1
  6. package/dist/cjs/enum/index.cjs +17 -4
  7. package/dist/cjs/testing/index.cjs +88 -4
  8. package/dist/esm/bin/builder-template/builder/primary-entry-point.builder.template +26 -0
  9. package/dist/esm/bin/builder-template/controller/autocomplete.controller.template +16 -0
  10. package/dist/esm/bin/builder-template/controller/channel-select-menu.controller.template +12 -0
  11. package/dist/esm/bin/builder-template/controller/context-menu.controller.template +2 -2
  12. package/dist/esm/bin/builder-template/controller/mentionable-select-menu.controller.template +12 -0
  13. package/dist/esm/bin/builder-template/controller/modal-submit.controller.template +1 -1
  14. package/dist/esm/bin/builder-template/controller/primary-entry-point.controller.template +12 -0
  15. package/dist/esm/bin/builder-template/controller/role-select-menu.controller.template +12 -0
  16. package/dist/esm/bin/builder-template/controller/slash.controller.template +1 -1
  17. package/dist/esm/bin/builder-template/controller/user-select-menu.controller.template +12 -0
  18. package/dist/esm/bin/generator.js +4 -9
  19. package/dist/esm/bin/helper/controller-generator.helper.js +24 -7
  20. package/dist/esm/core/meocord.app.js +247 -86
  21. package/dist/esm/decorator/controller.decorator.js +73 -10
  22. package/dist/esm/decorator/index.js +1 -1
  23. package/dist/esm/enum/controller.enum.js +23 -4
  24. package/dist/esm/testing/mock-interaction.js +89 -5
  25. package/dist/esm/util/interaction.util.js +174 -0
  26. package/dist/types/controller.enum-DYfhYaat.d.ts +36 -0
  27. package/dist/types/core/index.d.ts +73 -0
  28. package/dist/types/decorator/index.d.ts +60 -48
  29. package/dist/types/enum/index.d.ts +1 -1
  30. package/dist/types/interface/index.d.ts +87 -3
  31. package/dist/types/testing/index.d.ts +3 -21
  32. package/package.json +2 -2
  33. package/dist/cjs/_shared/controller.decorator-CC6BjHkS.cjs +0 -288
  34. package/dist/types/controller.enum-QA-IuReF.d.ts +0 -18
@@ -1,6 +1,6 @@
1
+ import { BuildableCommandType, CommandBuilderBase, CommandBuilderConstructor, CommandInteractionType, ReactionHandlerOptions, AutocompleteMetadata, CommandMetadata, GuardInterface } from '../interface/index.js';
2
+ import { AutocompleteInteraction, OmitPartialGroupDMChannel, Message, MessageReaction, PartialMessageReaction, ClientOptions, ActivityOptions } from 'discord.js';
1
3
  import { CommandType } from '../enum/index.js';
2
- import { SlashCommandBuilder, SlashCommandSubcommandsOnlyBuilder, ContextMenuCommandBuilder, ButtonInteraction, StringSelectMenuInteraction, ChatInputCommandInteraction, UserContextMenuCommandInteraction, MessageContextMenuCommandInteraction, ModalSubmitInteraction, OmitPartialGroupDMChannel, Message, MessageReaction, PartialMessageReaction, ClientOptions, ActivityOptions } from 'discord.js';
3
- import { ReactionHandlerOptions, GuardInterface } from '../interface/index.js';
4
4
  import { ServiceIdentifier } from 'inversify';
5
5
  import 'webpack';
6
6
 
@@ -29,44 +29,6 @@ import 'webpack';
29
29
  */
30
30
  declare function Service<T>(): (target: new (...args: any[]) => T) => void;
31
31
 
32
- /**
33
- * MeoCord Framework
34
- * Copyright (c) 2025 Ukasyah Rahmatullah Zada
35
- * SPDX-License-Identifier: MIT
36
- */
37
-
38
- /**
39
- * Base interface for a command builder.
40
- */
41
- interface CommandBuilderBase<T extends CommandType.SLASH | CommandType.CONTEXT_MENU = CommandType.SLASH | CommandType.CONTEXT_MENU> {
42
- /**
43
- * Builds the command structure using the specified command name.
44
- *
45
- * @param commandName - The name of the command.
46
- * @returns A SlashCommandBuilder or ContextMenuCommandBuilder instance.
47
- */
48
- build: (commandName: string) => T extends CommandType.SLASH ? SlashCommandBuilder | SlashCommandSubcommandsOnlyBuilder : T extends CommandType.CONTEXT_MENU ? ContextMenuCommandBuilder : never;
49
- }
50
- type CommandBuilderConstructor<T extends CommandType.SLASH | CommandType.CONTEXT_MENU> = new () => CommandBuilderBase<T>;
51
- /**
52
- * Command metadata describing a registered command method.
53
- */
54
- interface CommandMetadata<T extends string = string> {
55
- methodName: string;
56
- builder: ReturnType<CommandBuilderBase['build']> | undefined;
57
- type: CommandType;
58
- regex?: RegExp;
59
- dynamicParams?: T[];
60
- /**
61
- * How specific this pattern is; higher wins when more than one route matches the
62
- * same customId. A greedy parameter can overlap a more literal sibling —
63
- * `gi-profile-{uuid:*}-{uid}` also matches an id meant for
64
- * `gi-profile-summary-{ownerId}-{uid}` — so dispatch cannot rely on declaration order.
65
- */
66
- specificity?: number;
67
- }
68
- type CommandInteractionType<CBC extends CommandType.SLASH | CommandType.CONTEXT_MENU, T extends CommandBuilderConstructor<CBC> | CommandType> = T extends CommandType.BUTTON ? ButtonInteraction : T extends CommandType.SELECT_MENU ? StringSelectMenuInteraction : T extends CommandBuilderConstructor<CommandType.SLASH> ? ChatInputCommandInteraction : T extends CommandBuilderConstructor<CommandType.CONTEXT_MENU> ? UserContextMenuCommandInteraction | MessageContextMenuCommandInteraction : T extends CommandType.MODAL_SUBMIT ? ModalSubmitInteraction : never;
69
-
70
32
  /**
71
33
  * MeoCord Framework
72
34
  * Copyright (c) 2025 Ukasyah Rahmatullah Zada
@@ -91,7 +53,7 @@ type CommandInteractionType<CBC extends CommandType.SLASH | CommandType.CONTEXT_
91
53
  * @returns A decorator function that makes the target class injectable
92
54
  * and assigns the `commandType` metadata.
93
55
  */
94
- declare function CommandBuilder<T extends CommandType.SLASH | CommandType.CONTEXT_MENU>(commandType: T): (target: new () => CommandBuilderBase<T>) => void;
56
+ declare function CommandBuilder<T extends BuildableCommandType>(commandType: T): (target: new () => CommandBuilderBase<T>) => void;
95
57
 
96
58
  /**
97
59
  * MeoCord Framework
@@ -162,8 +124,13 @@ declare const PARAM_SEPARATOR = "/";
162
124
  /**
163
125
  * Decorator to register command methods in a controller.
164
126
  *
165
- * @param commandName - The name or pattern of the command.
166
- * @param builderOrType - A command builder class or a command type from `CommandType`.
127
+ * @param commandName - What the command is addressed by. Commands registered with
128
+ * Discord use their name, and a subcommand its full path — `settings notify email`,
129
+ * parts separated by a space, the way Discord displays it. Components use a customId
130
+ * pattern, where `{name}` captures one `/`-separated segment.
131
+ * @param builderOrType - A command builder class, or a `CommandType` for a handler that
132
+ * registers nothing of its own: every component, and every subcommand of a command
133
+ * whose builder already describes it.
167
134
  *
168
135
  * @example
169
136
  * ```typescript
@@ -172,13 +139,23 @@ declare const PARAM_SEPARATOR = "/";
172
139
  * await interaction.reply('This is the help command!')
173
140
  * }
174
141
  *
175
- * @Command('stats-{id}', CommandType.BUTTON)
176
- * public async handleStats(message: ButtonInteraction, { id }) {
177
- * await message.reply(`Fetching stats for ID: ${id}`);
142
+ * @Command('settings notify email', CommandType.SLASH)
143
+ * public async handleNotifyEmail(interaction: ChatInputCommandInteraction, { enabled }) {
144
+ * await interaction.reply(`Email notifications ${enabled ? 'on' : 'off'}`)
145
+ * }
146
+ *
147
+ * @Command('stats/{id}', CommandType.BUTTON)
148
+ * public async handleStats(interaction: ButtonInteraction, { id }) {
149
+ * await interaction.reply(`Fetching stats for ID: ${id}`);
150
+ * }
151
+ *
152
+ * @Command('assign/{taskId}', CommandType.USER_SELECT_MENU)
153
+ * public async handleAssign(interaction: UserSelectMenuInteraction, { taskId }) {
154
+ * await interaction.reply(`Assigned ${interaction.users.size} user(s) to ${taskId}`)
178
155
  * }
179
156
  * ```
180
157
  */
181
- declare function Command<CBC extends CommandType.SLASH | CommandType.CONTEXT_MENU, T extends CommandBuilderConstructor<CBC> | CommandType>(commandName: string, builderOrType: T): <P extends Record<string, any>, R extends Promise<void> | void>(target: object, propertyKey: string, _descriptor: TypedPropertyDescriptor<(interaction: CommandInteractionType<CBC, T>, params: P) => R> | TypedPropertyDescriptor<(interaction: CommandInteractionType<CBC, T>) => R>) => void;
158
+ declare function Command<CBC extends BuildableCommandType, T extends CommandBuilderConstructor<CBC> | CommandType>(commandName: string, builderOrType: T): <P extends Record<string, any>, R extends Promise<void> | void>(target: object, propertyKey: string, _descriptor: TypedPropertyDescriptor<(interaction: CommandInteractionType<CBC, T>, params: P) => R> | TypedPropertyDescriptor<(interaction: CommandInteractionType<CBC, T>) => R>) => void;
182
159
  /**
183
160
  * Retrieves the command map for a given controller.
184
161
  *
@@ -186,6 +163,41 @@ declare function Command<CBC extends CommandType.SLASH | CommandType.CONTEXT_MEN
186
163
  * @returns A record containing command metadata indexed by command names.
187
164
  */
188
165
  declare function getCommandMap<T extends string>(controller: any): Record<string, CommandMetadata<T>[]>;
166
+ /**
167
+ * Decorator to register an autocomplete handler for a chat input command's option.
168
+ *
169
+ * Autocomplete is a separate interaction from the command it belongs to, and Discord
170
+ * sends it while the user is still typing. It is not a `@Command`: nothing is
171
+ * registered for it — the option's own `setAutocomplete(true)` is what turns it on —
172
+ * and it is answered with `interaction.respond()` rather than a reply. Leaving it
173
+ * unhandled is not silent to the user: the client shows a loading state until the
174
+ * three-second window closes.
175
+ *
176
+ * @param commandPath - The command to complete, e.g. `settings` or `settings notify email`
177
+ * for a subcommand. Parts are separated by a single space, as Discord displays them.
178
+ * @param optionName - The option to complete. Omit to handle every option of the command,
179
+ * branching on `interaction.options.getFocused(true)`.
180
+ *
181
+ * @example
182
+ * ```typescript
183
+ * @Autocomplete('search', 'query')
184
+ * async completeQuery(interaction: AutocompleteInteraction) {
185
+ * const { value } = interaction.options.getFocused(true)
186
+ * await interaction.respond(this.search(value).map(name => ({ name, value: name })))
187
+ * }
188
+ * ```
189
+ */
190
+ declare function Autocomplete<R extends void | Promise<void>>(commandPath: string, optionName?: string): <P extends Record<string, any>>(target: object, propertyKey: string, _descriptor: TypedPropertyDescriptor<(interaction: AutocompleteInteraction, params: P) => R> | TypedPropertyDescriptor<(interaction: AutocompleteInteraction) => R>) => void;
191
+ /**
192
+ * Retrieves autocomplete handler metadata from a given controller.
193
+ *
194
+ * Handlers naming an option come first, so a command-wide handler acts as the fallback
195
+ * for options no specific handler claimed rather than shadowing them by declaration order.
196
+ *
197
+ * @param controller - The controller class instance.
198
+ * @returns The registered autocomplete handlers, most specific first.
199
+ */
200
+ declare function getAutocompleteHandlers(controller: any): AutocompleteMetadata[];
189
201
  /**
190
202
  * Decorator to mark a class as a controller that can later be registered to the App class `(app.ts)` using the `@MeoCord` decorator.
191
203
  *
@@ -355,4 +367,4 @@ declare function MeoCord(options: {
355
367
  services?: ServiceIdentifier[];
356
368
  }): (target: any) => void;
357
369
 
358
- export { Command, CommandBuilder, Controller, Guard, MeoCord, MessageHandler, PARAM_SEPARATOR, ReactionHandler, Service, UseGuard, findAmbiguousRoutes, getCommandMap, getMessageHandlers, getReactionHandlers };
370
+ export { Autocomplete, Command, CommandBuilder, Controller, Guard, MeoCord, MessageHandler, PARAM_SEPARATOR, ReactionHandler, Service, UseGuard, findAmbiguousRoutes, getAutocompleteHandlers, getCommandMap, getMessageHandlers, getReactionHandlers };
@@ -1,4 +1,4 @@
1
- export { C as CommandType, R as ReactionHandlerAction } from '../controller.enum-QA-IuReF.js';
1
+ export { C as CommandType, R as ReactionHandlerAction } from '../controller.enum-DYfhYaat.js';
2
2
 
3
3
  /**
4
4
  * MeoCord Framework
@@ -1,6 +1,90 @@
1
- import { User, PartialUser, BaseInteraction, Message, MessageReaction } from 'discord.js';
1
+ import { SlashCommandBuilder, SlashCommandSubcommandsOnlyBuilder, ContextMenuCommandBuilder, RESTPostAPIPrimaryEntryPointApplicationCommandJSONBody, ChatInputCommandInteraction, UserContextMenuCommandInteraction, MessageContextMenuCommandInteraction, PrimaryEntryPointCommandInteraction, ButtonInteraction, StringSelectMenuInteraction, UserSelectMenuInteraction, RoleSelectMenuInteraction, MentionableSelectMenuInteraction, ChannelSelectMenuInteraction, ModalSubmitInteraction, User, PartialUser, BaseInteraction, Message, MessageReaction } from 'discord.js';
2
2
  import { Configuration } from 'webpack';
3
- import { ReactionHandlerAction } from '../enum/index.js';
3
+ import { CommandType, ReactionHandlerAction } from '../enum/index.js';
4
+
5
+ /**
6
+ * MeoCord Framework
7
+ * Copyright (c) 2025 Ukasyah Rahmatullah Zada
8
+ * SPDX-License-Identifier: MIT
9
+ */
10
+
11
+ /**
12
+ * The command types that are registered with Discord, and so need a builder.
13
+ *
14
+ * Components are addressed by a customId the application makes up, so they have
15
+ * nothing to register; these three are published through `application.commands.set`.
16
+ */
17
+ type BuildableCommandType = CommandType.SLASH | CommandType.CONTEXT_MENU | CommandType.PRIMARY_ENTRY_POINT;
18
+ /**
19
+ * The body an entry point command is registered with.
20
+ *
21
+ * `description` is added back on top of the discord-api-types body: that type omits it
22
+ * from every non-chat-input command, but the API accepts one for entry point commands
23
+ * — Discord's own default activity command ships with it set.
24
+ */
25
+ type PrimaryEntryPointCommandData = RESTPostAPIPrimaryEntryPointApplicationCommandJSONBody & {
26
+ description?: string;
27
+ };
28
+ /**
29
+ * The payload `build()` returns for a given command type.
30
+ *
31
+ * `PRIMARY_ENTRY_POINT` yields a raw REST body rather than a builder because
32
+ * `@discordjs/builders` ships none for it; `ApplicationCommandManager#set` accepts the
33
+ * JSON body directly, so nothing is lost.
34
+ */
35
+ type CommandBuildResult<T extends BuildableCommandType> = T extends CommandType.SLASH ? SlashCommandBuilder | SlashCommandSubcommandsOnlyBuilder : T extends CommandType.CONTEXT_MENU ? ContextMenuCommandBuilder : T extends CommandType.PRIMARY_ENTRY_POINT ? PrimaryEntryPointCommandData : never;
36
+ /**
37
+ * Base interface for a command builder.
38
+ */
39
+ interface CommandBuilderBase<T extends BuildableCommandType = BuildableCommandType> {
40
+ /**
41
+ * Builds the command structure using the specified command name.
42
+ *
43
+ * @param commandName - The name of the command.
44
+ * @returns A builder instance, or a raw command body for entry point commands.
45
+ */
46
+ build: (commandName: string) => CommandBuildResult<T>;
47
+ }
48
+ type CommandBuilderConstructor<T extends BuildableCommandType> = new () => CommandBuilderBase<T>;
49
+ /**
50
+ * Command metadata describing a registered command method.
51
+ */
52
+ interface CommandMetadata<T extends string = string> {
53
+ methodName: string;
54
+ builder: ReturnType<CommandBuilderBase['build']> | undefined;
55
+ type: CommandType;
56
+ regex?: RegExp;
57
+ dynamicParams?: T[];
58
+ /**
59
+ * How specific this pattern is; higher wins when more than one route matches the
60
+ * same customId. A greedy parameter can overlap a more literal sibling —
61
+ * `gi-profile-{uuid:*}-{uid}` also matches an id meant for
62
+ * `gi-profile-summary-{ownerId}-{uid}` — so dispatch cannot rely on declaration order.
63
+ */
64
+ specificity?: number;
65
+ }
66
+ /** Metadata describing one `@Autocomplete` handler. */
67
+ interface AutocompleteMetadata {
68
+ /** The command path the handler serves, e.g. `settings` or `settings notify email`. */
69
+ commandPath: string;
70
+ /** The option it completes, or `undefined` to complete every option of that command. */
71
+ optionName?: string;
72
+ methodName: string;
73
+ }
74
+ /** The interaction class each non-buildable command type hands to its handler. */
75
+ interface ComponentInteractionMap {
76
+ [CommandType.BUTTON]: ButtonInteraction;
77
+ [CommandType.SELECT_MENU]: StringSelectMenuInteraction;
78
+ [CommandType.USER_SELECT_MENU]: UserSelectMenuInteraction;
79
+ [CommandType.ROLE_SELECT_MENU]: RoleSelectMenuInteraction;
80
+ [CommandType.MENTIONABLE_SELECT_MENU]: MentionableSelectMenuInteraction;
81
+ [CommandType.CHANNEL_SELECT_MENU]: ChannelSelectMenuInteraction;
82
+ [CommandType.MODAL_SUBMIT]: ModalSubmitInteraction;
83
+ [CommandType.SLASH]: ChatInputCommandInteraction;
84
+ [CommandType.CONTEXT_MENU]: UserContextMenuCommandInteraction | MessageContextMenuCommandInteraction;
85
+ [CommandType.PRIMARY_ENTRY_POINT]: PrimaryEntryPointCommandInteraction;
86
+ }
87
+ type CommandInteractionType<CBC extends BuildableCommandType, T extends CommandBuilderConstructor<CBC> | CommandType> = T extends CommandBuilderConstructor<CommandType.SLASH> ? ChatInputCommandInteraction : T extends CommandBuilderConstructor<CommandType.CONTEXT_MENU> ? UserContextMenuCommandInteraction | MessageContextMenuCommandInteraction : T extends CommandBuilderConstructor<CommandType.PRIMARY_ENTRY_POINT> ? PrimaryEntryPointCommandInteraction : T extends keyof ComponentInteractionMap ? ComponentInteractionMap[T] : never;
4
88
 
5
89
  /**
6
90
  * MeoCord Framework
@@ -92,4 +176,4 @@ interface MeoCordConfig {
92
176
  webpack?: (config: MeoCordWebpackConfig) => MeoCordWebpackConfig | undefined;
93
177
  }
94
178
 
95
- export type { GuardInterface, MeoCordConfig, MeoCordWebpackConfig, ReactionHandlerOptions };
179
+ export type { AutocompleteMetadata, BuildableCommandType, CommandBuildResult, CommandBuilderBase, CommandBuilderConstructor, CommandInteractionType, CommandMetadata, GuardInterface, MeoCordConfig, MeoCordWebpackConfig, ReactionHandlerOptions };
@@ -2,7 +2,7 @@ import { ServiceIdentifier, Container } from 'inversify';
2
2
  import { GuardInterface } from '../interface/index.js';
3
3
  import { CacheType, CommandInteractionOptionResolver, Channel, Client, Guild, Message, User } from 'discord.js';
4
4
  import 'webpack';
5
- import '../controller.enum-QA-IuReF.js';
5
+ import '../controller.enum-DYfhYaat.js';
6
6
 
7
7
  /**
8
8
  * MeoCord Framework
@@ -277,30 +277,12 @@ declare function createMockMessage(): DeepMocked<Message>;
277
277
  interface ChatInputOptions {
278
278
  subcommandGroup?: string | null;
279
279
  subcommand?: string | null;
280
+ /** The option the user is currently typing, for autocomplete interactions. */
281
+ focused?: string | null;
280
282
  [name: string]: string | number | boolean | {
281
283
  id: string;
282
284
  } | null | undefined;
283
285
  }
284
- /**
285
- * Builds a typed options resolver from a plain record. Mirrors how the real
286
- * `CommandInteractionOptionResolver` works: declare what options the command
287
- * was invoked with, and the resolver finds them by name.
288
- *
289
- * All explicit methods are mock functions — override per test with `.mockReturnValue()`.
290
- * Methods not listed (e.g. `getAttachment`) are auto-stubbed by the Proxy.
291
- *
292
- * @example
293
- * ```ts
294
- * const interaction = createMockInteraction(ChatInputCommandInteraction)
295
- * interaction.options = createChatInputOptions({
296
- * subcommandGroup: 'daily',
297
- * subcommand: 'notes',
298
- * uid: 12345678,
299
- * })
300
- * interaction.options.getSubcommand() // → 'notes'
301
- * interaction.options.getNumber('uid') // → 12345678
302
- * ```
303
- */
304
286
  declare function createChatInputOptions<Cached extends CacheType = any>(opts?: ChatInputOptions): DeepMocked<CommandInteractionOptionResolver<Cached>>;
305
287
 
306
288
  export { MeoCordTestingModule, TestingModule, TestingModuleBuilder, createChatInputOptions, createMock, createMockChannel, createMockClient, createMockFn, createMockGuild, createMockInteraction, createMockMessage, createMockUser, isMockFunction };
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "meocord",
3
3
  "description": "Decorator-based Discord bot framework built on discord.js. Brings NestJS-style controllers, dependency injection, guards, and testing utilities to bot development — with a full CLI and TypeScript-first design.",
4
- "version": "3.0.0",
4
+ "version": "3.1.0",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": ">=22"
8
8
  },
9
9
  "scripts": {
10
- "lint": "eslint --fix . && tsc --noEmit && tsc --noEmit --project tsconfig.test.json",
10
+ "lint": "prettier --write \"**/*.md\" && eslint --fix . && tsc --noEmit && tsc --noEmit --project tsconfig.test.json",
11
11
  "build": "rm -rf ./dist && rollup -c",
12
12
  "prepare": "husky",
13
13
  "test": "vitest run",
@@ -1,288 +0,0 @@
1
- 'use strict';
2
-
3
- require('reflect-metadata');
4
- var inversify = require('inversify');
5
- var discord_js = require('discord.js');
6
- var enum_index = require('../enum/index.cjs');
7
- var metadataKey_enum = require('./metadata-key.enum-BzzvGUId.cjs');
8
-
9
- const COMMAND_METADATA_KEY = Symbol('commands');
10
- const MESSAGE_HANDLER_METADATA_KEY = Symbol('message_handlers');
11
- const REACTION_HANDLER_METADATA_KEY = Symbol('reaction_handlers');
12
- /**
13
- * Decorator to register message handlers in the controller.
14
- *
15
- * @param keyword - An optional keyword to filter messages this handler should respond to.
16
- *
17
- * @example
18
- * ```typescript
19
- * @MessageHandler('hello')
20
- * async handleHelloMessage(message: Message) {
21
- * await message.reply('Hello! How can I help you?');
22
- * }
23
- *
24
- * @MessageHandler()
25
- * async handleAnyMessage(message: Message) {
26
- * console.log(`Received a message: ${message.content}`);
27
- * }
28
- * ```
29
- */ function MessageHandler(keyword) {
30
- return function(target, propertyKey, _descriptor) {
31
- const handlers = Reflect.getMetadata(MESSAGE_HANDLER_METADATA_KEY, target) || [];
32
- handlers.push({
33
- keyword,
34
- method: propertyKey.toString()
35
- });
36
- Reflect.defineMetadata(MESSAGE_HANDLER_METADATA_KEY, handlers, target);
37
- };
38
- }
39
- /**
40
- * Decorator to register reaction handlers in the controller.
41
- *
42
- * @param emoji - Optional emoji name to filter reactions this handler should respond to.
43
- *
44
- * @example
45
- * ```typescript
46
- * @ReactionHandler('👍')
47
- * async handleThumbsUpReaction(reaction: MessageReaction, { user }: ReactionHandlerOptions) {
48
- * console.log(`User ${user.username} reacted with 👍`);
49
- * }
50
- *
51
- * @ReactionHandler()
52
- * async handleAnyReaction(reaction: MessageReaction, { user }: ReactionHandlerOptions) {
53
- * console.log(`User ${user.username} reacted with ${reaction.emoji.name}`);
54
- * }
55
- * ```
56
- */ function ReactionHandler(emoji) {
57
- return function(target, propertyKey, _descriptor) {
58
- const handlers = Reflect.getMetadata(REACTION_HANDLER_METADATA_KEY, target) || [];
59
- handlers.push({
60
- emoji,
61
- method: propertyKey.toString()
62
- });
63
- Reflect.defineMetadata(REACTION_HANDLER_METADATA_KEY, handlers, target);
64
- };
65
- }
66
- /**
67
- * Retrieves reaction handlers metadata from a given controller.
68
- *
69
- * @param controller - The controller class instance.
70
- * @returns An array of reaction handler metadata objects.
71
- */ function getReactionHandlers(controller) {
72
- return Reflect.getMetadata(REACTION_HANDLER_METADATA_KEY, controller) || [];
73
- }
74
- /**
75
- * Retrieves message handlers metadata from a given controller.
76
- *
77
- * @param controller - The controller class instance.
78
- * @returns An array of message handler method names.
79
- */ function getMessageHandlers(controller) {
80
- return Reflect.getMetadata(MESSAGE_HANDLER_METADATA_KEY, controller) || [];
81
- }
82
- const PLACEHOLDER_PATTERN = /\{(\w+)}/g;
83
- /** The character a parameter will not cross, so one pattern segment maps to one value. */ const PARAM_SEPARATOR = '/';
84
- /** Escapes a literal stretch of a pattern so only placeholders stay meaningful. */ const escapeLiteral = (literal)=>literal.replace(/[/\\^$*+?.()|[\]{}]/g, '\\$&');
85
- /**
86
- * Helper function to create regex and parameter mappings from a pattern string.
87
- *
88
- * A `{name}` matches anything up to the next `/`, the same rule Express and Rails use
89
- * for a path segment. That is what lets a value the application does not control — a
90
- * uuid, an opaque vendor id, a slug — be captured whole without the author annotating
91
- * anything, since a hyphen inside it is data rather than structure.
92
- *
93
- * It also keeps neighbouring patterns apart: `profile/{uuid}` and `profile/{uuid}/{id}`
94
- * cannot both match one id, because a parameter cannot swallow the separator between
95
- * them. Patterns separated by `-` instead have no such boundary, so a pair like
96
- * `profile-{uuid}` and `profile-{uuid}-{id}` is ambiguous — {@link findAmbiguousRoutes}
97
- * reports those at registration.
98
- *
99
- * @param pattern - The pattern string to parse.
100
- * @returns The regex, the parameter names, and how specific the pattern is.
101
- */ function createRegexFromPattern(pattern) {
102
- const params = [];
103
- let regexPattern = '';
104
- let cursor = 0;
105
- let literalLength = 0;
106
- PLACEHOLDER_PATTERN.lastIndex = 0;
107
- let match;
108
- while((match = PLACEHOLDER_PATTERN.exec(pattern)) !== null){
109
- const [placeholder, param] = match;
110
- const literal = pattern.slice(cursor, match.index);
111
- const after = pattern[match.index + placeholder.length];
112
- // A parameter has to own its segment. Sharing one with a literal leaves no
113
- // boundary a sibling pattern can be told apart by, and the resulting overlap has
114
- // no correct reading -- `profile-{uuid}` and `profile-{uuid}-{id}` both take
115
- // `profile-a-b`. Registration is the last point where that is still fixable.
116
- if (literal !== '' && !literal.endsWith(PARAM_SEPARATOR) || after !== undefined && after !== PARAM_SEPARATOR) {
117
- throw new Error(`Invalid pattern "${pattern}": {${param}} must occupy a whole segment, so it has to be ` + `preceded and followed by "${PARAM_SEPARATOR}" or by the ends of the pattern. ` + `Write "a${PARAM_SEPARATOR}{${param}}" rather than "a-{${param}}".`);
118
- }
119
- literalLength += literal.length;
120
- regexPattern += escapeLiteral(literal);
121
- regexPattern += `(?<${param}>[^${PARAM_SEPARATOR}]+)`;
122
- params.push(param);
123
- cursor = match.index + placeholder.length;
124
- }
125
- const trailing = pattern.slice(cursor);
126
- literalLength += trailing.length;
127
- regexPattern += escapeLiteral(trailing);
128
- const regex = new RegExp(`^${regexPattern}$`);
129
- // Literal text is the signal: a pattern spelling out more of the id describes it
130
- // more exactly than one leaving it to a parameter. Fewer parameters breaks a tie
131
- // between equal-length patterns, so the ranking is total and never falls back to
132
- // declaration order.
133
- const specificity = literalLength * 1000 - params.length;
134
- return {
135
- regex,
136
- params,
137
- specificity
138
- };
139
- }
140
- /**
141
- * Decorator to register command methods in a controller.
142
- *
143
- * @param commandName - The name or pattern of the command.
144
- * @param builderOrType - A command builder class or a command type from `CommandType`.
145
- *
146
- * @example
147
- * ```typescript
148
- * @Command('help', CommandType.SLASH)
149
- * public async handleHelp(interaction: ChatInputCommandInteraction) {
150
- * await interaction.reply('This is the help command!')
151
- * }
152
- *
153
- * @Command('stats-{id}', CommandType.BUTTON)
154
- * public async handleStats(message: ButtonInteraction, { id }) {
155
- * await message.reply(`Fetching stats for ID: ${id}`);
156
- * }
157
- * ```
158
- */ function Command(commandName, builderOrType) {
159
- return function(target, propertyKey, _descriptor) {
160
- const originalMethod = _descriptor.value;
161
- if (!originalMethod) {
162
- throw new Error(`Missing implementation for method ${propertyKey}`);
163
- }
164
- // Wrap original method for interaction type validation
165
- _descriptor.value = function(interaction, params) {
166
- const expectedInteraction = commandType === enum_index.CommandType.BUTTON && interaction instanceof discord_js.ButtonInteraction || commandType === enum_index.CommandType.SELECT_MENU && interaction instanceof discord_js.StringSelectMenuInteraction || commandType === enum_index.CommandType.SLASH && interaction instanceof discord_js.ChatInputCommandInteraction || commandType === enum_index.CommandType.CONTEXT_MENU && interaction instanceof discord_js.ContextMenuCommandInteraction || commandType === enum_index.CommandType.MODAL_SUBMIT && interaction instanceof discord_js.ModalSubmitInteraction;
167
- if (!expectedInteraction) {
168
- throw new Error(`Invalid interaction type passed to @Command for method: ${propertyKey}`);
169
- }
170
- return originalMethod.apply(this, [
171
- interaction,
172
- params
173
- ]);
174
- };
175
- // Retrieve existing metadata or initialize it
176
- const commands = Reflect.getMetadata(COMMAND_METADATA_KEY, target) || {};
177
- let builderInstance;
178
- let commandType;
179
- let regex;
180
- let dynamicParams = [];
181
- let specificity;
182
- // Determine command type and builder
183
- if (typeof builderOrType === 'function') {
184
- const builderObj = new builderOrType();
185
- builderInstance = builderObj.build(commandName);
186
- commandType = Reflect.getMetadata(metadataKey_enum.MetadataKey.CommandType, builderOrType);
187
- if (!(commandType in enum_index.CommandType)) {
188
- throw new Error(`Metadata for 'commandType' is missing on builder ${builderOrType.name}`);
189
- }
190
- } else {
191
- commandType = builderOrType;
192
- }
193
- if (commandType !== enum_index.CommandType.SLASH && commandType !== enum_index.CommandType.CONTEXT_MENU) {
194
- const { regex: generatedRegex, params, specificity: patternSpecificity } = createRegexFromPattern(commandName);
195
- regex = generatedRegex;
196
- dynamicParams = params;
197
- specificity = patternSpecificity;
198
- }
199
- // Ensure commandName supports multiple entries
200
- if (!commands[commandName]) {
201
- commands[commandName] = [];
202
- }
203
- commands[commandName].push({
204
- methodName: propertyKey,
205
- builder: builderInstance,
206
- type: commandType,
207
- regex,
208
- dynamicParams,
209
- specificity
210
- });
211
- Reflect.defineMetadata(COMMAND_METADATA_KEY, commands, target);
212
- };
213
- }
214
- /**
215
- * Retrieves the command map for a given controller.
216
- *
217
- * @param controller - The controller class instance.
218
- * @returns A record containing command metadata indexed by command names.
219
- */ function getCommandMap(controller) {
220
- return Reflect.getMetadata(COMMAND_METADATA_KEY, controller);
221
- }
222
- /**
223
- * Decorator to mark a class as a controller that can later be registered to the App class `(app.ts)` using the `@MeoCord` decorator.
224
- *
225
- * @example
226
- * ```typescript
227
- * @Controller()
228
- * export class PingSlashController {
229
- * constructor(private pingService: PingService) {}
230
- *
231
- * @Command('ping', PingCommandBuilder)
232
- * async ping(interaction: ChatInputCommandInteraction) {
233
- * const response = await this.pingService.handlePing()
234
- * await interaction.reply(response)
235
- * }
236
- * }
237
- * ```
238
- */ function Controller() {
239
- return function(target) {
240
- if (!Reflect.hasMetadata(metadataKey_enum.MetadataKey.Injectable, target)) {
241
- inversify.injectable()(target);
242
- }
243
- };
244
- }
245
- /**
246
- * Finds pattern pairs that can both match one customId.
247
- *
248
- * Patterns of different segment counts are disjoint, because a parameter cannot cross
249
- * `/`. Within the same count, two patterns overlap unless some position holds literals
250
- * that differ: `a/{x}/c` and `a/b/{y}` both take `a/b/c`, and neither is more literal
251
- * than the other, so ranking cannot settle it either.
252
- *
253
- * @param patterns - The registered patterns.
254
- * @returns Each ambiguous pair, once, in the order the patterns were given.
255
- */ function findAmbiguousRoutes(patterns) {
256
- const isParam = (segment)=>PLACEHOLDER_PATTERN.test(segment);
257
- const segmentsOf = (pattern)=>pattern.split(PARAM_SEPARATOR);
258
- const collisions = [];
259
- for(let i = 0; i < patterns.length; i++){
260
- for(let j = i + 1; j < patterns.length; j++){
261
- const left = segmentsOf(patterns[i]);
262
- const right = segmentsOf(patterns[j]);
263
- if (left.length !== right.length) continue;
264
- const disjoint = left.some((segment, index)=>{
265
- PLACEHOLDER_PATTERN.lastIndex = 0;
266
- const leftIsParam = isParam(segment);
267
- PLACEHOLDER_PATTERN.lastIndex = 0;
268
- const rightIsParam = isParam(right[index]);
269
- return !leftIsParam && !rightIsParam && segment !== right[index];
270
- });
271
- if (!disjoint) collisions.push([
272
- patterns[i],
273
- patterns[j]
274
- ]);
275
- }
276
- }
277
- return collisions;
278
- }
279
-
280
- exports.Command = Command;
281
- exports.Controller = Controller;
282
- exports.MessageHandler = MessageHandler;
283
- exports.PARAM_SEPARATOR = PARAM_SEPARATOR;
284
- exports.ReactionHandler = ReactionHandler;
285
- exports.findAmbiguousRoutes = findAmbiguousRoutes;
286
- exports.getCommandMap = getCommandMap;
287
- exports.getMessageHandlers = getMessageHandlers;
288
- exports.getReactionHandlers = getReactionHandlers;
@@ -1,18 +0,0 @@
1
- declare enum CommandType {
2
- SLASH = "SLASH",
3
- BUTTON = "BUTTON",
4
- CONTEXT_MENU = "CONTEXT_MENU",
5
- SELECT_MENU = "SELECT_MENU",
6
- MODAL_SUBMIT = "MODAL_SUBMIT"
7
- }
8
- /**
9
- * Enum representing actions that can be performed on a message reaction.
10
- */
11
- declare enum ReactionHandlerAction {
12
- /** Reaction added to a message. */
13
- ADD = "ADD",
14
- /** Reaction removed from a message. */
15
- REMOVE = "REMOVE"
16
- }
17
-
18
- export { CommandType as C, ReactionHandlerAction as R };