@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.
- package/README.md +212 -0
- package/dist/index.d.mts +549 -0
- package/dist/index.d.ts +549 -0
- package/dist/index.js +661 -0
- package/dist/index.mjs +610 -0
- package/package.json +42 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
BaseCommand: () => BaseCommand,
|
|
34
|
+
Command: () => Command,
|
|
35
|
+
CommandRegistry: () => CommandRegistry,
|
|
36
|
+
Guard: () => Guard,
|
|
37
|
+
METADATA_KEYS: () => METADATA_KEYS,
|
|
38
|
+
MallyHandler: () => MallyHandler,
|
|
39
|
+
SimpleCommand: () => SimpleCommand,
|
|
40
|
+
Stoat: () => Stoat,
|
|
41
|
+
buildCommandMetadata: () => buildCommandMetadata,
|
|
42
|
+
buildSimpleCommandMetadata: () => buildSimpleCommandMetadata,
|
|
43
|
+
getCommandOptions: () => getCommandOptions,
|
|
44
|
+
getGuards: () => getGuards,
|
|
45
|
+
getSimpleCommands: () => getSimpleCommands,
|
|
46
|
+
isCommand: () => isCommand,
|
|
47
|
+
isStoatClass: () => isStoatClass
|
|
48
|
+
});
|
|
49
|
+
module.exports = __toCommonJS(index_exports);
|
|
50
|
+
|
|
51
|
+
// src/types.ts
|
|
52
|
+
var BaseCommand = class {
|
|
53
|
+
/**
|
|
54
|
+
* Optional: Called when an error occurs during command execution.
|
|
55
|
+
* Override this method to provide custom error handling.
|
|
56
|
+
*/
|
|
57
|
+
async onError(ctx, error) {
|
|
58
|
+
await ctx.reply(`An error occurred: ${error.message}`);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// src/decorators/Stoat.ts
|
|
63
|
+
var import_reflect_metadata = require("reflect-metadata");
|
|
64
|
+
|
|
65
|
+
// src/decorators/keys.ts
|
|
66
|
+
var METADATA_KEYS = {
|
|
67
|
+
COMMAND_OPTIONS: /* @__PURE__ */ Symbol("mally:command:options"),
|
|
68
|
+
IS_COMMAND: /* @__PURE__ */ Symbol("mally:command:isCommand"),
|
|
69
|
+
IS_STOAT_CLASS: /* @__PURE__ */ Symbol("mally:stoat:isClass"),
|
|
70
|
+
SIMPLE_COMMANDS: /* @__PURE__ */ Symbol("mally:stoat:simpleCommands"),
|
|
71
|
+
GUARDS: "mally:command:guards"
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// src/decorators/Stoat.ts
|
|
75
|
+
function Stoat() {
|
|
76
|
+
return (target) => {
|
|
77
|
+
Reflect.defineMetadata(METADATA_KEYS.IS_STOAT_CLASS, true, target);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function isStoatClass(target) {
|
|
81
|
+
return Reflect.getMetadata(METADATA_KEYS.IS_STOAT_CLASS, target) === true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/decorators/SimpleCommand.ts
|
|
85
|
+
var import_reflect_metadata2 = require("reflect-metadata");
|
|
86
|
+
function SimpleCommand(options = {}) {
|
|
87
|
+
return (target, propertyKey, descriptor) => {
|
|
88
|
+
const constructor = target.constructor;
|
|
89
|
+
const existingCommands = Reflect.getMetadata(METADATA_KEYS.SIMPLE_COMMANDS, constructor) || [];
|
|
90
|
+
existingCommands.push({
|
|
91
|
+
methodName: String(propertyKey),
|
|
92
|
+
options
|
|
93
|
+
});
|
|
94
|
+
Reflect.defineMetadata(METADATA_KEYS.SIMPLE_COMMANDS, existingCommands, constructor);
|
|
95
|
+
return descriptor;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function getSimpleCommands(target) {
|
|
99
|
+
return Reflect.getMetadata(METADATA_KEYS.SIMPLE_COMMANDS, target) || [];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/decorators/Command.ts
|
|
103
|
+
var import_reflect_metadata3 = require("reflect-metadata");
|
|
104
|
+
function Command(options = {}) {
|
|
105
|
+
return (target) => {
|
|
106
|
+
Reflect.defineMetadata(METADATA_KEYS.IS_COMMAND, true, target);
|
|
107
|
+
Reflect.defineMetadata(METADATA_KEYS.COMMAND_OPTIONS, options, target);
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function isCommand(target) {
|
|
111
|
+
return Reflect.getMetadata(METADATA_KEYS.IS_COMMAND, target) === true;
|
|
112
|
+
}
|
|
113
|
+
function getCommandOptions(target) {
|
|
114
|
+
return Reflect.getMetadata(METADATA_KEYS.COMMAND_OPTIONS, target);
|
|
115
|
+
}
|
|
116
|
+
function buildCommandMetadata(target, options, category) {
|
|
117
|
+
const className = target.name;
|
|
118
|
+
const derivedName = className.replace(/Command$/i, "").toLowerCase();
|
|
119
|
+
return {
|
|
120
|
+
name: options.name ?? derivedName,
|
|
121
|
+
description: options.description ?? "No description provided",
|
|
122
|
+
aliases: options.aliases ?? [],
|
|
123
|
+
permissions: options.permissions ?? [],
|
|
124
|
+
category: options.category ?? category ?? "uncategorized",
|
|
125
|
+
cooldown: options.cooldown ?? 0,
|
|
126
|
+
nsfw: options.nsfw ?? false,
|
|
127
|
+
ownerOnly: options.ownerOnly ?? false
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/decorators/Guard.ts
|
|
132
|
+
var import_reflect_metadata4 = require("reflect-metadata");
|
|
133
|
+
function Guard(guardClass) {
|
|
134
|
+
return (target) => {
|
|
135
|
+
const existingGuards = Reflect.getMetadata(METADATA_KEYS.GUARDS, target) || [];
|
|
136
|
+
existingGuards.push(guardClass);
|
|
137
|
+
Reflect.defineMetadata(METADATA_KEYS.GUARDS, existingGuards, target);
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function getGuards(target) {
|
|
141
|
+
return Reflect.getMetadata(METADATA_KEYS.GUARDS, target) || [];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/decorators/utils.ts
|
|
145
|
+
function buildSimpleCommandMetadata(options, methodName, category) {
|
|
146
|
+
return {
|
|
147
|
+
name: options.name ?? methodName.toLowerCase(),
|
|
148
|
+
description: options.description ?? "No description provided",
|
|
149
|
+
aliases: options.aliases ?? [],
|
|
150
|
+
permissions: options.permissions ?? [],
|
|
151
|
+
category: options.category ?? category ?? "uncategorized",
|
|
152
|
+
cooldown: options.cooldown ?? 0,
|
|
153
|
+
nsfw: options.nsfw ?? false,
|
|
154
|
+
ownerOnly: options.ownerOnly ?? false
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/registry.ts
|
|
159
|
+
var path = __toESM(require("path"));
|
|
160
|
+
var import_node_url = require("url");
|
|
161
|
+
var import_tinyglobby = require("tinyglobby");
|
|
162
|
+
var CommandRegistry = class {
|
|
163
|
+
constructor(extensions = [".js", ".ts"]) {
|
|
164
|
+
this.commands = /* @__PURE__ */ new Map();
|
|
165
|
+
this.aliases = /* @__PURE__ */ new Map();
|
|
166
|
+
this.extensions = extensions;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get the number of registered commands
|
|
170
|
+
*/
|
|
171
|
+
get size() {
|
|
172
|
+
return this.commands.size;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Load commands from a directory using glob pattern matching
|
|
176
|
+
*/
|
|
177
|
+
async loadFromDirectory(directory) {
|
|
178
|
+
const patterns = this.extensions.map((ext) => path.join(directory, "**", `*${ext}`).replace(/\\/g, "/"));
|
|
179
|
+
for (const pattern of patterns) {
|
|
180
|
+
const files = await (0, import_tinyglobby.glob)(pattern, {
|
|
181
|
+
ignore: ["**/*.d.ts", "**/*.test.ts", "**/*.spec.ts"],
|
|
182
|
+
absolute: true
|
|
183
|
+
});
|
|
184
|
+
for (const file of files) {
|
|
185
|
+
await this.loadFile(file, directory);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
console.log(`[Mally] Loaded ${this.commands.size} command(s)`);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Register a command instance
|
|
192
|
+
*/
|
|
193
|
+
register(instance, metadata, classConstructor, methodName) {
|
|
194
|
+
const name = metadata.name.toLowerCase();
|
|
195
|
+
if (this.commands.has(name)) {
|
|
196
|
+
console.warn(`[Mally] Duplicate command name: ${name}. Skipping...`);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
this.validateGuards(classConstructor, metadata.name);
|
|
200
|
+
if (!methodName) {
|
|
201
|
+
this.validateCooldown(instance, metadata);
|
|
202
|
+
}
|
|
203
|
+
this.commands.set(name, { instance, metadata, methodName, classConstructor });
|
|
204
|
+
for (const alias of metadata.aliases) {
|
|
205
|
+
const aliasLower = alias.toLowerCase();
|
|
206
|
+
if (this.aliases.has(aliasLower) || this.commands.has(aliasLower)) {
|
|
207
|
+
console.warn(`[Mally] Duplicate alias: ${aliasLower}. Skipping...`);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
this.aliases.set(aliasLower, name);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Get a command by name or alias
|
|
215
|
+
*/
|
|
216
|
+
get(name) {
|
|
217
|
+
const lowerName = name.toLowerCase();
|
|
218
|
+
const resolvedName = this.aliases.get(lowerName) ?? lowerName;
|
|
219
|
+
return this.commands.get(resolvedName);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Check if a command exists
|
|
223
|
+
*/
|
|
224
|
+
has(name) {
|
|
225
|
+
const lowerName = name.toLowerCase();
|
|
226
|
+
return this.commands.has(lowerName) || this.aliases.has(lowerName);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Get all registered commands
|
|
230
|
+
*/
|
|
231
|
+
getAll() {
|
|
232
|
+
return Array.from(this.commands.values());
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Get all command metadata
|
|
236
|
+
*/
|
|
237
|
+
getAllMetadata() {
|
|
238
|
+
return this.getAll().map((c) => c.metadata);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Get commands grouped by category
|
|
242
|
+
*/
|
|
243
|
+
getByCategory() {
|
|
244
|
+
const categories = /* @__PURE__ */ new Map();
|
|
245
|
+
for (const cmd of this.commands.values()) {
|
|
246
|
+
const category = cmd.metadata.category;
|
|
247
|
+
const existing = categories.get(category) ?? [];
|
|
248
|
+
existing.push(cmd);
|
|
249
|
+
categories.set(category, existing);
|
|
250
|
+
}
|
|
251
|
+
return categories;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Clear all commands
|
|
255
|
+
*/
|
|
256
|
+
clear() {
|
|
257
|
+
this.commands.clear();
|
|
258
|
+
this.aliases.clear();
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Iterate over commands
|
|
262
|
+
*/
|
|
263
|
+
[Symbol.iterator]() {
|
|
264
|
+
return this.commands.entries();
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Iterate over command values
|
|
268
|
+
*/
|
|
269
|
+
values() {
|
|
270
|
+
return this.commands.values();
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Iterate over command names
|
|
274
|
+
*/
|
|
275
|
+
keys() {
|
|
276
|
+
return this.commands.keys();
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Validate that all guards on a command implement the required methods
|
|
280
|
+
* @param commandClass
|
|
281
|
+
* @param commandName
|
|
282
|
+
* @private
|
|
283
|
+
*/
|
|
284
|
+
validateGuards(commandClass, commandName) {
|
|
285
|
+
const guards = Reflect.getMetadata("mally:command:guards", commandClass) || [];
|
|
286
|
+
for (const GuardClass of guards) {
|
|
287
|
+
const guardInstance = new GuardClass();
|
|
288
|
+
if (typeof guardInstance.run !== "function") {
|
|
289
|
+
console.error(
|
|
290
|
+
`[Mally] FATAL: Guard "${GuardClass.name}" on command "${commandName}" does not have a run() method.`
|
|
291
|
+
);
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
if (typeof guardInstance.guardFail !== "function") {
|
|
295
|
+
console.error(
|
|
296
|
+
`[Mally] FATAL: Guard "${GuardClass.name}" on command "${commandName}" does not have a guardFail() method.`
|
|
297
|
+
);
|
|
298
|
+
console.error(`[Mally] All guards must implement guardFail() to handle failed checks.`);
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Validate that commands with cooldowns implement the onCooldown method
|
|
305
|
+
* @param instance
|
|
306
|
+
* @param metadata
|
|
307
|
+
* @private
|
|
308
|
+
*/
|
|
309
|
+
validateCooldown(instance, metadata) {
|
|
310
|
+
if (metadata.cooldown > 0 && typeof instance.onCooldown !== "function") {
|
|
311
|
+
console.error(
|
|
312
|
+
`[Mally] FATAL: Command "${metadata.name}" has a cooldown of ${metadata.cooldown}ms but does not implement onCooldown() method.`
|
|
313
|
+
);
|
|
314
|
+
console.error(
|
|
315
|
+
`[Mally] Commands with cooldowns must implement onCooldown(ctx, remaining) to handle cooldown messages.`
|
|
316
|
+
);
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Load commands from a single file
|
|
322
|
+
*/
|
|
323
|
+
async loadFile(filePath, baseDir) {
|
|
324
|
+
try {
|
|
325
|
+
const fileUrl = (0, import_node_url.pathToFileURL)(filePath).href;
|
|
326
|
+
const module2 = await import(fileUrl);
|
|
327
|
+
for (const exportKey of Object.keys(module2)) {
|
|
328
|
+
const exported = module2[exportKey];
|
|
329
|
+
if (typeof exported !== "function") {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (isStoatClass(exported)) {
|
|
333
|
+
const instance2 = new exported();
|
|
334
|
+
const simpleCommands = getSimpleCommands(exported);
|
|
335
|
+
const category2 = this.getCategoryFromPath(filePath, baseDir);
|
|
336
|
+
if (simpleCommands.length === 0) {
|
|
337
|
+
console.warn(
|
|
338
|
+
`[Mally] Class ${exported.name} is decorated with @Stoat but has no @SimpleCommand methods. Skipping...`
|
|
339
|
+
);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
for (const cmdDef of simpleCommands) {
|
|
343
|
+
const method = instance2[cmdDef.methodName];
|
|
344
|
+
if (typeof method !== "function") {
|
|
345
|
+
console.warn(`[Mally] Method ${cmdDef.methodName} not found on ${exported.name}. Skipping...`);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
const metadata2 = buildSimpleCommandMetadata(cmdDef.options, cmdDef.methodName, category2);
|
|
349
|
+
this.register(instance2, metadata2, exported, cmdDef.methodName);
|
|
350
|
+
}
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (!isCommand(exported)) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const options = getCommandOptions(exported);
|
|
357
|
+
if (!options) continue;
|
|
358
|
+
const instance = new exported();
|
|
359
|
+
if (typeof instance.run !== "function") {
|
|
360
|
+
console.warn(
|
|
361
|
+
`[Mally] Class ${exported.name} is decorated with @Command but does not implement run() method. Skipping...`
|
|
362
|
+
);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
const category = this.getCategoryFromPath(filePath, baseDir);
|
|
366
|
+
const metadata = buildCommandMetadata(exported, options, category);
|
|
367
|
+
instance.metadata = metadata;
|
|
368
|
+
this.register(instance, metadata, exported);
|
|
369
|
+
}
|
|
370
|
+
} catch (error) {
|
|
371
|
+
console.error(`[Mally] Failed to load command file: ${filePath}`, error);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Derive category from file path relative to base directory
|
|
376
|
+
*/
|
|
377
|
+
getCategoryFromPath(filePath, baseDir) {
|
|
378
|
+
const relative2 = path.relative(baseDir, filePath);
|
|
379
|
+
const parts = relative2.split(path.sep);
|
|
380
|
+
if (parts.length > 1) {
|
|
381
|
+
return parts[0];
|
|
382
|
+
}
|
|
383
|
+
return void 0;
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// src/handler.ts
|
|
388
|
+
var import_reflect_metadata5 = require("reflect-metadata");
|
|
389
|
+
var MallyHandler = class {
|
|
390
|
+
constructor(options) {
|
|
391
|
+
this.cooldowns = /* @__PURE__ */ new Map();
|
|
392
|
+
this.client = options.client;
|
|
393
|
+
this.commandsDir = options.commandsDir;
|
|
394
|
+
this.prefixResolver = options.prefix;
|
|
395
|
+
this.owners = new Set(options.owners ?? []);
|
|
396
|
+
this.registry = new CommandRegistry(options.extensions);
|
|
397
|
+
this.disableMentionPrefix = options.disableMentionPrefix ?? false;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Initialize the handler - load all commands
|
|
401
|
+
*/
|
|
402
|
+
async init() {
|
|
403
|
+
await this.registry.loadFromDirectory(this.commandsDir);
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Parse a raw message into command context
|
|
407
|
+
*/
|
|
408
|
+
async parseMessage(rawContent, message, meta) {
|
|
409
|
+
const prefix = await this.resolvePrefix(meta.serverId);
|
|
410
|
+
let usedPrefix = prefix;
|
|
411
|
+
let withoutPrefix = "";
|
|
412
|
+
if (rawContent.startsWith(prefix)) {
|
|
413
|
+
withoutPrefix = rawContent.slice(prefix.length).trim();
|
|
414
|
+
usedPrefix = prefix;
|
|
415
|
+
} else if (!this.disableMentionPrefix && rawContent.match(/^<@!?[\w]+>/)) {
|
|
416
|
+
const mentionMatch = rawContent.match(/^<@!?([\w]+)>\s*/);
|
|
417
|
+
if (mentionMatch) {
|
|
418
|
+
const mentionedId = mentionMatch[1];
|
|
419
|
+
const botId = this.client.user?.id;
|
|
420
|
+
if (botId && mentionedId === botId) {
|
|
421
|
+
usedPrefix = mentionMatch[0];
|
|
422
|
+
withoutPrefix = rawContent.slice(mentionMatch[0].length).trim();
|
|
423
|
+
} else {
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (!withoutPrefix) {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
const [commandName, ...args] = withoutPrefix.split(/\s+/);
|
|
431
|
+
if (!commandName) {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
client: this.client,
|
|
436
|
+
content: rawContent,
|
|
437
|
+
authorId: meta.authorId,
|
|
438
|
+
channelId: meta.channelId,
|
|
439
|
+
serverId: meta.serverId,
|
|
440
|
+
args,
|
|
441
|
+
prefix: usedPrefix,
|
|
442
|
+
commandName: commandName.toLowerCase(),
|
|
443
|
+
reply: meta.reply,
|
|
444
|
+
message
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Handle a message object using the configured message adapter
|
|
449
|
+
*
|
|
450
|
+
* @example
|
|
451
|
+
* ```ts
|
|
452
|
+
* // With message adapter configured
|
|
453
|
+
* client.on('messageCreate', (message) => {
|
|
454
|
+
* handler.handle(message);
|
|
455
|
+
* });
|
|
456
|
+
* ```
|
|
457
|
+
*/
|
|
458
|
+
async handle(message) {
|
|
459
|
+
if (!message.channel || !message.author) {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
if (message.author.bot) {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
const rawContent = message.content;
|
|
466
|
+
const authorId = message.author.id;
|
|
467
|
+
const channelId = message.channel.id;
|
|
468
|
+
const serverId = message.server?.id;
|
|
469
|
+
const reply = async (content) => {
|
|
470
|
+
await message.channel.sendMessage(content);
|
|
471
|
+
};
|
|
472
|
+
return this.handleMessage(rawContent, message, {
|
|
473
|
+
authorId,
|
|
474
|
+
channelId,
|
|
475
|
+
serverId,
|
|
476
|
+
reply
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Handle a raw message string with metadata
|
|
481
|
+
*
|
|
482
|
+
* @example
|
|
483
|
+
* ```ts
|
|
484
|
+
* // Manual usage without message adapter
|
|
485
|
+
* client.on('messageCreate', (message) => {
|
|
486
|
+
* handler.handleMessage(message.content, message, {
|
|
487
|
+
* authorId: message.author.id,
|
|
488
|
+
* channelId: message.channel.id,
|
|
489
|
+
* serverId: message.server?.id,
|
|
490
|
+
* reply: (content) => message.channel.sendMessage(content),
|
|
491
|
+
* });
|
|
492
|
+
* });
|
|
493
|
+
* ```
|
|
494
|
+
*/
|
|
495
|
+
async handleMessage(rawContent, message, meta) {
|
|
496
|
+
const ctx = await this.parseMessage(rawContent, message, meta);
|
|
497
|
+
if (!ctx) {
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
return this.execute(ctx);
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Execute a command with the given context
|
|
504
|
+
*/
|
|
505
|
+
async execute(ctx) {
|
|
506
|
+
const registered = this.registry.get(ctx.commandName);
|
|
507
|
+
if (!registered) {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
const { instance, metadata, methodName, classConstructor } = registered;
|
|
511
|
+
if (metadata.ownerOnly && !this.owners.has(ctx.authorId)) {
|
|
512
|
+
await ctx.reply("This command is owner-only.");
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
const guards = Reflect.getMetadata("mally:command:guards", classConstructor) || [];
|
|
516
|
+
for (const guardClass of guards) {
|
|
517
|
+
const guardInstance = new guardClass();
|
|
518
|
+
if (typeof guardInstance.run === "function") {
|
|
519
|
+
const guardResult = await guardInstance.run(ctx);
|
|
520
|
+
if (!guardResult) {
|
|
521
|
+
if (typeof guardInstance.guardFail === "function") {
|
|
522
|
+
await guardInstance.guardFail(ctx);
|
|
523
|
+
} else {
|
|
524
|
+
console.error("[Mally] Guard check failed but no guardFail method defined on", guardClass.name);
|
|
525
|
+
}
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (!this.checkCooldown(ctx.authorId, metadata)) {
|
|
531
|
+
const remaining = this.getRemainingCooldown(ctx.authorId, metadata);
|
|
532
|
+
if (typeof instance.onCooldown === "function") {
|
|
533
|
+
await instance.onCooldown(ctx, remaining);
|
|
534
|
+
} else {
|
|
535
|
+
await ctx.reply(`Please wait ${(remaining / 1e3).toFixed(1)} seconds before using this command again.`);
|
|
536
|
+
}
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
try {
|
|
540
|
+
if (methodName) {
|
|
541
|
+
await instance[methodName](ctx);
|
|
542
|
+
} else {
|
|
543
|
+
await instance.run(ctx);
|
|
544
|
+
}
|
|
545
|
+
if (metadata.cooldown > 0) {
|
|
546
|
+
this.setCooldown(ctx.authorId, metadata);
|
|
547
|
+
}
|
|
548
|
+
return true;
|
|
549
|
+
} catch (error) {
|
|
550
|
+
if (typeof instance.onError === "function") {
|
|
551
|
+
await instance.onError(ctx, error);
|
|
552
|
+
} else {
|
|
553
|
+
console.error(`[Mally] Error in command ${metadata.name}:`, error);
|
|
554
|
+
await ctx.reply(`An error occurred: ${error.message}`);
|
|
555
|
+
}
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Get the command registry
|
|
561
|
+
*/
|
|
562
|
+
getRegistry() {
|
|
563
|
+
return this.registry;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Get a command by name or alias
|
|
567
|
+
*/
|
|
568
|
+
getCommand(name) {
|
|
569
|
+
return this.registry.get(name);
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Get all commands
|
|
573
|
+
*/
|
|
574
|
+
getCommands() {
|
|
575
|
+
return this.registry.getAll();
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Reload all commands
|
|
579
|
+
*/
|
|
580
|
+
async reload() {
|
|
581
|
+
this.registry.clear();
|
|
582
|
+
this.cooldowns.clear();
|
|
583
|
+
await this.registry.loadFromDirectory(this.commandsDir);
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Check if a user is an owner
|
|
587
|
+
*/
|
|
588
|
+
isOwner(userId) {
|
|
589
|
+
return this.owners.has(userId);
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Add an owner
|
|
593
|
+
*/
|
|
594
|
+
addOwner(userId) {
|
|
595
|
+
this.owners.add(userId);
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Remove an owner
|
|
599
|
+
*/
|
|
600
|
+
removeOwner(userId) {
|
|
601
|
+
this.owners.delete(userId);
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Resolve the prefix for a context
|
|
605
|
+
*/
|
|
606
|
+
async resolvePrefix(serverId) {
|
|
607
|
+
if (typeof this.prefixResolver === "function") {
|
|
608
|
+
return this.prefixResolver({ serverId });
|
|
609
|
+
}
|
|
610
|
+
return this.prefixResolver;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Check if user is on cooldown
|
|
614
|
+
*/
|
|
615
|
+
checkCooldown(userId, metadata) {
|
|
616
|
+
if (metadata.cooldown <= 0) return true;
|
|
617
|
+
const commandCooldowns = this.cooldowns.get(metadata.name);
|
|
618
|
+
if (!commandCooldowns) return true;
|
|
619
|
+
const userCooldown = commandCooldowns.get(userId);
|
|
620
|
+
if (!userCooldown) return true;
|
|
621
|
+
return Date.now() >= userCooldown;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Get remaining cooldown time in ms
|
|
625
|
+
*/
|
|
626
|
+
getRemainingCooldown(userId, metadata) {
|
|
627
|
+
const commandCooldowns = this.cooldowns.get(metadata.name);
|
|
628
|
+
if (!commandCooldowns) return 0;
|
|
629
|
+
const userCooldown = commandCooldowns.get(userId);
|
|
630
|
+
if (!userCooldown) return 0;
|
|
631
|
+
return Math.max(0, userCooldown - Date.now());
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Set cooldown for a user
|
|
635
|
+
*/
|
|
636
|
+
setCooldown(userId, metadata) {
|
|
637
|
+
if (!this.cooldowns.has(metadata.name)) {
|
|
638
|
+
this.cooldowns.set(metadata.name, /* @__PURE__ */ new Map());
|
|
639
|
+
}
|
|
640
|
+
const commandCooldowns = this.cooldowns.get(metadata.name);
|
|
641
|
+
commandCooldowns.set(userId, Date.now() + metadata.cooldown);
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
645
|
+
0 && (module.exports = {
|
|
646
|
+
BaseCommand,
|
|
647
|
+
Command,
|
|
648
|
+
CommandRegistry,
|
|
649
|
+
Guard,
|
|
650
|
+
METADATA_KEYS,
|
|
651
|
+
MallyHandler,
|
|
652
|
+
SimpleCommand,
|
|
653
|
+
Stoat,
|
|
654
|
+
buildCommandMetadata,
|
|
655
|
+
buildSimpleCommandMetadata,
|
|
656
|
+
getCommandOptions,
|
|
657
|
+
getGuards,
|
|
658
|
+
getSimpleCommands,
|
|
659
|
+
isCommand,
|
|
660
|
+
isStoatClass
|
|
661
|
+
});
|