@julanzw/ttoolbox-discordjs-framework 1.2.0 → 1.4.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/classes/Command.class.d.ts +93 -0
- package/dist/classes/Command.class.js +18 -1
- package/dist/classes/SubcommandGroup.class.d.ts +15 -14
- package/dist/classes/SubcommandGroup.class.js +18 -33
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/types/autocomplete.d.ts +25 -0
- package/dist/types/autocomplete.js +1 -0
- package/dist/utils/AutocompleteManager.class.d.ts +140 -0
- package/dist/utils/AutocompleteManager.class.js +200 -0
- package/package.json +1 -1
|
@@ -173,4 +173,97 @@ export declare abstract class Command {
|
|
|
173
173
|
* Set the error reporter for this command
|
|
174
174
|
*/
|
|
175
175
|
setErrorReporter(reporter: ErrorReporter): void;
|
|
176
|
+
/**
|
|
177
|
+
* Lifecycle hook called before command execution.
|
|
178
|
+
*
|
|
179
|
+
* Can be used for:
|
|
180
|
+
* - Custom validation
|
|
181
|
+
* - Logging/analytics
|
|
182
|
+
* - Loading user preferences
|
|
183
|
+
* - Rate limiting checks
|
|
184
|
+
* - And more...
|
|
185
|
+
*
|
|
186
|
+
* If this hook throws an error or returns false, execution is stopped.
|
|
187
|
+
*
|
|
188
|
+
* @param interaction - The command interaction
|
|
189
|
+
* @returns true to continue execution, false to stop (or throw an error)
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```typescript
|
|
193
|
+
* protected async beforeExecute(interaction: ChatInputCommandInteraction): Promise<boolean> {
|
|
194
|
+
* // Log command usage
|
|
195
|
+
* console.log(`${interaction.user.tag} used ${this.name}`);
|
|
196
|
+
*
|
|
197
|
+
* // Check custom rate limit
|
|
198
|
+
* if (await this.isRateLimited(interaction.user.id)) {
|
|
199
|
+
* await interaction.reply('You are rate limited!');
|
|
200
|
+
* return false; // Stop execution
|
|
201
|
+
* }
|
|
202
|
+
*
|
|
203
|
+
* return true; // Continue
|
|
204
|
+
* }
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
protected beforeExecute?(interaction: ChatInputCommandInteraction, client: Client): Promise<boolean | void>;
|
|
208
|
+
/**
|
|
209
|
+
* Lifecycle hook called after successful command execution.
|
|
210
|
+
*
|
|
211
|
+
* Can be used for:
|
|
212
|
+
* - Analytics tracking
|
|
213
|
+
* - Cleanup tasks
|
|
214
|
+
* - Success logging
|
|
215
|
+
* - Updating usage statistics
|
|
216
|
+
* - And more...
|
|
217
|
+
*
|
|
218
|
+
* Note: This is **NOT** called if the command throws an error (use onError for that).
|
|
219
|
+
*
|
|
220
|
+
* @param interaction - The command interaction
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* ```typescript
|
|
224
|
+
* protected async afterExecute(interaction: ChatInputCommandInteraction): Promise<void> {
|
|
225
|
+
* // Track command usage
|
|
226
|
+
* await analytics.track('command_used', {
|
|
227
|
+
* command: this.name,
|
|
228
|
+
* user: interaction.user.id,
|
|
229
|
+
* });
|
|
230
|
+
*
|
|
231
|
+
* // Update user stats
|
|
232
|
+
* await incrementCommandCount(interaction.user.id);
|
|
233
|
+
* }
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
protected afterExecute?(interaction: ChatInputCommandInteraction, client: Client): Promise<void>;
|
|
237
|
+
/**
|
|
238
|
+
* Lifecycle hook called when command execution fails.
|
|
239
|
+
*
|
|
240
|
+
* Can be used for:
|
|
241
|
+
* - Custom error handling
|
|
242
|
+
* - Error reporting to external services
|
|
243
|
+
* - User-friendly error messages
|
|
244
|
+
* - Cleanup after errors
|
|
245
|
+
* - And more...
|
|
246
|
+
*
|
|
247
|
+
* The error is rethrown after this hook.
|
|
248
|
+
*
|
|
249
|
+
* @param interaction - The command interaction
|
|
250
|
+
* @param error - The error that occurred
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* ```typescript
|
|
254
|
+
* protected async onError(interaction: ChatInputCommandInteraction, error: Error): Promise<void> {
|
|
255
|
+
* // Send to Sentry
|
|
256
|
+
* Sentry.captureException(error, {
|
|
257
|
+
* tags: { command: this.name },
|
|
258
|
+
* user: { id: interaction.user.id },
|
|
259
|
+
* });
|
|
260
|
+
*
|
|
261
|
+
* // Custom error message
|
|
262
|
+
* if (error.message.includes('DATABASE')) {
|
|
263
|
+
* await interaction.reply('Database is temporarily unavailable. Try again later.');
|
|
264
|
+
* }
|
|
265
|
+
* }
|
|
266
|
+
* ```
|
|
267
|
+
*/
|
|
268
|
+
protected onError?(interaction: ChatInputCommandInteraction, error: Error, client: Client): Promise<void>;
|
|
176
269
|
}
|
|
@@ -116,7 +116,24 @@ export class Command {
|
|
|
116
116
|
const error = this.validate(interaction);
|
|
117
117
|
if (error)
|
|
118
118
|
return await safeReply(interaction, error, true);
|
|
119
|
-
|
|
119
|
+
if (this.beforeExecute) {
|
|
120
|
+
const shouldContinue = await this.beforeExecute(interaction, client);
|
|
121
|
+
if (shouldContinue === false) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
await this.run(interaction, client);
|
|
127
|
+
if (this.afterExecute) {
|
|
128
|
+
await this.afterExecute(interaction, client);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
if (this.onError) {
|
|
133
|
+
await this.onError(interaction, err, client);
|
|
134
|
+
}
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
120
137
|
});
|
|
121
138
|
}
|
|
122
139
|
/**
|
|
@@ -35,20 +35,6 @@ export declare abstract class SubcommandGroup {
|
|
|
35
35
|
protected logger?: ILogger;
|
|
36
36
|
/** ErrorReporter instance to use inside the subcommand group */
|
|
37
37
|
private errorReporter?;
|
|
38
|
-
/**
|
|
39
|
-
* Safely executes a function with error handling and logging.
|
|
40
|
-
*
|
|
41
|
-
* Wraps the execution in a try-catch block, logs successful executions,
|
|
42
|
-
* and automatically handles errors by logging them and sending a user-friendly
|
|
43
|
-
* error message.
|
|
44
|
-
*
|
|
45
|
-
* @param subcommandName - The name of the subcommand
|
|
46
|
-
* @param scope - The logging scope for this execution
|
|
47
|
-
* @param interaction - The command interaction
|
|
48
|
-
* @param fn - The function to execute
|
|
49
|
-
* @private
|
|
50
|
-
*/
|
|
51
|
-
private safeExecute;
|
|
52
38
|
/**
|
|
53
39
|
* Executes the appropriate subcommand based on the user's interaction.
|
|
54
40
|
*
|
|
@@ -131,4 +117,19 @@ export declare abstract class SubcommandGroup {
|
|
|
131
117
|
* Set the error reporter for this subcommand group.
|
|
132
118
|
*/
|
|
133
119
|
setErrorReporter(reporter: ErrorReporter): this;
|
|
120
|
+
/**
|
|
121
|
+
* Lifecycle hook called before any subcommand in this group executes.
|
|
122
|
+
*
|
|
123
|
+
* @param interaction - The command interaction
|
|
124
|
+
* @returns true to continue, false to stop execution
|
|
125
|
+
*/
|
|
126
|
+
protected beforeExecute?(interaction: ChatInputCommandInteraction, client: Client): Promise<boolean | void>;
|
|
127
|
+
/**
|
|
128
|
+
* Lifecycle hook called after any subcommand in this group executes successfully.
|
|
129
|
+
*/
|
|
130
|
+
protected afterExecute?(interaction: ChatInputCommandInteraction, client: Client): Promise<void>;
|
|
131
|
+
/**
|
|
132
|
+
* Lifecycle hook called when any subcommand in this group fails.
|
|
133
|
+
*/
|
|
134
|
+
protected onError?(interaction: ChatInputCommandInteraction, error: Error, client: Client): Promise<void>;
|
|
134
135
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { SlashCommandBuilder, } from 'discord.js';
|
|
2
|
-
import { safeReply } from '../utils/editAndReply.js';
|
|
3
2
|
/**
|
|
4
3
|
* Abstract base class for Discord slash command groups with subcommands.
|
|
5
4
|
*
|
|
@@ -23,36 +22,6 @@ import { safeReply } from '../utils/editAndReply.js';
|
|
|
23
22
|
* ```
|
|
24
23
|
*/
|
|
25
24
|
export class SubcommandGroup {
|
|
26
|
-
/**
|
|
27
|
-
* Safely executes a function with error handling and logging.
|
|
28
|
-
*
|
|
29
|
-
* Wraps the execution in a try-catch block, logs successful executions,
|
|
30
|
-
* and automatically handles errors by logging them and sending a user-friendly
|
|
31
|
-
* error message.
|
|
32
|
-
*
|
|
33
|
-
* @param subcommandName - The name of the subcommand
|
|
34
|
-
* @param scope - The logging scope for this execution
|
|
35
|
-
* @param interaction - The command interaction
|
|
36
|
-
* @param fn - The function to execute
|
|
37
|
-
* @private
|
|
38
|
-
*/
|
|
39
|
-
async safeExecute(subcommandName, scope, interaction, fn) {
|
|
40
|
-
try {
|
|
41
|
-
await fn();
|
|
42
|
-
this.logger?.log(`${this.name} (${subcommandName}) command executed`, 'info', scope);
|
|
43
|
-
}
|
|
44
|
-
catch (err) {
|
|
45
|
-
this.logger?.log('An Error occured' + err, 'error', scope, true);
|
|
46
|
-
await this.errorReporter?.reportError(err, `Command: ${subcommandName} in SubcommandGroup: ${this.name}`, {
|
|
47
|
-
user: interaction.user.tag,
|
|
48
|
-
userId: interaction.user.id,
|
|
49
|
-
guild: interaction.guild?.name,
|
|
50
|
-
guildId: interaction.guildId,
|
|
51
|
-
channel: interaction.channel?.id,
|
|
52
|
-
});
|
|
53
|
-
return await safeReply(interaction, 'An unexpected error occurred.');
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
25
|
/**
|
|
57
26
|
* Executes the appropriate subcommand based on the user's interaction.
|
|
58
27
|
*
|
|
@@ -79,8 +48,24 @@ export class SubcommandGroup {
|
|
|
79
48
|
if (!subcommand) {
|
|
80
49
|
throw new Error(`Unknown subcommand: ${subcommandName}`);
|
|
81
50
|
}
|
|
82
|
-
|
|
83
|
-
|
|
51
|
+
if (this.beforeExecute) {
|
|
52
|
+
const shouldContinue = await this.beforeExecute(interaction, client);
|
|
53
|
+
if (shouldContinue === false) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
await subcommand.execute(interaction, client);
|
|
59
|
+
if (this.afterExecute) {
|
|
60
|
+
await this.afterExecute(interaction, client);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (this.onError) {
|
|
65
|
+
await this.onError(interaction, err, client);
|
|
66
|
+
}
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
84
69
|
}
|
|
85
70
|
/**
|
|
86
71
|
* Converts the command group to Discord API JSON format for registration.
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export type { Modal, ModalField } from './types/modal.js';
|
|
|
9
9
|
export type { ButtonType } from './types/button.js';
|
|
10
10
|
export type { ILogger } from './types/logger.js';
|
|
11
11
|
export type { AnySelectMenuInteraction, ButtonHandler, SelectMenuHandler, ComponentConfig } from './types/component.js';
|
|
12
|
+
export type { AutocompleteHandler } from './types/autocomplete.js';
|
|
12
13
|
export { getPermissionsForLevel } from './utils/permissions.js';
|
|
13
14
|
export { embedBuilder, createButton, createButtonsRow, createPaginationButtons, } from './utils/embeds.js';
|
|
14
15
|
export { stringOption, integerOption, booleanOption, userOption, channelOption, roleOption, } from './utils/slashCommandOptions.js';
|
|
@@ -18,4 +19,5 @@ export { TIMES_MILISECONDS } from './utils/miliseconds.js';
|
|
|
18
19
|
export { TToolboxLogger } from './utils/TToolboxLogger.class.js';
|
|
19
20
|
export { ErrorReporter } from './utils/ErrorReporter.js';
|
|
20
21
|
export { ComponentManager } from './utils/ComponentManager.class.js';
|
|
22
|
+
export { AutocompleteManager } from './utils/AutocompleteManager.class.js';
|
|
21
23
|
export { InteractionError } from './classes/InteractionError.class.js';
|
package/dist/index.js
CHANGED
|
@@ -15,5 +15,6 @@ export { TIMES_MILISECONDS } from './utils/miliseconds.js';
|
|
|
15
15
|
export { TToolboxLogger } from './utils/TToolboxLogger.class.js';
|
|
16
16
|
export { ErrorReporter } from './utils/ErrorReporter.js';
|
|
17
17
|
export { ComponentManager } from './utils/ComponentManager.class.js';
|
|
18
|
+
export { AutocompleteManager } from './utils/AutocompleteManager.class.js';
|
|
18
19
|
// Errors
|
|
19
20
|
export { InteractionError } from './classes/InteractionError.class.js';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { AutocompleteInteraction } from "discord.js";
|
|
2
|
+
/**
|
|
3
|
+
* Handler function for autocomplete interactions.
|
|
4
|
+
*
|
|
5
|
+
* @param interaction - The autocomplete interaction
|
|
6
|
+
* @param focusedValue - The current value the user is typing
|
|
7
|
+
* @returns Array of choices to display (max 25)
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* async (interaction, focusedValue) => {
|
|
12
|
+
* const feeds = await getFeeds(interaction.user.id);
|
|
13
|
+
* return feeds
|
|
14
|
+
* .filter(f => f.name.toLowerCase().includes(focusedValue.toLowerCase()))
|
|
15
|
+
* .map(f => ({ name: f.name, value: f.name }));
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export type AutocompleteHandler = (interaction: AutocompleteInteraction, focusedValue: string) => Promise<Array<{
|
|
20
|
+
name: string;
|
|
21
|
+
value: string;
|
|
22
|
+
}>> | Array<{
|
|
23
|
+
name: string;
|
|
24
|
+
value: string;
|
|
25
|
+
}>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { AutocompleteInteraction } from 'discord.js';
|
|
2
|
+
import type { ILogger } from '../types/logger.js';
|
|
3
|
+
import { AutocompleteHandler } from '../types/autocomplete.js';
|
|
4
|
+
/**
|
|
5
|
+
* Manages autocomplete interactions for slash commands.
|
|
6
|
+
*
|
|
7
|
+
* Provides centralized registration and handling of autocomplete options.
|
|
8
|
+
* Supports per-command, per-option handlers with automatic filtering and
|
|
9
|
+
* Discord's 25-choice limit.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const autocompleteManager = new AutocompleteManager(logger);
|
|
14
|
+
*
|
|
15
|
+
* // Register autocomplete for a specific command option
|
|
16
|
+
* autocompleteManager.register('item', 'name', async (interaction, value) => {
|
|
17
|
+
* const items = await prisma.item.findMany({
|
|
18
|
+
* where: {
|
|
19
|
+
* userId: interaction.user.id,
|
|
20
|
+
* name: { contains: value },
|
|
21
|
+
* },
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* return items.map(item => ({ name: item.name, value: item.name }));
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* // Handle autocomplete interactions
|
|
28
|
+
* client.on('interactionCreate', async (interaction) => {
|
|
29
|
+
* if (interaction.isAutocomplete()) {
|
|
30
|
+
* await autocompleteManager.handle(interaction);
|
|
31
|
+
* }
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare class AutocompleteManager {
|
|
36
|
+
private logger?;
|
|
37
|
+
private handlers;
|
|
38
|
+
constructor(logger?: ILogger | undefined);
|
|
39
|
+
/**
|
|
40
|
+
* Register an autocomplete handler for a command option.
|
|
41
|
+
*
|
|
42
|
+
* @param commandName - The name of the slash command
|
|
43
|
+
* @param optionName - The name of the option with autocomplete enabled
|
|
44
|
+
* @param handler - The handler function that returns choices
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* // Simple static choices
|
|
49
|
+
* autocompleteManager.register('config', 'setting', async () => {
|
|
50
|
+
* return [
|
|
51
|
+
* { name: 'Notifications', value: 'notifications' },
|
|
52
|
+
* { name: 'Language', value: 'language' },
|
|
53
|
+
* { name: 'Theme', value: 'theme' },
|
|
54
|
+
* ];
|
|
55
|
+
* });
|
|
56
|
+
*
|
|
57
|
+
* // Dynamic choices from database
|
|
58
|
+
* autocompleteManager.register('item', 'name', async (interaction, value) => {
|
|
59
|
+
* const items = await prisma.item.findMany({
|
|
60
|
+
* where: {
|
|
61
|
+
* userId: interaction.user.id,
|
|
62
|
+
* name: { contains: value },
|
|
63
|
+
* },
|
|
64
|
+
* take: 25,
|
|
65
|
+
* });
|
|
66
|
+
*
|
|
67
|
+
* return items.map(item => ({ name: item.name, value: item.id }));
|
|
68
|
+
* });
|
|
69
|
+
*
|
|
70
|
+
* // With fuzzy search
|
|
71
|
+
* autocompleteManager.register('user', 'username', async (interaction, value) => {
|
|
72
|
+
* const users = await searchUsers(value);
|
|
73
|
+
* return users.map(u => ({ name: u.tag, value: u.id }));
|
|
74
|
+
* });
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
register(commandName: string, optionName: string, handler: AutocompleteHandler): void;
|
|
78
|
+
/**
|
|
79
|
+
* Handle an autocomplete interaction.
|
|
80
|
+
*
|
|
81
|
+
* Automatically calls the appropriate handler and responds with choices.
|
|
82
|
+
* Handles errors gracefully and enforces Discord's 25-choice limit.
|
|
83
|
+
*
|
|
84
|
+
* @param interaction - The autocomplete interaction to handle
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```typescript
|
|
88
|
+
* client.on('interactionCreate', async (interaction) => {
|
|
89
|
+
* if (interaction.isAutocomplete()) {
|
|
90
|
+
* await autocompleteManager.handle(interaction);
|
|
91
|
+
* }
|
|
92
|
+
* });
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
handle(interaction: AutocompleteInteraction): Promise<void>;
|
|
96
|
+
/**
|
|
97
|
+
* Unregister an autocomplete handler.
|
|
98
|
+
*
|
|
99
|
+
* @param commandName - The command name
|
|
100
|
+
* @param optionName - The option name
|
|
101
|
+
* @returns true if the handler was removed, false if it didn't exist
|
|
102
|
+
*/
|
|
103
|
+
unregister(commandName: string, optionName: string): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Unregister all handlers for a command.
|
|
106
|
+
*
|
|
107
|
+
* @param commandName - The command name
|
|
108
|
+
* @returns true if any handlers were removed
|
|
109
|
+
*/
|
|
110
|
+
unregisterCommand(commandName: string): boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Check if a handler is registered for a command option.
|
|
113
|
+
*
|
|
114
|
+
* @param commandName - The command name
|
|
115
|
+
* @param optionName - The option name
|
|
116
|
+
* @returns true if a handler exists
|
|
117
|
+
*/
|
|
118
|
+
has(commandName: string, optionName: string): boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Get all registered command names.
|
|
121
|
+
*/
|
|
122
|
+
getCommands(): string[];
|
|
123
|
+
/**
|
|
124
|
+
* Get all registered option names for a command.
|
|
125
|
+
*
|
|
126
|
+
* @param commandName - The command name
|
|
127
|
+
* @returns Array of option names, or empty array if command not found
|
|
128
|
+
*/
|
|
129
|
+
getOptions(commandName: string): string[];
|
|
130
|
+
/**
|
|
131
|
+
* Clear all registered handlers.
|
|
132
|
+
*
|
|
133
|
+
* Useful for cleanup or testing.
|
|
134
|
+
*/
|
|
135
|
+
clear(): void;
|
|
136
|
+
/**
|
|
137
|
+
* Get the total number of registered handlers across all commands.
|
|
138
|
+
*/
|
|
139
|
+
get handlerCount(): number;
|
|
140
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages autocomplete interactions for slash commands.
|
|
3
|
+
*
|
|
4
|
+
* Provides centralized registration and handling of autocomplete options.
|
|
5
|
+
* Supports per-command, per-option handlers with automatic filtering and
|
|
6
|
+
* Discord's 25-choice limit.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const autocompleteManager = new AutocompleteManager(logger);
|
|
11
|
+
*
|
|
12
|
+
* // Register autocomplete for a specific command option
|
|
13
|
+
* autocompleteManager.register('item', 'name', async (interaction, value) => {
|
|
14
|
+
* const items = await prisma.item.findMany({
|
|
15
|
+
* where: {
|
|
16
|
+
* userId: interaction.user.id,
|
|
17
|
+
* name: { contains: value },
|
|
18
|
+
* },
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* return items.map(item => ({ name: item.name, value: item.name }));
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // Handle autocomplete interactions
|
|
25
|
+
* client.on('interactionCreate', async (interaction) => {
|
|
26
|
+
* if (interaction.isAutocomplete()) {
|
|
27
|
+
* await autocompleteManager.handle(interaction);
|
|
28
|
+
* }
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export class AutocompleteManager {
|
|
33
|
+
constructor(logger) {
|
|
34
|
+
this.logger = logger;
|
|
35
|
+
// Map structure: Map<commandName, Map<optionName, handler>>
|
|
36
|
+
this.handlers = new Map();
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Register an autocomplete handler for a command option.
|
|
40
|
+
*
|
|
41
|
+
* @param commandName - The name of the slash command
|
|
42
|
+
* @param optionName - The name of the option with autocomplete enabled
|
|
43
|
+
* @param handler - The handler function that returns choices
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* // Simple static choices
|
|
48
|
+
* autocompleteManager.register('config', 'setting', async () => {
|
|
49
|
+
* return [
|
|
50
|
+
* { name: 'Notifications', value: 'notifications' },
|
|
51
|
+
* { name: 'Language', value: 'language' },
|
|
52
|
+
* { name: 'Theme', value: 'theme' },
|
|
53
|
+
* ];
|
|
54
|
+
* });
|
|
55
|
+
*
|
|
56
|
+
* // Dynamic choices from database
|
|
57
|
+
* autocompleteManager.register('item', 'name', async (interaction, value) => {
|
|
58
|
+
* const items = await prisma.item.findMany({
|
|
59
|
+
* where: {
|
|
60
|
+
* userId: interaction.user.id,
|
|
61
|
+
* name: { contains: value },
|
|
62
|
+
* },
|
|
63
|
+
* take: 25,
|
|
64
|
+
* });
|
|
65
|
+
*
|
|
66
|
+
* return items.map(item => ({ name: item.name, value: item.id }));
|
|
67
|
+
* });
|
|
68
|
+
*
|
|
69
|
+
* // With fuzzy search
|
|
70
|
+
* autocompleteManager.register('user', 'username', async (interaction, value) => {
|
|
71
|
+
* const users = await searchUsers(value);
|
|
72
|
+
* return users.map(u => ({ name: u.tag, value: u.id }));
|
|
73
|
+
* });
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
register(commandName, optionName, handler) {
|
|
77
|
+
if (!this.handlers.has(commandName)) {
|
|
78
|
+
this.handlers.set(commandName, new Map());
|
|
79
|
+
}
|
|
80
|
+
this.handlers.get(commandName).set(optionName, handler);
|
|
81
|
+
this.logger?.info(`Registered autocomplete handler: ${commandName}.${optionName}`, 'autocomplete-manager');
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Handle an autocomplete interaction.
|
|
85
|
+
*
|
|
86
|
+
* Automatically calls the appropriate handler and responds with choices.
|
|
87
|
+
* Handles errors gracefully and enforces Discord's 25-choice limit.
|
|
88
|
+
*
|
|
89
|
+
* @param interaction - The autocomplete interaction to handle
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```typescript
|
|
93
|
+
* client.on('interactionCreate', async (interaction) => {
|
|
94
|
+
* if (interaction.isAutocomplete()) {
|
|
95
|
+
* await autocompleteManager.handle(interaction);
|
|
96
|
+
* }
|
|
97
|
+
* });
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
async handle(interaction) {
|
|
101
|
+
const commandHandlers = this.handlers.get(interaction.commandName);
|
|
102
|
+
if (!commandHandlers) {
|
|
103
|
+
this.logger?.warn(`No autocomplete handlers for command: ${interaction.commandName}`, 'autocomplete-manager');
|
|
104
|
+
await interaction.respond([]);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const focusedOption = interaction.options.getFocused(true);
|
|
108
|
+
const handler = commandHandlers.get(focusedOption.name);
|
|
109
|
+
if (!handler) {
|
|
110
|
+
this.logger?.warn(`No autocomplete handler for ${interaction.commandName}.${focusedOption.name}`, 'autocomplete-manager');
|
|
111
|
+
await interaction.respond([]);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const choices = await handler(interaction, focusedOption.value);
|
|
116
|
+
// Discord's 25-choice limit
|
|
117
|
+
const limitedChoices = choices.slice(0, 25);
|
|
118
|
+
if (choices.length > 25) {
|
|
119
|
+
this.logger?.warn(`Autocomplete handler for ${interaction.commandName}.${focusedOption.name} returned ${choices.length} choices, truncating to 25`, 'autocomplete-manager');
|
|
120
|
+
}
|
|
121
|
+
await interaction.respond(limitedChoices);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
this.logger?.error(`Error in autocomplete handler for ${interaction.commandName}.${focusedOption.name}: ${err.message}`, 'autocomplete-manager');
|
|
125
|
+
// Empty array on error to prevent interaction failure
|
|
126
|
+
await interaction.respond([]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Unregister an autocomplete handler.
|
|
131
|
+
*
|
|
132
|
+
* @param commandName - The command name
|
|
133
|
+
* @param optionName - The option name
|
|
134
|
+
* @returns true if the handler was removed, false if it didn't exist
|
|
135
|
+
*/
|
|
136
|
+
unregister(commandName, optionName) {
|
|
137
|
+
const commandHandlers = this.handlers.get(commandName);
|
|
138
|
+
if (!commandHandlers)
|
|
139
|
+
return false;
|
|
140
|
+
const removed = commandHandlers.delete(optionName);
|
|
141
|
+
if (commandHandlers.size === 0) {
|
|
142
|
+
this.handlers.delete(commandName);
|
|
143
|
+
}
|
|
144
|
+
return removed;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Unregister all handlers for a command.
|
|
148
|
+
*
|
|
149
|
+
* @param commandName - The command name
|
|
150
|
+
* @returns true if any handlers were removed
|
|
151
|
+
*/
|
|
152
|
+
unregisterCommand(commandName) {
|
|
153
|
+
return this.handlers.delete(commandName);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Check if a handler is registered for a command option.
|
|
157
|
+
*
|
|
158
|
+
* @param commandName - The command name
|
|
159
|
+
* @param optionName - The option name
|
|
160
|
+
* @returns true if a handler exists
|
|
161
|
+
*/
|
|
162
|
+
has(commandName, optionName) {
|
|
163
|
+
return this.handlers.get(commandName)?.has(optionName) || false;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Get all registered command names.
|
|
167
|
+
*/
|
|
168
|
+
getCommands() {
|
|
169
|
+
return Array.from(this.handlers.keys());
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Get all registered option names for a command.
|
|
173
|
+
*
|
|
174
|
+
* @param commandName - The command name
|
|
175
|
+
* @returns Array of option names, or empty array if command not found
|
|
176
|
+
*/
|
|
177
|
+
getOptions(commandName) {
|
|
178
|
+
const commandHandlers = this.handlers.get(commandName);
|
|
179
|
+
return commandHandlers ? Array.from(commandHandlers.keys()) : [];
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Clear all registered handlers.
|
|
183
|
+
*
|
|
184
|
+
* Useful for cleanup or testing.
|
|
185
|
+
*/
|
|
186
|
+
clear() {
|
|
187
|
+
this.handlers.clear();
|
|
188
|
+
this.logger?.info('Cleared all autocomplete handlers', 'autocomplete-manager');
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Get the total number of registered handlers across all commands.
|
|
192
|
+
*/
|
|
193
|
+
get handlerCount() {
|
|
194
|
+
let count = 0;
|
|
195
|
+
for (const commandHandlers of this.handlers.values()) {
|
|
196
|
+
count += commandHandlers.size;
|
|
197
|
+
}
|
|
198
|
+
return count;
|
|
199
|
+
}
|
|
200
|
+
}
|
package/package.json
CHANGED