@whanext/core 0.7.0 → 0.10.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/CHANGELOG.md +48 -0
- package/README.md +158 -2
- package/SECURITY.md +1 -1
- package/dist/index.d.ts +288 -67
- package/dist/index.js +875 -88
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2,18 +2,27 @@
|
|
|
2
2
|
var MemoryCache = class {
|
|
3
3
|
#entries = /* @__PURE__ */ new Map();
|
|
4
4
|
#maxEntries;
|
|
5
|
+
#hits = 0;
|
|
6
|
+
#misses = 0;
|
|
7
|
+
#sets = 0;
|
|
8
|
+
#evictions = 0;
|
|
9
|
+
#expirations = 0;
|
|
5
10
|
constructor(options = {}) {
|
|
6
11
|
this.#maxEntries = Math.max(1, options.maxEntries ?? 1e3);
|
|
7
12
|
}
|
|
8
13
|
async get(key) {
|
|
9
14
|
const entry = this.#entries.get(key);
|
|
10
15
|
if (!entry) {
|
|
16
|
+
this.#misses += 1;
|
|
11
17
|
return void 0;
|
|
12
18
|
}
|
|
13
19
|
if (entry.expiresAt !== void 0 && entry.expiresAt <= Date.now()) {
|
|
14
20
|
this.#entries.delete(key);
|
|
21
|
+
this.#misses += 1;
|
|
22
|
+
this.#expirations += 1;
|
|
15
23
|
return void 0;
|
|
16
24
|
}
|
|
25
|
+
this.#hits += 1;
|
|
17
26
|
this.#entries.delete(key);
|
|
18
27
|
this.#entries.set(key, entry);
|
|
19
28
|
return entry.value;
|
|
@@ -23,11 +32,14 @@ var MemoryCache = class {
|
|
|
23
32
|
if (ttlMs !== void 0) {
|
|
24
33
|
entry.expiresAt = Date.now() + ttlMs;
|
|
25
34
|
}
|
|
35
|
+
this.#sets += 1;
|
|
36
|
+
this.#entries.delete(key);
|
|
26
37
|
this.#entries.set(key, entry);
|
|
27
38
|
while (this.#entries.size > this.#maxEntries) {
|
|
28
39
|
const oldest = this.#entries.keys().next().value;
|
|
29
40
|
if (oldest === void 0) return;
|
|
30
41
|
this.#entries.delete(oldest);
|
|
42
|
+
this.#evictions += 1;
|
|
31
43
|
}
|
|
32
44
|
}
|
|
33
45
|
async delete(key) {
|
|
@@ -36,6 +48,28 @@ var MemoryCache = class {
|
|
|
36
48
|
async clear() {
|
|
37
49
|
this.#entries.clear();
|
|
38
50
|
}
|
|
51
|
+
prune(now = Date.now()) {
|
|
52
|
+
let removed = 0;
|
|
53
|
+
for (const [key, entry] of this.#entries) {
|
|
54
|
+
if (entry.expiresAt !== void 0 && entry.expiresAt <= now) {
|
|
55
|
+
this.#entries.delete(key);
|
|
56
|
+
removed += 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
this.#expirations += removed;
|
|
60
|
+
return removed;
|
|
61
|
+
}
|
|
62
|
+
stats() {
|
|
63
|
+
return {
|
|
64
|
+
size: this.#entries.size,
|
|
65
|
+
maxEntries: this.#maxEntries,
|
|
66
|
+
hits: this.#hits,
|
|
67
|
+
misses: this.#misses,
|
|
68
|
+
sets: this.#sets,
|
|
69
|
+
evictions: this.#evictions,
|
|
70
|
+
expirations: this.#expirations
|
|
71
|
+
};
|
|
72
|
+
}
|
|
39
73
|
};
|
|
40
74
|
|
|
41
75
|
// src/errors/error.ts
|
|
@@ -298,94 +332,752 @@ var ArgsParser = class {
|
|
|
298
332
|
}
|
|
299
333
|
};
|
|
300
334
|
|
|
335
|
+
// src/commands/command.ts
|
|
336
|
+
function defineCommand(command) {
|
|
337
|
+
return command;
|
|
338
|
+
}
|
|
339
|
+
function defineSubcommand(command) {
|
|
340
|
+
return command;
|
|
341
|
+
}
|
|
342
|
+
function defineCommandGroup(group) {
|
|
343
|
+
return group;
|
|
344
|
+
}
|
|
345
|
+
function defineCommands(...commands) {
|
|
346
|
+
return commands;
|
|
347
|
+
}
|
|
348
|
+
function isCommandGroup(definition) {
|
|
349
|
+
return "subcommands" in definition;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// src/commands/concurrency.ts
|
|
353
|
+
var CommandConcurrencyController = class {
|
|
354
|
+
#states = /* @__PURE__ */ new Map();
|
|
355
|
+
async run(key, options, execute) {
|
|
356
|
+
const strategy = options?.strategy ?? "parallel";
|
|
357
|
+
if (strategy === "parallel") {
|
|
358
|
+
await execute(new AbortController().signal);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const max = Math.max(1, options?.max ?? 1);
|
|
362
|
+
const state = this.#states.get(key) ?? { active: 0, queue: [], controllers: /* @__PURE__ */ new Set() };
|
|
363
|
+
this.#states.set(key, state);
|
|
364
|
+
if (strategy === "replace") {
|
|
365
|
+
for (const controller2 of state.controllers) controller2.abort();
|
|
366
|
+
state.controllers.clear();
|
|
367
|
+
} else if (strategy === "reject" && state.active >= max) {
|
|
368
|
+
throw new WhaNextError("COMMAND_BUSY", "This command is already running.", {
|
|
369
|
+
context: { key, max },
|
|
370
|
+
recoverable: true
|
|
371
|
+
});
|
|
372
|
+
} else if (strategy === "queue" && state.active >= max) {
|
|
373
|
+
await new Promise((resolve2) => state.queue.push(resolve2));
|
|
374
|
+
}
|
|
375
|
+
const controller = new AbortController();
|
|
376
|
+
state.controllers.add(controller);
|
|
377
|
+
state.active += 1;
|
|
378
|
+
try {
|
|
379
|
+
await execute(controller.signal);
|
|
380
|
+
} finally {
|
|
381
|
+
state.controllers.delete(controller);
|
|
382
|
+
state.active -= 1;
|
|
383
|
+
state.queue.shift()?.();
|
|
384
|
+
if (state.active === 0 && state.queue.length === 0) {
|
|
385
|
+
this.#states.delete(key);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// src/commands/context.ts
|
|
392
|
+
var CommandContextImplementation = class {
|
|
393
|
+
message;
|
|
394
|
+
user;
|
|
395
|
+
chat;
|
|
396
|
+
group;
|
|
397
|
+
command;
|
|
398
|
+
options;
|
|
399
|
+
args;
|
|
400
|
+
locale;
|
|
401
|
+
signal;
|
|
402
|
+
client;
|
|
403
|
+
messages;
|
|
404
|
+
mediaService;
|
|
405
|
+
groups;
|
|
406
|
+
members;
|
|
407
|
+
chats;
|
|
408
|
+
users;
|
|
409
|
+
muteService;
|
|
410
|
+
#lastReply;
|
|
411
|
+
constructor(options) {
|
|
412
|
+
this.message = options.message;
|
|
413
|
+
this.user = options.message.sender;
|
|
414
|
+
this.chat = { id: options.message.chatId, isGroup: options.message.isGroup };
|
|
415
|
+
this.command = options.command;
|
|
416
|
+
this.options = options.options;
|
|
417
|
+
this.args = options.args;
|
|
418
|
+
this.locale = options.locale;
|
|
419
|
+
this.signal = options.signal;
|
|
420
|
+
this.client = options.services;
|
|
421
|
+
this.messages = options.services.messages;
|
|
422
|
+
this.mediaService = options.services.media;
|
|
423
|
+
this.groups = options.services.groups;
|
|
424
|
+
this.members = options.services.members;
|
|
425
|
+
this.chats = options.services.chats;
|
|
426
|
+
this.users = options.services.users;
|
|
427
|
+
this.muteService = options.services.mute;
|
|
428
|
+
this.group = options.message.isGroup ? this.#createGroupContext(options.message) : void 0;
|
|
429
|
+
Object.assign(this, options.message);
|
|
430
|
+
}
|
|
431
|
+
async reply(content, options = {}) {
|
|
432
|
+
const sent = await this.messages.reply(this.message, normalizeContent(content));
|
|
433
|
+
this.#lastReply = sent;
|
|
434
|
+
this.#scheduleDeletion(sent, options.deleteAfterMs);
|
|
435
|
+
return sent;
|
|
436
|
+
}
|
|
437
|
+
async defer(content = "\u23F3 _Processando..._") {
|
|
438
|
+
const sent = await this.reply(content);
|
|
439
|
+
return new DeferredReply(this.messages, sent, (message) => {
|
|
440
|
+
this.#lastReply = message;
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
async edit(content) {
|
|
444
|
+
if (!this.#lastReply) {
|
|
445
|
+
throw new WhaNextError(
|
|
446
|
+
"MESSAGE_NOT_FOUND",
|
|
447
|
+
"There is no command reply to edit. Call reply() or defer() first."
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
const edited = await this.messages.edit(this.#lastReply, content);
|
|
451
|
+
this.#lastReply = edited;
|
|
452
|
+
return edited;
|
|
453
|
+
}
|
|
454
|
+
react(emoji) {
|
|
455
|
+
return this.messages.react(this.message, emoji);
|
|
456
|
+
}
|
|
457
|
+
unreact() {
|
|
458
|
+
return this.messages.unreact(this.message);
|
|
459
|
+
}
|
|
460
|
+
delete() {
|
|
461
|
+
return this.messages.delete(this.message);
|
|
462
|
+
}
|
|
463
|
+
async deleteReply(options = {}) {
|
|
464
|
+
if (!this.#lastReply) return;
|
|
465
|
+
if (options.deleteAfterMs !== void 0 && options.deleteAfterMs > 0) {
|
|
466
|
+
this.#scheduleDeletion(this.#lastReply, options.deleteAfterMs);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
await this.messages.delete(this.#lastReply);
|
|
470
|
+
this.#lastReply = void 0;
|
|
471
|
+
}
|
|
472
|
+
#createGroupContext(message) {
|
|
473
|
+
return {
|
|
474
|
+
id: message.chatId,
|
|
475
|
+
metadata: (refresh) => this.groups.metadata(message.chatId, refresh),
|
|
476
|
+
isUserAdmin: () => this.groups.isAdmin(message.chatId, message.senderIds),
|
|
477
|
+
isBotAdmin: () => this.groups.isCurrentUserAdmin(message.chatId)
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
#scheduleDeletion(message, delayMs) {
|
|
481
|
+
if (delayMs === void 0 || delayMs <= 0) return;
|
|
482
|
+
const timer = setTimeout(() => {
|
|
483
|
+
void this.messages.delete(message).catch(() => void 0);
|
|
484
|
+
}, delayMs);
|
|
485
|
+
timer.unref?.();
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
var DeferredReply = class {
|
|
489
|
+
#messages;
|
|
490
|
+
#message;
|
|
491
|
+
#onEdit;
|
|
492
|
+
constructor(messages, message, onEdit) {
|
|
493
|
+
this.#messages = messages;
|
|
494
|
+
this.#message = message;
|
|
495
|
+
this.#onEdit = onEdit;
|
|
496
|
+
}
|
|
497
|
+
async edit(content) {
|
|
498
|
+
this.#message = await this.#messages.edit(this.#message, content);
|
|
499
|
+
this.#onEdit(this.#message);
|
|
500
|
+
return this.#message;
|
|
501
|
+
}
|
|
502
|
+
delete() {
|
|
503
|
+
return this.#messages.delete(this.#message);
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
function createCommandContext(options) {
|
|
507
|
+
return new CommandContextImplementation(options);
|
|
508
|
+
}
|
|
509
|
+
function normalizeContent(content) {
|
|
510
|
+
return typeof content === "string" ? { text: content } : content;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// src/commands/options.ts
|
|
514
|
+
var option = {
|
|
515
|
+
string(definition) {
|
|
516
|
+
return { kind: "string", ...definition };
|
|
517
|
+
},
|
|
518
|
+
number(definition) {
|
|
519
|
+
return { kind: "number", ...definition };
|
|
520
|
+
},
|
|
521
|
+
boolean(definition) {
|
|
522
|
+
return { kind: "boolean", ...definition };
|
|
523
|
+
},
|
|
524
|
+
user(definition) {
|
|
525
|
+
return { kind: "user", ...definition };
|
|
526
|
+
},
|
|
527
|
+
duration(definition) {
|
|
528
|
+
return { kind: "duration", ...definition };
|
|
529
|
+
},
|
|
530
|
+
enum(values, definition) {
|
|
531
|
+
return { kind: "enum", values, ...definition };
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
var ParsedCommandOptions = class {
|
|
535
|
+
#values;
|
|
536
|
+
constructor(values) {
|
|
537
|
+
this.#values = values;
|
|
538
|
+
}
|
|
539
|
+
get(name) {
|
|
540
|
+
return this.#values[name];
|
|
541
|
+
}
|
|
542
|
+
string(name) {
|
|
543
|
+
return this.get(name);
|
|
544
|
+
}
|
|
545
|
+
number(name) {
|
|
546
|
+
return this.get(name);
|
|
547
|
+
}
|
|
548
|
+
boolean(name) {
|
|
549
|
+
return this.get(name);
|
|
550
|
+
}
|
|
551
|
+
user(name) {
|
|
552
|
+
return this.get(name);
|
|
553
|
+
}
|
|
554
|
+
duration(name) {
|
|
555
|
+
return this.get(name);
|
|
556
|
+
}
|
|
557
|
+
enum(name) {
|
|
558
|
+
return this.get(name);
|
|
559
|
+
}
|
|
560
|
+
toJSON() {
|
|
561
|
+
return { ...this.#values };
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
async function parseCommandOptions(schema, tokens, message, users) {
|
|
565
|
+
const args = new ArgsParser(tokens);
|
|
566
|
+
const values = {};
|
|
567
|
+
for (const [name, definition] of Object.entries(schema ?? {})) {
|
|
568
|
+
const optional = definition.required !== true;
|
|
569
|
+
if (definition.kind === "user") {
|
|
570
|
+
const hasImplicitUser = message.mentionedUsers.length > 0 || message.quoted?.sender !== void 0;
|
|
571
|
+
if (optional && !hasImplicitUser && args.remaining === 0) {
|
|
572
|
+
values[name] = void 0;
|
|
573
|
+
} else {
|
|
574
|
+
values[name] = await users.resolve(message, args);
|
|
575
|
+
}
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
const argumentOptions = optional ? { optional: true } : void 0;
|
|
579
|
+
if (definition.kind === "string") {
|
|
580
|
+
const value = definition.rest ? args.rest() : args.string(name, argumentOptions);
|
|
581
|
+
if (definition.required && !value) {
|
|
582
|
+
throw missing(name);
|
|
583
|
+
}
|
|
584
|
+
if (value !== void 0 && definition.minLength !== void 0 && value.length < definition.minLength) {
|
|
585
|
+
throw invalid(name, value, `at least ${definition.minLength} characters`);
|
|
586
|
+
}
|
|
587
|
+
if (value !== void 0 && definition.maxLength !== void 0 && value.length > definition.maxLength) {
|
|
588
|
+
throw invalid(name, value, `at most ${definition.maxLength} characters`);
|
|
589
|
+
}
|
|
590
|
+
values[name] = value || void 0;
|
|
591
|
+
} else if (definition.kind === "number") {
|
|
592
|
+
const value = args.number(name, argumentOptions);
|
|
593
|
+
if (value !== void 0 && definition.min !== void 0 && value < definition.min) {
|
|
594
|
+
throw invalid(name, value, `at least ${definition.min}`);
|
|
595
|
+
}
|
|
596
|
+
if (value !== void 0 && definition.max !== void 0 && value > definition.max) {
|
|
597
|
+
throw invalid(name, value, `at most ${definition.max}`);
|
|
598
|
+
}
|
|
599
|
+
values[name] = value;
|
|
600
|
+
} else if (definition.kind === "boolean") {
|
|
601
|
+
values[name] = args.boolean(name, argumentOptions);
|
|
602
|
+
} else if (definition.kind === "duration") {
|
|
603
|
+
values[name] = args.duration(name, argumentOptions);
|
|
604
|
+
} else {
|
|
605
|
+
values[name] = args.enum(definition.values, name, argumentOptions);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (schema !== void 0 && args.remaining > 0) {
|
|
609
|
+
throw new WhaNextError("ARGUMENT_INVALID", "Too many arguments were provided.", {
|
|
610
|
+
context: { remaining: args.remaining }
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
return new ParsedCommandOptions(values);
|
|
614
|
+
}
|
|
615
|
+
function missing(name) {
|
|
616
|
+
return new WhaNextError("ARGUMENT_MISSING", `The argument "${name}" is required.`, {
|
|
617
|
+
context: { name }
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
function invalid(name, received, expected) {
|
|
621
|
+
return new WhaNextError("ARGUMENT_INVALID", `The argument "${name}" must be ${expected}.`, {
|
|
622
|
+
context: { name, received, expected }
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// src/services/user-service.ts
|
|
627
|
+
var UserService = class {
|
|
628
|
+
#group;
|
|
629
|
+
constructor(group) {
|
|
630
|
+
this.#group = group;
|
|
631
|
+
}
|
|
632
|
+
async resolve(message, args) {
|
|
633
|
+
const mentioned = message.mentionedUsers[0];
|
|
634
|
+
let user;
|
|
635
|
+
if (mentioned) {
|
|
636
|
+
if (args.peek()?.startsWith("@")) {
|
|
637
|
+
args.skip();
|
|
638
|
+
}
|
|
639
|
+
user = mentioned;
|
|
640
|
+
} else if (message.quoted?.sender) {
|
|
641
|
+
user = message.quoted.sender;
|
|
642
|
+
} else {
|
|
643
|
+
user = args.user("membro");
|
|
644
|
+
}
|
|
645
|
+
return this.#group.resolveUser(message.chatId, user);
|
|
646
|
+
}
|
|
647
|
+
from(identity) {
|
|
648
|
+
if (!identity.includes("@")) {
|
|
649
|
+
return User.fromPhoneNumber(identity);
|
|
650
|
+
}
|
|
651
|
+
return User.fromIdentities([identity]);
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
|
|
301
655
|
// src/commands/router.ts
|
|
302
656
|
var CommandRouter = class {
|
|
303
|
-
#
|
|
657
|
+
#roots = /* @__PURE__ */ new Map();
|
|
658
|
+
#definitions = /* @__PURE__ */ new Set();
|
|
304
659
|
#prefix;
|
|
305
|
-
#
|
|
306
|
-
#
|
|
307
|
-
|
|
308
|
-
|
|
660
|
+
#services;
|
|
661
|
+
#legacyOnError;
|
|
662
|
+
#globalMiddleware = [];
|
|
663
|
+
#errorHandlers = [];
|
|
664
|
+
#cooldowns = /* @__PURE__ */ new Map();
|
|
665
|
+
#concurrency = new CommandConcurrencyController();
|
|
666
|
+
#beforeExecute;
|
|
667
|
+
#afterExecute;
|
|
668
|
+
#cooldownOperations = 0;
|
|
669
|
+
constructor(servicesOrGroup, options = {}) {
|
|
670
|
+
this.#services = isRuntimeServices(servicesOrGroup) ? servicesOrGroup : createLegacyServices(servicesOrGroup);
|
|
309
671
|
this.#prefix = options.prefix ?? "!";
|
|
310
|
-
this.#
|
|
672
|
+
this.#legacyOnError = options.onError;
|
|
673
|
+
this.#beforeExecute = options.beforeExecute;
|
|
674
|
+
this.#afterExecute = options.afterExecute;
|
|
675
|
+
if (options.onCommandError) this.#errorHandlers.push(options.onCommandError);
|
|
311
676
|
if (this.#prefix.length === 0 || /\s/.test(this.#prefix)) {
|
|
312
677
|
throw new WhaNextError(
|
|
313
678
|
"ARGUMENT_INVALID",
|
|
314
679
|
"The command prefix cannot be empty or contain whitespace.",
|
|
315
|
-
{
|
|
316
|
-
context: { prefix: this.#prefix }
|
|
317
|
-
}
|
|
680
|
+
{ context: { prefix: this.#prefix } }
|
|
318
681
|
);
|
|
319
682
|
}
|
|
320
683
|
}
|
|
684
|
+
get prefix() {
|
|
685
|
+
return this.#prefix;
|
|
686
|
+
}
|
|
687
|
+
get size() {
|
|
688
|
+
return this.catalog({ includeHidden: true }).length;
|
|
689
|
+
}
|
|
321
690
|
command(definition) {
|
|
322
|
-
|
|
323
|
-
for (const name of
|
|
324
|
-
const normalized = name.toLowerCase();
|
|
325
|
-
if (this.#
|
|
691
|
+
this.#validateTree(definition, []);
|
|
692
|
+
for (const name of commandNames(definition)) {
|
|
693
|
+
const normalized = name.value.toLowerCase();
|
|
694
|
+
if (this.#roots.has(normalized)) {
|
|
326
695
|
throw new WhaNextError(
|
|
327
696
|
"ARGUMENT_INVALID",
|
|
328
697
|
`The command "${normalized}" is already registered.`
|
|
329
698
|
);
|
|
330
699
|
}
|
|
331
|
-
this.#
|
|
700
|
+
this.#roots.set(normalized, {
|
|
701
|
+
definition,
|
|
702
|
+
...name.locale ? { locale: name.locale } : {}
|
|
703
|
+
});
|
|
332
704
|
}
|
|
705
|
+
this.#definitions.add(definition);
|
|
706
|
+
return this;
|
|
707
|
+
}
|
|
708
|
+
use(middleware) {
|
|
709
|
+
this.#globalMiddleware.push(middleware);
|
|
333
710
|
return this;
|
|
334
711
|
}
|
|
712
|
+
onError(handler) {
|
|
713
|
+
this.#errorHandlers.push(handler);
|
|
714
|
+
return () => {
|
|
715
|
+
const index = this.#errorHandlers.indexOf(handler);
|
|
716
|
+
if (index >= 0) this.#errorHandlers.splice(index, 1);
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
catalog(options = {}) {
|
|
720
|
+
const commands = [...this.#definitions].flatMap((definition) => flattenCommands(definition));
|
|
721
|
+
return commands.filter((command) => (options.includeHidden || !command.definition.hidden) && (!options.category || command.category === options.category));
|
|
722
|
+
}
|
|
723
|
+
categories() {
|
|
724
|
+
return [...new Set(this.catalog().map((command) => command.category))].sort();
|
|
725
|
+
}
|
|
726
|
+
has(path2) {
|
|
727
|
+
return this.find(path2) !== void 0;
|
|
728
|
+
}
|
|
729
|
+
values() {
|
|
730
|
+
return this.catalog({ includeHidden: true });
|
|
731
|
+
}
|
|
732
|
+
find(path2) {
|
|
733
|
+
const normalized = path2.trim().toLowerCase().split(/\s+/);
|
|
734
|
+
return this.catalog({ includeHidden: true }).find((command) => command.path.join(" ").toLowerCase() === normalized.join(" ") || command.aliases.some((alias) => alias.toLowerCase() === normalized.at(-1)));
|
|
735
|
+
}
|
|
736
|
+
async help(context, options = {}) {
|
|
737
|
+
const commands = this.catalog(options);
|
|
738
|
+
const title = options.title ?? (options.category ? `\u{1F4DA} *${options.category}*` : "\u{1F4DA} *Comandos*");
|
|
739
|
+
const lines = commands.map((command) => {
|
|
740
|
+
const usage = command.definition.usage ?? `${this.#prefix}${command.path.join(" ")}${formatOptions(command.definition)}`;
|
|
741
|
+
const description = context.locale ? command.definition.localizations?.[context.locale]?.description ?? command.definition.description : command.definition.description;
|
|
742
|
+
return `\u2022 *${usage}*
|
|
743
|
+
${description}`;
|
|
744
|
+
});
|
|
745
|
+
const text = lines.length > 0 ? `${title}
|
|
746
|
+
|
|
747
|
+
${lines.join("\n\n")}` : `${title}
|
|
748
|
+
|
|
749
|
+
_Nenhum comando dispon\xEDvel._`;
|
|
750
|
+
return context.reply(text);
|
|
751
|
+
}
|
|
335
752
|
async dispatch(message) {
|
|
336
753
|
const text = message.text?.trim();
|
|
337
|
-
if (!text?.startsWith(this.#prefix))
|
|
338
|
-
return false;
|
|
339
|
-
}
|
|
754
|
+
if (!text?.startsWith(this.#prefix)) return false;
|
|
340
755
|
const tokens = tokenize(text.slice(this.#prefix.length));
|
|
341
|
-
const
|
|
342
|
-
if (!
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if (!command) {
|
|
347
|
-
return false;
|
|
348
|
-
}
|
|
756
|
+
const rootName = tokens.shift()?.toLowerCase();
|
|
757
|
+
if (!rootName) return false;
|
|
758
|
+
const root = this.#roots.get(rootName);
|
|
759
|
+
if (!root) return false;
|
|
760
|
+
let resolved;
|
|
349
761
|
try {
|
|
350
|
-
|
|
351
|
-
await command.execute(message, new ArgsParser(tokens));
|
|
352
|
-
return true;
|
|
762
|
+
resolved = this.#resolve(root, tokens);
|
|
353
763
|
} catch (error) {
|
|
354
|
-
const normalized = toWhaNextError(error, { command:
|
|
355
|
-
|
|
356
|
-
|
|
764
|
+
const normalized = toWhaNextError(error, { command: root.definition.name, messageId: message.id });
|
|
765
|
+
const fallbackDefinition = {
|
|
766
|
+
...root.definition,
|
|
767
|
+
execute: () => void 0
|
|
768
|
+
};
|
|
769
|
+
const context2 = createCommandContext({
|
|
770
|
+
message,
|
|
771
|
+
command: {
|
|
772
|
+
definition: fallbackDefinition,
|
|
773
|
+
root: root.definition,
|
|
774
|
+
path: [root.definition.name],
|
|
775
|
+
aliases: root.definition.aliases ?? [],
|
|
776
|
+
category: root.definition.category ?? "general"
|
|
777
|
+
},
|
|
778
|
+
options: new ParsedCommandOptions({}),
|
|
779
|
+
args: new ArgsParser(tokens),
|
|
780
|
+
services: this.#services,
|
|
781
|
+
signal: new AbortController().signal,
|
|
782
|
+
...root.locale ? { locale: root.locale } : {}
|
|
783
|
+
});
|
|
784
|
+
if (root.definition.hooks?.onError) {
|
|
785
|
+
await root.definition.hooks.onError(context2, normalized);
|
|
786
|
+
return true;
|
|
787
|
+
}
|
|
788
|
+
if (this.#errorHandlers.length > 0) {
|
|
789
|
+
for (const handler of this.#errorHandlers) await handler(context2, normalized);
|
|
790
|
+
return true;
|
|
791
|
+
}
|
|
792
|
+
if (this.#legacyOnError) {
|
|
793
|
+
await this.#legacyOnError(normalized, message);
|
|
357
794
|
return true;
|
|
358
795
|
}
|
|
359
796
|
throw normalized;
|
|
360
797
|
}
|
|
798
|
+
const legacyArgs = new ArgsParser(resolved.tokens);
|
|
799
|
+
let context;
|
|
800
|
+
try {
|
|
801
|
+
const parsedOptions = await parseCommandOptions(
|
|
802
|
+
resolved.registered.definition.options,
|
|
803
|
+
resolved.tokens,
|
|
804
|
+
message,
|
|
805
|
+
this.#services.users
|
|
806
|
+
);
|
|
807
|
+
const concurrency = [...resolved.layers].reverse().find((layer) => layer.concurrency)?.concurrency;
|
|
808
|
+
const concurrencyKey = this.#executionKey(
|
|
809
|
+
resolved,
|
|
810
|
+
message,
|
|
811
|
+
concurrency?.scope ?? "user-chat"
|
|
812
|
+
);
|
|
813
|
+
await this.#concurrency.run(
|
|
814
|
+
concurrencyKey,
|
|
815
|
+
concurrency,
|
|
816
|
+
async (signal) => {
|
|
817
|
+
context = createCommandContext({
|
|
818
|
+
message,
|
|
819
|
+
command: resolved.registered,
|
|
820
|
+
options: parsedOptions,
|
|
821
|
+
args: legacyArgs,
|
|
822
|
+
services: this.#services,
|
|
823
|
+
signal,
|
|
824
|
+
...resolved.locale ? { locale: resolved.locale } : {}
|
|
825
|
+
});
|
|
826
|
+
await this.#authorize(resolved.layers, context);
|
|
827
|
+
this.#consumeCooldown(resolved, context);
|
|
828
|
+
await this.#execute(resolved, context);
|
|
829
|
+
}
|
|
830
|
+
);
|
|
831
|
+
return true;
|
|
832
|
+
} catch (error) {
|
|
833
|
+
const normalized = toWhaNextError(error, {
|
|
834
|
+
command: resolved.registered.path.join(" "),
|
|
835
|
+
messageId: message.id
|
|
836
|
+
});
|
|
837
|
+
context ??= createCommandContext({
|
|
838
|
+
message,
|
|
839
|
+
command: resolved.registered,
|
|
840
|
+
options: new ParsedCommandOptions({}),
|
|
841
|
+
args: legacyArgs,
|
|
842
|
+
services: this.#services,
|
|
843
|
+
signal: new AbortController().signal,
|
|
844
|
+
...resolved.locale ? { locale: resolved.locale } : {}
|
|
845
|
+
});
|
|
846
|
+
if (await this.#handleError(resolved, context, normalized)) return true;
|
|
847
|
+
throw normalized;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
#resolve(root, inputTokens) {
|
|
851
|
+
const tokens = [...inputTokens];
|
|
852
|
+
const layers = [root.definition];
|
|
853
|
+
let current = root.definition;
|
|
854
|
+
let locale = root.locale;
|
|
855
|
+
while (isCommandGroup(current)) {
|
|
856
|
+
const name = tokens.shift()?.toLowerCase();
|
|
857
|
+
if (!name) {
|
|
858
|
+
throw new WhaNextError("ARGUMENT_MISSING", `Choose a subcommand for "${current.name}".`, {
|
|
859
|
+
context: { command: current.name }
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
const found = findChild(current.subcommands, name);
|
|
863
|
+
if (!found) {
|
|
864
|
+
throw new WhaNextError("ARGUMENT_INVALID", `The subcommand "${name}" does not exist.`, {
|
|
865
|
+
context: { command: current.name, subcommand: name }
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
current = found.definition;
|
|
869
|
+
locale ??= found.locale;
|
|
870
|
+
layers.push(current);
|
|
871
|
+
}
|
|
872
|
+
const path2 = layers.map((definition) => definition.name);
|
|
873
|
+
return {
|
|
874
|
+
registered: {
|
|
875
|
+
definition: current,
|
|
876
|
+
root: root.definition,
|
|
877
|
+
path: path2,
|
|
878
|
+
aliases: current.aliases ?? [],
|
|
879
|
+
category: current.category ?? root.definition.category ?? "general"
|
|
880
|
+
},
|
|
881
|
+
layers,
|
|
882
|
+
tokens,
|
|
883
|
+
...locale ? { locale } : {}
|
|
884
|
+
};
|
|
361
885
|
}
|
|
362
|
-
async #authorize(
|
|
363
|
-
|
|
886
|
+
async #authorize(layers, context) {
|
|
887
|
+
for (const command of layers) {
|
|
888
|
+
await this.#authorizeLegacy(command, context);
|
|
889
|
+
for (const guard of command.guards ?? []) await runGuard(guard, context);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
async #authorizeLegacy(command, context) {
|
|
893
|
+
if (command.onlyGroup && !context.isGroup) {
|
|
364
894
|
throw new WhaNextError("COMMAND_NOT_ALLOWED", "This command can only be used in groups.");
|
|
365
895
|
}
|
|
366
|
-
if (command.onlyPrivate &&
|
|
367
|
-
throw new WhaNextError(
|
|
368
|
-
"COMMAND_NOT_ALLOWED",
|
|
369
|
-
"This command can only be used in private chats."
|
|
370
|
-
);
|
|
896
|
+
if (command.onlyPrivate && context.isGroup) {
|
|
897
|
+
throw new WhaNextError("COMMAND_NOT_ALLOWED", "This command can only be used in private chats.");
|
|
371
898
|
}
|
|
372
|
-
if (command.onlyAdmin && !await this.#
|
|
373
|
-
throw new WhaNextError(
|
|
374
|
-
"COMMAND_NOT_ALLOWED",
|
|
375
|
-
"This command can only be used by group administrators."
|
|
376
|
-
);
|
|
899
|
+
if (command.onlyAdmin && !await this.#services.groups.isAdmin(context.chatId, context.senderIds)) {
|
|
900
|
+
throw new WhaNextError("COMMAND_NOT_ALLOWED", "This command can only be used by group administrators.");
|
|
377
901
|
}
|
|
378
|
-
if (command.botMustBeAdmin && !await this.#
|
|
379
|
-
throw new WhaNextError(
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
902
|
+
if (command.botMustBeAdmin && !await this.#services.groups.isCurrentUserAdmin(context.chatId)) {
|
|
903
|
+
throw new WhaNextError("BOT_NOT_ADMIN", "The connected WhatsApp account must be a group administrator.");
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
async #execute(resolved, context) {
|
|
907
|
+
const hooks = resolved.layers.map((layer) => layer.hooks).filter(Boolean);
|
|
908
|
+
if (this.#beforeExecute) await this.#beforeExecute(context);
|
|
909
|
+
for (const hook of hooks) await hook.beforeExecute?.(context);
|
|
910
|
+
const middleware = [
|
|
911
|
+
...this.#globalMiddleware,
|
|
912
|
+
...resolved.layers.flatMap((layer) => layer.middleware ?? [])
|
|
913
|
+
];
|
|
914
|
+
await composeMiddleware(middleware, context, async () => {
|
|
915
|
+
await resolved.registered.definition.execute(context, context.args);
|
|
916
|
+
});
|
|
917
|
+
for (const hook of [...hooks].reverse()) await hook.afterExecute?.(context);
|
|
918
|
+
if (this.#afterExecute) await this.#afterExecute(context);
|
|
919
|
+
}
|
|
920
|
+
#consumeCooldown(resolved, context) {
|
|
921
|
+
const config = [...resolved.layers].reverse().find((layer) => layer.cooldown)?.cooldown;
|
|
922
|
+
if (!config || config.durationMs <= 0) return;
|
|
923
|
+
const key = this.#executionKey(resolved, context, config.scope ?? "user");
|
|
924
|
+
const now = Date.now();
|
|
925
|
+
this.#cooldownOperations += 1;
|
|
926
|
+
if (this.#cooldownOperations % 256 === 0) this.#pruneCooldowns(now);
|
|
927
|
+
const expiresAt = this.#cooldowns.get(key) ?? 0;
|
|
928
|
+
if (expiresAt > now) {
|
|
929
|
+
throw new WhaNextError("COMMAND_COOLDOWN", "This command is on cooldown.", {
|
|
930
|
+
context: { retryAfterMs: expiresAt - now, key },
|
|
931
|
+
recoverable: true
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
this.#cooldowns.set(key, now + config.durationMs);
|
|
935
|
+
}
|
|
936
|
+
#pruneCooldowns(now) {
|
|
937
|
+
for (const [key, expiresAt] of this.#cooldowns) {
|
|
938
|
+
if (expiresAt <= now) this.#cooldowns.delete(key);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
#executionKey(resolved, message, scope) {
|
|
942
|
+
const command = resolved.registered.path.join("/").toLowerCase();
|
|
943
|
+
if (scope === "global") return command;
|
|
944
|
+
if (scope === "user") return `${command}:user:${message.senderId}`;
|
|
945
|
+
if (scope === "chat") return `${command}:chat:${message.chatId}`;
|
|
946
|
+
return `${command}:user-chat:${message.senderId}:${message.chatId}`;
|
|
947
|
+
}
|
|
948
|
+
async #handleError(resolved, context, error) {
|
|
949
|
+
for (const layer of [...resolved.layers].reverse()) {
|
|
950
|
+
if (layer.hooks?.onError) {
|
|
951
|
+
await layer.hooks.onError(context, error);
|
|
952
|
+
return true;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
if (this.#errorHandlers.length > 0) {
|
|
956
|
+
for (const handler of this.#errorHandlers) await handler(context, error);
|
|
957
|
+
return true;
|
|
958
|
+
}
|
|
959
|
+
if (this.#legacyOnError) {
|
|
960
|
+
await this.#legacyOnError(error, context.message);
|
|
961
|
+
return true;
|
|
962
|
+
}
|
|
963
|
+
return false;
|
|
964
|
+
}
|
|
965
|
+
#validateTree(definition, parents) {
|
|
966
|
+
if (!definition.name.trim() || /\s/.test(definition.name)) {
|
|
967
|
+
throw new WhaNextError("ARGUMENT_INVALID", "Command names cannot be empty or contain whitespace.", {
|
|
968
|
+
context: { name: definition.name }
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
for (const name of commandNames(definition)) {
|
|
972
|
+
if (!name.value.trim() || /\s/.test(name.value)) {
|
|
973
|
+
throw new WhaNextError(
|
|
974
|
+
"ARGUMENT_INVALID",
|
|
975
|
+
"Command names and aliases cannot be empty or contain whitespace.",
|
|
976
|
+
{ context: { name: name.value } }
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
if (!isCommandGroup(definition)) return;
|
|
981
|
+
if (definition.subcommands.length === 0) {
|
|
982
|
+
throw new WhaNextError("ARGUMENT_INVALID", `The command group "${definition.name}" is empty.`);
|
|
983
|
+
}
|
|
984
|
+
const names = /* @__PURE__ */ new Set();
|
|
985
|
+
for (const child of definition.subcommands) {
|
|
986
|
+
for (const name of commandNames(child)) {
|
|
987
|
+
const normalized = name.value.toLowerCase();
|
|
988
|
+
if (names.has(normalized)) {
|
|
989
|
+
throw new WhaNextError("ARGUMENT_INVALID", `Duplicate subcommand "${normalized}".`, {
|
|
990
|
+
context: { path: [...parents, definition.name].join(" ") }
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
names.add(normalized);
|
|
994
|
+
}
|
|
995
|
+
this.#validateTree(child, [...parents, definition.name]);
|
|
383
996
|
}
|
|
384
997
|
}
|
|
385
998
|
};
|
|
999
|
+
async function runGuard(guard, context) {
|
|
1000
|
+
const result = await guard(context);
|
|
1001
|
+
if (result === void 0 || result === true) return;
|
|
1002
|
+
const normalized = result === false ? { allowed: false } : result;
|
|
1003
|
+
if (normalized.allowed) return;
|
|
1004
|
+
throw new WhaNextError(
|
|
1005
|
+
normalized.code ?? "COMMAND_NOT_ALLOWED",
|
|
1006
|
+
normalized.message ?? "This command is not allowed in the current context."
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
async function composeMiddleware(middleware, context, execute) {
|
|
1010
|
+
let index = -1;
|
|
1011
|
+
const dispatch = async (position) => {
|
|
1012
|
+
if (position <= index) throw new Error("next() was called more than once.");
|
|
1013
|
+
index = position;
|
|
1014
|
+
const current = middleware[position];
|
|
1015
|
+
if (!current) return execute();
|
|
1016
|
+
await current(context, () => dispatch(position + 1));
|
|
1017
|
+
};
|
|
1018
|
+
await dispatch(0);
|
|
1019
|
+
}
|
|
1020
|
+
function commandNames(definition) {
|
|
1021
|
+
const names = [
|
|
1022
|
+
definition.name,
|
|
1023
|
+
...definition.aliases ?? []
|
|
1024
|
+
].map((value) => ({ value }));
|
|
1025
|
+
for (const [locale, localization] of Object.entries(definition.localizations ?? {})) {
|
|
1026
|
+
if (localization.name) names.push({ value: localization.name, locale });
|
|
1027
|
+
for (const alias of localization.aliases ?? []) names.push({ value: alias, locale });
|
|
1028
|
+
}
|
|
1029
|
+
return names;
|
|
1030
|
+
}
|
|
1031
|
+
function findChild(definitions, name) {
|
|
1032
|
+
for (const definition of definitions) {
|
|
1033
|
+
const found = commandNames(definition).find((candidate) => candidate.value.toLowerCase() === name);
|
|
1034
|
+
if (found) return { definition, ...found.locale ? { locale: found.locale } : {} };
|
|
1035
|
+
}
|
|
1036
|
+
return void 0;
|
|
1037
|
+
}
|
|
1038
|
+
function flattenCommands(root, parents = []) {
|
|
1039
|
+
if (isCommandGroup(root)) {
|
|
1040
|
+
return root.subcommands.flatMap((child) => flattenCommands(child, [...parents, root]));
|
|
1041
|
+
}
|
|
1042
|
+
const pathDefinitions = [...parents, root];
|
|
1043
|
+
return [{
|
|
1044
|
+
definition: root,
|
|
1045
|
+
root: pathDefinitions[0] ?? root,
|
|
1046
|
+
path: pathDefinitions.map((definition) => definition.name),
|
|
1047
|
+
aliases: root.aliases ?? [],
|
|
1048
|
+
category: root.category ?? [...parents].reverse().find((parent) => parent.category)?.category ?? "general"
|
|
1049
|
+
}];
|
|
1050
|
+
}
|
|
1051
|
+
function formatOptions(definition) {
|
|
1052
|
+
return Object.entries(definition.options ?? {}).map(([name, option2]) => option2.required ? ` <${name}>` : ` [${name}]`).join("");
|
|
1053
|
+
}
|
|
386
1054
|
function tokenize(input) {
|
|
387
1055
|
return input.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map((token) => token.replace(/^(["'])(.*)\1$/, "$2")) ?? [];
|
|
388
1056
|
}
|
|
1057
|
+
function isRuntimeServices(value) {
|
|
1058
|
+
return "groups" in value && "messages" in value;
|
|
1059
|
+
}
|
|
1060
|
+
function createLegacyServices(group) {
|
|
1061
|
+
const unavailable = new Proxy({}, {
|
|
1062
|
+
get() {
|
|
1063
|
+
return () => {
|
|
1064
|
+
throw new WhaNextError(
|
|
1065
|
+
"PROVIDER_ERROR",
|
|
1066
|
+
"This CommandRouter was created without the full application services."
|
|
1067
|
+
);
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
return {
|
|
1072
|
+
groups: group,
|
|
1073
|
+
users: new UserService(group),
|
|
1074
|
+
messages: unavailable,
|
|
1075
|
+
media: unavailable,
|
|
1076
|
+
members: unavailable,
|
|
1077
|
+
chats: unavailable,
|
|
1078
|
+
mute: unavailable
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
389
1081
|
|
|
390
1082
|
// src/logger/logger.ts
|
|
391
1083
|
var priorities = {
|
|
@@ -1076,6 +1768,9 @@ var MediaService = class {
|
|
|
1076
1768
|
audio(chatId, content) {
|
|
1077
1769
|
return this.#provider.sendMessage(chatId, content);
|
|
1078
1770
|
}
|
|
1771
|
+
sticker(chatId, content) {
|
|
1772
|
+
return this.#provider.sendMessage(chatId, content);
|
|
1773
|
+
}
|
|
1079
1774
|
download(message) {
|
|
1080
1775
|
const key = "keys" in message ? message.keys : message;
|
|
1081
1776
|
return this.#provider.downloadMedia(key);
|
|
@@ -1187,35 +1882,6 @@ var MessageService = class {
|
|
|
1187
1882
|
}
|
|
1188
1883
|
};
|
|
1189
1884
|
|
|
1190
|
-
// src/services/user-service.ts
|
|
1191
|
-
var UserService = class {
|
|
1192
|
-
#group;
|
|
1193
|
-
constructor(group) {
|
|
1194
|
-
this.#group = group;
|
|
1195
|
-
}
|
|
1196
|
-
async resolve(message, args) {
|
|
1197
|
-
const mentioned = message.mentionedUsers[0];
|
|
1198
|
-
let user;
|
|
1199
|
-
if (mentioned) {
|
|
1200
|
-
if (args.peek()?.startsWith("@")) {
|
|
1201
|
-
args.skip();
|
|
1202
|
-
}
|
|
1203
|
-
user = mentioned;
|
|
1204
|
-
} else if (message.quoted?.sender) {
|
|
1205
|
-
user = message.quoted.sender;
|
|
1206
|
-
} else {
|
|
1207
|
-
user = args.user("membro");
|
|
1208
|
-
}
|
|
1209
|
-
return this.#group.resolveUser(message.chatId, user);
|
|
1210
|
-
}
|
|
1211
|
-
from(identity) {
|
|
1212
|
-
if (!identity.includes("@")) {
|
|
1213
|
-
return User.fromPhoneNumber(identity);
|
|
1214
|
-
}
|
|
1215
|
-
return User.fromIdentities([identity]);
|
|
1216
|
-
}
|
|
1217
|
-
};
|
|
1218
|
-
|
|
1219
1885
|
// src/app/whanext-app.ts
|
|
1220
1886
|
var WhaNextApp = class {
|
|
1221
1887
|
message;
|
|
@@ -1226,6 +1892,7 @@ var WhaNextApp = class {
|
|
|
1226
1892
|
user;
|
|
1227
1893
|
mute;
|
|
1228
1894
|
logger;
|
|
1895
|
+
commands;
|
|
1229
1896
|
#provider;
|
|
1230
1897
|
#phone;
|
|
1231
1898
|
#events = new TypedEventEmitter();
|
|
@@ -1248,10 +1915,19 @@ var WhaNextApp = class {
|
|
|
1248
1915
|
const muteEnabled = options.mute?.enabled === true || options.mute?.store !== void 0;
|
|
1249
1916
|
const muteStore = muteEnabled ? options.mute?.store ?? new SqliteMuteStore(options.mute?.database) : void 0;
|
|
1250
1917
|
this.mute = new MuteService(provider, muteStore);
|
|
1251
|
-
this
|
|
1918
|
+
this.commands = new CommandRouter({
|
|
1919
|
+
messages: this.message,
|
|
1920
|
+
media: this.media,
|
|
1921
|
+
groups: this.group,
|
|
1922
|
+
members: this.member,
|
|
1923
|
+
chats: this.chat,
|
|
1924
|
+
users: this.user,
|
|
1925
|
+
mute: this.mute
|
|
1926
|
+
}, {
|
|
1252
1927
|
...options.router,
|
|
1253
1928
|
...options.prefix !== void 0 ? { prefix: options.prefix } : {}
|
|
1254
1929
|
});
|
|
1930
|
+
this.#router = this.commands;
|
|
1255
1931
|
this.#bind();
|
|
1256
1932
|
this.logger.debug("Application initialized", {
|
|
1257
1933
|
muteEnabled: this.mute.enabled,
|
|
@@ -1656,6 +2332,12 @@ var BaileysProvider = class {
|
|
|
1656
2332
|
#logger;
|
|
1657
2333
|
#messageStore = /* @__PURE__ */ new Map();
|
|
1658
2334
|
#messageCacheSize;
|
|
2335
|
+
#groupMetadataCache = /* @__PURE__ */ new Map();
|
|
2336
|
+
#groupMetadataRequests = /* @__PURE__ */ new Map();
|
|
2337
|
+
#groupMetadataGenerations = /* @__PURE__ */ new Map();
|
|
2338
|
+
#groupMetadataCacheEnabled;
|
|
2339
|
+
#groupMetadataCacheTtlMs;
|
|
2340
|
+
#groupMetadataCacheSize;
|
|
1659
2341
|
#socket;
|
|
1660
2342
|
#saveCredentials;
|
|
1661
2343
|
#saveQueue = Promise.resolve();
|
|
@@ -1667,6 +2349,9 @@ var BaileysProvider = class {
|
|
|
1667
2349
|
this.#options = options;
|
|
1668
2350
|
this.#logger = options.logger ?? new Logger("silent");
|
|
1669
2351
|
this.#messageCacheSize = Math.max(1, options.messageCacheSize ?? 1e3);
|
|
2352
|
+
this.#groupMetadataCacheEnabled = options.groupMetadataCache?.enabled !== false;
|
|
2353
|
+
this.#groupMetadataCacheTtlMs = Math.max(1, options.groupMetadataCache?.ttlMs ?? 3e5);
|
|
2354
|
+
this.#groupMetadataCacheSize = Math.max(1, options.groupMetadataCache?.maxEntries ?? 1e3);
|
|
1670
2355
|
}
|
|
1671
2356
|
on(event, listener) {
|
|
1672
2357
|
return this.#events.on(event, listener);
|
|
@@ -1691,6 +2376,7 @@ var BaileysProvider = class {
|
|
|
1691
2376
|
markOnlineOnConnect: false,
|
|
1692
2377
|
enableAutoSessionRecreation: true,
|
|
1693
2378
|
enableRecentMessageCache: true,
|
|
2379
|
+
cachedGroupMetadata: async (jid) => this.#getGroupMetadata(jid),
|
|
1694
2380
|
getMessage: async (key) => this.#messageStore.get(this.#messageStoreKey(key))?.message ?? void 0
|
|
1695
2381
|
});
|
|
1696
2382
|
this.#socket = socket;
|
|
@@ -1814,7 +2500,7 @@ var BaileysProvider = class {
|
|
|
1814
2500
|
await this.#requireSocket().sendMessage(key.chatId, { delete: this.#toWaKey(key) });
|
|
1815
2501
|
}
|
|
1816
2502
|
async getGroup(groupId) {
|
|
1817
|
-
const metadata = await this.#
|
|
2503
|
+
const metadata = await this.#getGroupMetadata(groupId);
|
|
1818
2504
|
return {
|
|
1819
2505
|
id: metadata.id,
|
|
1820
2506
|
subject: metadata.subject,
|
|
@@ -1832,6 +2518,7 @@ var BaileysProvider = class {
|
|
|
1832
2518
|
async setGroupAccess(groupId, access) {
|
|
1833
2519
|
const setting = access === "closed" ? "announcement" : "not_announcement";
|
|
1834
2520
|
await this.#requireSocket().groupSettingUpdate(groupId, setting);
|
|
2521
|
+
this.#invalidateGroupMetadata(groupId);
|
|
1835
2522
|
}
|
|
1836
2523
|
async getGroupInviteCode(groupId) {
|
|
1837
2524
|
const code = await this.#requireSocket().groupInviteCode(groupId);
|
|
@@ -1862,6 +2549,7 @@ var BaileysProvider = class {
|
|
|
1862
2549
|
}
|
|
1863
2550
|
async updateParticipant(groupId, memberId, action) {
|
|
1864
2551
|
const [result] = await this.#requireSocket().groupParticipantsUpdate(groupId, [memberId], action);
|
|
2552
|
+
this.#invalidateGroupMetadata(groupId);
|
|
1865
2553
|
const status = result?.status ?? "unknown";
|
|
1866
2554
|
return {
|
|
1867
2555
|
success: status === "200",
|
|
@@ -1894,10 +2582,14 @@ var BaileysProvider = class {
|
|
|
1894
2582
|
});
|
|
1895
2583
|
socket.ev.on("groups.update", (groups) => {
|
|
1896
2584
|
for (const group of groups) {
|
|
1897
|
-
if (group.id)
|
|
2585
|
+
if (group.id) {
|
|
2586
|
+
this.#invalidateGroupMetadata(group.id);
|
|
2587
|
+
void this.#events.emit("groupChanged", { groupId: group.id });
|
|
2588
|
+
}
|
|
1898
2589
|
}
|
|
1899
2590
|
});
|
|
1900
2591
|
socket.ev.on("group-participants.update", (update) => {
|
|
2592
|
+
this.#invalidateGroupMetadata(update.id);
|
|
1901
2593
|
const change = this.#groupParticipantsChanged(update);
|
|
1902
2594
|
void this.#events.emit("groupParticipantsChanged", change);
|
|
1903
2595
|
const { id } = update;
|
|
@@ -2000,6 +2692,9 @@ var BaileysProvider = class {
|
|
|
2000
2692
|
...content.gif !== void 0 ? { gifPlayback: content.gif } : {}
|
|
2001
2693
|
};
|
|
2002
2694
|
}
|
|
2695
|
+
if ("sticker" in content) {
|
|
2696
|
+
return { sticker: this.#media(content.sticker) };
|
|
2697
|
+
}
|
|
2003
2698
|
return {
|
|
2004
2699
|
audio: this.#media(content.audio),
|
|
2005
2700
|
...content.mimetype ? { mimetype: content.mimetype } : {},
|
|
@@ -2078,6 +2773,52 @@ var BaileysProvider = class {
|
|
|
2078
2773
|
if (oldest) this.#messageStore.delete(oldest);
|
|
2079
2774
|
}
|
|
2080
2775
|
}
|
|
2776
|
+
async #getGroupMetadata(groupId) {
|
|
2777
|
+
if (this.#groupMetadataCacheEnabled) {
|
|
2778
|
+
const cached = this.#groupMetadataCache.get(groupId);
|
|
2779
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
2780
|
+
this.#groupMetadataCache.delete(groupId);
|
|
2781
|
+
this.#groupMetadataCache.set(groupId, cached);
|
|
2782
|
+
return cached.value;
|
|
2783
|
+
}
|
|
2784
|
+
if (cached) this.#groupMetadataCache.delete(groupId);
|
|
2785
|
+
}
|
|
2786
|
+
const generation = this.#groupMetadataGenerations.get(groupId) ?? 0;
|
|
2787
|
+
const pending = this.#groupMetadataRequests.get(groupId);
|
|
2788
|
+
if (pending?.generation === generation) return pending.promise;
|
|
2789
|
+
const request = this.#requireSocket().groupMetadata(groupId);
|
|
2790
|
+
const requestEntry = { generation, promise: request };
|
|
2791
|
+
this.#groupMetadataRequests.set(groupId, requestEntry);
|
|
2792
|
+
try {
|
|
2793
|
+
const metadata = await request;
|
|
2794
|
+
if ((this.#groupMetadataGenerations.get(groupId) ?? 0) === generation) {
|
|
2795
|
+
this.#rememberGroupMetadata(groupId, metadata);
|
|
2796
|
+
}
|
|
2797
|
+
return metadata;
|
|
2798
|
+
} finally {
|
|
2799
|
+
if (this.#groupMetadataRequests.get(groupId) === requestEntry) {
|
|
2800
|
+
this.#groupMetadataRequests.delete(groupId);
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
#rememberGroupMetadata(groupId, metadata) {
|
|
2805
|
+
if (!this.#groupMetadataCacheEnabled) return;
|
|
2806
|
+
this.#groupMetadataCache.delete(groupId);
|
|
2807
|
+
this.#groupMetadataCache.set(groupId, {
|
|
2808
|
+
value: metadata,
|
|
2809
|
+
expiresAt: Date.now() + this.#groupMetadataCacheTtlMs
|
|
2810
|
+
});
|
|
2811
|
+
while (this.#groupMetadataCache.size > this.#groupMetadataCacheSize) {
|
|
2812
|
+
const oldest = this.#groupMetadataCache.keys().next().value;
|
|
2813
|
+
if (oldest === void 0) return;
|
|
2814
|
+
this.#groupMetadataCache.delete(oldest);
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
#invalidateGroupMetadata(groupId) {
|
|
2818
|
+
this.#groupMetadataCache.delete(groupId);
|
|
2819
|
+
const generation = this.#groupMetadataGenerations.get(groupId) ?? 0;
|
|
2820
|
+
this.#groupMetadataGenerations.set(groupId, generation + 1);
|
|
2821
|
+
}
|
|
2081
2822
|
#messageStoreKey(key) {
|
|
2082
2823
|
const chatId = "chatId" in key ? key.chatId : key.remoteJid;
|
|
2083
2824
|
return `${chatId ?? ""}:${key.id ?? ""}`;
|
|
@@ -2092,6 +2833,10 @@ async function create(options = {}) {
|
|
|
2092
2833
|
browser: options.browser ?? "windows" /* Windows */,
|
|
2093
2834
|
logger: logger.child("provider"),
|
|
2094
2835
|
...options.messageCacheSize !== void 0 ? { messageCacheSize: options.messageCacheSize } : {},
|
|
2836
|
+
groupMetadataCache: {
|
|
2837
|
+
...options.cache?.groupTtlMs !== void 0 ? { ttlMs: options.cache.groupTtlMs } : {},
|
|
2838
|
+
...options.cache?.memoryMaxEntries !== void 0 ? { maxEntries: options.cache.memoryMaxEntries } : {}
|
|
2839
|
+
},
|
|
2095
2840
|
...options.reconnect ? { reconnect: options.reconnect } : {}
|
|
2096
2841
|
});
|
|
2097
2842
|
return new WhaNextApp(provider, {
|
|
@@ -2104,13 +2849,47 @@ async function create(options = {}) {
|
|
|
2104
2849
|
}, logger);
|
|
2105
2850
|
}
|
|
2106
2851
|
|
|
2107
|
-
// src/commands/
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2852
|
+
// src/commands/guards.ts
|
|
2853
|
+
var guards = {
|
|
2854
|
+
group() {
|
|
2855
|
+
return (context) => context.isGroup || {
|
|
2856
|
+
allowed: false,
|
|
2857
|
+
code: "COMMAND_NOT_ALLOWED",
|
|
2858
|
+
message: "This command can only be used in groups."
|
|
2859
|
+
};
|
|
2860
|
+
},
|
|
2861
|
+
private() {
|
|
2862
|
+
return (context) => !context.isGroup || {
|
|
2863
|
+
allowed: false,
|
|
2864
|
+
code: "COMMAND_NOT_ALLOWED",
|
|
2865
|
+
message: "This command can only be used in private chats."
|
|
2866
|
+
};
|
|
2867
|
+
},
|
|
2868
|
+
userAdmin() {
|
|
2869
|
+
return async (context) => context.isGroup && await context.groups.isAdmin(context.chatId, context.senderIds) || {
|
|
2870
|
+
allowed: false,
|
|
2871
|
+
code: "COMMAND_NOT_ALLOWED",
|
|
2872
|
+
message: "This command can only be used by group administrators."
|
|
2873
|
+
};
|
|
2874
|
+
},
|
|
2875
|
+
botAdmin() {
|
|
2876
|
+
return async (context) => context.isGroup && await context.groups.isCurrentUserAdmin(context.chatId) || {
|
|
2877
|
+
allowed: false,
|
|
2878
|
+
code: "BOT_NOT_ADMIN",
|
|
2879
|
+
message: "The connected WhatsApp account must be a group administrator."
|
|
2880
|
+
};
|
|
2881
|
+
},
|
|
2882
|
+
botEnabled(check) {
|
|
2883
|
+
return async (context) => await check(context) || {
|
|
2884
|
+
allowed: false,
|
|
2885
|
+
code: "COMMAND_NOT_ALLOWED",
|
|
2886
|
+
message: "Commands are disabled in this chat."
|
|
2887
|
+
};
|
|
2888
|
+
},
|
|
2889
|
+
custom(guard) {
|
|
2890
|
+
return guard;
|
|
2891
|
+
}
|
|
2892
|
+
};
|
|
2114
2893
|
|
|
2115
2894
|
// src/commands/load-commands.ts
|
|
2116
2895
|
import { readdir } from "fs/promises";
|
|
@@ -2183,23 +2962,31 @@ async function importCommands(filePath) {
|
|
|
2183
2962
|
return definitions;
|
|
2184
2963
|
}
|
|
2185
2964
|
function isCommandDefinition(value) {
|
|
2186
|
-
|
|
2965
|
+
const candidate = value;
|
|
2966
|
+
return typeof value === "object" && value !== null && typeof candidate.name === "string" && (typeof candidate.execute === "function" || Array.isArray(candidate.subcommands));
|
|
2187
2967
|
}
|
|
2188
2968
|
export {
|
|
2189
2969
|
ArgsParser,
|
|
2190
2970
|
Browser,
|
|
2191
2971
|
CommandRouter,
|
|
2972
|
+
DeferredReply,
|
|
2192
2973
|
Logger,
|
|
2193
2974
|
MemoryCache,
|
|
2194
2975
|
MuteService,
|
|
2976
|
+
ParsedCommandOptions,
|
|
2195
2977
|
SqliteMuteStore,
|
|
2196
2978
|
User,
|
|
2197
2979
|
WhaNextApp,
|
|
2198
2980
|
WhaNextError,
|
|
2199
2981
|
create,
|
|
2200
2982
|
defineCommand,
|
|
2983
|
+
defineCommandGroup,
|
|
2201
2984
|
defineCommands,
|
|
2985
|
+
defineSubcommand,
|
|
2986
|
+
guards,
|
|
2987
|
+
isCommandGroup,
|
|
2202
2988
|
loadCommands,
|
|
2989
|
+
option,
|
|
2203
2990
|
toWhaNextError
|
|
2204
2991
|
};
|
|
2205
2992
|
//# sourceMappingURL=index.js.map
|