meocord 4.2.1 → 4.2.3
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 +188 -0
- package/dist/cjs/_shared/{controller.decorator-XKMlcUZz.cjs → controller.decorator-hp5xdJU4.cjs} +550 -189
- package/dist/cjs/_shared/{startup-roots-CgfPQrNl.cjs → event-sources-DzqTODXg.cjs} +274 -88
- package/dist/cjs/_shared/{providers-BZgJyfZS.cjs → providers-BBvbnmLP.cjs} +837 -295
- package/dist/cjs/_shared/{sharded-cooldown-store-CsJJyCZd.cjs → sharded-cooldown-store-B62g5O88.cjs} +11 -9
- package/dist/cjs/_shared/{translate-error-BUAR1WYG.cjs → translate-error-wsgfsUET.cjs} +1 -1
- package/dist/cjs/common/index.cjs +4 -104
- package/dist/cjs/core/index.cjs +435 -182
- package/dist/cjs/decorator/index.cjs +116 -121
- package/dist/cjs/testing/index.cjs +1923 -808
- package/dist/cli.json +1 -1
- package/dist/esm/bin/app-template/package.json.template +9 -9
- package/dist/esm/bin/app-template/src/main.ts.template +3 -2
- package/dist/esm/bin/generator.js +6 -4
- package/dist/esm/bin/helper/filter-generator.helper.js +6 -4
- package/dist/esm/bin/helper/guard-generator.helper.js +6 -4
- package/dist/esm/bin/helper/interceptor-generator.helper.js +6 -4
- package/dist/esm/bin/helper/observer-generator.helper.js +6 -4
- package/dist/esm/bin/helper/pipe-generator.helper.js +6 -4
- package/dist/esm/bin/helper/service-generator.helper.js +6 -4
- package/dist/esm/bin/meocord.js +106 -48
- package/dist/esm/build/native-addons.js +75 -19
- package/dist/esm/build/rsbuild-config.js +11 -3
- package/dist/esm/build/typed-externals.js +15 -2
- package/dist/esm/common/cooldown-store.js +38 -14
- package/dist/esm/common/deprecation.js +4 -1
- package/dist/esm/common/execution-context.js +3 -1
- package/dist/esm/common/explained-error.js +7 -3
- package/dist/esm/common/log-level.js +13 -9
- package/dist/esm/common/logger.js +4 -2
- package/dist/esm/common/meocord-text.js +11 -6
- package/dist/esm/common/redis-cooldown-store.js +3 -7
- package/dist/esm/common/response/flags.js +17 -1
- package/dist/esm/common/response/install-context.js +27 -16
- package/dist/esm/common/response/presenter.js +7 -3
- package/dist/esm/common/response/response-state.js +32 -10
- package/dist/esm/common/sharded-cooldown-store.js +11 -9
- package/dist/esm/common/theme.js +3 -1
- package/dist/esm/common/translator.js +27 -8
- package/dist/esm/core/autocomplete-routes.js +26 -0
- package/dist/esm/core/command-conflicts.js +15 -7
- package/dist/esm/core/command-registration.js +15 -1
- package/dist/esm/core/cooldown-runner.js +7 -9
- package/dist/esm/core/defer.js +2 -5
- package/dist/esm/core/dispatcher.js +92 -66
- package/dist/esm/core/event-requirements.js +67 -19
- package/dist/esm/core/event-sources.js +100 -0
- package/dist/esm/core/fallback.js +3 -1
- package/dist/esm/core/filter-runner.js +6 -6
- package/dist/esm/core/guard-runner.js +173 -52
- package/dist/esm/core/handler-input.js +24 -3
- package/dist/esm/core/handler-pipeline.js +54 -32
- package/dist/esm/core/handler-registry.js +51 -21
- package/dist/esm/core/input-runner.js +7 -8
- package/dist/esm/core/interceptor-runner.js +4 -5
- package/dist/esm/core/lifecycle-hooks.js +3 -8
- package/dist/esm/core/lifecycle-order.js +1 -15
- package/dist/esm/core/meocord-classes.js +14 -1
- package/dist/esm/core/meocord-factory.js +83 -54
- package/dist/esm/core/meocord.app.js +174 -89
- package/dist/esm/core/message-help.js +37 -15
- package/dist/esm/core/message-params.js +126 -46
- package/dist/esm/core/message-routes.js +225 -45
- package/dist/esm/core/message-words.js +30 -2
- package/dist/esm/core/observer-runner.js +4 -2
- package/dist/esm/core/process-state.js +19 -0
- package/dist/esm/core/providers.js +39 -4
- package/dist/esm/core/scalar-types.js +57 -16
- package/dist/esm/core/shard-context.js +11 -10
- package/dist/esm/core/shard-exit.js +3 -3
- package/dist/esm/core/shard-manager.js +16 -6
- package/dist/esm/core/stage-scope.js +2 -1
- package/dist/esm/core/startup-roots.js +28 -8
- package/dist/esm/core/theme-resolvers.js +26 -11
- package/dist/esm/core/theme-runtime.js +23 -13
- package/dist/esm/core/theme-scope.js +31 -30
- package/dist/esm/core/theme-validation.js +1 -1
- package/dist/esm/decorator/app.decorator.js +37 -1
- package/dist/esm/decorator/command-builder.decorator.js +14 -0
- package/dist/esm/decorator/controller-class.decorator.js +1 -1
- package/dist/esm/decorator/controller.decorator.js +48 -19
- package/dist/esm/decorator/defer.decorator.js +2 -2
- package/dist/esm/decorator/event.decorator.js +4 -5
- package/dist/esm/decorator/filter.decorator.js +4 -4
- package/dist/esm/decorator/guard.decorator.js +4 -61
- package/dist/esm/decorator/inject.decorator.js +4 -1
- package/dist/esm/decorator/interceptor.decorator.js +2 -2
- package/dist/esm/decorator/theme.decorator.js +4 -3
- package/dist/esm/decorator/validation.decorator.js +5 -5
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/catalog.js +32 -25
- package/dist/esm/testing/cooldown-store-suite.js +3 -3
- package/dist/esm/testing/discord-defaults.js +449 -335
- package/dist/esm/testing/inspect-handler.js +91 -10
- package/dist/esm/testing/meocord-testing-module.js +217 -91
- package/dist/esm/testing/mock-fn.js +8 -2
- package/dist/esm/testing/mock-interaction.js +864 -124
- package/dist/esm/testing/response-log.js +8 -0
- package/dist/esm/testing/response.js +7 -5
- package/dist/esm/testing/routing.js +48 -6
- package/dist/esm/testing/strict-mocks.js +8 -4
- package/dist/esm/testing/theme.js +3 -2
- package/dist/esm/util/common.util.js +30 -9
- package/dist/esm/util/generator-cli.util.js +6 -4
- package/dist/esm/util/inherited-env.util.js +72 -13
- package/dist/esm/util/json.util.js +10 -3
- package/dist/esm/util/meocord-cli.util.js +3 -2
- package/dist/esm/util/meocord-config-loader.util.js +14 -1
- package/dist/esm/util/meocord-config-validation.util.js +13 -3
- package/dist/esm/util/meocord-source-config.util.js +13 -3
- package/dist/esm/util/platform.util.js +3 -2
- package/dist/esm/util/refusal.util.js +29 -11
- package/dist/esm/util/registration-mode.util.js +14 -1
- package/dist/esm/util/timer-limit.util.js +4 -1
- package/dist/esm/util/tsconfig-paths.util.js +38 -7
- package/dist/esm/util/tsconfig.util.js +16 -58
- package/dist/esm/util/user-text.util.js +8 -4
- package/dist/esm/util/value.util.js +33 -5
- package/dist/types/common/index.d.cts +17 -10
- package/dist/types/common/index.d.ts +17 -10
- package/dist/types/core/index.d.cts +21 -11
- package/dist/types/core/index.d.ts +21 -11
- package/dist/types/decorator/index.d.cts +63 -63
- package/dist/types/decorator/index.d.ts +63 -63
- package/dist/types/{index-DKJclIi-.d.ts → index-Brkd6QGL.d.ts} +81 -35
- package/dist/types/{index-Dlf0ZZtS.d.cts → index-CqXImwF_.d.cts} +81 -35
- package/dist/types/interface/index.d.cts +1 -1
- package/dist/types/interface/index.d.ts +1 -1
- package/dist/types/{route-Bh1U13yy.d.ts → route-IQrK-MN2.d.cts} +11 -3
- package/dist/types/{route-Bh1U13yy.d.cts → route-bI-okq7g.d.ts} +11 -3
- package/dist/types/testing/index.d.cts +158 -61
- package/dist/types/testing/index.d.ts +158 -61
- package/dist/types/{theme-resolvers-CyEz9pnv.d.cts → theme-resolvers-AtgeaYqF.d.cts} +1 -1
- package/dist/types/{theme-resolvers-DA204O-q.d.ts → theme-resolvers-BgqysV8d.d.ts} +1 -1
- package/dist/types/{translator-Bg7hwxDF.d.cts → translator-D8aydlCE.d.ts} +12 -11
- package/dist/types/{translator-CeP4bCzp.d.ts → translator-iJhDlgwL.d.cts} +12 -11
- package/package.json +5 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var controller_decorator = require('./controller.decorator-
|
|
3
|
+
var controller_decorator = require('./controller.decorator-hp5xdJU4.cjs');
|
|
4
4
|
require('reflect-metadata');
|
|
5
5
|
var node_async_hooks = require('node:async_hooks');
|
|
6
6
|
var inversify = require('inversify');
|
|
@@ -11,8 +11,6 @@ var fs = require('fs');
|
|
|
11
11
|
var path = require('path');
|
|
12
12
|
var Table = require('cli-table3');
|
|
13
13
|
var node_crypto = require('node:crypto');
|
|
14
|
-
require('node:fs');
|
|
15
|
-
require('node:url');
|
|
16
14
|
|
|
17
15
|
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
18
16
|
|
|
@@ -90,7 +88,8 @@ var Table__default = /*#__PURE__*/_interopDefaultCompat(Table);
|
|
|
90
88
|
}
|
|
91
89
|
}
|
|
92
90
|
/** The class of a stage entry. */ function stageClass(entry) {
|
|
93
|
-
|
|
91
|
+
// Entries were checked as they were declared, so a function is the class itself
|
|
92
|
+
return typeof entry === 'function' ? entry : entry.provide;
|
|
94
93
|
}
|
|
95
94
|
/** The context types a stage declared, or `undefined` when it runs for every type. */ function stageTypes(entry) {
|
|
96
95
|
return Reflect.getMetadata(controller_decorator.META.stageTypes, stageClass(entry));
|
|
@@ -103,13 +102,12 @@ function eventDecorator(event, once) {
|
|
|
103
102
|
// Generic over the method, so it is checked against the event's arguments rather than required to match exactly
|
|
104
103
|
return controller_decorator.declaring(function(target, propertyKey, _descriptor) {
|
|
105
104
|
controller_decorator.refuseOnClass(once ? '@Once' : '@On', target, propertyKey);
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
// One the class inherits for the same method and event is replaced, as for every other kind of handler
|
|
106
|
+
controller_decorator.addOwnHandler(controller_decorator.META.eventHandlers, target, {
|
|
108
107
|
event,
|
|
109
108
|
method: propertyKey,
|
|
110
109
|
once
|
|
111
|
-
});
|
|
112
|
-
Reflect.defineMetadata(controller_decorator.META.eventHandlers, handlers, target);
|
|
110
|
+
}, (other)=>other.method === propertyKey && other.event === event);
|
|
113
111
|
});
|
|
114
112
|
}
|
|
115
113
|
/**
|
|
@@ -305,14 +303,14 @@ const perCallKeys = new WeakMap();
|
|
|
305
303
|
});
|
|
306
304
|
}
|
|
307
305
|
}
|
|
308
|
-
const logger$
|
|
306
|
+
const logger$5 = new controller_decorator.Logger('Guard');
|
|
309
307
|
const warnedShared = new WeakMap();
|
|
310
308
|
/** Warns once per guard in each app that its params are set on the one instance every call shares, and why. */ function warnSharedParams(container, guardClass, why, remedy) {
|
|
311
309
|
let warned = warnedShared.get(container);
|
|
312
310
|
if (!warned) warnedShared.set(container, warned = new Set());
|
|
313
311
|
if (warned.has(guardClass)) return;
|
|
314
312
|
warned.add(guardClass);
|
|
315
|
-
logger$
|
|
313
|
+
logger$5.warn(`${guardClass.name} is one instance every call shares, and ${why}, so each call's params are set on that ` + `instance: calls that overlap can read each other's. ${remedy}, or stop binding the guard, in services, ` + `providers or a constructor that injects it, so each call gets its own.`);
|
|
316
314
|
}
|
|
317
315
|
/** Whether each of these properties can be made to read the call's params, which a frozen or sealed instance's cannot. */ function canReadPerCall(instance, keys) {
|
|
318
316
|
return keys.every((key)=>{
|
|
@@ -426,6 +424,28 @@ const warnedShared = new WeakMap();
|
|
|
426
424
|
}
|
|
427
425
|
return undefined;
|
|
428
426
|
}
|
|
427
|
+
/**
|
|
428
|
+
* The prototypes on the chain that declare `methodName` as written, base first: the handler and each override of it,
|
|
429
|
+
* looking through methods a class-level `@UseGuard` re-declared to wrap an inherited handler. A handler's method-level
|
|
430
|
+
* stages are read from each, so an override keeps its base method's, as it keeps its route.
|
|
431
|
+
*/ function stageOwners(prototype, methodName) {
|
|
432
|
+
const owners = [];
|
|
433
|
+
for(let current = prototype; current && current !== Object.prototype; current = Object.getPrototypeOf(current)){
|
|
434
|
+
if (!Object.prototype.hasOwnProperty.call(current, methodName)) continue;
|
|
435
|
+
if (!Reflect.getOwnMetadata(controller_decorator.META.inheritedFrom, current, methodName)) owners.push(current);
|
|
436
|
+
}
|
|
437
|
+
return owners.reverse();
|
|
438
|
+
}
|
|
439
|
+
/** The entries a method-level stage lists under `key` on each of a handler's {@link stageOwners}, base first. */ function methodStages(prototype, methodName, key) {
|
|
440
|
+
return stageOwners(prototype, methodName).flatMap((owner)=>Reflect.getOwnMetadata(key, owner, methodName) ?? []);
|
|
441
|
+
}
|
|
442
|
+
/** The value a method-level stage holds under `key` on the nearest of a handler's {@link stageOwners} that declares one. */ function nearestMethodStage(prototype, methodName, key) {
|
|
443
|
+
for (const owner of stageOwners(prototype, methodName).reverse()){
|
|
444
|
+
const value = Reflect.getOwnMetadata(key, owner, methodName);
|
|
445
|
+
if (value !== undefined) return value;
|
|
446
|
+
}
|
|
447
|
+
return undefined;
|
|
448
|
+
}
|
|
429
449
|
/**
|
|
430
450
|
* Caches a function of a class's prototype and one of its handlers. Handler metadata is fixed once
|
|
431
451
|
* the classes are decorated, so dispatch resolves each handler's stages once, not on every call.
|
|
@@ -477,50 +497,165 @@ const warnedShared = new WeakMap();
|
|
|
477
497
|
* The guards dispatch runs before a handler: its classes' class-level guards, outermost first, then
|
|
478
498
|
* the method's. A method that is no handler has only its own, since class guards apply to handlers.
|
|
479
499
|
*/ const handlerGuards = perHandler((prototype, methodName)=>{
|
|
480
|
-
|
|
481
|
-
if (!source) return [];
|
|
500
|
+
if (!sourcePrototype(prototype, methodName)) return [];
|
|
482
501
|
return [
|
|
483
502
|
...handlerMethods$1(prototype).has(methodName) ? stageClasses(prototype, methodName).flatMap(classLevelGuards) : [],
|
|
484
|
-
...
|
|
503
|
+
...methodStages(prototype, methodName, controller_decorator.META.methodGuards)
|
|
485
504
|
];
|
|
486
505
|
});
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
506
|
+
/**
|
|
507
|
+
* The type a direct call to a handler runs as, which is the one dispatch gives it: `'event'` for an `@On` or `@Once`
|
|
508
|
+
* handler, as its first argument may be a message or an interaction; otherwise read from the first argument.
|
|
509
|
+
*/ const directCallType = perHandler((prototype, methodName)=>{
|
|
510
|
+
if (!getEventHandlers(prototype).some((handler)=>handler.method === methodName)) return undefined;
|
|
511
|
+
const routed = [
|
|
512
|
+
...Object.values(controller_decorator.getCommandMap(prototype) ?? {}).flat().map((command)=>command.methodName),
|
|
513
|
+
...controller_decorator.getMessageHandlers(prototype).map((handler)=>handler.method),
|
|
514
|
+
...controller_decorator.getReactionHandlers(prototype).map((handler)=>handler.method),
|
|
515
|
+
...controller_decorator.getAutocompleteHandlers(prototype).map((handler)=>handler.methodName)
|
|
516
|
+
];
|
|
517
|
+
// A method dispatch also routes as a command, message or reaction has no one type
|
|
518
|
+
return routed.includes(methodName) ? undefined : 'event';
|
|
519
|
+
});
|
|
520
|
+
/** Whether `@UseGuard` wraps the method anywhere on the chain, an override's base included. */ const isWrapped = perHandler((prototype, methodName)=>{
|
|
521
|
+
for(let current = prototype; current; current = Object.getPrototypeOf(current)){
|
|
522
|
+
if (Object.prototype.hasOwnProperty.call(current, methodName) && Reflect.getOwnMetadata(controller_decorator.META.guardWrappers, current, methodName)) return true;
|
|
523
|
+
}
|
|
524
|
+
return false;
|
|
490
525
|
});
|
|
491
526
|
/**
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
/** Takes one pass for the wrapper being entered, if dispatch left one for this call. */ function consumeDispatchMark(first, instance, methodName) {
|
|
501
|
-
const marks = dispatched.get(markKey(first, instance));
|
|
502
|
-
const remaining = marks?.get(methodName);
|
|
503
|
-
if (!marks || !remaining) return false;
|
|
504
|
-
if (remaining > 1) marks.set(methodName, remaining - 1);
|
|
505
|
-
else marks.delete(methodName);
|
|
527
|
+
* The pass of the guarded call running. Kept per call, so a concurrent call with the same object and method runs its own
|
|
528
|
+
* guards, and closed when the call returns, so work it leaves running, such as a timer, does not carry it.
|
|
529
|
+
*/ const guardedPasses = new node_async_hooks.AsyncLocalStorage();
|
|
530
|
+
/** Takes the pass for `wrapper` when it belongs to a call whose guards have already run and has not let it through yet. */ function takeGuardedPass(wrapper, first, instance, methodName) {
|
|
531
|
+
const pass = guardedPasses.getStore();
|
|
532
|
+
if (!pass?.open || pass.instance !== instance || pass.methodName !== methodName || pass.first !== first) return false;
|
|
533
|
+
if (pass.entered.has(wrapper)) return false;
|
|
534
|
+
pass.entered.add(wrapper);
|
|
506
535
|
return true;
|
|
507
536
|
}
|
|
537
|
+
/** Runs a call whose guards have run, with its pass; `wrapper` is the one that ran them, which it has entered. */ async function runWithPass(instance, methodName, args, call, wrapper) {
|
|
538
|
+
const pass = {
|
|
539
|
+
instance,
|
|
540
|
+
methodName,
|
|
541
|
+
first: args[0],
|
|
542
|
+
entered: new Set(wrapper ? [
|
|
543
|
+
wrapper
|
|
544
|
+
] : []),
|
|
545
|
+
open: true
|
|
546
|
+
};
|
|
547
|
+
try {
|
|
548
|
+
return await guardedPasses.run(pass, call);
|
|
549
|
+
} finally{
|
|
550
|
+
pass.open = false;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Adds guards to a method's class or method list, then republishes the effective list under
|
|
555
|
+
* `META.guards`, in the order they run: the bases' before the class's own, and within one class or method the
|
|
556
|
+
* latest decorator first, since it wraps outermost.
|
|
557
|
+
*/ function recordGuards(key, guards, prototype, methodName) {
|
|
558
|
+
const existing = Reflect.getOwnMetadata(key, prototype, methodName) ?? [];
|
|
559
|
+
const bases = key === controller_decorator.META.classGuards ? Reflect.getOwnMetadata(controller_decorator.META.baseClassGuards, prototype, methodName) ?? 0 : 0;
|
|
560
|
+
Reflect.defineMetadata(key, [
|
|
561
|
+
...existing.slice(0, bases),
|
|
562
|
+
...guards,
|
|
563
|
+
...existing.slice(bases)
|
|
564
|
+
], prototype, methodName);
|
|
565
|
+
const [classGuards, inheritedGuards] = [
|
|
566
|
+
controller_decorator.META.classGuards,
|
|
567
|
+
controller_decorator.META.inheritedGuards
|
|
568
|
+
].map((list)=>Reflect.getOwnMetadata(list, prototype, methodName) ?? []);
|
|
569
|
+
// The method's guards and those of the methods it overrides, base first, as dispatch runs them
|
|
570
|
+
const methodGuards = methodStages(prototype, methodName, controller_decorator.META.methodGuards);
|
|
571
|
+
Reflect.defineMetadata(controller_decorator.META.guards, [
|
|
572
|
+
...inheritedGuards,
|
|
573
|
+
...classGuards,
|
|
574
|
+
...methodGuards
|
|
575
|
+
], prototype, methodName);
|
|
576
|
+
}
|
|
577
|
+
/** The classes {@link guardOwnHandlersWithBaseGuards} has handled, as `@Controller` and each app's startup both call it. */ const guardedWithBases = new WeakSet();
|
|
578
|
+
/**
|
|
579
|
+
* Gives each handler a class declares itself the class-level guards of the classes it extends,
|
|
580
|
+
* as its inherited handlers have: recorded under `META.guards`, and wrapped so a direct call
|
|
581
|
+
* runs them when no other guard wraps the handler. `@Controller` calls this as it decorates, and
|
|
582
|
+
* {@link prepareDirectCalls} for every class an app runs, a service included. Once per class.
|
|
583
|
+
*/ function guardOwnHandlersWithBaseGuards(target) {
|
|
584
|
+
if (guardedWithBases.has(target)) return;
|
|
585
|
+
guardedWithBases.add(target);
|
|
586
|
+
const prototype = target.prototype;
|
|
587
|
+
for (const methodName of handlerMethods$1(prototype)){
|
|
588
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, methodName);
|
|
589
|
+
// Inherited handlers have the chain of the class that declares them
|
|
590
|
+
if (typeof descriptor?.value !== 'function' || Reflect.getOwnMetadata(controller_decorator.META.inheritedFrom, prototype, methodName)) continue;
|
|
591
|
+
// Base first, as dispatch runs them
|
|
592
|
+
const guards = classChain(prototype, prototype).slice(1).reverse().flatMap(classLevelGuards);
|
|
593
|
+
if (guards.length === 0) continue;
|
|
594
|
+
if (!Reflect.getOwnMetadata(controller_decorator.META.guardWrappers, prototype, methodName)) {
|
|
595
|
+
applyGuards(descriptor, prototype, methodName);
|
|
596
|
+
Object.defineProperty(prototype, methodName, descriptor);
|
|
597
|
+
}
|
|
598
|
+
recordGuards(controller_decorator.META.inheritedGuards, guards, prototype, methodName);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
508
601
|
/** The container that made each instance, so a direct call runs in its own app or testing module. */ const instanceContainers = new WeakMap();
|
|
509
602
|
/**
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
|
|
603
|
+
* Readies direct calls to the handlers of the classes an app or testing module runs, controllers and services alike:
|
|
604
|
+
* - stamps each class with the container, and records each instance the container makes as its own, so two testing
|
|
605
|
+
* modules made from one class each run a direct call on their own instance in themselves;
|
|
606
|
+
* - gives each handler a class declares itself the class-level guards of the classes it extends, and wraps each
|
|
607
|
+
* override of a guarded handler, so a direct call to it runs the guards it inherits before its body.
|
|
608
|
+
*/ function prepareDirectCalls(container, classes) {
|
|
513
609
|
for (const cls of classes){
|
|
610
|
+
Reflect.defineMetadata(controller_decorator.META.container, container, cls);
|
|
611
|
+
// Each class on the chain, base first, so an override above the one the app runs is wrapped too
|
|
612
|
+
for (const owner of classesOf(cls).reverse())guardOwnHandlersWithBaseGuards(owner);
|
|
613
|
+
wrapGuardedOverrides(cls.prototype);
|
|
514
614
|
container.onActivation(cls, (_context, instance)=>{
|
|
515
615
|
if (typeof instance === 'object' && instance !== null) instanceContainers.set(instance, container);
|
|
516
616
|
return instance;
|
|
517
617
|
});
|
|
518
618
|
}
|
|
519
619
|
}
|
|
620
|
+
/** `cls` and the classes it extends, innermost first. */ function classesOf(cls) {
|
|
621
|
+
const classes = [];
|
|
622
|
+
for(let current = cls.prototype; current && current !== Object.prototype; current = Object.getPrototypeOf(current)){
|
|
623
|
+
classes.push(current.constructor);
|
|
624
|
+
}
|
|
625
|
+
return classes;
|
|
626
|
+
}
|
|
627
|
+
/** Wraps each method on the chain that overrides a handler a base method guards and is not wrapped itself. */ function wrapGuardedOverrides(prototype) {
|
|
628
|
+
for (const methodName of handlerMethods$1(prototype)){
|
|
629
|
+
let guardedBelow = false;
|
|
630
|
+
for (const owner of stageOwners(prototype, methodName)){
|
|
631
|
+
const descriptor = Object.getOwnPropertyDescriptor(owner, methodName);
|
|
632
|
+
if (guardedBelow && typeof descriptor?.value === 'function' && !Reflect.getOwnMetadata(controller_decorator.META.guardWrappers, owner, methodName)) {
|
|
633
|
+
applyGuards(descriptor, owner, methodName);
|
|
634
|
+
Object.defineProperty(owner, methodName, descriptor);
|
|
635
|
+
}
|
|
636
|
+
if ((Reflect.getOwnMetadata(controller_decorator.META.methodGuards, owner, methodName) ?? []).length > 0) guardedBelow = true;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Wraps a method so a direct call runs its guards first: the wrapper entered first runs the whole
|
|
642
|
+
* chain dispatch resolves for the handler, and lets the ones inside it through. A call from dispatch,
|
|
643
|
+
* which has already run the guards, passes through every wrapper.
|
|
644
|
+
*/ function applyGuards(descriptor, prototype, propertyKey) {
|
|
645
|
+
const originalMethod = descriptor.value;
|
|
646
|
+
// Which wrapper a call has passed, so it passes each once
|
|
647
|
+
const wrapper = {};
|
|
648
|
+
descriptor.value = controller_decorator.wrapsMethod(async function(...args) {
|
|
649
|
+
if (takeGuardedPass(wrapper, args[0], this, propertyKey)) return originalMethod.apply(this, args);
|
|
650
|
+
return runDirectCall(this, propertyKey, args, ()=>originalMethod.apply(this, args), wrapper);
|
|
651
|
+
}, originalMethod);
|
|
652
|
+
const wrappers = Reflect.getOwnMetadata(controller_decorator.META.guardWrappers, prototype, propertyKey) ?? 0;
|
|
653
|
+
Reflect.defineMetadata(controller_decorator.META.guardWrappers, wrappers + 1, prototype, propertyKey);
|
|
654
|
+
}
|
|
520
655
|
/**
|
|
521
|
-
* Runs a direct call to a guarded handler: the handler's guards, as dispatch resolves them, then
|
|
522
|
-
* call with a pass
|
|
523
|
-
*/ async function runDirectCall(instance, methodName, args, call) {
|
|
656
|
+
* Runs a direct call to a guarded handler: the handler's guards, as dispatch resolves and types them, then
|
|
657
|
+
* the call with a pass every wrapper inside the one entered takes, so no guard runs twice.
|
|
658
|
+
*/ async function runDirectCall(instance, methodName, args, call, wrapper) {
|
|
524
659
|
const prototype = Object.getPrototypeOf(instance);
|
|
525
660
|
const controller = instance.constructor;
|
|
526
661
|
// The container that made the instance, or else the app that made the class or a class it extends: a subclass the
|
|
@@ -529,48 +664,30 @@ const wrapperCount = perHandler((prototype, methodName)=>{
|
|
|
529
664
|
if (!container) {
|
|
530
665
|
throw new Error(`${controller.name}.${methodName}: @UseGuard runs its guards through the app that made ${controller.name}, and this ` + `instance was made outside one. Inject ${controller.name}, or call it on an instance the app made.`);
|
|
531
666
|
}
|
|
667
|
+
const type = directCallType(prototype, methodName);
|
|
532
668
|
if (!await runGuards(handlerGuards(prototype, methodName), {
|
|
533
669
|
container,
|
|
534
670
|
controller,
|
|
535
671
|
methodName,
|
|
536
|
-
args
|
|
672
|
+
args,
|
|
673
|
+
type
|
|
537
674
|
})) return undefined;
|
|
538
|
-
|
|
539
|
-
if (inner <= 0) return call();
|
|
540
|
-
const key = markKey(args[0], instance);
|
|
541
|
-
let marks = dispatched.get(key);
|
|
542
|
-
if (!marks) dispatched.set(key, marks = new Map());
|
|
543
|
-
marks.set(methodName, inner);
|
|
544
|
-
try {
|
|
545
|
-
return await call();
|
|
546
|
-
} finally{
|
|
547
|
-
marks.delete(methodName);
|
|
548
|
-
}
|
|
675
|
+
return runWithPass(instance, methodName, args, call, wrapper);
|
|
549
676
|
}
|
|
550
677
|
/**
|
|
551
|
-
* Calls a handler dispatch has already guarded. Its wrappers let this one call through; the
|
|
552
|
-
*
|
|
678
|
+
* Calls a handler dispatch has already guarded. Its wrappers let this one call through; the pass
|
|
679
|
+
* closes afterwards, so a later direct call with the same (cached) object runs its guards.
|
|
553
680
|
*/ async function callGuardedHandler(instance, methodName, args) {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
const key = markKey(args[0], instance);
|
|
557
|
-
let marks = dispatched.get(key);
|
|
558
|
-
if (!marks) dispatched.set(key, marks = new Map());
|
|
559
|
-
marks.set(methodName, count);
|
|
560
|
-
try {
|
|
561
|
-
return await instance[methodName](...args);
|
|
562
|
-
} finally{
|
|
563
|
-
marks.delete(methodName);
|
|
564
|
-
}
|
|
681
|
+
if (!isWrapped(Object.getPrototypeOf(instance), methodName)) return instance[methodName](...args);
|
|
682
|
+
return runWithPass(instance, methodName, args, ()=>instance[methodName](...args));
|
|
565
683
|
}
|
|
566
684
|
|
|
567
|
-
/** The cooldowns on a handler: each class's, base first, then the method's. */ const handlerCooldowns = perHandler((prototype, methodName)=>{
|
|
568
|
-
|
|
569
|
-
if (!source) return [];
|
|
685
|
+
/** The cooldowns on a handler: each class's, base first, then the method's and its overrides', base first. */ const handlerCooldowns = perHandler((prototype, methodName)=>{
|
|
686
|
+
if (!sourcePrototype(prototype, methodName)) return [];
|
|
570
687
|
return [
|
|
571
688
|
// Base first, as guards run
|
|
572
689
|
...stageClasses(prototype, methodName).flatMap((cls)=>Reflect.getOwnMetadata(controller_decorator.META.classCooldowns, cls) ?? []),
|
|
573
|
-
...
|
|
690
|
+
...methodCooldowns(prototype, methodName)
|
|
574
691
|
];
|
|
575
692
|
});
|
|
576
693
|
/** The counted cooldowns of each list {@link handlerCooldowns} gives, which is the same for a handler. */ const countedCooldowns = new WeakMap();
|
|
@@ -611,9 +728,8 @@ const wrapperCount = perHandler((prototype, methodName)=>{
|
|
|
611
728
|
}
|
|
612
729
|
return counted;
|
|
613
730
|
}
|
|
614
|
-
/** The cooldowns declared on a method itself. */ function methodCooldowns(prototype, methodName) {
|
|
615
|
-
|
|
616
|
-
return source ? Reflect.getOwnMetadata(controller_decorator.META.methodCooldowns, source, methodName) ?? [] : [];
|
|
731
|
+
/** The cooldowns declared on a method itself and on the methods it overrides, base first. */ function methodCooldowns(prototype, methodName) {
|
|
732
|
+
return methodStages(prototype, methodName, controller_decorator.META.methodCooldowns);
|
|
617
733
|
}
|
|
618
734
|
/** Who and where a call came from: an interaction's or a message's user, server and channel. */ function caller(first) {
|
|
619
735
|
if (first instanceof discord_js.BaseInteraction) return {
|
|
@@ -650,7 +766,7 @@ const DEFAULT_POLICY = {
|
|
|
650
766
|
/** The app's cooldown policy, else refusing calls after a second without an answer. */ function cooldownPolicyOf(container) {
|
|
651
767
|
return container.isBound(COOLDOWN_POLICY) ? container.get(COOLDOWN_POLICY) : DEFAULT_POLICY;
|
|
652
768
|
}
|
|
653
|
-
const logger$
|
|
769
|
+
const logger$4 = new controller_decorator.Logger('Cooldown');
|
|
654
770
|
/**
|
|
655
771
|
* How long after its last failure a store must answer for its outage to end. A store that fails some calls and answers
|
|
656
772
|
* others, as a cluster with one node down does, stays in one outage rather than starting a new one with each failure.
|
|
@@ -671,15 +787,15 @@ const logger$3 = new controller_decorator.Logger('Cooldown');
|
|
|
671
787
|
});
|
|
672
788
|
const name = store.constructor.name;
|
|
673
789
|
const reason = error.timedOut ? `did not answer within ${timeoutMs} ms` : `failed: ${String(error.cause?.message ?? error.cause)}`;
|
|
674
|
-
if (failure === 'allow') logger$
|
|
675
|
-
else logger$
|
|
790
|
+
if (failure === 'allow') logger$4.warn(`The cooldown store ${name} ${reason}. Calls run uncounted until it answers again.`);
|
|
791
|
+
else logger$4.error(`The cooldown store ${name} ${reason}. Calls with a cooldown are refused until it answers again.`, error.cause ?? '');
|
|
676
792
|
}
|
|
677
793
|
/** Logs that a store answers again, once it has gone {@link OUTAGE_QUIET_MS} without a failure. */ function reportRecovery(store) {
|
|
678
794
|
const outage = outages.get(store);
|
|
679
795
|
if (!outage || Date.now() - outage.last < OUTAGE_QUIET_MS) return;
|
|
680
796
|
outages.delete(store);
|
|
681
797
|
const seconds = Math.round((outage.last - outage.since) / 1000);
|
|
682
|
-
logger$
|
|
798
|
+
logger$4.log(`The cooldown store ${store.constructor.name} answers again, after ${outage.failures} failed call(s) over ${seconds}s.`);
|
|
683
799
|
}
|
|
684
800
|
/** What a container's calls wait for before asking its store: the store's own `onReady`, which may connect it. */ const storesReady = new WeakMap();
|
|
685
801
|
/** The store operations a container's calls have under way, which its store's `onShutdown` waits for. */ const storeOperations = new WeakMap();
|
|
@@ -741,7 +857,7 @@ const logger$3 = new controller_decorator.Logger('Cooldown');
|
|
|
741
857
|
} catch (error) {
|
|
742
858
|
if (error instanceof controller_decorator.CooldownStoreError && error.timedOut && !peek && !keepLate) {
|
|
743
859
|
// Under way from now, so the store does not shut down before the call it counted is given back
|
|
744
|
-
const release = attempt.then((verdict)=>verdict.allowed ? verdict.release?.() : undefined).catch((failure)=>logger$
|
|
860
|
+
const release = attempt.then((verdict)=>verdict.allowed ? verdict.release?.() : undefined).catch((failure)=>logger$4.debug(`Could not release a call the cooldown store counted too late: ${String(failure)}`));
|
|
745
861
|
trackStoreOperation(container, release);
|
|
746
862
|
}
|
|
747
863
|
throw error instanceof controller_decorator.CooldownStoreError ? error : new controller_decorator.CooldownStoreError(error, false);
|
|
@@ -884,7 +1000,7 @@ const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[
|
|
|
884
1000
|
try {
|
|
885
1001
|
return (await askWithin(container, cooldownStoreOf(container), notices, cooldownPolicyOf(container).timeoutMs, false)).allowed;
|
|
886
1002
|
} catch (error) {
|
|
887
|
-
logger$
|
|
1003
|
+
logger$4.debug(`Could not ask the cooldown store whether to tell a refused caller: ${String(error.cause ?? error)}`);
|
|
888
1004
|
return false;
|
|
889
1005
|
}
|
|
890
1006
|
}
|
|
@@ -1058,6 +1174,7 @@ function describeLength(value) {
|
|
|
1058
1174
|
if (existing === undefined) {
|
|
1059
1175
|
const name = typeof body.name === 'string' ? body.name : commandName;
|
|
1060
1176
|
unlocalizable.push(...localizationProblems(name, body));
|
|
1177
|
+
const builderName = builderClass?.name;
|
|
1061
1178
|
byKey.set(key, {
|
|
1062
1179
|
builderClass: builderClass ?? builder,
|
|
1063
1180
|
command: {
|
|
@@ -1065,6 +1182,9 @@ function describeLength(value) {
|
|
|
1065
1182
|
body,
|
|
1066
1183
|
...guilds && {
|
|
1067
1184
|
guilds
|
|
1185
|
+
},
|
|
1186
|
+
...typeof builderName === 'string' && builderName && {
|
|
1187
|
+
builder: builderName
|
|
1068
1188
|
}
|
|
1069
1189
|
}
|
|
1070
1190
|
});
|
|
@@ -1102,6 +1222,14 @@ const scopeKey = (scope)=>scope === 'global' ? 'global' : `guild-${scope.guild}`
|
|
|
1102
1222
|
}
|
|
1103
1223
|
/** Whether a command is an activity's primary entry point, which Discord takes only globally. */ const isEntryPoint = (command)=>command.body.type === discord_js.ApplicationCommandType.PrimaryEntryPoint;
|
|
1104
1224
|
/** What a blank guild to register every command to says, as an unset variable gives one. */ const BLANK_GUILD = 'No commands were registered: the guild to register to is blank. Give a guild id, or leave it out to register where commands.* says.';
|
|
1225
|
+
/**
|
|
1226
|
+
* Names every command whose builder lists guilds but no guild id, each with its builder, and says the run registers
|
|
1227
|
+
* none of them yet still succeeds.
|
|
1228
|
+
*/ function blankGuildsWarning(commands) {
|
|
1229
|
+
const one = commands.length === 1;
|
|
1230
|
+
const lines = commands.map((command)=>` ${command.builder ? `${command.builder}: ` : ''}"${command.name}"`);
|
|
1231
|
+
return `${commands.length} ${one ? 'command is' : 'commands are'} not registered: ${one ? 'its builder lists' : 'their builders list'} ` + 'no guild ids, as when the variable holding them is unset.\n' + `${lines.join('\n')}\n` + 'Set the guild ids; a builder without guilds registers where commands.* says. `meocord register` still exits 0 for ' + 'these; the next major version (5.0) exits 1.';
|
|
1232
|
+
}
|
|
1105
1233
|
/**
|
|
1106
1234
|
* Where each command is sent: one bulk update per scope. A scope's update replaces everything the application has
|
|
1107
1235
|
* there, so the default scope is included even when it ends up empty, unless `commands.guilds` lists no guild id.
|
|
@@ -1145,6 +1273,7 @@ const scopeKey = (scope)=>scope === 'global' ? 'global' : `guild-${scope.guild}`
|
|
|
1145
1273
|
guild
|
|
1146
1274
|
});
|
|
1147
1275
|
const unregistered = [];
|
|
1276
|
+
const blankGuilds = [];
|
|
1148
1277
|
for (const command of commands){
|
|
1149
1278
|
if (isEntryPoint(command)) {
|
|
1150
1279
|
if (command.guilds) {
|
|
@@ -1154,7 +1283,7 @@ const scopeKey = (scope)=>scope === 'global' ? 'global' : `guild-${scope.guild}`
|
|
|
1154
1283
|
} else if (command.guilds) {
|
|
1155
1284
|
const own = ids(command.guilds);
|
|
1156
1285
|
// Falling back to the default scope could publish a staff-only command everywhere.
|
|
1157
|
-
if (own.length === 0)
|
|
1286
|
+
if (own.length === 0) blankGuilds.push(command);
|
|
1158
1287
|
for (const guild of own)add({
|
|
1159
1288
|
guild
|
|
1160
1289
|
}, command);
|
|
@@ -1168,6 +1297,7 @@ const scopeKey = (scope)=>scope === 'global' ? 'global' : `guild-${scope.guild}`
|
|
|
1168
1297
|
}, command);
|
|
1169
1298
|
}
|
|
1170
1299
|
}
|
|
1300
|
+
if (blankGuilds.length > 0) logger.warn(blankGuildsWarning(blankGuilds));
|
|
1171
1301
|
if (nowhere) {
|
|
1172
1302
|
const outcome = unregistered.length === 0 ? targets.has('global') ? 'only the entry point command is registered globally' : 'nothing is registered globally' : `${unregistered.join(', ')} ${unregistered.length === 1 ? 'is' : 'are'} not registered, rather than registered globally`;
|
|
1173
1303
|
logger.warn(`commands.guilds lists no guild id, as an unset environment variable leaves it, so ${outcome}. ` + `Set the guild ids, or remove commands.guilds to register globally.`);
|
|
@@ -1378,10 +1508,7 @@ function logRegistered(target, logger) {
|
|
|
1378
1508
|
|
|
1379
1509
|
/** How long `@Defer({ mode: 'auto' })` waits for an answer by default. */ const AUTO_DEFER_AFTER_MS = 1500;
|
|
1380
1510
|
/** The latest `'auto'` acknowledges, counted from the interaction's creation: Discord allows three seconds. */ const AUTO_DEFER_LIMIT_MS = 2500;
|
|
1381
|
-
/** The `@Defer` options of a handler,
|
|
1382
|
-
const source = sourcePrototype(prototype, methodName);
|
|
1383
|
-
return source ? Reflect.getOwnMetadata(controller_decorator.META.deferOptions, source, methodName) : undefined;
|
|
1384
|
-
});
|
|
1511
|
+
/** The `@Defer` options of a handler: its own, or else those of the nearest method it overrides that has them. */ const handlerDefer = perHandler((prototype, methodName)=>nearestMethodStage(prototype, methodName, controller_decorator.META.deferOptions));
|
|
1385
1512
|
/** The kind of non-interaction handler a method is, if any: `@Defer` cannot apply to one. */ function nonInteractionHandler(prototype, methodName) {
|
|
1386
1513
|
if (controller_decorator.getMessageHandlers(prototype).some((handler)=>handler.method === methodName)) return 'message';
|
|
1387
1514
|
if (controller_decorator.getReactionHandlers(prototype).some((handler)=>handler.method === methodName)) return 'reaction';
|
|
@@ -1438,21 +1565,8 @@ function logRegistered(target, logger) {
|
|
|
1438
1565
|
*/ function isAppClassToken(token) {
|
|
1439
1566
|
return typeof token === 'function' && token !== discord_js.Client && !NATIVE_SOURCE$1.test(Function.prototype.toString.call(token));
|
|
1440
1567
|
}
|
|
1441
|
-
/**
|
|
1442
|
-
* The bound classes `cls` injects, by constructor type or `@inject` token: the classes whose hooks
|
|
1443
|
-
* run before its own.
|
|
1444
|
-
*/ function lifecycleDependencies(container, cls) {
|
|
1445
|
-
return injectedTokens(cls).filter((token)=>isAppClassToken(token) && container.isBound(token));
|
|
1446
|
-
}
|
|
1447
|
-
/** The units for a list of classes already in dependency order, as the app runs them without providers. */ function classUnits(container, classes) {
|
|
1448
|
-
return classes.map((cls)=>({
|
|
1449
|
-
token: cls,
|
|
1450
|
-
name: cls.name,
|
|
1451
|
-
dependencies: lifecycleDependencies(container, cls)
|
|
1452
|
-
}));
|
|
1453
|
-
}
|
|
1454
1568
|
|
|
1455
|
-
const logger$
|
|
1569
|
+
const logger$3 = new controller_decorator.Logger('Interceptor');
|
|
1456
1570
|
/** A function's source when the engine wrote it, as for a bound function, a built-in, or what `await` hands a promise. */ const NATIVE_SOURCE = /\{\s*\[native code\]\s*\}$/;
|
|
1457
1571
|
/**
|
|
1458
1572
|
* Whether `onRejected` is the app's own handling of a rejection, rather than an engine's step passing it on. The
|
|
@@ -1468,13 +1582,12 @@ function isInterceptorWithParams(entry) {
|
|
|
1468
1582
|
}
|
|
1469
1583
|
/**
|
|
1470
1584
|
* The interceptors around a handler: each class's, base first, down to the controller it is dispatched on, then the
|
|
1471
|
-
* method's.
|
|
1585
|
+
* method's and its overrides', base first.
|
|
1472
1586
|
*/ const handlerInterceptors = perHandler((prototype, methodName)=>{
|
|
1473
|
-
|
|
1474
|
-
if (!source) return [];
|
|
1587
|
+
if (!sourcePrototype(prototype, methodName)) return [];
|
|
1475
1588
|
return [
|
|
1476
1589
|
...stageClasses(prototype, methodName).flatMap((cls)=>Reflect.getOwnMetadata(controller_decorator.META.classInterceptors, cls) ?? []),
|
|
1477
|
-
...
|
|
1590
|
+
...methodStages(prototype, methodName, controller_decorator.META.methodInterceptors)
|
|
1478
1591
|
];
|
|
1479
1592
|
});
|
|
1480
1593
|
/** Binds `cls` and its unbound dependencies as singletons, refusing any that injects `ExecutionContext`. */ function bindShared(container, cls) {
|
|
@@ -1623,7 +1736,7 @@ function isInterceptorWithParams(entry) {
|
|
|
1623
1736
|
// Once the handlers chained from it have had their turn, and those the app's code returned a promise from settled
|
|
1624
1737
|
void taken.settled.then(()=>new Promise((resolve)=>setTimeout(resolve, 0))).then(()=>Promise.all(taken.disposing)).then(()=>{
|
|
1625
1738
|
if (!taken.failure || taken.caught) return;
|
|
1626
|
-
logger$
|
|
1739
|
+
logger$3.warn(`${cls.name} returned before ${context.getController().name}.${context.getHandlerName()} finished, which then threw; ` + 'nothing caught it, so the call could not report it:', taken.failure.error);
|
|
1627
1740
|
});
|
|
1628
1741
|
}
|
|
1629
1742
|
// Only a run left on a chain that ends unhandled is the call's; one the interceptor took on, as a timeout
|
|
@@ -1659,13 +1772,13 @@ function isInterceptorWithParams(entry) {
|
|
|
1659
1772
|
];
|
|
1660
1773
|
}
|
|
1661
1774
|
/** A handler's method and class filters, the classes innermost first, resolved once. */ const ownFilterLevels = perHandler((prototype, methodName)=>{
|
|
1662
|
-
|
|
1663
|
-
if (!source) return {
|
|
1775
|
+
if (!sourcePrototype(prototype, methodName)) return {
|
|
1664
1776
|
method: [],
|
|
1665
1777
|
classes: []
|
|
1666
1778
|
};
|
|
1667
1779
|
return {
|
|
1668
|
-
|
|
1780
|
+
// An override's filters before those of the methods it overrides, innermost first as the classes are
|
|
1781
|
+
method: stageOwners(prototype, methodName).reverse().flatMap((owner)=>Reflect.getOwnMetadata(controller_decorator.META.methodFilters, owner, methodName) ?? []),
|
|
1669
1782
|
classes: [
|
|
1670
1783
|
...stageClasses(prototype, methodName)
|
|
1671
1784
|
].reverse().flatMap((cls)=>Reflect.getOwnMetadata(controller_decorator.META.classFilters, cls) ?? [])
|
|
@@ -1684,7 +1797,7 @@ function isInterceptorWithParams(entry) {
|
|
|
1684
1797
|
// Every filter reaching here was checked for @Catch at startup, so its types are always recorded
|
|
1685
1798
|
const types = Reflect.getOwnMetadata(controller_decorator.META.catchTypes, filterClass(entry));
|
|
1686
1799
|
// Only classes match: anything else given to @Catch, warned of as it applied, matches no error
|
|
1687
|
-
if (types.length === 0 || types.some((type)=>controller_decorator.
|
|
1800
|
+
if (types.length === 0 || types.some((type)=>controller_decorator.isConstructible(type) && error instanceof type)) return entry;
|
|
1688
1801
|
}
|
|
1689
1802
|
}
|
|
1690
1803
|
return undefined;
|
|
@@ -1709,17 +1822,16 @@ function isPipeWithParams(entry) {
|
|
|
1709
1822
|
const asList = (entries)=>Array.isArray(entries) ? entries : [
|
|
1710
1823
|
entries
|
|
1711
1824
|
];
|
|
1712
|
-
/**
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
const validation = Reflect.getOwnMetadata(controller_decorator.META.methodValidation, source, methodName);
|
|
1825
|
+
/**
|
|
1826
|
+
* The validation and pipes that run on a handler's input: `@Validate`'s first, then `@UsePipe`'s. The `@Validate` is the
|
|
1827
|
+
* handler's own, or else that of the nearest method it overrides that has one; `@UsePipe`'s are every one's, base first.
|
|
1828
|
+
*/ const handlerInputStages = perHandler((prototype, methodName)=>{
|
|
1829
|
+
const validation = nearestMethodStage(prototype, methodName, controller_decorator.META.methodValidation);
|
|
1718
1830
|
const inline = Object.entries(validation?.pipes ?? {}).flatMap(([key, entries])=>asList(entries).map((entry)=>({
|
|
1719
1831
|
key,
|
|
1720
1832
|
entry
|
|
1721
1833
|
})));
|
|
1722
|
-
const used =
|
|
1834
|
+
const used = methodStages(prototype, methodName, controller_decorator.META.methodPipes);
|
|
1723
1835
|
return {
|
|
1724
1836
|
schema: validation?.schema,
|
|
1725
1837
|
pipes: [
|
|
@@ -1785,11 +1897,11 @@ const asList = (entries)=>Array.isArray(entries) ? entries : [
|
|
|
1785
1897
|
}
|
|
1786
1898
|
|
|
1787
1899
|
/** Where a container keeps the observers its calls are reported to, in order. */ const OBSERVERS = Symbol('observers');
|
|
1788
|
-
const logger$
|
|
1900
|
+
const logger$2 = new controller_decorator.Logger('Observer');
|
|
1789
1901
|
/** Reports each entry that is not an `@Observer` class, naming where it was listed. */ function assertObservers(where, observers) {
|
|
1790
1902
|
for (const observer of observers){
|
|
1791
1903
|
if (typeof observer !== 'function' || !Reflect.getMetadata(controller_decorator.META.observerClass, observer)) {
|
|
1792
|
-
const name = typeof observer === 'function' ?
|
|
1904
|
+
const name = controller_decorator.isConstructor(observer) ? observer.name || 'an anonymous class' : typeof observer === 'function' ? 'a function' : String(observer);
|
|
1793
1905
|
controller_decorator.startupError(new Error(`${where} takes classes decorated with @Observer(), not ${name}.`));
|
|
1794
1906
|
continue;
|
|
1795
1907
|
}
|
|
@@ -1831,7 +1943,7 @@ function observersOf(container) {
|
|
|
1831
1943
|
*/ function notifyStart(container, context) {
|
|
1832
1944
|
const pending = [];
|
|
1833
1945
|
for (const observer of observersFor(container, context)){
|
|
1834
|
-
const report = (error)=>logger$
|
|
1946
|
+
const report = (error)=>logger$2.error(`Observer ${observer.name} threw while starting ${describeCall(context)}:`, error);
|
|
1835
1947
|
try {
|
|
1836
1948
|
const instance = container.get(observer);
|
|
1837
1949
|
if (typeof instance.onStart !== 'function') continue;
|
|
@@ -1845,8 +1957,9 @@ function observersOf(container) {
|
|
|
1845
1957
|
}
|
|
1846
1958
|
/**
|
|
1847
1959
|
* Where an interaction's answer stands: a repliable one's `ResponseState`, and an autocomplete answered
|
|
1848
|
-
* or not. `undefined` for anything that is not an interaction.
|
|
1960
|
+
* or not. `undefined` for anything that is not an interaction's call, an event carrying one included.
|
|
1849
1961
|
*/ function responsePhaseOf(context) {
|
|
1962
|
+
if (context.getType() === 'event') return undefined;
|
|
1850
1963
|
const interaction = context.getInteraction();
|
|
1851
1964
|
if (!interaction) return undefined;
|
|
1852
1965
|
if (interaction.isAutocomplete()) return interaction.responded ? 'replied' : 'unanswered';
|
|
@@ -1868,13 +1981,14 @@ function observersOf(container) {
|
|
|
1868
1981
|
try {
|
|
1869
1982
|
await container.get(observer).onSettled(context, result);
|
|
1870
1983
|
} catch (error) {
|
|
1871
|
-
logger$
|
|
1984
|
+
logger$2.error(`Observer ${observer.name} threw while observing ${describeCall(context)}:`, error);
|
|
1872
1985
|
}
|
|
1873
1986
|
}
|
|
1874
1987
|
}
|
|
1875
1988
|
|
|
1876
|
-
/** Classes with a `@UseTheme`, on the class or one of its methods. */ const THEMED_CLASSES = new WeakSet();
|
|
1877
|
-
|
|
1989
|
+
/** Classes with a `@UseTheme`, on the class or one of its methods, from either build of this version. */ const THEMED_CLASSES = controller_decorator.shared('themedClasses', ()=>new WeakSet());
|
|
1990
|
+
// One per process for this meocord version, so an app started through one build finds its theme from the other
|
|
1991
|
+
const apps = controller_decorator.shared('appThemes', ()=>new WeakMap());
|
|
1878
1992
|
/** The classes on `cls`'s prototype chain, itself first. */ function chainOf(cls) {
|
|
1879
1993
|
const chain = [];
|
|
1880
1994
|
for(let prototype = cls.prototype; prototype && prototype !== Object.prototype; prototype = Object.getPrototypeOf(prototype)){
|
|
@@ -1905,6 +2019,13 @@ const apps = new WeakMap();
|
|
|
1905
2019
|
}
|
|
1906
2020
|
controller_decorator.attachThemeCache(container.get(controller_decorator.ThemeCache), resolvers);
|
|
1907
2021
|
}
|
|
2022
|
+
/** Counts `classes`, whose handlers are prepared once the app's providers are made, among those a theme can vary by. */ function prepareLateThemes(container, classes) {
|
|
2023
|
+
const themes = apps.get(container);
|
|
2024
|
+
if (!themes || themes.varies || !classes.some((cls)=>chainOf(cls).some((link)=>THEMED_CLASSES.has(link)))) return;
|
|
2025
|
+
themes.varies = true;
|
|
2026
|
+
// Built again on its next read, with each handler's own theme
|
|
2027
|
+
themes.version = -1;
|
|
2028
|
+
}
|
|
1908
2029
|
/** The app's themes, built again when the layers beneath the app's have changed. */ function current(container) {
|
|
1909
2030
|
const themes = apps.get(container);
|
|
1910
2031
|
if (themes && themes.version !== controller_decorator.themeLayersVersion()) {
|
|
@@ -1919,13 +2040,12 @@ const apps = new WeakMap();
|
|
|
1919
2040
|
}
|
|
1920
2041
|
/**
|
|
1921
2042
|
* Decides whether the app's calls need a scope, again whenever the layers beneath it or the app read outside a call
|
|
1922
|
-
* change. They do when the handlers can differ in theme. Otherwise a call reads the theme outside a call, so it
|
|
1923
|
-
*
|
|
1924
|
-
*
|
|
2043
|
+
* change. They do when the handlers can differ in theme. Otherwise a call reads the theme outside a call, so it needs
|
|
2044
|
+
* one unless its app owns that theme: another app may claim it while the call runs, and the call keeps the theme it
|
|
2045
|
+
* started with.
|
|
1925
2046
|
*/ function rescope(container, themes) {
|
|
1926
2047
|
themes.ambientVersion = controller_decorator.ambientThemeVersion();
|
|
1927
|
-
|
|
1928
|
-
themes.scoped = themes.varies || !readsOwnTheme ? themes.app : undefined;
|
|
2048
|
+
themes.scoped = themes.varies || !controller_decorator.ownsAmbientTheme(container) ? themes.app : undefined;
|
|
1929
2049
|
}
|
|
1930
2050
|
/** The app's theme: MeoCord's defaults and the app's `@MeoCord({ theme })`, or the defaults for a container with none. */ function appTheme(container) {
|
|
1931
2051
|
return current(container)?.app ?? controller_decorator.defaultTheme();
|
|
@@ -1943,8 +2063,10 @@ const apps = new WeakMap();
|
|
|
1943
2063
|
for (const cls of stageClasses(prototype, methodName)){
|
|
1944
2064
|
theme = controller_decorator.mergeTheme(theme, Reflect.getOwnMetadata(controller_decorator.META.classTheme, cls));
|
|
1945
2065
|
}
|
|
1946
|
-
|
|
1947
|
-
|
|
2066
|
+
// The method's layer over its class's, and an override's over the method it overrides
|
|
2067
|
+
for (const owner of stageOwners(prototype, methodName)){
|
|
2068
|
+
theme = controller_decorator.mergeTheme(theme, Reflect.getOwnMetadata(controller_decorator.META.methodTheme, owner, methodName));
|
|
2069
|
+
}
|
|
1948
2070
|
byMethod.set(methodName, theme);
|
|
1949
2071
|
return theme;
|
|
1950
2072
|
}
|
|
@@ -1983,7 +2105,7 @@ const apps = new WeakMap();
|
|
|
1983
2105
|
ready
|
|
1984
2106
|
};
|
|
1985
2107
|
}
|
|
1986
|
-
const clientApps = new WeakMap();
|
|
2108
|
+
const clientApps = controller_decorator.shared('clientThemes', ()=>new WeakMap());
|
|
1987
2109
|
/** Records which app a client's interactions come to, so an answer outside any call can find its theme. */ function registerClientTheme(client, container) {
|
|
1988
2110
|
clientApps.set(client, container);
|
|
1989
2111
|
}
|
|
@@ -2154,19 +2276,35 @@ const stagesByGlobals = new WeakMap();
|
|
|
2154
2276
|
for (const entry of globals.filters)prepareFilter(container, entry);
|
|
2155
2277
|
for (const controller of controllers){
|
|
2156
2278
|
const prototype = controller.prototype;
|
|
2157
|
-
for (const method of handlerMethods(prototype))
|
|
2158
|
-
const kind = handlerDefer(prototype, method) ? nonInteractionHandler(prototype, method) : undefined;
|
|
2159
|
-
if (kind) {
|
|
2160
|
-
controller_decorator.startupError(deferMisuseError(controller.name, method, kind));
|
|
2161
|
-
continue;
|
|
2162
|
-
}
|
|
2163
|
-
for (const entry of handlerInterceptors(prototype, method))prepareInterceptor(container, entry);
|
|
2164
|
-
for (const entry of handlerFilterLevels(prototype, method, []).flat())prepareFilter(container, entry);
|
|
2165
|
-
for (const { entry } of handlerInputStages(prototype, method).pipes)preparePipe(container, entry);
|
|
2166
|
-
}
|
|
2279
|
+
for (const method of handlerMethods(prototype))prepareHandler(container, controller, method);
|
|
2167
2280
|
assertInputStagesOnInteractions(controller, prototype);
|
|
2168
2281
|
}
|
|
2169
2282
|
}
|
|
2283
|
+
/**
|
|
2284
|
+
* Prepares one `@On` or `@Once` handler of a class the app reaches only as something it makes, as
|
|
2285
|
+
* {@link prepareHandlerStages} prepares a controller's.
|
|
2286
|
+
*
|
|
2287
|
+
* @returns The errors a controller's handler would be refused with; none when the handler is ready to run.
|
|
2288
|
+
*/ function prepareEventHandler(container, cls, method) {
|
|
2289
|
+
try {
|
|
2290
|
+
controller_decorator.collectStartupErrors(()=>{
|
|
2291
|
+
prepareHandler(container, cls, method);
|
|
2292
|
+
assertInputStages(cls, cls.prototype, method, 'event');
|
|
2293
|
+
});
|
|
2294
|
+
return [];
|
|
2295
|
+
} catch (error) {
|
|
2296
|
+
if (!controller_decorator.isRefusal(error)) throw error;
|
|
2297
|
+
return controller_decorator.startupErrorsOf(error);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
/** Binds what one handler's interceptors, filters and pipes need, refusing `@Defer` where it cannot apply. */ function prepareHandler(container, controller, method) {
|
|
2301
|
+
const prototype = controller.prototype;
|
|
2302
|
+
const kind = handlerDefer(prototype, method) ? nonInteractionHandler(prototype, method) : undefined;
|
|
2303
|
+
if (kind) return controller_decorator.startupError(deferMisuseError(controller.name, method, kind));
|
|
2304
|
+
for (const entry of handlerInterceptors(prototype, method))prepareInterceptor(container, entry);
|
|
2305
|
+
for (const entry of handlerFilterLevels(prototype, method, []).flat())prepareFilter(container, entry);
|
|
2306
|
+
for (const { entry } of handlerInputStages(prototype, method).pipes)preparePipe(container, entry);
|
|
2307
|
+
}
|
|
2170
2308
|
/** Records the app's theme for the handlers of `controllers`, once the startup checks have passed: it makes the theme cache. */ function prepareThemes(container, controllers) {
|
|
2171
2309
|
const globals = globalStagesOf(container);
|
|
2172
2310
|
configureThemes(container, globals.theme, controllers, globals.themeFor);
|
|
@@ -2212,17 +2350,17 @@ const stagesByGlobals = new WeakMap();
|
|
|
2212
2350
|
'event'
|
|
2213
2351
|
])
|
|
2214
2352
|
];
|
|
2215
|
-
for (const [method, kind] of others)
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
}
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
}
|
|
2353
|
+
for (const [method, kind] of others)assertInputStages(controller, prototype, method, kind);
|
|
2354
|
+
}
|
|
2355
|
+
/** Refuses `@Validate` and `@UsePipe` on one handler of `kind` with no params to check, and `@Cooldown` where it never counts. */ function assertInputStages(controller, prototype, method, kind) {
|
|
2356
|
+
const { schema, pipes } = handlerInputStages(prototype, method);
|
|
2357
|
+
const handler = kind === 'message' ? 'a message handler without a pattern' : `${kind === 'autocomplete' || kind === 'event' ? 'an' : 'a'} ${kind} handler`;
|
|
2358
|
+
if (schema || pipes.length > 0) {
|
|
2359
|
+
return controller_decorator.startupError(new Error(`${controller.name}.${method}: @Validate and @UsePipe are for interaction and patterned message handlers, ` + `whose options, customId params, modal fields and pattern params they check, and this is ${handler}.`));
|
|
2360
|
+
}
|
|
2361
|
+
// A controller's own @Cooldown skips these handlers; one on the method itself is a mistake.
|
|
2362
|
+
if (kind !== 'message' && methodCooldowns(prototype, method).length > 0) {
|
|
2363
|
+
controller_decorator.startupError(new Error(`${controller.name}.${method}: @Cooldown is for interaction and message handlers, and this is ${handler}.`));
|
|
2226
2364
|
}
|
|
2227
2365
|
}
|
|
2228
2366
|
/**
|
|
@@ -2250,7 +2388,7 @@ const stagesByGlobals = new WeakMap();
|
|
|
2250
2388
|
if (response) result.response = response;
|
|
2251
2389
|
return result;
|
|
2252
2390
|
}
|
|
2253
|
-
const logger = new controller_decorator.Logger('ExceptionFilter');
|
|
2391
|
+
const logger$1 = new controller_decorator.Logger('ExceptionFilter');
|
|
2254
2392
|
/**
|
|
2255
2393
|
* Hands a failed call's error to the first matching filter, level by level, then to the fallback. A
|
|
2256
2394
|
* filter that throws is logged and the fallback answers the original error; without a fallback, the
|
|
@@ -2264,7 +2402,7 @@ const logger = new controller_decorator.Logger('ExceptionFilter');
|
|
|
2264
2402
|
} catch (filterError) {
|
|
2265
2403
|
if (!fallback) throw filterError;
|
|
2266
2404
|
const name = (typeof filter === 'object' ? filter.provide : filter).name;
|
|
2267
|
-
logger.error(`Filter ${name} threw while handling an error:`, filterError);
|
|
2405
|
+
logger$1.error(`Filter ${name} threw while handling an error:`, filterError);
|
|
2268
2406
|
}
|
|
2269
2407
|
}
|
|
2270
2408
|
if (!fallback) throw error;
|
|
@@ -2332,7 +2470,9 @@ const logger = new controller_decorator.Logger('ExceptionFilter');
|
|
|
2332
2470
|
denial
|
|
2333
2471
|
})) {
|
|
2334
2472
|
outcome = 'denied';
|
|
2335
|
-
|
|
2473
|
+
// Without @Defer too, so a denied component is acknowledged rather than left to fail
|
|
2474
|
+
const denied = response ?? (type === 'interaction' && first?.isRepliable?.() ? controller_decorator.responseOf(first) : undefined);
|
|
2475
|
+
await denied?.abandon();
|
|
2336
2476
|
return {
|
|
2337
2477
|
ran: false
|
|
2338
2478
|
};
|
|
@@ -2388,14 +2528,18 @@ const logger = new controller_decorator.Logger('ExceptionFilter');
|
|
|
2388
2528
|
};
|
|
2389
2529
|
} finally{
|
|
2390
2530
|
await response?.release();
|
|
2391
|
-
if (options.onUnanswered && (ran || returnedEarly) && outcome === 'ran' && type === 'interaction') {
|
|
2531
|
+
if (options.onUnanswered && (ran || returnedEarly) && outcome === 'ran' && (type === 'interaction' || type === 'autocomplete')) {
|
|
2392
2532
|
const phase = responsePhaseOf(contextOf());
|
|
2393
2533
|
// The handler is to blame only once it has finished; until then, the interceptor that ended the call is
|
|
2394
|
-
const
|
|
2534
|
+
const returnedBy = !finished && returnedEarly ? {
|
|
2395
2535
|
interceptor: returnedEarly,
|
|
2396
2536
|
handlerStarted: ran
|
|
2397
2537
|
} : undefined;
|
|
2398
|
-
if (phase === 'unanswered' || phase === 'deferred') options.onUnanswered(
|
|
2538
|
+
if (phase === 'unanswered' || phase === 'deferred') options.onUnanswered({
|
|
2539
|
+
type,
|
|
2540
|
+
phase,
|
|
2541
|
+
returnedBy
|
|
2542
|
+
});
|
|
2399
2543
|
}
|
|
2400
2544
|
if (options.awaitObservers) await starting;
|
|
2401
2545
|
// After the answer and the release, so the duration covers the whole call
|
|
@@ -2464,6 +2608,23 @@ const logger = new controller_decorator.Logger('ExceptionFilter');
|
|
|
2464
2608
|
}, options);
|
|
2465
2609
|
}
|
|
2466
2610
|
|
|
2611
|
+
/** The longest delay a timer keeps: 2^31 - 1 ms, about 24.8 days. Node fires a longer one at once. */ const MAX_TIMER_MS = 2147483647;
|
|
2612
|
+
const MS_PER_SECOND = 1000;
|
|
2613
|
+
/** The most whole seconds a timer keeps, as a message about an option given in seconds names the limit. */ const MAX_TIMER_SECONDS = Math.floor(MAX_TIMER_MS / MS_PER_SECOND);
|
|
2614
|
+
/** Whether a timer keeps a delay given in seconds, a fraction of a second included. */ const timerKeepsSeconds = (seconds)=>seconds * MS_PER_SECOND <= MAX_TIMER_MS;
|
|
2615
|
+
/** A value as a refusal quotes it: a number as itself, `Infinity` and `NaN` included, a string in quotes. */ function quoted(value) {
|
|
2616
|
+
if (typeof value === 'string') return JSON.stringify(value);
|
|
2617
|
+
if (typeof value === 'number' || typeof value === 'boolean' || value === null) return String(value);
|
|
2618
|
+
return Array.isArray(value) ? 'an array' : typeof value;
|
|
2619
|
+
}
|
|
2620
|
+
/**
|
|
2621
|
+
* What is wrong with a timeout option, or `undefined` when it is a number of milliseconds a timer keeps: finite, above
|
|
2622
|
+
* 0, or 0 or more with `allowZero`, and at most `max`, {@link MAX_TIMER_MS} unless less. The caller names the option.
|
|
2623
|
+
*/ function timeoutProblem(value, { allowZero = false, max = MAX_TIMER_MS } = {}) {
|
|
2624
|
+
if (typeof value === 'number' && Number.isFinite(value) && (allowZero ? value >= 0 : value > 0) && value <= max) return undefined;
|
|
2625
|
+
return `must be a number of milliseconds ${allowZero ? '0 or more' : 'above 0'}, at most ${max} (got ${quoted(value)})`;
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2467
2628
|
/** Fetches in flight for a client, by kind, server and ID, so callers asking at once share one request. */ const inFlight = new WeakMap();
|
|
2468
2629
|
/** The one request for `key`: the one in flight, or a new one from `start`, forgotten once it settles. */ function singleFlight(client, key, start) {
|
|
2469
2630
|
let requests = inFlight.get(client);
|
|
@@ -2675,7 +2836,8 @@ const isFlagNameChar = (code, first)=>code >= 97 && code <= 122 || code >= 65 &&
|
|
|
2675
2836
|
}
|
|
2676
2837
|
flags.push({
|
|
2677
2838
|
name,
|
|
2678
|
-
value
|
|
2839
|
+
value,
|
|
2840
|
+
at: words.length
|
|
2679
2841
|
});
|
|
2680
2842
|
cuts.push(start, j);
|
|
2681
2843
|
i = j;
|
|
@@ -2710,6 +2872,33 @@ const isFlagNameChar = (code, first)=>code >= 97 && code <= 122 || code >= 65 &&
|
|
|
2710
2872
|
}
|
|
2711
2873
|
return (rest + text.slice(from)).trimEnd();
|
|
2712
2874
|
}
|
|
2875
|
+
const caseKey = (word, caseSensitive)=>caseSensitive ? word : word.toLowerCase();
|
|
2876
|
+
/** Whether a flag, given bare in place `at` among the other words, is a pattern's own `--` word `literal`. */ const isLiteralFlag = (flag, literal, at, caseSensitive)=>flag.at === at && flag.value === undefined && caseKey(`--${flag.name}`, caseSensitive) === caseKey(literal, caseSensitive);
|
|
2877
|
+
/**
|
|
2878
|
+
* The flags a pattern's own `--` words take from a message, as indexes into `flags`: where the pattern has such a word,
|
|
2879
|
+
* a flag of that name given bare in that place among the message's other words is the word, not a flag. A word typed in
|
|
2880
|
+
* quotes is matched as any word is, and is taken first.
|
|
2881
|
+
*/ function literalFlags(tokens, caseSensitive, words, flags) {
|
|
2882
|
+
const taken = new Set();
|
|
2883
|
+
if (flags.length === 0) return taken;
|
|
2884
|
+
let w = 0;
|
|
2885
|
+
for (const token of tokens){
|
|
2886
|
+
if (!('literal' in token)) {
|
|
2887
|
+
// Literal words come before the optional and rest params, which a pattern ends with
|
|
2888
|
+
if (token.rest || token.optional) break;
|
|
2889
|
+
w++;
|
|
2890
|
+
continue;
|
|
2891
|
+
}
|
|
2892
|
+
if (w < words.length && caseKey(words[w].value, caseSensitive) === caseKey(token.literal, caseSensitive)) {
|
|
2893
|
+
w++;
|
|
2894
|
+
continue;
|
|
2895
|
+
}
|
|
2896
|
+
const index = flags.findIndex((flag, i)=>!taken.has(i) && isLiteralFlag(flag, token.literal, w, caseSensitive));
|
|
2897
|
+
if (index === -1) break;
|
|
2898
|
+
taken.add(index);
|
|
2899
|
+
}
|
|
2900
|
+
return taken;
|
|
2901
|
+
}
|
|
2713
2902
|
|
|
2714
2903
|
/** The ID a mention or a bare snowflake names, for the mention's kind. */ function idOf(word, mention) {
|
|
2715
2904
|
return (mention.exec(word) ?? /^(\d{17,20})$/.exec(word))?.[1];
|
|
@@ -2717,6 +2906,10 @@ const isFlagNameChar = (code, first)=>code >= 97 && code <= 122 || code >= 65 &&
|
|
|
2717
2906
|
const USER_MENTION = /^<@!?(\d{17,20})>$/;
|
|
2718
2907
|
const ROLE_MENTION = /^<@&(\d{17,20})>$/;
|
|
2719
2908
|
const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
|
|
2909
|
+
/** The server's role a word names: its mention or ID, else its name in any case. */ function roleNamed(guild, word) {
|
|
2910
|
+
const id = idOf(word, ROLE_MENTION);
|
|
2911
|
+
return id ? guild.roles.cache.get(id) : guild.roles.cache.find((candidate)=>candidate.name.toLowerCase() === word.toLowerCase());
|
|
2912
|
+
}
|
|
2720
2913
|
/** The scalar types, each turning a word into its value or `undefined`, and the Discord ones, which resolve below. */ const BUILT_IN_TYPES = controller_decorator.lookupTable({
|
|
2721
2914
|
string: (word)=>word,
|
|
2722
2915
|
int: (word)=>controller_decorator.number(word, true),
|
|
@@ -2747,7 +2940,18 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
|
|
|
2747
2940
|
'channel'
|
|
2748
2941
|
]);
|
|
2749
2942
|
/** Whether a param type is found only in a server. */ const isGuildType = (type)=>GUILD_TYPES.has(type);
|
|
2750
|
-
/** Whether a route's pattern takes a
|
|
2943
|
+
/** Whether a route's pattern takes a `member`, `role` or `channel` param, optional or not. */ const takesGuildParam = (route)=>route.tokens.some((token)=>'param' in token && token.type !== undefined && GUILD_TYPES.has(token.type));
|
|
2944
|
+
/**
|
|
2945
|
+
* Whether a route works only in a server, as dispatch decides it: a `member`, `role` or `channel` param, optional or
|
|
2946
|
+
* not, or a required flag of one of those types. An optional flag of those types refuses a DM only when it is given.
|
|
2947
|
+
*/ const needsGuild = (route)=>takesGuildParam(route) || route.flags.some((flag)=>!flag.optional && flag.type !== undefined && GUILD_TYPES.has(flag.type));
|
|
2948
|
+
/**
|
|
2949
|
+
* Where a route works: its scope, narrowed to servers by a `member`, `role` or `channel` param, or required flag, as
|
|
2950
|
+
* dispatch narrows it.
|
|
2951
|
+
*/ function effectiveScope(route) {
|
|
2952
|
+
if (route.scope !== 'any') return route.scope;
|
|
2953
|
+
return needsGuild(route) ? 'guild' : 'any';
|
|
2954
|
+
}
|
|
2751
2955
|
/** Whether a pattern's `{name:type}` names a type: built in, words to choose from, or one the app adds. */ function isKnownParamType(type, types) {
|
|
2752
2956
|
return type in BUILT_IN_TYPES || Boolean(controller_decorator.choicesOf(type)) || appType(types, type) !== undefined;
|
|
2753
2957
|
}
|
|
@@ -2827,14 +3031,7 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
|
|
|
2827
3031
|
*/ function assertMessageScope(route, message, start) {
|
|
2828
3032
|
const inGuild = message.guildId !== null && message.guildId !== undefined;
|
|
2829
3033
|
if (route.scope === 'guild' && !inGuild) {
|
|
2830
|
-
throw
|
|
2831
|
-
controller_decorator.usageIssue({
|
|
2832
|
-
key: 'meocord.usage.serverOnly'
|
|
2833
|
-
})
|
|
2834
|
-
], {
|
|
2835
|
-
serverOnly: true,
|
|
2836
|
-
quiet: start === ''
|
|
2837
|
-
});
|
|
3034
|
+
throw serverOnly(usageOf(route, start), start === '');
|
|
2838
3035
|
}
|
|
2839
3036
|
if (route.scope === 'dm' && inGuild) {
|
|
2840
3037
|
throw new controller_decorator.MessageUsageError(usageOf(route, start), [
|
|
@@ -2847,6 +3044,15 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
|
|
|
2847
3044
|
});
|
|
2848
3045
|
}
|
|
2849
3046
|
}
|
|
3047
|
+
/** The usage error for a message sent outside a server to a command, or with a flag, that only a server has. */ const serverOnly = (usage, quiet)=>new controller_decorator.MessageUsageError(usage, [
|
|
3048
|
+
controller_decorator.usageIssue({
|
|
3049
|
+
key: 'meocord.usage.serverOnly'
|
|
3050
|
+
})
|
|
3051
|
+
], {
|
|
3052
|
+
serverOnly: true,
|
|
3053
|
+
quiet
|
|
3054
|
+
});
|
|
3055
|
+
/** Whether a message gives one of a route's flags of a type only a server has, such as `--in=#general`. */ const givesGuildFlag = (route, flags)=>flags.some((given)=>route.flags.some((flag)=>flag.type !== undefined && GUILD_TYPES.has(flag.type) && caseKey(flag.flag, route.caseSensitive) === caseKey(given.name, route.caseSensitive)));
|
|
2850
3056
|
/** Whether a value an app's `parse` returned is a ref to resolve after the guards. */ const isRef = (value)=>typeof value === 'object' && value !== null && 'cached' in value && typeof value.resolve === 'function';
|
|
2851
3057
|
/**
|
|
2852
3058
|
* The params a matched route's guards see, read from the words with no request to Discord: typed params and lists as
|
|
@@ -2869,18 +3075,20 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
|
|
|
2869
3075
|
const issues = [];
|
|
2870
3076
|
const items = [];
|
|
2871
3077
|
for (const token of route.tokens){
|
|
2872
|
-
|
|
3078
|
+
// Only a word the message gave is typed: an own one, never what Object.prototype has under the param's name
|
|
3079
|
+
const word = 'param' in token ? controller_decorator.ownValue(raw, token.param) : undefined;
|
|
3080
|
+
if (!('param' in token) || token.type === undefined || word === undefined) continue;
|
|
2873
3081
|
if (!token.rest) {
|
|
2874
3082
|
items.push({
|
|
2875
3083
|
key: token.param,
|
|
2876
3084
|
label: token.param,
|
|
2877
3085
|
type: token.type,
|
|
2878
|
-
word
|
|
3086
|
+
word
|
|
2879
3087
|
});
|
|
2880
3088
|
continue;
|
|
2881
3089
|
}
|
|
2882
|
-
const words = splitWords(
|
|
2883
|
-
params
|
|
3090
|
+
const words = splitWords(word);
|
|
3091
|
+
controller_decorator.setOwn(params, token.param, new Array(words.length));
|
|
2884
3092
|
words.forEach(({ value }, index)=>items.push({
|
|
2885
3093
|
key: token.param,
|
|
2886
3094
|
label: token.param,
|
|
@@ -2890,19 +3098,12 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
|
|
|
2890
3098
|
}));
|
|
2891
3099
|
}
|
|
2892
3100
|
const flagIssues = route.flags.length > 0 ? readFlags(route, message, start, params, items) : [];
|
|
3101
|
+
// readFlags takes out each flag's key, so one left out whose name Object.prototype has reads as left out again
|
|
3102
|
+
if (route.inheritedNames.length > 0) controller_decorator.shadowInherited(params, route.inheritedNames);
|
|
2893
3103
|
const guild = message.guild;
|
|
2894
|
-
if (!guild && (needsGuild(route) || items.some((item)=>GUILD_TYPES.has(item.type))))
|
|
2895
|
-
throw new controller_decorator.MessageUsageError(usage, [
|
|
2896
|
-
controller_decorator.usageIssue({
|
|
2897
|
-
key: 'meocord.usage.serverOnly'
|
|
2898
|
-
})
|
|
2899
|
-
], {
|
|
2900
|
-
serverOnly: true,
|
|
2901
|
-
quiet
|
|
2902
|
-
});
|
|
2903
|
-
}
|
|
3104
|
+
if (!guild && (needsGuild(route) || items.some((item)=>GUILD_TYPES.has(item.type)))) throw serverOnly(usage, quiet);
|
|
2904
3105
|
const put = (item, value)=>{
|
|
2905
|
-
if (item.index === undefined) params
|
|
3106
|
+
if (item.index === undefined) controller_decorator.setOwn(params, item.key, value);
|
|
2906
3107
|
else params[item.key][item.index] = value;
|
|
2907
3108
|
};
|
|
2908
3109
|
const refs = [];
|
|
@@ -2939,8 +3140,7 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
|
|
|
2939
3140
|
}
|
|
2940
3141
|
} else if (type === 'role') {
|
|
2941
3142
|
// Roles are cached with the Guilds intent, so a role is known here or not at all
|
|
2942
|
-
const
|
|
2943
|
-
const role = id ? guild.roles.cache.get(id) : guild.roles.cache.find((candidate)=>candidate.name.toLowerCase() === word.toLowerCase());
|
|
3143
|
+
const role = roleNamed(guild, word);
|
|
2944
3144
|
if (role) {
|
|
2945
3145
|
refer(item, 'role', new MessageEntityRef('role', role.id, message.client, guild));
|
|
2946
3146
|
continue;
|
|
@@ -2980,9 +3180,9 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
|
|
|
2980
3180
|
const params = {
|
|
2981
3181
|
...parsed.params
|
|
2982
3182
|
};
|
|
2983
|
-
for (const [key, value] of Object.entries(params))if (Array.isArray(value)) params
|
|
3183
|
+
for (const [key, value] of Object.entries(params))if (Array.isArray(value)) controller_decorator.setOwn(params, key, [
|
|
2984
3184
|
...value
|
|
2985
|
-
];
|
|
3185
|
+
]);
|
|
2986
3186
|
const issues = [];
|
|
2987
3187
|
for (const slot of refs){
|
|
2988
3188
|
const value = slot.ref instanceof MessageEntityRef ? found.get(slot.ref) : await slot.ref.resolve();
|
|
@@ -2990,7 +3190,7 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
|
|
|
2990
3190
|
issues.push(missingEntity(slot));
|
|
2991
3191
|
continue;
|
|
2992
3192
|
}
|
|
2993
|
-
if (slot.index === undefined) params
|
|
3193
|
+
if (slot.index === undefined) controller_decorator.setOwn(params, slot.key, value);
|
|
2994
3194
|
else params[slot.key][slot.index] = value;
|
|
2995
3195
|
}
|
|
2996
3196
|
if (issues.length > 0) throw new controller_decorator.MessageUsageError(usage, issues, {
|
|
@@ -3056,7 +3256,11 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
|
|
|
3056
3256
|
return {
|
|
3057
3257
|
parseArgs: async (args)=>{
|
|
3058
3258
|
assertMessageScope(route, message, start);
|
|
3059
|
-
if (given !== undefined)
|
|
3259
|
+
if (given !== undefined) {
|
|
3260
|
+
// A DM to a command only a server has is told so before what its words lack, as one whose words fit is
|
|
3261
|
+
if (!message.guild && (needsGuild(route) || givesGuildFlag(route, given.flags))) throw serverOnly(usageOf(route, start), start === '');
|
|
3262
|
+
throw new controller_decorator.MessageUsageError(usageOf(route, start), missingParams(route, given, types, message.guild));
|
|
3263
|
+
}
|
|
3060
3264
|
if (!hasTypedParams(route)) return args;
|
|
3061
3265
|
parsed = await parseMessageParams(route, params, message, start, types);
|
|
3062
3266
|
return [
|
|
@@ -3107,7 +3311,11 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
|
|
|
3107
3311
|
const given = new Map();
|
|
3108
3312
|
const unknown = new Set();
|
|
3109
3313
|
const text = (message.content ?? '').trim();
|
|
3110
|
-
|
|
3314
|
+
const split = splitFlagWords(text.slice(start.length));
|
|
3315
|
+
// A flag the pattern's own `--` word took is that word, and neither one of its flags nor unknown
|
|
3316
|
+
const literal = literalFlags(route.tokens, route.caseSensitive, split.words, split.flags);
|
|
3317
|
+
for (const [index, flag] of split.flags.entries()){
|
|
3318
|
+
if (literal.has(index)) continue;
|
|
3111
3319
|
const declared = route.flags.find((candidate)=>key(candidate.flag) === key(flag.name));
|
|
3112
3320
|
if (declared) given.set(declared.flag, flag);
|
|
3113
3321
|
else if (!unknown.has(flag.name)) {
|
|
@@ -3133,7 +3341,7 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
|
|
|
3133
3341
|
value: controller_decorator.userWords(value)
|
|
3134
3342
|
}
|
|
3135
3343
|
}, flag.flag));
|
|
3136
|
-
else params
|
|
3344
|
+
else controller_decorator.setOwn(params, flag.flag, on);
|
|
3137
3345
|
} else if (!given.has(flag.flag)) {
|
|
3138
3346
|
if (!flag.optional) issues.push(controller_decorator.usageIssue({
|
|
3139
3347
|
key: 'meocord.usage.missing',
|
|
@@ -3161,29 +3369,137 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
|
|
|
3161
3369
|
return issues;
|
|
3162
3370
|
}
|
|
3163
3371
|
/**
|
|
3164
|
-
* What is wrong with a command whose words a message named but whose pattern it does not fit,
|
|
3165
|
-
*
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3372
|
+
* What is wrong with a command whose words a message named but whose pattern it does not fit, read word by word
|
|
3373
|
+
* against the pattern: each word or required param it left out, a word given another in its place, else words
|
|
3374
|
+
* beyond what the pattern takes, else the first word its param's built-in type does not take; and then each required
|
|
3375
|
+
* flag it left out, as a message whose words fit is told. With none of those true, there are none, and the usage stands
|
|
3376
|
+
* alone.
|
|
3377
|
+
*/ function missingParams(route, given, types, guild) {
|
|
3378
|
+
const { words, flags } = given;
|
|
3379
|
+
const missing = [];
|
|
3380
|
+
// The flags given bare in place of the pattern's own `--` words, which are those words and set no flag
|
|
3381
|
+
const literal = new Set();
|
|
3382
|
+
let unfit;
|
|
3383
|
+
let w = 0;
|
|
3384
|
+
for (const token of route.tokens){
|
|
3385
|
+
if ('literal' in token) {
|
|
3386
|
+
if (w < words.length && caseKey(words[w].value, route.caseSensitive) === caseKey(token.literal, route.caseSensitive)) {
|
|
3387
|
+
w++;
|
|
3388
|
+
continue;
|
|
3173
3389
|
}
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3390
|
+
// A pattern's own `--` word given bare in its place takes no word; another word in its place is not taken
|
|
3391
|
+
const word = flags.find((flag)=>!literal.has(flag) && isLiteralFlag(flag, token.literal, w, route.caseSensitive));
|
|
3392
|
+
if (word) literal.add(word);
|
|
3393
|
+
else missing.push(controller_decorator.usageIssue({
|
|
3394
|
+
key: 'meocord.usage.missing',
|
|
3395
|
+
params: {
|
|
3396
|
+
param: token.literal
|
|
3397
|
+
}
|
|
3398
|
+
}));
|
|
3399
|
+
} else if (w === words.length) {
|
|
3400
|
+
if (!token.optional) missing.push(controller_decorator.usageIssue({
|
|
3401
|
+
key: 'meocord.usage.missing',
|
|
3402
|
+
params: {
|
|
3403
|
+
param: token.param
|
|
3404
|
+
}
|
|
3405
|
+
}, token.param));
|
|
3406
|
+
} else if (token.rest) {
|
|
3407
|
+
w = words.length;
|
|
3408
|
+
} else {
|
|
3409
|
+
// Words fill the params in order, as the usage shows them
|
|
3410
|
+
const word = words[w++].value;
|
|
3411
|
+
const type = token.type;
|
|
3412
|
+
if (!unfit && type !== undefined && isUnfit(type, word, route.caseSensitive, guild)) {
|
|
3413
|
+
unfit = {
|
|
3414
|
+
key: token.param,
|
|
3415
|
+
label: token.param,
|
|
3416
|
+
type,
|
|
3417
|
+
word
|
|
3418
|
+
};
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
// With none of these, the reason may be one MeoCord has no text for, as a role's name the matcher takes only as a
|
|
3423
|
+
// mention or an ID
|
|
3424
|
+
const reasons = missing.length > 0 ? missing : w < words.length ? [
|
|
3177
3425
|
controller_decorator.usageIssue({
|
|
3178
3426
|
key: 'meocord.usage.tooManyWords'
|
|
3179
3427
|
})
|
|
3428
|
+
] : unfit ? [
|
|
3429
|
+
wrongType(unfit, types)
|
|
3430
|
+
] : [];
|
|
3431
|
+
// So one reply names all the message lacks, rather than the flags only once its words fit
|
|
3432
|
+
const missingFlags = route.flags.filter((flag)=>flag.type !== undefined && !flag.optional && !flags.some((sent)=>!literal.has(sent) && caseKey(sent.name, route.caseSensitive) === caseKey(flag.flag, route.caseSensitive))).map((flag)=>controller_decorator.usageIssue({
|
|
3433
|
+
key: 'meocord.usage.missing',
|
|
3434
|
+
params: {
|
|
3435
|
+
param: `--${flag.flag}`
|
|
3436
|
+
}
|
|
3437
|
+
}, flag.flag));
|
|
3438
|
+
return [
|
|
3439
|
+
...reasons,
|
|
3440
|
+
...missingFlags
|
|
3180
3441
|
];
|
|
3181
3442
|
}
|
|
3443
|
+
/**
|
|
3444
|
+
* Whether a param of a type is known not to take a word: a built-in type or words to choose from that the word doesn't
|
|
3445
|
+
* fit. A role is also found by its name, so only a word that names none of the server's roles is unfit.
|
|
3446
|
+
*/ function isUnfit(type, word, caseSensitive, guild) {
|
|
3447
|
+
if (type === 'role') return guild !== null && roleNamed(guild, word) === undefined;
|
|
3448
|
+
return (type in BUILT_IN_TYPES || Boolean(controller_decorator.choicesOf(type))) && !fitsParamType(type, word, caseSensitive);
|
|
3449
|
+
}
|
|
3182
3450
|
/** Whether a route's params need resolving: it declares a type for one, or has flags. */ const hasTypedParams = (route)=>route.flags.length > 0 || route.tokens.some((token)=>'param' in token && token.type !== undefined);
|
|
3183
3451
|
|
|
3452
|
+
const logger = new controller_decorator.Logger('MeoCord');
|
|
3184
3453
|
/** Whether a route's scope lets it run where the message was sent. */ const fitsScope = (scope, inGuild)=>inGuild === undefined || scope === 'any' || scope === 'guild' === inGuild;
|
|
3185
3454
|
const PARAM = /^\{(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\.\.\.)?(\?)?\}$/;
|
|
3186
3455
|
const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
|
|
3456
|
+
/**
|
|
3457
|
+
* A whole brace pair that PARAM or FLAG refused, read loosely to tell its name from its type: anything up to `:` or `}`,
|
|
3458
|
+
* then the marks a param ends with, `...` and `?`, wherever they were written.
|
|
3459
|
+
*/ const LOOSE_FLAG = /^\{--([^:}]*?)(?::([^}]*?))?(\.\.\.)?(\?)?\}$/;
|
|
3460
|
+
const LOOSE_PARAM = /^\{([^:}]*?)(?::([^}]*?))?(\.\.\.)?(\?)?\}$/;
|
|
3461
|
+
/** A name or type with marks after it, as one written out of order carries them. */ const MARKED = /^(.*?)((?:\.\.\.|\?)+)$/;
|
|
3462
|
+
/** A param's name, and a flag's, which also starts with a letter, as a message could not give it otherwise. */ const PARAM_NAME = /^\w+$/;
|
|
3463
|
+
const FLAG_NAME = /^[A-Za-z]\w*$/;
|
|
3464
|
+
/** A param as its pattern writes it, which the grammar spells only one way. */ const writtenParam = (token)=>`{${token.param}${token.type ? `:${token.type}` : ''}${token.rest ? '...' : ''}${token.optional ? '?' : ''}}`;
|
|
3465
|
+
/** A flag as its pattern writes it. */ const writtenFlag = (flag)=>`{--${flag.flag}${flag.type ? `:${flag.type}` : ''}${flag.optional ? '?' : ''}}`;
|
|
3466
|
+
/**
|
|
3467
|
+
* Why a word with a brace in it is no param or flag: text around its braces, or a whole pair whose name, type or marks
|
|
3468
|
+
* the syntax doesn't take. Each names the rule broken, with the word mended as an example.
|
|
3469
|
+
*/ function notAParam(word) {
|
|
3470
|
+
const flag = LOOSE_FLAG.exec(word);
|
|
3471
|
+
if (flag) {
|
|
3472
|
+
const [, name, type, rest] = flag;
|
|
3473
|
+
if (rest || word.includes('...')) {
|
|
3474
|
+
return `"${word}" is not a flag: a flag doesn't take the rest of the message; a param does, such as {${mended(unmarked(name), 'param')}...}.`;
|
|
3475
|
+
}
|
|
3476
|
+
if (unmarked(name) !== name || type !== undefined && unmarked(type) !== type) {
|
|
3477
|
+
const typed = type === undefined ? '' : `:${unmarked(type)}`;
|
|
3478
|
+
return `"${word}" is not a flag: its ? goes last, as in {--${unmarked(name)}${typed}?}.`;
|
|
3479
|
+
}
|
|
3480
|
+
if (!FLAG_NAME.test(name)) {
|
|
3481
|
+
return `"${word}" is not a flag: a flag's name starts with a letter, then letters, digits or _, such as {--${mended(name, 'flag')}}.`;
|
|
3482
|
+
}
|
|
3483
|
+
return `"${word}" is not a flag: a type is a name, or words to choose from joined by |, such as {--${name}:on|off}.`;
|
|
3484
|
+
}
|
|
3485
|
+
const param = LOOSE_PARAM.exec(word);
|
|
3486
|
+
if (param) {
|
|
3487
|
+
const [, name, type] = param;
|
|
3488
|
+
if (unmarked(name) !== name || type !== undefined && unmarked(type) !== type) {
|
|
3489
|
+
const typed = type === undefined ? '' : `:${unmarked(type)}`;
|
|
3490
|
+
const ordered = `{${unmarked(name)}${typed}${word.includes('...') ? '...' : ''}${word.includes('?') ? '?' : ''}}`;
|
|
3491
|
+
return `"${word}" is not a param: its marks go last, ... then ?, as in ${ordered}.`;
|
|
3492
|
+
}
|
|
3493
|
+
if (!PARAM_NAME.test(name)) return `"${word}" is not a param: a param's name is ASCII letters, digits and _, such as {${mended(name, 'param')}}.`;
|
|
3494
|
+
return `"${word}" is not a param: a type is a name, or words to choose from joined by |, such as {${name}:on|off}.`;
|
|
3495
|
+
}
|
|
3496
|
+
return `"${word}" is not a param: a param is a whole word, such as {name}, {name:type}, {name...}, {name?} or a flag, {--name}.`;
|
|
3497
|
+
}
|
|
3498
|
+
/** A name or type without the marks written after it, `...` and `?`. */ const unmarked = (text)=>MARKED.exec(text)?.[1] ?? text;
|
|
3499
|
+
/** A name with each character the syntax doesn't take as `_`, and a flag's led by a letter, for an example. */ function mended(name, kind) {
|
|
3500
|
+
const word = name.replace(/\W/g, '_') || kind;
|
|
3501
|
+
return kind === 'flag' && !/^[A-Za-z]/.test(word) ? `flag${word}` : word;
|
|
3502
|
+
}
|
|
3187
3503
|
/**
|
|
3188
3504
|
* Reads a pattern into its words and flags. Throws for a pattern of flags alone, a param that is not a whole word, a
|
|
3189
3505
|
* name given twice, a rest param that is not last, a word after an optional param that is not optional too, an untyped
|
|
@@ -3219,9 +3535,7 @@ const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
|
|
|
3219
3535
|
words.forEach((word, index)=>{
|
|
3220
3536
|
const match = PARAM.exec(word);
|
|
3221
3537
|
if (!match) {
|
|
3222
|
-
if (/[{}]/.test(word))
|
|
3223
|
-
throw new Error(`"${word}" is not a param: a param is a whole word, such as {name}, {name:type}, {name...}, {name?} or a flag, {--name}.`);
|
|
3224
|
-
}
|
|
3538
|
+
if (/[{}]/.test(word)) throw new Error(notAParam(word));
|
|
3225
3539
|
tokens.push({
|
|
3226
3540
|
literal: word
|
|
3227
3541
|
});
|
|
@@ -3244,7 +3558,7 @@ const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
|
|
|
3244
3558
|
const next = tokens[index + 1];
|
|
3245
3559
|
if (!('param' in token) || !token.optional || !next) return;
|
|
3246
3560
|
if (!('param' in next) || !next.optional) {
|
|
3247
|
-
throw new Error(
|
|
3561
|
+
throw new Error(`${writtenParam(token)} is optional, so only optional params may follow it; "${'param' in next ? writtenParam(next) : next.literal}" is not.`);
|
|
3248
3562
|
}
|
|
3249
3563
|
if (token.type === undefined || token.type === 'string') {
|
|
3250
3564
|
throw new Error(`{${token.param}${token.type ? ':string' : ''}?} comes before another optional param, so it needs a type that tells its words ` + `from the next param's, such as {${token.param}:int?}: as text, it would take every word.`);
|
|
@@ -3257,10 +3571,15 @@ const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
|
|
|
3257
3571
|
rest: params.some((token)=>token.rest),
|
|
3258
3572
|
optional: params.some((token)=>token.optional)
|
|
3259
3573
|
});
|
|
3574
|
+
const inheritedNames = [
|
|
3575
|
+
...params.map((token)=>token.param),
|
|
3576
|
+
...flags.map((flag)=>flag.flag)
|
|
3577
|
+
].filter((name)=>name in Object.prototype);
|
|
3260
3578
|
return {
|
|
3261
3579
|
tokens,
|
|
3262
3580
|
flags,
|
|
3263
|
-
specificity
|
|
3581
|
+
specificity,
|
|
3582
|
+
inheritedNames
|
|
3264
3583
|
};
|
|
3265
3584
|
}
|
|
3266
3585
|
/** A prefix setting as a list, where `''` stands for none. */ function prefixList(prefix) {
|
|
@@ -3311,7 +3630,20 @@ const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
|
|
|
3311
3630
|
...mention
|
|
3312
3631
|
]);
|
|
3313
3632
|
}
|
|
3314
|
-
/**
|
|
3633
|
+
/** Where two routes of equal rank and shape part: the one with a required param where the other has an optional one comes first. */ function compareOptional(a, b) {
|
|
3634
|
+
for(let i = 0; i < Math.min(a.length, b.length); i++){
|
|
3635
|
+
const [aToken, bToken] = [
|
|
3636
|
+
a[i],
|
|
3637
|
+
b[i]
|
|
3638
|
+
];
|
|
3639
|
+
if ('param' in aToken && 'param' in bToken && aToken.optional !== bToken.optional) return aToken.optional ? 1 : -1;
|
|
3640
|
+
}
|
|
3641
|
+
return 0;
|
|
3642
|
+
}
|
|
3643
|
+
/**
|
|
3644
|
+
* Whether two routes accept some of the same messages: a start they share, and words that match alike. `'optional'`
|
|
3645
|
+
* when they do only because a required param and an optional one, in one position, both take a word there.
|
|
3646
|
+
*/ function sameMessages(a, b, options) {
|
|
3315
3647
|
// A case-insensitive word matches every message the case-sensitive one does
|
|
3316
3648
|
const exact = a.caseSensitive && b.caseSensitive;
|
|
3317
3649
|
const key = (start)=>typeof start === 'string' && !exact ? start.toLowerCase() : start;
|
|
@@ -3329,13 +3661,55 @@ const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
|
|
|
3329
3661
|
].some((start)=>theirs.has(key(start)));
|
|
3330
3662
|
});
|
|
3331
3663
|
if (!startAlike || a.tokens.length !== b.tokens.length) return false;
|
|
3332
|
-
|
|
3664
|
+
let byOptional = false;
|
|
3665
|
+
const alike = a.tokens.every((token, i)=>{
|
|
3333
3666
|
const other = b.tokens[i];
|
|
3334
3667
|
if ('literal' in token) {
|
|
3335
3668
|
return 'literal' in other && (exact ? token.literal === other.literal : token.literal.toLowerCase() === other.literal.toLowerCase());
|
|
3336
3669
|
}
|
|
3337
|
-
|
|
3670
|
+
if (!('param' in other) || token.rest !== other.rest) return false;
|
|
3671
|
+
if (token.optional !== other.optional) byOptional = true;
|
|
3672
|
+
return true;
|
|
3338
3673
|
});
|
|
3674
|
+
return alike && (byOptional ? 'optional' : true);
|
|
3675
|
+
}
|
|
3676
|
+
/** What a static empty prefix list does now, which a prefix function's `[]` doesn't: take messages as they are. */ const EMPTY_PREFIX_LIST = "`[]` takes messages as they are, with no prefix; write `''` for that";
|
|
3677
|
+
/**
|
|
3678
|
+
* The warnings for message handlers a bot starts with today though a message can't reach them as written, each once,
|
|
3679
|
+
* for the app or the handler: a static empty prefix list, which takes messages as they are, where a prefix function's
|
|
3680
|
+
* `[]` starts no command; and two flags of a command that reads flags in any case whose names differ only in case, the
|
|
3681
|
+
* second of which is never set. The next major version (5.0) changes the first and refuses the second.
|
|
3682
|
+
*/ function messageRouteWarnings(routes, options) {
|
|
3683
|
+
const warnings = [];
|
|
3684
|
+
if (Array.isArray(options.prefix) && options.prefix.length === 0) {
|
|
3685
|
+
warnings.push(`@MeoCord({ messages: { prefix: [] } }): ${EMPTY_PREFIX_LIST}. In the next major version (5.0), \`[]\` starts no command, ` + "as a prefix function's `[]` does.");
|
|
3686
|
+
}
|
|
3687
|
+
const seen = new Map();
|
|
3688
|
+
for (const route of routes){
|
|
3689
|
+
// A handler's aliases share its options and flags, so each is read once; one method can carry several handlers
|
|
3690
|
+
const pattern = route.aliasOf ?? route.pattern;
|
|
3691
|
+
const methods = seen.get(route.controllerClass) ?? seen.set(route.controllerClass, new Map()).get(route.controllerClass);
|
|
3692
|
+
const patterns = methods.get(route.method) ?? methods.set(route.method, new Set()).get(route.method);
|
|
3693
|
+
if (patterns.has(pattern)) continue;
|
|
3694
|
+
patterns.add(pattern);
|
|
3695
|
+
const handler = `${route.controllerClass.name}.${route.method}: @MessageHandler('${pattern}'`;
|
|
3696
|
+
const own = controller_decorator.getMessageHandlers(route.controllerClass.prototype).find((candidate)=>candidate.method === route.method && candidate.pattern === pattern)?.options;
|
|
3697
|
+
if (Array.isArray(own?.prefix) && own.prefix.length === 0) {
|
|
3698
|
+
warnings.push(`${handler}, { prefix: [] }): ${EMPTY_PREFIX_LIST}, or \`false\` for no prefix and no mention. In the next major ` + 'version (5.0), `[]` starts no command.');
|
|
3699
|
+
}
|
|
3700
|
+
if (route.caseSensitive) continue;
|
|
3701
|
+
const first = new Map();
|
|
3702
|
+
for (const flag of route.flags){
|
|
3703
|
+
const same = first.get(flag.flag.toLowerCase());
|
|
3704
|
+
if (!same) {
|
|
3705
|
+
first.set(flag.flag.toLowerCase(), flag);
|
|
3706
|
+
continue;
|
|
3707
|
+
}
|
|
3708
|
+
warnings.push(`${handler}): ${writtenFlag(same)} and ${writtenFlag(flag)} differ only in case, and this command reads flags in any ` + `case, so --${flag.flag} always sets ${writtenFlag(same)} and ${writtenFlag(flag)} is never set. Rename one, or set ` + 'caseSensitive: true. The next major version (5.0) refuses this.');
|
|
3709
|
+
break;
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
return warnings;
|
|
3339
3713
|
}
|
|
3340
3714
|
/**
|
|
3341
3715
|
* Every patterned message handler of the given controllers, most specific first, read from metadata
|
|
@@ -3404,14 +3778,21 @@ const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
|
|
|
3404
3778
|
});
|
|
3405
3779
|
}
|
|
3406
3780
|
}
|
|
3407
|
-
routes.sort((a, b)=>b.specificity - a.specificity || compareShapes(a.tokens, b.tokens) || Number(takesAppStart(a)) - Number(takesAppStart(b)));
|
|
3781
|
+
routes.sort((a, b)=>b.specificity - a.specificity || compareShapes(a.tokens, b.tokens) || compareOptional(a.tokens, b.tokens) || Number(takesAppStart(a)) - Number(takesAppStart(b)));
|
|
3408
3782
|
for(let i = 0; i < routes.length; i++){
|
|
3409
3783
|
for(let j = i + 1; j < routes.length; j++){
|
|
3410
3784
|
const [a, b] = [
|
|
3411
3785
|
routes[i],
|
|
3412
3786
|
routes[j]
|
|
3413
3787
|
];
|
|
3414
|
-
|
|
3788
|
+
const same = sameMessages(a, b, options);
|
|
3789
|
+
if (!same) continue;
|
|
3790
|
+
// Ranked by their params, so the same one answers whatever the order; 5.0 refuses these as it does the rest
|
|
3791
|
+
if (same === 'optional') {
|
|
3792
|
+
if (a.controllerClass === b.controllerClass && a.method === b.method) continue;
|
|
3793
|
+
controller_decorator.warnOnce(logger, `${a.controllerClass.name}.${a.method}: ${describeRoute(a)} and ${describeRoute(b)} in ${b.controllerClass.name}.${b.method} can ` + `take the same messages, as one takes a word where the other has an optional param; ${a.controllerClass.name}.${a.method} ` + 'answers those. Change one pattern, or give one its own prefix. The next major version (5.0) stops the bot at startup for these.');
|
|
3794
|
+
continue;
|
|
3795
|
+
}
|
|
3415
3796
|
// One handler under two spellings, such as 'hello' and 'Hello', is one route, its pattern's over an alias's
|
|
3416
3797
|
if (a.controllerClass === b.controllerClass && a.method === b.method) {
|
|
3417
3798
|
if (a.aliasOf && !b.aliasOf) routes[i] = b;
|
|
@@ -3456,7 +3837,7 @@ const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
|
|
|
3456
3837
|
if (scope !== 'dm') return;
|
|
3457
3838
|
const param = tokens.find((token)=>'param' in token && token.type !== undefined && isGuildType(token.type));
|
|
3458
3839
|
const flag = flags.find((candidate)=>candidate.type !== undefined && isGuildType(candidate.type));
|
|
3459
|
-
const shown = param ?
|
|
3840
|
+
const shown = param ? writtenParam(param) : flag && writtenFlag(flag);
|
|
3460
3841
|
if (shown) throw new Error(`scope is 'dm', but ${shown} is found only in a server.`);
|
|
3461
3842
|
}
|
|
3462
3843
|
/** Refuses a handler's `mention` other than `'only'`. */ function assertMention({ mention }) {
|
|
@@ -3568,11 +3949,11 @@ const trieNode = ()=>({
|
|
|
3568
3949
|
for(let t = 0; t < tail.length && w < words.length; t++){
|
|
3569
3950
|
const token = tail[t];
|
|
3570
3951
|
if (token.rest) {
|
|
3571
|
-
params
|
|
3952
|
+
controller_decorator.setOwn(params, token.param, restFrom(text, words[w].start, cuts));
|
|
3572
3953
|
return params;
|
|
3573
3954
|
}
|
|
3574
3955
|
if (t < tail.length - 1 && !fitsParamType(token.type, words[w].value, caseSensitive)) continue;
|
|
3575
|
-
params
|
|
3956
|
+
controller_decorator.setOwn(params, token.param, words[w++].value);
|
|
3576
3957
|
}
|
|
3577
3958
|
return w === words.length ? params : undefined;
|
|
3578
3959
|
}
|
|
@@ -3617,9 +3998,12 @@ const wordKey = (word, caseSensitive)=>caseSensitive ? word : word.toLowerCase()
|
|
|
3617
3998
|
return;
|
|
3618
3999
|
}
|
|
3619
4000
|
if ('literal' in token) {
|
|
4001
|
+
var _node1;
|
|
3620
4002
|
const key = wordKey(token.literal, route.caseSensitive);
|
|
3621
4003
|
let next = node.words.get(key);
|
|
3622
4004
|
if (!next) node.words.set(key, next = trieNode());
|
|
4005
|
+
// Such a word typed bare is taken out as a flag before the words are walked, so it is matched as one too
|
|
4006
|
+
if (route.flags.length > 0 && token.literal.startsWith('--')) ((_node1 = node).flagWords ?? (_node1.flagWords = new Map())).set(key, next);
|
|
3623
4007
|
node = next;
|
|
3624
4008
|
continue;
|
|
3625
4009
|
}
|
|
@@ -3638,6 +4022,8 @@ const wordKey = (word, caseSensitive)=>caseSensitive ? word : word.toLowerCase()
|
|
|
3638
4022
|
];
|
|
3639
4023
|
for (const group of all){
|
|
3640
4024
|
const flagged = group.flagged;
|
|
4025
|
+
// A pattern's own leading `--` word stays reachable there: given bare, it is taken out as a flag, so the message
|
|
4026
|
+
// seems to start with one
|
|
3641
4027
|
if (flagged) group.flaggedAfterFlag = {
|
|
3642
4028
|
...flagged,
|
|
3643
4029
|
words: new Map()
|
|
@@ -3678,7 +4064,16 @@ const wordKey = (word, caseSensitive)=>caseSensitive ? word : word.toLowerCase()
|
|
|
3678
4064
|
if (first === undefined || node.param || node.rests.length > 0 || node.tails.length > 0 || node.ends.length > 0) return true;
|
|
3679
4065
|
return node.words.has(first);
|
|
3680
4066
|
}
|
|
3681
|
-
/**
|
|
4067
|
+
/**
|
|
4068
|
+
* The ranks of every route the words reach from `node`. A pattern's own `--` word is reached by that word, or by a flag
|
|
4069
|
+
* of its name given bare in its place, which takes no word.
|
|
4070
|
+
*/ function reach(node, words, i, text, cuts, caseSensitive, flags, found) {
|
|
4071
|
+
if (node.flagWords) {
|
|
4072
|
+
for (const flag of flags){
|
|
4073
|
+
const next = flag.at === i && flag.value === undefined ? node.flagWords.get(wordKey(`--${flag.name}`, caseSensitive)) : undefined;
|
|
4074
|
+
if (next) reach(next, words, i, text, cuts, caseSensitive, flags, found);
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
3682
4077
|
for (const tail of node.tails)if (assignTail(tail.tokens, words, i, text, cuts, caseSensitive)) found.push(tail.rank);
|
|
3683
4078
|
if (i === words.length) {
|
|
3684
4079
|
for (const rank of node.ends)found.push(rank);
|
|
@@ -3686,22 +4081,34 @@ const wordKey = (word, caseSensitive)=>caseSensitive ? word : word.toLowerCase()
|
|
|
3686
4081
|
}
|
|
3687
4082
|
for (const rank of node.rests)found.push(rank);
|
|
3688
4083
|
const next = node.words.get(wordKey(words[i].value, caseSensitive));
|
|
3689
|
-
if (next) reach(next, words, i + 1, text, cuts, caseSensitive, found);
|
|
3690
|
-
if (node.param) reach(node.param, words, i + 1, text, cuts, caseSensitive, found);
|
|
4084
|
+
if (next) reach(next, words, i + 1, text, cuts, caseSensitive, flags, found);
|
|
4085
|
+
if (node.param) reach(node.param, words, i + 1, text, cuts, caseSensitive, flags, found);
|
|
3691
4086
|
}
|
|
3692
|
-
/**
|
|
4087
|
+
/**
|
|
4088
|
+
* The params a route's words capture, given words its pattern is known to match. A pattern's own `--` word that the
|
|
4089
|
+
* message gave as a flag, one of `taken`, takes no word.
|
|
4090
|
+
*/ function paramsOf$1(route, words, text, cuts, taken) {
|
|
3693
4091
|
const tail = tailStart(route.tokens);
|
|
3694
|
-
const params =
|
|
3695
|
-
|
|
4092
|
+
const params = {};
|
|
4093
|
+
let left = taken;
|
|
4094
|
+
let w = 0;
|
|
4095
|
+
for(let i = 0; i < (tail === -1 ? route.tokens.length : tail) && w < words.length; i++){
|
|
3696
4096
|
const token = route.tokens[i];
|
|
3697
|
-
if ('literal' in token)
|
|
4097
|
+
if ('literal' in token) {
|
|
4098
|
+
if (left > 0 && token.literal.startsWith('--') && wordKey(words[w].value, route.caseSensitive) !== wordKey(token.literal, route.caseSensitive)) left--;
|
|
4099
|
+
else w++;
|
|
4100
|
+
continue;
|
|
4101
|
+
}
|
|
3698
4102
|
if (token.rest) {
|
|
3699
|
-
params
|
|
3700
|
-
|
|
4103
|
+
controller_decorator.setOwn(params, token.param, restFrom(text, words[w].start, cuts));
|
|
4104
|
+
return params;
|
|
3701
4105
|
}
|
|
3702
|
-
params
|
|
4106
|
+
controller_decorator.setOwn(params, token.param, words[w++].value);
|
|
3703
4107
|
}
|
|
3704
|
-
return params
|
|
4108
|
+
return tail === -1 ? params : {
|
|
4109
|
+
...params,
|
|
4110
|
+
...assignTail(route.tokens.slice(tail), words, w, text, cuts, route.caseSensitive)
|
|
4111
|
+
};
|
|
3705
4112
|
}
|
|
3706
4113
|
/**
|
|
3707
4114
|
* The text after the start a message used for the group's routes, or `undefined` when it used none: a mention
|
|
@@ -3754,7 +4161,7 @@ const indexes = new WeakMap();
|
|
|
3754
4161
|
const found = [];
|
|
3755
4162
|
if (mayEnter(group.root, first)) {
|
|
3756
4163
|
const words = wordsOf(rest);
|
|
3757
|
-
reach(group.root, words, 0, rest, NONE, group.caseSensitive, found);
|
|
4164
|
+
reach(group.root, words, 0, rest, NONE, group.caseSensitive, NONE, found);
|
|
3758
4165
|
for (const rank of found){
|
|
3759
4166
|
if (fitsScope(routes[rank].scope, starts.inGuild)) {
|
|
3760
4167
|
if (!best || rank < best.rank) best = {
|
|
@@ -3783,7 +4190,7 @@ const indexes = new WeakMap();
|
|
|
3783
4190
|
};
|
|
3784
4191
|
found.length = 0;
|
|
3785
4192
|
const from = scanned.words[0]?.start === 0 ? flagged : group.flaggedAfterFlag;
|
|
3786
|
-
reach(from, scanned.words, 0, rest, scanned.cuts, group.caseSensitive, found);
|
|
4193
|
+
reach(from, scanned.words, 0, rest, scanned.cuts, group.caseSensitive, scanned.flags, found);
|
|
3787
4194
|
for (const rank of found){
|
|
3788
4195
|
const candidate = {
|
|
3789
4196
|
rank,
|
|
@@ -3800,11 +4207,16 @@ const indexes = new WeakMap();
|
|
|
3800
4207
|
const chosen = best ?? outside;
|
|
3801
4208
|
if (!chosen) return undefined;
|
|
3802
4209
|
const route = routes[chosen.rank];
|
|
3803
|
-
|
|
3804
|
-
|
|
4210
|
+
// The flags the pattern's own `--` words took are those words, and set none of its flags
|
|
4211
|
+
const taken = literalFlags(route.tokens, route.caseSensitive, chosen.words, chosen.flags);
|
|
4212
|
+
const params = paramsOf$1(route, chosen.words, chosen.rest, chosen.cuts, taken.size);
|
|
4213
|
+
chosen.flags.forEach(({ name, value }, index)=>{
|
|
4214
|
+
if (taken.has(index)) return;
|
|
3805
4215
|
const declared = route.flags.find((flag)=>wordKey(flag.flag, route.caseSensitive) === wordKey(name, route.caseSensitive));
|
|
3806
|
-
if (declared) params
|
|
3807
|
-
}
|
|
4216
|
+
if (declared) controller_decorator.setOwn(params, declared.flag, value ?? '');
|
|
4217
|
+
});
|
|
4218
|
+
// A param or flag left out reads as left out, not as what Object.prototype has under its name
|
|
4219
|
+
if (route.inheritedNames.length > 0) controller_decorator.shadowInherited(params, route.inheritedNames);
|
|
3808
4220
|
return {
|
|
3809
4221
|
route,
|
|
3810
4222
|
params,
|
|
@@ -3834,19 +4246,28 @@ const indexes = new WeakMap();
|
|
|
3834
4246
|
if (first !== undefined && !group.commands.has(first)) continue;
|
|
3835
4247
|
const plain = splitWords(rest);
|
|
3836
4248
|
// A route with flags counts the words left once the message's flags are taken out
|
|
3837
|
-
const
|
|
4249
|
+
const scanned = mayEnter(group.flagged, first) && rest.includes('--') ? splitFlagWords(rest) : {
|
|
4250
|
+
words: plain,
|
|
4251
|
+
flags: NONE
|
|
4252
|
+
};
|
|
3838
4253
|
const candidates = group.commands.get(wordKey(plain[0]?.value ?? '', group.caseSensitive)) ?? [];
|
|
3839
4254
|
for (const rank of candidates){
|
|
3840
4255
|
if (best && rank >= best.rank) break;
|
|
3841
|
-
const
|
|
3842
|
-
const
|
|
3843
|
-
|
|
4256
|
+
const flagged = routes[rank].flags.length > 0;
|
|
4257
|
+
const words = flagged ? scanned.words : plain;
|
|
4258
|
+
const flags = flagged ? scanned.flags : NONE;
|
|
4259
|
+
const taken = commandWordsTaken(commandWordsOf(routes[rank].tokens), words, flags, group.caseSensitive);
|
|
4260
|
+
if (taken !== -1) {
|
|
3844
4261
|
const named = {
|
|
3845
4262
|
rank,
|
|
3846
4263
|
start: text.slice(0, text.length - rest.length),
|
|
3847
|
-
given:
|
|
4264
|
+
given: {
|
|
4265
|
+
words,
|
|
4266
|
+
flags
|
|
4267
|
+
}
|
|
3848
4268
|
};
|
|
3849
|
-
|
|
4269
|
+
// Where it works as dispatch answers it, so a DM's usage comes from a route that works there
|
|
4270
|
+
if (fitsScope(effectiveScope(routes[rank]), starts.inGuild)) {
|
|
3850
4271
|
best = named;
|
|
3851
4272
|
break;
|
|
3852
4273
|
}
|
|
@@ -3861,6 +4282,17 @@ const indexes = new WeakMap();
|
|
|
3861
4282
|
given: chosen.given
|
|
3862
4283
|
};
|
|
3863
4284
|
}
|
|
4285
|
+
/**
|
|
4286
|
+
* How many of a message's words a route's command words take, or -1 when the message names another command. A
|
|
4287
|
+
* command's own `--` word may be given bare as a flag in its place, and takes no word.
|
|
4288
|
+
*/ function commandWordsTaken(command, words, flags, caseSensitive) {
|
|
4289
|
+
let w = 0;
|
|
4290
|
+
for (const word of command){
|
|
4291
|
+
if (words[w] && wordKey(words[w].value, caseSensitive) === wordKey(word, caseSensitive)) w++;
|
|
4292
|
+
else if (!flags.some((flag)=>isLiteralFlag(flag, word, w, caseSensitive))) return -1;
|
|
4293
|
+
}
|
|
4294
|
+
return w;
|
|
4295
|
+
}
|
|
3864
4296
|
/**
|
|
3865
4297
|
* The subcommands a message names the parent of, when no pattern matches it and it names no command: the routes whose
|
|
3866
4298
|
* command words begin with the most of the message's words, short of all of them, as `!config` does for `config set …`
|
|
@@ -3903,11 +4335,18 @@ const indexes = new WeakMap();
|
|
|
3903
4335
|
}
|
|
3904
4336
|
return found.length > 0 ? found : undefined;
|
|
3905
4337
|
}
|
|
4338
|
+
/**
|
|
4339
|
+
* Whether a message has text a handler could take; dispatch runs no handler for one without. Content that isn't a
|
|
4340
|
+
* string, as a mock's placeholder without strict mocks, is no text.
|
|
4341
|
+
*/ function hasMessageText(content) {
|
|
4342
|
+
return typeof content === 'string' && content.trim() !== '';
|
|
4343
|
+
}
|
|
3906
4344
|
/**
|
|
3907
4345
|
* What a test calling a handler with a message alone passes as its params, as dispatch builds them: `undefined` for a
|
|
3908
|
-
* listener, `{}` for a message
|
|
3909
|
-
* the command without fitting it, the route, start and `given`, the
|
|
3910
|
-
* dispatch would give the message to this handler among
|
|
4346
|
+
* listener, `{}` for a message whose content isn't a string, as a mock's without strict mocks, else what the pattern
|
|
4347
|
+
* captures with the route and start; for one naming the command without fitting it, the route, start and `given`, the
|
|
4348
|
+
* message's words and flags. Either only when dispatch would give the message to this handler among
|
|
4349
|
+
* `controllers`; otherwise why it does not reach the handler, a message with no text included.
|
|
3911
4350
|
*/ async function messageParamsFor(controllerClass, methodName, message, options, controllers = [
|
|
3912
4351
|
controllerClass
|
|
3913
4352
|
]) {
|
|
@@ -3919,6 +4358,13 @@ const indexes = new WeakMap();
|
|
|
3919
4358
|
if (typeof message.content !== 'string') return {
|
|
3920
4359
|
params: {}
|
|
3921
4360
|
};
|
|
4361
|
+
const handler = `${controllerClass.name}.${methodName}`;
|
|
4362
|
+
const pattern = routes.find((route)=>!route.aliasOf)?.pattern ?? routes[0].pattern;
|
|
4363
|
+
if (!hasMessageText(message.content)) {
|
|
4364
|
+
return {
|
|
4365
|
+
mismatch: `message has no text, so dispatch runs no handler for it: give the mock the content ${handler}'s pattern '${pattern}' takes.`
|
|
4366
|
+
};
|
|
4367
|
+
}
|
|
3922
4368
|
const botId = message.client?.user?.id;
|
|
3923
4369
|
const all = buildMessageRoutes(controllers, options);
|
|
3924
4370
|
const starts = await messageStartsFor(all, options, message, typeof botId === 'string' ? botId : undefined);
|
|
@@ -3927,7 +4373,7 @@ const indexes = new WeakMap();
|
|
|
3927
4373
|
if (target && (target.route.controllerClass !== controllerClass || target.route.method !== methodName)) {
|
|
3928
4374
|
const other = `${target.route.controllerClass.name}.${target.route.method}`;
|
|
3929
4375
|
return {
|
|
3930
|
-
mismatch: `message '${message.content}' does not reach ${
|
|
4376
|
+
mismatch: `message '${message.content}' does not reach ${handler}: dispatch runs ${other}.`
|
|
3931
4377
|
};
|
|
3932
4378
|
}
|
|
3933
4379
|
if (target && 'params' in target) return {
|
|
@@ -3941,9 +4387,8 @@ const indexes = new WeakMap();
|
|
|
3941
4387
|
start: target.start,
|
|
3942
4388
|
given: target.given
|
|
3943
4389
|
};
|
|
3944
|
-
const pattern = routes.find((route)=>!route.aliasOf)?.pattern ?? routes[0].pattern;
|
|
3945
4390
|
return {
|
|
3946
|
-
mismatch: `message '${message.content}' does not match ${
|
|
4391
|
+
mismatch: `message '${message.content}' does not match ${handler}'s pattern '${pattern}'.`
|
|
3947
4392
|
};
|
|
3948
4393
|
}
|
|
3949
4394
|
|
|
@@ -3972,12 +4417,13 @@ const indexes = new WeakMap();
|
|
|
3972
4417
|
const text = content.trim();
|
|
3973
4418
|
const rest = afterStart(text, starts.prefixes.filter((prefix)=>prefix !== ''), starts.mention, caseSensitive);
|
|
3974
4419
|
if (!rest) return undefined;
|
|
3975
|
-
const [first] = splitWords(rest);
|
|
4420
|
+
const [first, second] = splitWords(rest);
|
|
3976
4421
|
const key = (word)=>caseSensitive ? word : word.toLowerCase();
|
|
3977
4422
|
if (!first || !words.some((word)=>key(word) === key(first.value))) return undefined;
|
|
4423
|
+
// From where the next word starts, as the help word may have been written in quotes
|
|
3978
4424
|
return {
|
|
3979
4425
|
start: text.slice(0, text.length - rest.length),
|
|
3980
|
-
query: rest.slice(
|
|
4426
|
+
query: second ? rest.slice(second.start).trim() : ''
|
|
3981
4427
|
};
|
|
3982
4428
|
}
|
|
3983
4429
|
/** The start a caller would type a route with here: its own prefix, a mention, or the start the help message used. */ function displayStart(route, start, starts) {
|
|
@@ -3986,11 +4432,6 @@ const indexes = new WeakMap();
|
|
|
3986
4432
|
if (route.prefix !== undefined) return route.prefix[0] ?? '';
|
|
3987
4433
|
return start;
|
|
3988
4434
|
}
|
|
3989
|
-
/** Where a route works: its scope, narrowed to servers by a `member`, `role` or `channel` param or flag. */ function effectiveScope(route) {
|
|
3990
|
-
if (route.scope !== 'any') return route.scope;
|
|
3991
|
-
const guildOnly = route.tokens.some((token)=>'param' in token && token.type !== undefined && isGuildType(token.type)) || route.flags.some((flag)=>flag.type !== undefined && isGuildType(flag.type));
|
|
3992
|
-
return guildOnly ? 'guild' : 'any';
|
|
3993
|
-
}
|
|
3994
4435
|
/** A route's params and flags, each with what it takes in words. */ function paramsOf(route, types, render) {
|
|
3995
4436
|
const params = route.tokens.flatMap((token)=>{
|
|
3996
4437
|
if (!('param' in token)) return [];
|
|
@@ -4190,22 +4631,48 @@ const sortedEntries = (entries)=>entries.sort((a, b)=>a.usage < b.usage ? -1 : a
|
|
|
4190
4631
|
scope
|
|
4191
4632
|
].filter((line)=>line !== undefined).join('\n');
|
|
4192
4633
|
}
|
|
4193
|
-
|
|
4634
|
+
// Built on first use, for a line longer than a message
|
|
4635
|
+
let graphemes;
|
|
4636
|
+
/**
|
|
4637
|
+
* Where a line longer than one message is cut: after the last character a reader sees as one, such as an emoji, a flag
|
|
4638
|
+
* or a letter with its accent, that still fits. A single such character longer than a message is cut between code points.
|
|
4639
|
+
*/ function fittingCut(piece) {
|
|
4640
|
+
let cut = 0;
|
|
4641
|
+
graphemes ?? (graphemes = new Intl.Segmenter(undefined, {
|
|
4642
|
+
granularity: 'grapheme'
|
|
4643
|
+
}));
|
|
4644
|
+
for (const { index, segment } of graphemes.segment(piece)){
|
|
4645
|
+
if (index + segment.length > MAX_REPLY_LENGTH) break;
|
|
4646
|
+
cut = index + segment.length;
|
|
4647
|
+
}
|
|
4648
|
+
if (cut > 0) return cut;
|
|
4649
|
+
const high = piece.charCodeAt(MAX_REPLY_LENGTH - 1);
|
|
4650
|
+
return high >= 0xd800 && high <= 0xdbff ? MAX_REPLY_LENGTH - 1 : MAX_REPLY_LENGTH;
|
|
4651
|
+
}
|
|
4652
|
+
/**
|
|
4653
|
+
* Splits a reply into Discord-sized messages at line breaks, a line longer than one message cut where it must be. A
|
|
4654
|
+
* message of only whitespace, which Discord refuses as empty, is left out.
|
|
4655
|
+
*/ function splitReply(text) {
|
|
4194
4656
|
const chunks = [];
|
|
4657
|
+
const push = (chunk)=>{
|
|
4658
|
+
if (chunk.trim() !== '') chunks.push(chunk);
|
|
4659
|
+
};
|
|
4195
4660
|
let current = '';
|
|
4196
4661
|
for (const line of text.split('\n')){
|
|
4197
|
-
for(let piece = line;;
|
|
4198
|
-
const
|
|
4662
|
+
for(let piece = line;;){
|
|
4663
|
+
const cut = piece.length <= MAX_REPLY_LENGTH ? piece.length : fittingCut(piece);
|
|
4664
|
+
const part = piece.slice(0, cut);
|
|
4199
4665
|
const joined = current ? `${current}\n${part}` : part;
|
|
4200
4666
|
if (joined.length <= MAX_REPLY_LENGTH) current = joined;
|
|
4201
4667
|
else {
|
|
4202
|
-
|
|
4668
|
+
push(current);
|
|
4203
4669
|
current = part;
|
|
4204
4670
|
}
|
|
4205
|
-
if (piece.length
|
|
4671
|
+
if (cut === piece.length) break;
|
|
4672
|
+
piece = piece.slice(cut);
|
|
4206
4673
|
}
|
|
4207
4674
|
}
|
|
4208
|
-
|
|
4675
|
+
push(current);
|
|
4209
4676
|
return chunks;
|
|
4210
4677
|
}
|
|
4211
4678
|
|
|
@@ -4265,6 +4732,28 @@ const COMMAND_TYPES = new Set([
|
|
|
4265
4732
|
};
|
|
4266
4733
|
}
|
|
4267
4734
|
/** The message routes the app's dispatcher reads, by the app's registry, so its help and dispatch read one table. */ const dispatchedRoutes = new WeakMap();
|
|
4735
|
+
/** What every entry has: its class and method, and the metadata on them. */ function entryBase(controller, method) {
|
|
4736
|
+
const context = new controller_decorator.HandlerExecutionContext({
|
|
4737
|
+
controller,
|
|
4738
|
+
methodName: method,
|
|
4739
|
+
args: []
|
|
4740
|
+
});
|
|
4741
|
+
return {
|
|
4742
|
+
controller,
|
|
4743
|
+
method,
|
|
4744
|
+
get: context.get.bind(context),
|
|
4745
|
+
getAll: context.getAll.bind(context)
|
|
4746
|
+
};
|
|
4747
|
+
}
|
|
4748
|
+
const lateEventSources = new WeakMap();
|
|
4749
|
+
// The late sources each registry's cached entries were read with, so a list made before they arrived is read again
|
|
4750
|
+
const collectedWith = new WeakMap();
|
|
4751
|
+
/**
|
|
4752
|
+
* Has `registry` list the `@On` and `@Once` handlers that run on `sources`, the instances the app attaches them for,
|
|
4753
|
+
* where they are on a class it does not list already.
|
|
4754
|
+
*/ function listEventSources(registry, sources) {
|
|
4755
|
+
lateEventSources.set(registry, sources);
|
|
4756
|
+
}
|
|
4268
4757
|
/** Has `registry` work out message help from `routes`, the table the app's dispatcher routes messages with. */ function shareMessageRoutes(registry, routes) {
|
|
4269
4758
|
dispatchedRoutes.set(registry, routes);
|
|
4270
4759
|
}
|
|
@@ -4305,7 +4794,11 @@ const COMMAND_TYPES = new Set([
|
|
|
4305
4794
|
* @param filter - Narrows the list by kind, controller, or both.
|
|
4306
4795
|
* @returns The handlers, class by class in the order the app makes its classes, each after what it injects.
|
|
4307
4796
|
*/ list(filter = {}) {
|
|
4308
|
-
|
|
4797
|
+
const late = lateEventSources.get(this);
|
|
4798
|
+
if (!this.entries || collectedWith.get(this) !== late) {
|
|
4799
|
+
this.entries = this.collect();
|
|
4800
|
+
collectedWith.set(this, late);
|
|
4801
|
+
}
|
|
4309
4802
|
return this.entries.filter((entry)=>(filter.kind === undefined || entry.kind === filter.kind) && (filter.controller === undefined || entry.controller === filter.controller));
|
|
4310
4803
|
}
|
|
4311
4804
|
/**
|
|
@@ -4329,8 +4822,9 @@ const COMMAND_TYPES = new Set([
|
|
|
4329
4822
|
* }
|
|
4330
4823
|
* ```
|
|
4331
4824
|
*/ async messageHelp(message, query) {
|
|
4825
|
+
// Until a dispatcher shares its table, the same one built from the same controllers, so help lists what dispatch reaches
|
|
4332
4826
|
const routes = dispatchedRoutes.get(this) ?? this.messageRoutes ?? (this.messageRoutes = buildMessageRoutes([
|
|
4333
|
-
...this.
|
|
4827
|
+
...this.controllers
|
|
4334
4828
|
], this.messages));
|
|
4335
4829
|
const starts = await messageStarts(this.messages, message, message.client?.user?.id);
|
|
4336
4830
|
const text = (message.content ?? '').trim();
|
|
@@ -4360,19 +4854,7 @@ const COMMAND_TYPES = new Set([
|
|
|
4360
4854
|
const entries = [];
|
|
4361
4855
|
for (const controller of this.classes){
|
|
4362
4856
|
const prototype = controller.prototype;
|
|
4363
|
-
const base = (method)=>
|
|
4364
|
-
const context = new controller_decorator.HandlerExecutionContext({
|
|
4365
|
-
controller,
|
|
4366
|
-
methodName: method,
|
|
4367
|
-
args: []
|
|
4368
|
-
});
|
|
4369
|
-
return {
|
|
4370
|
-
controller,
|
|
4371
|
-
method,
|
|
4372
|
-
get: context.get.bind(context),
|
|
4373
|
-
getAll: context.getAll.bind(context)
|
|
4374
|
-
};
|
|
4375
|
-
};
|
|
4857
|
+
const base = (method)=>entryBase(controller, method);
|
|
4376
4858
|
for (const [name, metas] of Object.entries(controller_decorator.getCommandMap(prototype) ?? {})){
|
|
4377
4859
|
for (const { methodName, type, builder } of metas){
|
|
4378
4860
|
if (COMMAND_TYPES.has(type)) {
|
|
@@ -4435,34 +4917,35 @@ const COMMAND_TYPES = new Set([
|
|
|
4435
4917
|
});
|
|
4436
4918
|
}
|
|
4437
4919
|
}
|
|
4920
|
+
// The classes the app reaches only as something it makes, listed once each, with the handlers that run on them
|
|
4921
|
+
const listed = new Set(this.classes);
|
|
4922
|
+
for (const { cls, handlers } of lateEventSources.get(this) ?? []){
|
|
4923
|
+
if (listed.has(cls)) continue;
|
|
4924
|
+
listed.add(cls);
|
|
4925
|
+
for (const { event, method, once } of handlers)entries.push({
|
|
4926
|
+
...entryBase(cls, method),
|
|
4927
|
+
kind: 'event',
|
|
4928
|
+
name: event,
|
|
4929
|
+
once
|
|
4930
|
+
});
|
|
4931
|
+
}
|
|
4438
4932
|
return entries;
|
|
4439
4933
|
}
|
|
4440
4934
|
/**
|
|
4441
|
-
* @param classes - The app's classes to read handlers from. The factory fills the
|
|
4442
|
-
* once the app is bound; entries are read on the first {@link list}.
|
|
4935
|
+
* @param classes - The app's classes to read handlers from, controllers and services alike. The factory fills the
|
|
4936
|
+
* list once the app is bound; entries are read on the first {@link list}.
|
|
4443
4937
|
* @param messages - The app's `messages` options, whose `caseSensitive` message entries' `matches` follows.
|
|
4444
4938
|
* @param translator - The app's translator, read when {@link messageHelp} words its labels; none for English.
|
|
4445
|
-
|
|
4939
|
+
* @param controllers - The app's controllers, the classes dispatch routes messages to, which {@link messageHelp}
|
|
4940
|
+
* lists; `classes` when not given.
|
|
4941
|
+
*/ constructor(classes, messages = {}, translator = ()=>undefined, controllers = classes){
|
|
4446
4942
|
this.classes = classes;
|
|
4447
4943
|
this.messages = messages;
|
|
4448
4944
|
this.translator = translator;
|
|
4945
|
+
this.controllers = controllers;
|
|
4449
4946
|
}
|
|
4450
4947
|
}
|
|
4451
4948
|
|
|
4452
|
-
/** The longest delay a timer keeps: 2^31 - 1 ms, about 24.8 days. Node fires a longer one at once. */ const MAX_TIMER_MS = 2147483647;
|
|
4453
|
-
/** A value as a refusal quotes it: a number as itself, `Infinity` and `NaN` included, a string in quotes. */ function quoted(value) {
|
|
4454
|
-
if (typeof value === 'string') return JSON.stringify(value);
|
|
4455
|
-
if (typeof value === 'number' || typeof value === 'boolean' || value === null) return String(value);
|
|
4456
|
-
return Array.isArray(value) ? 'an array' : typeof value;
|
|
4457
|
-
}
|
|
4458
|
-
/**
|
|
4459
|
-
* What is wrong with a timeout option, or `undefined` when it is a number of milliseconds a timer keeps: finite, above
|
|
4460
|
-
* 0, or 0 or more with `allowZero`, and at most `max`, {@link MAX_TIMER_MS} unless less. The caller names the option.
|
|
4461
|
-
*/ function timeoutProblem(value, { allowZero = false, max = MAX_TIMER_MS } = {}) {
|
|
4462
|
-
if (typeof value === 'number' && Number.isFinite(value) && (allowZero ? value >= 0 : value > 0) && value <= max) return undefined;
|
|
4463
|
-
return `must be a number of milliseconds ${allowZero ? '0 or more' : 'above 0'}, at most ${max} (got ${quoted(value)})`;
|
|
4464
|
-
}
|
|
4465
|
-
|
|
4466
4949
|
/** Where each shard's client keeps the function that runs a `ShardContext.call` in that shard. */ const SHARD_CALL_KEY = Symbol.for('meocord.shardCall');
|
|
4467
4950
|
const packArgs = (args)=>Object.assign({
|
|
4468
4951
|
length: args.length
|
|
@@ -4570,11 +5053,11 @@ async function withTimeout(promise, ms, what) {
|
|
|
4570
5053
|
*
|
|
4571
5054
|
* With process sharding that is one result per shard; with one process, a single result listing every
|
|
4572
5055
|
* shard. The arguments and the result pass as JSON in every mode, one process and tests included, so a
|
|
4573
|
-
* `Date` arrives as a string
|
|
4574
|
-
* `BigInt`, fails the call. A method whose params JSON would
|
|
4575
|
-
* the argument is refused, naming what to declare instead. An
|
|
4576
|
-
* that throws, lacks the service, or does not answer within
|
|
4577
|
-
* others.
|
|
5056
|
+
* `Date` arrives as a string, a `Map` as `{}` and a class instance as its own fields, without its getters, wherever
|
|
5057
|
+
* it runs, and a value JSON cannot carry, such as a `BigInt`, fails the call. A method whose params JSON would
|
|
5058
|
+
* change, such as one taking a `Date`, cannot be called: the argument is refused, naming what to declare instead. An
|
|
5059
|
+
* `undefined` argument arrives as `undefined`. A process that throws, lacks the service, or does not answer within
|
|
5060
|
+
* 10 seconds gives an error result instead of failing the others.
|
|
4578
5061
|
*
|
|
4579
5062
|
* @param service - The controller, the service, or a class a provider stands in for; each process resolves its own
|
|
4580
5063
|
* instance.
|
|
@@ -4680,6 +5163,18 @@ async function withTimeout(promise, ms, what) {
|
|
|
4680
5163
|
controller_decorator.ThemeCache,
|
|
4681
5164
|
controller_decorator.Translator
|
|
4682
5165
|
];
|
|
5166
|
+
/**
|
|
5167
|
+
* The tokens the bot binds before an app's providers, which `@MeoCord({ providers })` cannot provide: `Translator` too
|
|
5168
|
+
* when the app gives `i18n`.
|
|
5169
|
+
*/ const boundBeforeProviders = (i18n)=>[
|
|
5170
|
+
discord_js.Client,
|
|
5171
|
+
HandlerRegistry,
|
|
5172
|
+
ShardContext,
|
|
5173
|
+
controller_decorator.CooldownStore,
|
|
5174
|
+
...i18n ? [
|
|
5175
|
+
controller_decorator.Translator
|
|
5176
|
+
] : []
|
|
5177
|
+
];
|
|
4683
5178
|
const created = (cls)=>`create it with \`new ${cls.name}(…)\``;
|
|
4684
5179
|
/** What to do in place of injecting each of MeoCord's classes an app makes itself; the testing module adds its own. */ const advice = new Map([
|
|
4685
5180
|
[
|
|
@@ -4718,6 +5213,18 @@ const isFactoryProvider = (provider)=>'useFactory' in provider;
|
|
|
4718
5213
|
return `'${String(token)}'`;
|
|
4719
5214
|
}
|
|
4720
5215
|
const isToken = (value)=>typeof value === 'function' || typeof value === 'string' || typeof value === 'symbol';
|
|
5216
|
+
/**
|
|
5217
|
+
* Checks one of an app's own `@MeoCord({ providers })` as the bot does. A provider of a token MeoCord binds later, or
|
|
5218
|
+
* for each call, is warned about until 5.0 refuses it; one of a token `isOwn` names, which MeoCord binds before the
|
|
5219
|
+
* providers, is refused.
|
|
5220
|
+
*
|
|
5221
|
+
* @returns The refusal, for the caller to report, or `undefined`.
|
|
5222
|
+
*/ function appProviderRefusal(appName, token, isOwn, logger) {
|
|
5223
|
+
if (token === controller_decorator.ThemeCache || token === controller_decorator.ExecutionContext) {
|
|
5224
|
+
controller_decorator.warnOnce(logger, `${appName}: @MeoCord({ providers }) provides ${tokenName(token)}, which MeoCord binds itself; ` + 'the next major version (5.0) refuses it. Remove the provider.');
|
|
5225
|
+
}
|
|
5226
|
+
return isOwn(token) ? new Error(`${appName}: @MeoCord({ providers }) cannot provide ${tokenName(token)}, which MeoCord binds itself.`) : undefined;
|
|
5227
|
+
}
|
|
4721
5228
|
/**
|
|
4722
5229
|
* Checks each provider's shape and indexes them by token, reporting each that cannot be bound, such as one with no
|
|
4723
5230
|
* token, not exactly one of `useValue`, `useClass` and `useFactory`, one that injects `ExecutionContext`, or a token
|
|
@@ -4747,7 +5254,7 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
4747
5254
|
controller_decorator.startupError(new Error(`${where}: the provider for ${name} needs exactly one of useValue, useClass and useFactory.`));
|
|
4748
5255
|
continue;
|
|
4749
5256
|
}
|
|
4750
|
-
if ('useClass' in entry &&
|
|
5257
|
+
if ('useClass' in entry && !controller_decorator.isConstructor(entry.useClass)) {
|
|
4751
5258
|
controller_decorator.startupError(new Error(`${where}: the provider for ${name} has a useClass that is not a class.`));
|
|
4752
5259
|
continue;
|
|
4753
5260
|
}
|
|
@@ -4777,6 +5284,10 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
4777
5284
|
}
|
|
4778
5285
|
return map;
|
|
4779
5286
|
}
|
|
5287
|
+
/** For each container, how to forget the value each factory provider made, so a start that failed is made afresh. */ const madeValues = new WeakMap();
|
|
5288
|
+
/** Forgets every value `container`'s factory providers made, so the next resolve runs each factory again. */ function forgetFactoryValues(container) {
|
|
5289
|
+
for (const forget of madeValues.get(container) ?? [])forget();
|
|
5290
|
+
}
|
|
4780
5291
|
/**
|
|
4781
5292
|
* Binds one provider into `container` as a singleton. `bindClass` binds a class a provider depends
|
|
4782
5293
|
* on, as the app binds its own, so a provided class or a factory's `inject` can name any class.
|
|
@@ -4797,6 +5308,10 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
4797
5308
|
// Made once, as a singleton, but kept only once made: a factory that throws or rejects runs again on the next
|
|
4798
5309
|
// resolve, so a start() after it failed tries it again. Resolves while one is pending share it.
|
|
4799
5310
|
let kept;
|
|
5311
|
+
madeValues.set(container, [
|
|
5312
|
+
...madeValues.get(container) ?? [],
|
|
5313
|
+
()=>kept = undefined
|
|
5314
|
+
]);
|
|
4800
5315
|
container.bind(token).toDynamicValue((context)=>{
|
|
4801
5316
|
if (kept) return kept.value;
|
|
4802
5317
|
// Resolved in dependency order before anything needs it, so each injected value is already made
|
|
@@ -4824,6 +5339,15 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
4824
5339
|
});
|
|
4825
5340
|
for (const dependency of inject)if (isAppClassToken(dependency)) bindClass(dependency);
|
|
4826
5341
|
}
|
|
5342
|
+
/**
|
|
5343
|
+
* Reports each service listed by a string or symbol token that no provider provides, which would otherwise be bound
|
|
5344
|
+
* as if it were a class.
|
|
5345
|
+
*/ function assertProvidedServices(appName, services, providers, where) {
|
|
5346
|
+
for (const service of services){
|
|
5347
|
+
if (typeof service === 'function' || providers.has(service)) continue;
|
|
5348
|
+
controller_decorator.startupError(new Error(`${appName}: @MeoCord({ services }) lists ${tokenName(service)}, which nothing provides: add a provider for it to ${where}.`));
|
|
5349
|
+
}
|
|
5350
|
+
}
|
|
4827
5351
|
/**
|
|
4828
5352
|
* Reports a class or a factory that asks for a string or symbol token nothing binds, naming both, rather than failing
|
|
4829
5353
|
* when the class is first resolved. A class token is bound on demand, so it is never missing.
|
|
@@ -4959,11 +5483,13 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
4959
5483
|
* Runs every factory in `order`, awaiting those that return a promise, so each value is made before
|
|
4960
5484
|
* anything that injects it is resolved. Rejects with {@link providerFailure} for the first that fails. With `control`,
|
|
4961
5485
|
* it makes no further value once the app is stopping.
|
|
5486
|
+
*
|
|
5487
|
+
* @returns Whether every factory was made: `false` when the app began stopping first.
|
|
4962
5488
|
*/ async function resolveProviders(container, providers, order, control) {
|
|
4963
5489
|
for (const token of order){
|
|
4964
5490
|
const provider = providers.get(token);
|
|
4965
5491
|
if (!provider || !isFactoryProvider(provider)) continue;
|
|
4966
|
-
if (control?.stopped()) return;
|
|
5492
|
+
if (control?.stopped()) return false;
|
|
4967
5493
|
control?.pending(tokenName(token));
|
|
4968
5494
|
try {
|
|
4969
5495
|
await container.getAsync(token);
|
|
@@ -4973,12 +5499,14 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
4973
5499
|
control?.pending(undefined);
|
|
4974
5500
|
}
|
|
4975
5501
|
}
|
|
5502
|
+
return true;
|
|
4976
5503
|
}
|
|
4977
5504
|
|
|
4978
5505
|
exports.BUILT_IN_TYPES = BUILT_IN_TYPES;
|
|
4979
5506
|
exports.COOLDOWN_POLICY = COOLDOWN_POLICY;
|
|
4980
5507
|
exports.HandlerRegistry = HandlerRegistry;
|
|
4981
5508
|
exports.MAX_TIMER_MS = MAX_TIMER_MS;
|
|
5509
|
+
exports.MAX_TIMER_SECONDS = MAX_TIMER_SECONDS;
|
|
4982
5510
|
exports.On = On;
|
|
4983
5511
|
exports.Once = Once;
|
|
4984
5512
|
exports.SENT_AS = SENT_AS;
|
|
@@ -4988,9 +5516,14 @@ exports.THEMED_CLASSES = THEMED_CLASSES;
|
|
|
4988
5516
|
exports.adviseInPlaceOfInjecting = adviseInPlaceOfInjecting;
|
|
4989
5517
|
exports.appObservers = appObservers;
|
|
4990
5518
|
exports.appPresenterOf = appPresenterOf;
|
|
5519
|
+
exports.appProviderRefusal = appProviderRefusal;
|
|
4991
5520
|
exports.appStages = appStages;
|
|
5521
|
+
exports.appTheme = appTheme;
|
|
5522
|
+
exports.appliesTo = appliesTo;
|
|
5523
|
+
exports.applyGuards = applyGuards;
|
|
4992
5524
|
exports.assertObservers = assertObservers;
|
|
4993
5525
|
exports.assertProvided = assertProvided;
|
|
5526
|
+
exports.assertProvidedServices = assertProvidedServices;
|
|
4994
5527
|
exports.assertStageEntries = assertStageEntries;
|
|
4995
5528
|
exports.assertStartupChecked = assertStartupChecked;
|
|
4996
5529
|
exports.assertTypedParameters = assertTypedParameters;
|
|
@@ -4999,37 +5532,41 @@ exports.bindGlobalStages = bindGlobalStages;
|
|
|
4999
5532
|
exports.bindObservers = bindObservers;
|
|
5000
5533
|
exports.bindProvider = bindProvider;
|
|
5001
5534
|
exports.bindsOwnToken = bindsOwnToken;
|
|
5535
|
+
exports.boundBeforeProviders = boundBeforeProviders;
|
|
5002
5536
|
exports.buildMessageRoutes = buildMessageRoutes;
|
|
5003
5537
|
exports.claimAmbientAppTheme = claimAmbientAppTheme;
|
|
5004
5538
|
exports.claimCooldownNotice = claimCooldownNotice;
|
|
5005
5539
|
exports.claimStoreDownNotice = claimStoreDownNotice;
|
|
5006
|
-
exports.classChain = classChain;
|
|
5007
5540
|
exports.classDecorators = classDecorators;
|
|
5008
5541
|
exports.classLevelGuards = classLevelGuards;
|
|
5009
|
-
exports.classUnits = classUnits;
|
|
5010
5542
|
exports.commandNameOf = commandNameOf;
|
|
5011
5543
|
exports.commandWordsOf = commandWordsOf;
|
|
5012
5544
|
exports.computeMessageHelp = computeMessageHelp;
|
|
5013
|
-
exports.consumeDispatchMark = consumeDispatchMark;
|
|
5014
5545
|
exports.cooldownPolicyFrom = cooldownPolicyFrom;
|
|
5015
5546
|
exports.declaringPrototype = declaringPrototype;
|
|
5016
5547
|
exports.deferMisuseError = deferMisuseError;
|
|
5017
5548
|
exports.defineStageTypes = defineStageTypes;
|
|
5018
5549
|
exports.fitsScope = fitsScope;
|
|
5550
|
+
exports.forgetFactoryValues = forgetFactoryValues;
|
|
5019
5551
|
exports.getEventHandlers = getEventHandlers;
|
|
5020
5552
|
exports.globalStagesOf = globalStagesOf;
|
|
5553
|
+
exports.guardOwnHandlersWithBaseGuards = guardOwnHandlersWithBaseGuards;
|
|
5021
5554
|
exports.handleUnroutedError = handleUnroutedError;
|
|
5022
5555
|
exports.handlerCooldowns = handlerCooldowns;
|
|
5023
5556
|
exports.handlerMethods = handlerMethods$1;
|
|
5024
5557
|
exports.handlerStageClasses = handlerStageClasses;
|
|
5025
5558
|
exports.handlerStages = handlerStages;
|
|
5559
|
+
exports.hasMessageText = hasMessageText;
|
|
5026
5560
|
exports.helpInvocation = helpInvocation;
|
|
5027
5561
|
exports.helpWords = helpWords;
|
|
5028
5562
|
exports.injectedTokens = injectedTokens;
|
|
5029
5563
|
exports.isAppClassToken = isAppClassToken;
|
|
5030
5564
|
exports.isClassProvider = isClassProvider;
|
|
5031
5565
|
exports.isGuardWithParams = isGuardWithParams;
|
|
5566
|
+
exports.isGuildType = isGuildType;
|
|
5032
5567
|
exports.isListable = isListable;
|
|
5568
|
+
exports.isValueProvider = isValueProvider;
|
|
5569
|
+
exports.listEventSources = listEventSources;
|
|
5033
5570
|
exports.makeInjectable = makeInjectable;
|
|
5034
5571
|
exports.markStartupChecked = markStartupChecked;
|
|
5035
5572
|
exports.matchHelpRequest = matchHelpRequest;
|
|
@@ -5039,17 +5576,21 @@ exports.matchMessageSubcommands = matchMessageSubcommands;
|
|
|
5039
5576
|
exports.meocordClasses = meocordClasses;
|
|
5040
5577
|
exports.messageCommandHooks = messageCommandHooks;
|
|
5041
5578
|
exports.messageParamsFor = messageParamsFor;
|
|
5579
|
+
exports.messageRouteWarnings = messageRouteWarnings;
|
|
5042
5580
|
exports.messageStarts = messageStarts;
|
|
5043
5581
|
exports.messageStartsFor = messageStartsFor;
|
|
5044
5582
|
exports.needsGuild = needsGuild;
|
|
5045
5583
|
exports.nonInteractionHandler = nonInteractionHandler;
|
|
5046
5584
|
exports.observeUnclaimed = observeUnclaimed;
|
|
5047
5585
|
exports.prepareAppPresenter = prepareAppPresenter;
|
|
5586
|
+
exports.prepareDirectCalls = prepareDirectCalls;
|
|
5587
|
+
exports.prepareEventHandler = prepareEventHandler;
|
|
5048
5588
|
exports.prepareHandlerStages = prepareHandlerStages;
|
|
5589
|
+
exports.prepareLateThemes = prepareLateThemes;
|
|
5049
5590
|
exports.prepareThemes = prepareThemes;
|
|
5050
5591
|
exports.providerMap = providerMap;
|
|
5051
5592
|
exports.reachableClasses = reachableClasses;
|
|
5052
|
-
exports.
|
|
5593
|
+
exports.recordGuards = recordGuards;
|
|
5053
5594
|
exports.registerClientTheme = registerClientTheme;
|
|
5054
5595
|
exports.registerCommands = registerCommands;
|
|
5055
5596
|
exports.registrationKey = registrationKey;
|
|
@@ -5057,7 +5598,6 @@ exports.releaseAmbientAppTheme = releaseAmbientAppTheme;
|
|
|
5057
5598
|
exports.renderMessageHelp = renderMessageHelp;
|
|
5058
5599
|
exports.resolutionOrder = resolutionOrder;
|
|
5059
5600
|
exports.resolveProviders = resolveProviders;
|
|
5060
|
-
exports.runDirectCall = runDirectCall;
|
|
5061
5601
|
exports.runGuards = runGuards;
|
|
5062
5602
|
exports.runHandler = runHandler;
|
|
5063
5603
|
exports.runInAppTheme = runInAppTheme;
|
|
@@ -5071,7 +5611,9 @@ exports.stageTypes = stageTypes;
|
|
|
5071
5611
|
exports.staticMessageStarts = staticMessageStarts;
|
|
5072
5612
|
exports.storeOperationsSettled = storeOperationsSettled;
|
|
5073
5613
|
exports.subcommandUsageError = subcommandUsageError;
|
|
5614
|
+
exports.takesGuildParam = takesGuildParam;
|
|
5074
5615
|
exports.timeoutProblem = timeoutProblem;
|
|
5616
|
+
exports.timerKeepsSeconds = timerKeepsSeconds;
|
|
5075
5617
|
exports.tokenDependencies = tokenDependencies;
|
|
5076
5618
|
exports.tokenName = tokenName;
|
|
5077
5619
|
exports.usesAppPrefix = usesAppPrefix;
|