@marshmallow-stoat/mally 0.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.
@@ -0,0 +1,549 @@
1
+ import { Client, Message } from 'stoat.js';
2
+
3
+ /**
4
+ * Permission types for commands
5
+ */
6
+ type Permission = "SendMessages" | "ManageMessages" | "ManageChannels" | "ManageServer" | "KickMembers" | "BanMembers" | "Administrator" | (string & {});
7
+ /**
8
+ * Command metadata options passed to @Command decorator
9
+ */
10
+ interface CommandOptions {
11
+ /** Command name (defaults to class name without 'Command' suffix) */
12
+ name?: string;
13
+ /** Command description */
14
+ description?: string;
15
+ /** Command aliases */
16
+ aliases?: string[];
17
+ /** Required permissions to run the command */
18
+ permissions?: Permission[];
19
+ /** Command category (auto-detected from directory if not provided) */
20
+ category?: string;
21
+ /** Cooldown in milliseconds */
22
+ cooldown?: number;
23
+ /** Whether the command is NSFW only */
24
+ nsfw?: boolean;
25
+ /** Whether the command is owner only */
26
+ ownerOnly?: boolean;
27
+ }
28
+ /**
29
+ * Simple command options passed to @SimpleCommand decorator
30
+ * Used with @Stoat() decorated classes for method-based commands
31
+ */
32
+ interface SimpleCommandOptions {
33
+ /** Command name (defaults to method name) */
34
+ name?: string;
35
+ /** Command description */
36
+ description?: string;
37
+ /** Command aliases */
38
+ aliases?: string[];
39
+ /** Required permissions to run the command */
40
+ permissions?: Permission[];
41
+ /** Command category (auto-detected from directory if not provided) */
42
+ category?: string;
43
+ /** Cooldown in milliseconds */
44
+ cooldown?: number;
45
+ /** Whether the command is NSFW only */
46
+ nsfw?: boolean;
47
+ /** Whether the command is owner only */
48
+ ownerOnly?: boolean;
49
+ }
50
+ /**
51
+ * Resolved command metadata with required fields
52
+ */
53
+ interface CommandMetadata {
54
+ name: string;
55
+ description: string;
56
+ aliases: string[];
57
+ permissions: Permission[];
58
+ category: string;
59
+ cooldown: number;
60
+ nsfw: boolean;
61
+ ownerOnly: boolean;
62
+ }
63
+ /**
64
+ * Command execution context
65
+ */
66
+ interface CommandContext {
67
+ /** The client instance */
68
+ client: Client;
69
+ /** The raw message content */
70
+ content: string;
71
+ /** The author ID */
72
+ authorId: string;
73
+ /** The channel ID */
74
+ channelId: string;
75
+ /** The server/guild ID (if applicable) */
76
+ serverId?: string;
77
+ /** Parsed command arguments */
78
+ args: string[];
79
+ /** The prefix used */
80
+ prefix: string;
81
+ /** The command name used (could be an alias) */
82
+ commandName: string;
83
+ /** Reply to the message */
84
+ reply: (content: string) => Promise<void>;
85
+ /** The original message object (platform-specific) */
86
+ message: Message;
87
+ }
88
+ /**
89
+ * Interface that all command classes must implement
90
+ */
91
+ interface MallyCommand {
92
+ /** Command metadata (injected by registry) */
93
+ metadata: CommandMetadata;
94
+ /**
95
+ * Execute the command
96
+ */
97
+ run(ctx: CommandContext): Promise<void>;
98
+ /**
99
+ * Optional: Called when an error occurs during command execution
100
+ */
101
+ onError?(ctx: CommandContext, error: Error): Promise<void>;
102
+ guardFail?(ctx: CommandContext): Promise<void>;
103
+ onCooldown?(ctx: CommandContext, remaining: number): Promise<void>;
104
+ }
105
+ interface MallyGuard {
106
+ run(ctx: CommandContext): Promise<boolean> | boolean;
107
+ guardFail?(ctx: CommandContext): Promise<void> | void;
108
+ }
109
+ /**
110
+ * Abstract base class for commands.
111
+ * Extend this class to create commands without boilerplate.
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * @Command({ name: 'ping', description: 'Replies with Pong!' })
116
+ * export class PingCommand extends BaseCommand {
117
+ * async run(ctx) {
118
+ * await ctx.reply('Pong!');
119
+ * }
120
+ * }
121
+ * ```
122
+ */
123
+ declare abstract class BaseCommand implements MallyCommand {
124
+ /** Command metadata (injected by registry) */
125
+ metadata: CommandMetadata;
126
+ /** Typed context for use in subclasses */
127
+ protected ctx: CommandContext;
128
+ /**
129
+ * Execute the command - must be implemented by subclasses
130
+ */
131
+ abstract run(ctx: CommandContext): Promise<void>;
132
+ /**
133
+ * Optional: Called when an error occurs during command execution.
134
+ * Override this method to provide custom error handling.
135
+ */
136
+ onError(ctx: CommandContext, error: Error): Promise<void>;
137
+ }
138
+ /**
139
+ * Constructor type for command classes
140
+ */
141
+ type CommandConstructor = new () => MallyCommand;
142
+ /**
143
+ * Handler options
144
+ */
145
+ interface MallyHandlerOptions {
146
+ /** The client instance */
147
+ client: Client;
148
+ /** Directory to scan for commands (absolute path) */
149
+ commandsDir: string;
150
+ /** Command prefix or prefix resolver function */
151
+ prefix: string | ((ctx: {
152
+ serverId?: string;
153
+ }) => string | Promise<string>);
154
+ /** Owner IDs for owner-only commands */
155
+ owners?: string[];
156
+ /** File extensions to load (default: ['.js', '.ts']) */
157
+ extensions?: string[];
158
+ /** Disable mention prefix support (default: false) */
159
+ disableMentionPrefix?: boolean;
160
+ }
161
+
162
+ /**
163
+ * @Stoat
164
+ * Marks a class as a Stoat command container.
165
+ * Use this decorator on classes that contain @SimpleCommand methods.
166
+ *
167
+ * @example
168
+ * ```ts
169
+ * import { Stoat, SimpleCommand, CommandContext } from '@marshmallow/mally';
170
+ *
171
+ * @Stoat()
172
+ * class ModerationCommands {
173
+ * @SimpleCommand({ name: 'ban', description: 'Ban a user' })
174
+ * async ban(ctx: CommandContext) {
175
+ * await ctx.reply('User banned!');
176
+ * }
177
+ *
178
+ * @SimpleCommand({ name: 'kick', description: 'Kick a user' })
179
+ * async kick(ctx: CommandContext) {
180
+ * await ctx.reply('User kicked!');
181
+ * }
182
+ * }
183
+ * ```
184
+ */
185
+ declare function Stoat(): ClassDecorator;
186
+ /**
187
+ * Check if a class is decorated with @Stoat
188
+ */
189
+ declare function isStoatClass(target: Function): boolean;
190
+
191
+ /**
192
+ * Stored simple command metadata from method decorator
193
+ */
194
+ interface SimpleCommandDefinition {
195
+ methodName: string;
196
+ options: SimpleCommandOptions;
197
+ }
198
+ /**
199
+ * @SimpleCommand
200
+ * Marks a method as a simple command within a @Stoat() decorated class.
201
+ *
202
+ * @example
203
+ * ```ts
204
+ * @Stoat()
205
+ * class Example {
206
+ * @SimpleCommand({ name: 'ping', description: 'Replies with Pong!' })
207
+ * async ping(ctx: CommandContext) {
208
+ * await ctx.reply('Pong!');
209
+ * }
210
+ *
211
+ * @SimpleCommand({ aliases: ['perm'], name: 'permission' })
212
+ * async permission(ctx: CommandContext) {
213
+ * await ctx.reply('Access granted');
214
+ * }
215
+ * }
216
+ * ```
217
+ */
218
+ declare function SimpleCommand(options?: SimpleCommandOptions): MethodDecorator;
219
+ /**
220
+ * Get all simple command definitions from a @Stoat class
221
+ */
222
+ declare function getSimpleCommands(target: Function): SimpleCommandDefinition[];
223
+
224
+ /**
225
+ * @Command
226
+ * Marks a class as a command and attaches metadata.
227
+ * This is the legacy/class-based approach where each command is a separate class.
228
+ *
229
+ * @example
230
+ * ```ts
231
+ * import { Command, MallyCommand, CommandContext } from '@marshmallow/mally';
232
+ *
233
+ * @Command({
234
+ * description: 'Ban a user from the server',
235
+ * aliases: ['b'],
236
+ * permissions: ['BanMembers']
237
+ * })
238
+ * export class BanCommand implements MallyCommand {
239
+ * metadata!: CommandMetadata;
240
+ *
241
+ * async run(ctx: CommandContext) {
242
+ * const userId = ctx.args[0];
243
+ * await ctx.reply(`Banned user ${userId}`);
244
+ * }
245
+ * }
246
+ * ```
247
+ */
248
+ declare function Command(options?: CommandOptions): ClassDecorator;
249
+ /**
250
+ * Check if a class is decorated with @Command
251
+ */
252
+ declare function isCommand(target: Function): boolean;
253
+ /**
254
+ * Get command options from a decorated class
255
+ */
256
+ declare function getCommandOptions(target: Function): CommandOptions | undefined;
257
+ /**
258
+ * Build complete CommandMetadata from options and class name
259
+ */
260
+ declare function buildCommandMetadata(target: Function, options: CommandOptions, category?: string): CommandMetadata;
261
+
262
+ /**
263
+ * @Guard
264
+ * Runs before a command to check if it should execute.
265
+ * Should return true to allow execution, false to block.
266
+ * Can be applied to both @Command classes and @Stoat classes.
267
+ *
268
+ * @example
269
+ * ```ts
270
+ * import { Guard, Stoat, SimpleCommand, CommandContext } from '@marshmallow/mally';
271
+ *
272
+ * // Define a guard
273
+ * class NotBot implements MallyGuard {
274
+ * run(ctx: CommandContext): boolean {
275
+ * return !ctx.message.author.bot;
276
+ * }
277
+ *
278
+ * guardFail(ctx: CommandContext): void {
279
+ * ctx.reply("Bots cannot use this command!");
280
+ * }
281
+ * }
282
+ *
283
+ * @Stoat()
284
+ * @Guard(NotBot)
285
+ * class AdminCommands {
286
+ * @SimpleCommand({ name: 'admin', description: 'Admin only command' })
287
+ * async admin(ctx: CommandContext) {
288
+ * ctx.reply("You passed the guard check!");
289
+ * }
290
+ * }
291
+ * ```
292
+ */
293
+ declare function Guard(guardClass: Function): ClassDecorator;
294
+ /**
295
+ * Get all guards from a decorated class
296
+ */
297
+ declare function getGuards(target: Function): Function[];
298
+
299
+ /**
300
+ * Build CommandMetadata from SimpleCommandOptions
301
+ */
302
+ declare function buildSimpleCommandMetadata(options: SimpleCommandOptions, methodName: string, category?: string): CommandMetadata;
303
+
304
+ /**
305
+ * Metadata keys used by decorators
306
+ */
307
+ declare const METADATA_KEYS: {
308
+ readonly COMMAND_OPTIONS: symbol;
309
+ readonly IS_COMMAND: symbol;
310
+ readonly IS_STOAT_CLASS: symbol;
311
+ readonly SIMPLE_COMMANDS: symbol;
312
+ readonly GUARDS: "mally:command:guards";
313
+ };
314
+
315
+ /**
316
+ * Stored command entry (supports both class-based and method-based commands)
317
+ */
318
+ interface RegisteredCommand {
319
+ /** Instance of the command class */
320
+ instance: MallyCommand | object;
321
+ /** Command metadata */
322
+ metadata: CommandMetadata;
323
+ /** Method name to call (for @SimpleCommand methods) */
324
+ methodName?: string;
325
+ /** The original class constructor (for guard validation) */
326
+ classConstructor: Function;
327
+ }
328
+ /**
329
+ * CommandRegistry - Scans directories and stores commands in a Map
330
+ *
331
+ * @example
332
+ * ```ts
333
+ * const registry = new CommandRegistry();
334
+ * await registry.loadFromDirectory('./src/commands');
335
+ *
336
+ * const ping = registry.get('ping');
337
+ * const allCommands = registry.getAll();
338
+ * ```
339
+ */
340
+ declare class CommandRegistry {
341
+ private readonly commands;
342
+ private readonly aliases;
343
+ private readonly extensions;
344
+ constructor(extensions?: string[]);
345
+ /**
346
+ * Get the number of registered commands
347
+ */
348
+ get size(): number;
349
+ /**
350
+ * Load commands from a directory using glob pattern matching
351
+ */
352
+ loadFromDirectory(directory: string): Promise<void>;
353
+ /**
354
+ * Register a command instance
355
+ */
356
+ register(instance: MallyCommand | object, metadata: CommandMetadata, classConstructor: Function, methodName?: string): void;
357
+ /**
358
+ * Get a command by name or alias
359
+ */
360
+ get(name: string): RegisteredCommand | undefined;
361
+ /**
362
+ * Check if a command exists
363
+ */
364
+ has(name: string): boolean;
365
+ /**
366
+ * Get all registered commands
367
+ */
368
+ getAll(): RegisteredCommand[];
369
+ /**
370
+ * Get all command metadata
371
+ */
372
+ getAllMetadata(): CommandMetadata[];
373
+ /**
374
+ * Get commands grouped by category
375
+ */
376
+ getByCategory(): Map<string, RegisteredCommand[]>;
377
+ /**
378
+ * Clear all commands
379
+ */
380
+ clear(): void;
381
+ /**
382
+ * Iterate over commands
383
+ */
384
+ [Symbol.iterator](): IterableIterator<[string, RegisteredCommand]>;
385
+ /**
386
+ * Iterate over command values
387
+ */
388
+ values(): IterableIterator<RegisteredCommand>;
389
+ /**
390
+ * Iterate over command names
391
+ */
392
+ keys(): IterableIterator<string>;
393
+ /**
394
+ * Validate that all guards on a command implement the required methods
395
+ * @param commandClass
396
+ * @param commandName
397
+ * @private
398
+ */
399
+ private validateGuards;
400
+ /**
401
+ * Validate that commands with cooldowns implement the onCooldown method
402
+ * @param instance
403
+ * @param metadata
404
+ * @private
405
+ */
406
+ private validateCooldown;
407
+ /**
408
+ * Load commands from a single file
409
+ */
410
+ private loadFile;
411
+ /**
412
+ * Derive category from file path relative to base directory
413
+ */
414
+ private getCategoryFromPath;
415
+ }
416
+
417
+ /**
418
+ * MallyHandler - The execution engine for commands
419
+ *
420
+ * Handles message parsing, middleware execution, and command dispatching
421
+ *
422
+ * @example
423
+ * ```ts
424
+ * import { MallyHandler } from '@marshmallow/mally';
425
+ * import { Client } from 'stoat.js';
426
+ *
427
+ * const client = new Client();
428
+ *
429
+ * const handler = new MallyHandler({
430
+ * client,
431
+ * commandsDir: path.join(__dirname, 'commands'),
432
+ * prefix: '!',
433
+ * owners: ['owner-user-id'],
434
+ * });
435
+ *
436
+ * await handler.init();
437
+ *
438
+ * client.on('message', (message) => {
439
+ * handler.handleMessage(message);
440
+ * });
441
+ * ```
442
+ */
443
+ declare class MallyHandler {
444
+ private readonly commandsDir;
445
+ private readonly prefixResolver;
446
+ private readonly owners;
447
+ private readonly registry;
448
+ private readonly cooldowns;
449
+ private readonly disableMentionPrefix;
450
+ private readonly client;
451
+ constructor(options: MallyHandlerOptions);
452
+ /**
453
+ * Initialize the handler - load all commands
454
+ */
455
+ init(): Promise<void>;
456
+ /**
457
+ * Parse a raw message into command context
458
+ */
459
+ parseMessage(rawContent: string, message: Message, meta: {
460
+ authorId: string;
461
+ channelId: string;
462
+ serverId?: string;
463
+ reply: (content: string) => Promise<void>;
464
+ }): Promise<CommandContext | null>;
465
+ /**
466
+ * Handle a message object using the configured message adapter
467
+ *
468
+ * @example
469
+ * ```ts
470
+ * // With message adapter configured
471
+ * client.on('messageCreate', (message) => {
472
+ * handler.handle(message);
473
+ * });
474
+ * ```
475
+ */
476
+ handle(message: any): Promise<boolean>;
477
+ /**
478
+ * Handle a raw message string with metadata
479
+ *
480
+ * @example
481
+ * ```ts
482
+ * // Manual usage without message adapter
483
+ * client.on('messageCreate', (message) => {
484
+ * handler.handleMessage(message.content, message, {
485
+ * authorId: message.author.id,
486
+ * channelId: message.channel.id,
487
+ * serverId: message.server?.id,
488
+ * reply: (content) => message.channel.sendMessage(content),
489
+ * });
490
+ * });
491
+ * ```
492
+ */
493
+ handleMessage(rawContent: string, message: Message, meta: {
494
+ authorId: string;
495
+ channelId: string;
496
+ serverId?: string;
497
+ reply: (content: string) => Promise<void>;
498
+ }): Promise<boolean>;
499
+ /**
500
+ * Execute a command with the given context
501
+ */
502
+ execute(ctx: CommandContext): Promise<boolean>;
503
+ /**
504
+ * Get the command registry
505
+ */
506
+ getRegistry(): CommandRegistry;
507
+ /**
508
+ * Get a command by name or alias
509
+ */
510
+ getCommand(name: string): RegisteredCommand | undefined;
511
+ /**
512
+ * Get all commands
513
+ */
514
+ getCommands(): RegisteredCommand[];
515
+ /**
516
+ * Reload all commands
517
+ */
518
+ reload(): Promise<void>;
519
+ /**
520
+ * Check if a user is an owner
521
+ */
522
+ isOwner(userId: string): boolean;
523
+ /**
524
+ * Add an owner
525
+ */
526
+ addOwner(userId: string): void;
527
+ /**
528
+ * Remove an owner
529
+ */
530
+ removeOwner(userId: string): void;
531
+ /**
532
+ * Resolve the prefix for a context
533
+ */
534
+ private resolvePrefix;
535
+ /**
536
+ * Check if user is on cooldown
537
+ */
538
+ private checkCooldown;
539
+ /**
540
+ * Get remaining cooldown time in ms
541
+ */
542
+ private getRemainingCooldown;
543
+ /**
544
+ * Set cooldown for a user
545
+ */
546
+ private setCooldown;
547
+ }
548
+
549
+ export { BaseCommand, Command, type CommandConstructor, type CommandContext, type CommandMetadata, type CommandOptions, CommandRegistry, type CommandContext as Context, Guard, METADATA_KEYS, type MallyCommand, type MallyGuard, MallyHandler, type MallyHandlerOptions, type Permission, type RegisteredCommand, SimpleCommand, type SimpleCommandDefinition, type SimpleCommandOptions, Stoat, buildCommandMetadata, buildSimpleCommandMetadata, getCommandOptions, getGuards, getSimpleCommands, isCommand, isStoatClass };