meocord 4.2.1 → 4.2.2
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 +58 -0
- package/dist/cjs/_shared/{controller.decorator-XKMlcUZz.cjs → controller.decorator-U2NBWcoU.cjs} +45 -13
- package/dist/cjs/_shared/{providers-BZgJyfZS.cjs → providers-B4MOFBrW.cjs} +58 -15
- package/dist/cjs/_shared/{sharded-cooldown-store-CsJJyCZd.cjs → sharded-cooldown-store-HTUb3Ii_.cjs} +1 -1
- package/dist/cjs/_shared/{startup-roots-CgfPQrNl.cjs → startup-roots-lA5xkBFq.cjs} +12 -11
- package/dist/cjs/_shared/{translate-error-BUAR1WYG.cjs → translate-error-CbBHUu5P.cjs} +1 -1
- package/dist/cjs/common/index.cjs +3 -3
- package/dist/cjs/core/index.cjs +8 -6
- package/dist/cjs/decorator/index.cjs +33 -3
- package/dist/cjs/testing/index.cjs +295 -60
- package/dist/cli.json +1 -1
- package/dist/esm/common/response/response-state.js +5 -5
- package/dist/esm/common/translator.js +15 -5
- package/dist/esm/core/dispatcher.js +9 -8
- package/dist/esm/core/filter-runner.js +2 -2
- package/dist/esm/core/handler-pipeline.js +10 -4
- package/dist/esm/core/meocord-factory.js +4 -2
- package/dist/esm/core/message-help.js +32 -6
- package/dist/esm/core/observer-runner.js +2 -1
- package/dist/esm/core/providers.js +12 -2
- package/dist/esm/core/stage-scope.js +2 -1
- package/dist/esm/core/theme-scope.js +4 -1
- package/dist/esm/decorator/app.decorator.js +31 -0
- package/dist/esm/decorator/filter.decorator.js +2 -2
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/catalog.js +4 -13
- package/dist/esm/testing/discord-defaults.js +21 -1
- package/dist/esm/testing/meocord-testing-module.js +24 -16
- package/dist/esm/testing/mock-interaction.js +242 -28
- package/dist/esm/testing/strict-mocks.js +7 -4
- package/dist/esm/util/value.util.js +19 -2
- package/dist/types/common/index.d.cts +4 -1
- package/dist/types/common/index.d.ts +4 -1
- package/dist/types/testing/index.d.cts +28 -14
- package/dist/types/testing/index.d.ts +28 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
# meocord
|
|
2
2
|
|
|
3
|
+
## 4.2.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#658](https://github.com/meocord/meocord/pull/658) [`e67cd51`](https://github.com/meocord/meocord/commit/e67cd51a6fdef4a21f1db6541e54b2bb421fbeb1) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A button, select menu or message modal whose guard returns `false` is now acknowledged invisibly, with or without `@Defer`, so the user no longer sees "This interaction failed". The call still reports `ran: false`, and observers still see the outcome `'denied'`. A command, and a modal opened by a command, are left as they were: their only acknowledgement is a reply of their own.
|
|
8
|
+
|
|
9
|
+
A test that expects `getResponse(interaction).calls` to be `[]` after a denied click now sees `['deferUpdate']`, as a test of a handler with `@Defer` already did.
|
|
10
|
+
|
|
11
|
+
A guard that answers the click itself before returning `false` should await that answer. A reply still on its way when the guard returns races the acknowledgement, and Discord refuses whichever comes second, possibly the reply.
|
|
12
|
+
|
|
13
|
+
An app with both a route and a collector for one customId, where the guard returns `false` so the collector answers the click, should leave the click to one of them. Otherwise the two acknowledgements race, and the collector's can fail with "already acknowledged".
|
|
14
|
+
|
|
15
|
+
- [#653](https://github.com/meocord/meocord/pull/653) [`fcd7cbf`](https://github.com/meocord/meocord/commit/fcd7cbf703618541ff3f42497254cfa33b729939) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `warnUnanswered` now names an `@Autocomplete` handler that returns without calling `interaction.respond()`, as it names any other handler that leaves its interaction unanswered. The user sees the autocomplete options fail to load in that case. The warning is shown once per handler: under `meocord start --dev`, and in tests whose app sets `warnUnanswered: true`. `@MeoCord({ warnUnanswered: false })` turns it off. Nothing about the answer itself changes.
|
|
16
|
+
|
|
17
|
+
- [#656](https://github.com/meocord/meocord/pull/656) [`b951297`](https://github.com/meocord/meocord/commit/b9512974067160eddb0029a69e52ff228ba36e76) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `bindTheme`'s documentation now says that a bound function runs in the whole theme of the call that bound it, the server's and user's themes included. Every later call, such as another member's click on a collector, is answered in the starting member's theme. Its example comment said "the handler's theme", which left that out. Nothing about the behaviour changes.
|
|
18
|
+
|
|
19
|
+
- [#652](https://github.com/meocord/meocord/pull/652) [`f8369b6`](https://github.com/meocord/meocord/commit/f8369b6e556ab317331ca70a14a093a0dbc4af2d) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A catalog group whose keys include `other`, such as `reasons: { spam: 'Spam', other: 'Other' }`, is read as a group, as the types and `expectCompleteCatalog` read it. Reading the group's own key, `t('reasons')`, shows the key, and in development logs the warning, as any other group does, where it showed the `other` text. Only untyped code, such as code reading a JSON catalog, can make that call; to show the text, read `t('reasons.other')`. A plural is an object whose every key is a plural category, and a translation's plural without an `other` form still falls back to the next catalog.
|
|
20
|
+
|
|
21
|
+
- [#659](https://github.com/meocord/meocord/pull/659) [`e9d25a4`](https://github.com/meocord/meocord/commit/e9d25a4bd27bf1e3adaa0506c1b165e8fe6ff20c) Thanks [@l7aromeo](https://github.com/l7aromeo)! - The built-in `help` reply over 2,000 characters is now split into messages Discord shows as written:
|
|
22
|
+
|
|
23
|
+
- A line longer than a message is cut after the last whole character that fits. An emoji, a flag, a joined emoji or a letter with its accent is no longer split across two messages.
|
|
24
|
+
- A message that would be only whitespace isn't sent. Discord refused it as empty, and the rest of the reply was lost with it.
|
|
25
|
+
|
|
26
|
+
A help text with no long line and no blank stretch at a cut is split as before.
|
|
27
|
+
|
|
28
|
+
- [#665](https://github.com/meocord/meocord/pull/665) [`1a224e8`](https://github.com/meocord/meocord/commit/1a224e89d747b1dc0a29858f5bae8969d8657091) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Under `useStrictMocks()`, a mock interaction is marked `replied`, `deferred` or `responded` once its answer resolves, as discord.js marks it after Discord takes the answer, rather than when the answer is called. Code that doesn't await an answer now fails in a test as it would on a bot:
|
|
29
|
+
|
|
30
|
+
- an `editReply()`, `followUp()` or `deleteReply()` made before the first answer resolves rejects with `InteractionNotReplied`;
|
|
31
|
+
- a second answer sent while the first is in flight passes discord.js's check and is refused with Discord's 40060, unless the first one failed;
|
|
32
|
+
- `replied`, `deferred`, `responded` and `ephemeral` read in the same run as an un-awaited answer are as before it.
|
|
33
|
+
|
|
34
|
+
This catches overlaps within one synchronous run of the test's code. A mock answer resolves on the next microtask, so an answer still in flight across an `await`, as a guard's `void interaction.reply()` followed by `return false` is on a bot, isn't caught.
|
|
35
|
+
|
|
36
|
+
In both modes, an answer that fails no longer undoes an answer made after it. A test could make the first of two un-awaited replies reject, and the interaction then read as unanswered though the second reply went through; it now reads as answered by the second. A failed answer's change is undone once every change after it is undone too, so an interaction whose answers all fail still reads as unanswered.
|
|
37
|
+
|
|
38
|
+
In default mode the flags are still set at the call. The first time an interaction does what a bot would refuse, an edit or a second answer while its first is in flight, a warning says what the bot would do. In the next major version (5.0), every mock interaction marks its answers when they resolve, as strict mocks do now.
|
|
39
|
+
|
|
40
|
+
- [#663](https://github.com/meocord/meocord/pull/663) [`3894f77`](https://github.com/meocord/meocord/commit/3894f77b099bc2bb82bed37b43944f9e6dc09feb) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Under `useStrictMocks()`, a mock interaction's `ephemeral` is `null` until `reply()` or `deferReply()` decides it, as discord.js has it; `update()` and `deferUpdate()` leave it `null`. In default mode it still reads `false` before an answer, and a test reading it there logs a warning once. MeoCord's own reads while it answers log nothing. The next major version (5.0) reads `null` without the call.
|
|
41
|
+
|
|
42
|
+
- [#651](https://github.com/meocord/meocord/pull/651) [`8a56464`](https://github.com/meocord/meocord/commit/8a56464a42c47bddc3959bddc0748a710f656ffd) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A mock guild's `emojis`, `stickers`, `scheduledEvents`, `autoModerationRules` and `invites` are managers, in both modes, as on a message's guild. Before, calling `create()`, `fetch()` or `cache.get()` on one threw a `TypeError`.
|
|
43
|
+
|
|
44
|
+
- `create()` resolves to a mock emoji, sticker, scheduled event or AutoMod rule in the guild, and caches it, as discord.js does.
|
|
45
|
+
- `invites.create(channel)` resolves to an invite for that channel in the guild, keyed by its `code`, and caches nothing, as discord.js does.
|
|
46
|
+
- `fetch(id)` resolves to the cached item, or makes and caches one; a list fetch resolves to an empty collection.
|
|
47
|
+
- Each item is named, and what discord.js formats from it computes: an emoji's mention (`String(emoji)`) and `identifier`, and a scheduled event's and an invite's `url`.
|
|
48
|
+
|
|
49
|
+
- [#657](https://github.com/meocord/meocord/pull/657) [`8d1d3fc`](https://github.com/meocord/meocord/commit/8d1d3fc6eb0d4f59139ffc3033b1c416a507f804) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `MockedFunction`'s and `MockInstance`'s documentation says what their types check: calling a mock is checked against the function it mocks, and the values its mock API takes and records, such as `mockResolvedValue`'s and each of `mock.calls`' arguments, are not, for any function. A partial fake such as `guild.members.fetch.mockResolvedValue({ id })` type-checks. Types are unchanged.
|
|
50
|
+
|
|
51
|
+
- [#654](https://github.com/meocord/meocord/pull/654) [`1cc5a4f`](https://github.com/meocord/meocord/commit/1cc5a4fd2243ff00be9ae3d89b389f11df3b337c) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Wherever MeoCord takes a class and is given something else, it now refuses the value and names where it was given, instead of crashing with an empty `TypeError` or reporting "This is a bug in MeoCord". TypeScript already rejects these values, so the refusal matters for plain JavaScript, `any` and casts.
|
|
52
|
+
|
|
53
|
+
- **`@MeoCord`'s `presenter`, `cooldownStore`, `controllers` and `services`** refuse an object or a function that isn't a class where the app is declared, for example `App: @MeoCord({ presenter }) takes a class implementing ResponsePresenter, not an object.`
|
|
54
|
+
- **A service listed by a string or symbol token** that no provider provides is refused as the app is created, naming the token.
|
|
55
|
+
- **Stage entries** given to `@UseGuard`, `@UseInterceptor`, `@UseFilter`, `@UsePipe` and `@MeoCord({ guards, interceptors, filters })` refuse an arrow function, a method, an async function or a generator function: "a function is not a class". A `function` declaration still works as a class.
|
|
56
|
+
- **`@Catch`** warns about an arrow function, as it already does for any other value that isn't a class, and treats it as matching no error. Before this fix, every call whose error reached that filter rejected with a `TypeError` from `instanceof`. A bound error class still matches.
|
|
57
|
+
- **A testing module** refuses a controller or an observer that isn't a class, and reports the refusal together with the other startup errors. After `reportAllStartupErrors()`, `fromApp()` on an app whose `@MeoCord` was refused throws the reason, not "is not a @MeoCord app".
|
|
58
|
+
|
|
59
|
+
A bot that started before starts unchanged.
|
|
60
|
+
|
|
3
61
|
## 4.2.1
|
|
4
62
|
|
|
5
63
|
### Patch Changes
|
package/dist/cjs/_shared/{controller.decorator-XKMlcUZz.cjs → controller.decorator-U2NBWcoU.cjs}
RENAMED
|
@@ -128,7 +128,7 @@ let problem;
|
|
|
128
128
|
try {
|
|
129
129
|
// Called through a variable so a bundler does not try to resolve the path while building the
|
|
130
130
|
// application; the file is only there once the build is done.
|
|
131
|
-
const load = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('_shared/controller.decorator-
|
|
131
|
+
const load = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('_shared/controller.decorator-U2NBWcoU.cjs', document.baseURI).href)));
|
|
132
132
|
const loaded = load(compiledPath);
|
|
133
133
|
return loaded.default ?? loaded;
|
|
134
134
|
} catch (error) {
|
|
@@ -403,7 +403,7 @@ const warned = new Set();
|
|
|
403
403
|
}
|
|
404
404
|
});
|
|
405
405
|
|
|
406
|
-
var version = "4.2.
|
|
406
|
+
var version = "4.2.2";
|
|
407
407
|
var packageJson = {
|
|
408
408
|
version: version};
|
|
409
409
|
|
|
@@ -595,6 +595,9 @@ let themeOutsideCalls;
|
|
|
595
595
|
* Such a listener runs in its emitter's context, so without it a `useTheme()` inside reads the theme outside any call.
|
|
596
596
|
* Timers and promises the handler starts keep its theme without it.
|
|
597
597
|
*
|
|
598
|
+
* The theme is the binding call's whole theme, its server's and user's themes included, so every later call, whoever
|
|
599
|
+
* it is for, is answered in the starting member's theme.
|
|
600
|
+
*
|
|
598
601
|
* @param fn - The function to run in the call's theme; `this` and its arguments are passed through.
|
|
599
602
|
* @returns A function that runs `fn` in the theme `useTheme()` returns where `bindTheme` is called.
|
|
600
603
|
*
|
|
@@ -604,7 +607,7 @@ let themeOutsideCalls;
|
|
|
604
607
|
* async poll(message: Message) {
|
|
605
608
|
* const collector = message.createMessageComponentCollector({ componentType: ComponentType.Button, time: 60_000 })
|
|
606
609
|
* collector.on('collect', bindTheme(async (click: ButtonInteraction) => {
|
|
607
|
-
* const { emojis } = useTheme() // the
|
|
610
|
+
* const { emojis } = useTheme() // the starting member's theme, @UseTheme included
|
|
608
611
|
* await click.reply(`${emojis.success} Picked`)
|
|
609
612
|
* }))
|
|
610
613
|
* }
|
|
@@ -1246,16 +1249,26 @@ const logger$5 = new Logger('SetMetadata');
|
|
|
1246
1249
|
}
|
|
1247
1250
|
const DISCORD_LOCALES = new Set(Object.values(discord_js.Locale));
|
|
1248
1251
|
const languageOf = (locale)=>locale.split('-')[0];
|
|
1249
|
-
|
|
1252
|
+
const PLURAL_CATEGORIES = new Set([
|
|
1253
|
+
'zero',
|
|
1254
|
+
'one',
|
|
1255
|
+
'two',
|
|
1256
|
+
'few',
|
|
1257
|
+
'many',
|
|
1258
|
+
'other'
|
|
1259
|
+
]);
|
|
1260
|
+
/** A plural's shape: a non-empty object whose every key is a plural category, as the types read one; any other is a group. */ const isPluralShape = (value)=>typeof value === 'object' && value !== null && Object.keys(value).length > 0 && Object.keys(value).every((key)=>PLURAL_CATEGORIES.has(key));
|
|
1261
|
+
/**
|
|
1262
|
+
* A message by its dotted key, or undefined when the catalog lacks it or the key names a group. A plural without an
|
|
1263
|
+
* `other` form reads as missing, so a translation that left it out falls back to the next catalog.
|
|
1264
|
+
*/ function lookup(catalog, key) {
|
|
1250
1265
|
let current = catalog;
|
|
1251
1266
|
for (const part of key.split('.')){
|
|
1252
1267
|
if (typeof current !== 'object' || current === null) return undefined;
|
|
1253
1268
|
current = current[part];
|
|
1254
1269
|
}
|
|
1255
1270
|
if (typeof current === 'string') return current;
|
|
1256
|
-
if (
|
|
1257
|
-
return current;
|
|
1258
|
-
}
|
|
1271
|
+
if (isPluralShape(current) && typeof current.other === 'string') return current;
|
|
1259
1272
|
return undefined;
|
|
1260
1273
|
}
|
|
1261
1274
|
const plurals = new Map();
|
|
@@ -1978,7 +1991,24 @@ brand(CooldownStoreError, 'CooldownStoreError');
|
|
|
1978
1991
|
else logger.error(`Could not ${what}:`, error);
|
|
1979
1992
|
}
|
|
1980
1993
|
|
|
1981
|
-
/** Whether a value
|
|
1994
|
+
/** Whether `new` takes a value, as `instanceof` needs of its right side, such as a `@Catch` type. */ function isConstructible(value) {
|
|
1995
|
+
if (typeof value !== 'function') return false;
|
|
1996
|
+
try {
|
|
1997
|
+
// Fails unless value can be new.target, which only a constructor can; none of value's constructor code runs
|
|
1998
|
+
Reflect.construct(String, [], value);
|
|
1999
|
+
return true;
|
|
2000
|
+
} catch {
|
|
2001
|
+
return false;
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* Whether a value is a class, which a stage entry or an app option must be: constructible, with an object prototype for
|
|
2006
|
+
* the metadata MeoCord reads. An arrow function, a method, an async or a generator function is none.
|
|
2007
|
+
*/ function isConstructor(value) {
|
|
2008
|
+
if (!isConstructible(value)) return false;
|
|
2009
|
+
const prototype = value.prototype;
|
|
2010
|
+
return typeof prototype === 'object' && prototype !== null;
|
|
2011
|
+
}
|
|
1982
2012
|
/** A value as a refusal or a warning names it: a string quoted, `null` and `undefined` as written, else its kind. */ function describeValue(value) {
|
|
1983
2013
|
if (value === null || value === undefined) return String(value);
|
|
1984
2014
|
if (typeof value === 'string') return quoteForLog(value);
|
|
@@ -3383,15 +3413,15 @@ function toJson(value) {
|
|
|
3383
3413
|
});
|
|
3384
3414
|
}
|
|
3385
3415
|
/**
|
|
3386
|
-
*
|
|
3387
|
-
*
|
|
3388
|
-
* nothing. Never throws.
|
|
3416
|
+
* Ends a call a guard denied silently. A component still unanswered is acknowledged invisibly, with or without
|
|
3417
|
+
* `@Defer`, since Discord tells the user an unanswered call failed; so is a command `@Defer`'s 'auto' timer was
|
|
3418
|
+
* waiting on, whose deferred reply is then deleted, as `@Defer`'s is while nothing was sent into it. Never throws.
|
|
3389
3419
|
*/ abandon() {
|
|
3390
3420
|
const waiting = this.timer !== undefined;
|
|
3391
3421
|
this.cancelScheduled();
|
|
3392
3422
|
return this.queued(async ()=>{
|
|
3393
|
-
//
|
|
3394
|
-
if (waiting) await this.acknowledgeUnanswered({
|
|
3423
|
+
// A command's acknowledgement shows "thinking…", so one is made only for the timer that would have made it
|
|
3424
|
+
if (waiting || !answersWithOwnMessage(this.interaction)) await this.acknowledgeUnanswered({
|
|
3395
3425
|
ephemeral: true
|
|
3396
3426
|
});
|
|
3397
3427
|
this.sync();
|
|
@@ -5559,9 +5589,11 @@ exports.hideInLogs = hideInLogs;
|
|
|
5559
5589
|
exports.inferContextType = inferContextType;
|
|
5560
5590
|
exports.interactionLocale = interactionLocale;
|
|
5561
5591
|
exports.isBuiltApplication = isBuiltApplication;
|
|
5592
|
+
exports.isConstructible = isConstructible;
|
|
5562
5593
|
exports.isConstructor = isConstructor;
|
|
5563
5594
|
exports.isCustomIdRouted = isCustomIdRouted;
|
|
5564
5595
|
exports.isExplainedError = isExplainedError;
|
|
5596
|
+
exports.isPluralShape = isPluralShape;
|
|
5565
5597
|
exports.isRefusal = isRefusal;
|
|
5566
5598
|
exports.isTooLarge = isTooLarge;
|
|
5567
5599
|
exports.isUserOutcome = isUserOutcome;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var controller_decorator = require('./controller.decorator-
|
|
3
|
+
var controller_decorator = require('./controller.decorator-U2NBWcoU.cjs');
|
|
4
4
|
require('reflect-metadata');
|
|
5
5
|
var node_async_hooks = require('node:async_hooks');
|
|
6
6
|
var inversify = require('inversify');
|
|
@@ -90,7 +90,8 @@ var Table__default = /*#__PURE__*/_interopDefaultCompat(Table);
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
/** The class of a stage entry. */ function stageClass(entry) {
|
|
93
|
-
|
|
93
|
+
// Entries were checked as they were declared, so a function is the class itself
|
|
94
|
+
return typeof entry === 'function' ? entry : entry.provide;
|
|
94
95
|
}
|
|
95
96
|
/** The context types a stage declared, or `undefined` when it runs for every type. */ function stageTypes(entry) {
|
|
96
97
|
return Reflect.getMetadata(controller_decorator.META.stageTypes, stageClass(entry));
|
|
@@ -1684,7 +1685,7 @@ function isInterceptorWithParams(entry) {
|
|
|
1684
1685
|
// Every filter reaching here was checked for @Catch at startup, so its types are always recorded
|
|
1685
1686
|
const types = Reflect.getOwnMetadata(controller_decorator.META.catchTypes, filterClass(entry));
|
|
1686
1687
|
// 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.
|
|
1688
|
+
if (types.length === 0 || types.some((type)=>controller_decorator.isConstructible(type) && error instanceof type)) return entry;
|
|
1688
1689
|
}
|
|
1689
1690
|
}
|
|
1690
1691
|
return undefined;
|
|
@@ -1789,7 +1790,7 @@ const logger$1 = new controller_decorator.Logger('Observer');
|
|
|
1789
1790
|
/** Reports each entry that is not an `@Observer` class, naming where it was listed. */ function assertObservers(where, observers) {
|
|
1790
1791
|
for (const observer of observers){
|
|
1791
1792
|
if (typeof observer !== 'function' || !Reflect.getMetadata(controller_decorator.META.observerClass, observer)) {
|
|
1792
|
-
const name = typeof observer === 'function' ?
|
|
1793
|
+
const name = controller_decorator.isConstructor(observer) ? observer.name || 'an anonymous class' : typeof observer === 'function' ? 'a function' : String(observer);
|
|
1793
1794
|
controller_decorator.startupError(new Error(`${where} takes classes decorated with @Observer(), not ${name}.`));
|
|
1794
1795
|
continue;
|
|
1795
1796
|
}
|
|
@@ -2332,7 +2333,9 @@ const logger = new controller_decorator.Logger('ExceptionFilter');
|
|
|
2332
2333
|
denial
|
|
2333
2334
|
})) {
|
|
2334
2335
|
outcome = 'denied';
|
|
2335
|
-
|
|
2336
|
+
// Without @Defer too, so a denied component is acknowledged rather than left to fail
|
|
2337
|
+
const denied = response ?? (type === 'interaction' && first?.isRepliable?.() ? controller_decorator.responseOf(first) : undefined);
|
|
2338
|
+
await denied?.abandon();
|
|
2336
2339
|
return {
|
|
2337
2340
|
ran: false
|
|
2338
2341
|
};
|
|
@@ -2388,14 +2391,18 @@ const logger = new controller_decorator.Logger('ExceptionFilter');
|
|
|
2388
2391
|
};
|
|
2389
2392
|
} finally{
|
|
2390
2393
|
await response?.release();
|
|
2391
|
-
if (options.onUnanswered && (ran || returnedEarly) && outcome === 'ran' && type === 'interaction') {
|
|
2394
|
+
if (options.onUnanswered && (ran || returnedEarly) && outcome === 'ran' && (type === 'interaction' || type === 'autocomplete')) {
|
|
2392
2395
|
const phase = responsePhaseOf(contextOf());
|
|
2393
2396
|
// The handler is to blame only once it has finished; until then, the interceptor that ended the call is
|
|
2394
|
-
const
|
|
2397
|
+
const returnedBy = !finished && returnedEarly ? {
|
|
2395
2398
|
interceptor: returnedEarly,
|
|
2396
2399
|
handlerStarted: ran
|
|
2397
2400
|
} : undefined;
|
|
2398
|
-
if (phase === 'unanswered' || phase === 'deferred') options.onUnanswered(
|
|
2401
|
+
if (phase === 'unanswered' || phase === 'deferred') options.onUnanswered({
|
|
2402
|
+
type,
|
|
2403
|
+
phase,
|
|
2404
|
+
returnedBy
|
|
2405
|
+
});
|
|
2399
2406
|
}
|
|
2400
2407
|
if (options.awaitObservers) await starting;
|
|
2401
2408
|
// After the answer and the release, so the duration covers the whole call
|
|
@@ -4190,22 +4197,48 @@ const sortedEntries = (entries)=>entries.sort((a, b)=>a.usage < b.usage ? -1 : a
|
|
|
4190
4197
|
scope
|
|
4191
4198
|
].filter((line)=>line !== undefined).join('\n');
|
|
4192
4199
|
}
|
|
4193
|
-
|
|
4200
|
+
// Built on first use, for a line longer than a message
|
|
4201
|
+
let graphemes;
|
|
4202
|
+
/**
|
|
4203
|
+
* Where a line longer than one message is cut: after the last character a reader sees as one, such as an emoji, a flag
|
|
4204
|
+
* or a letter with its accent, that still fits. A single such character longer than a message is cut between code points.
|
|
4205
|
+
*/ function fittingCut(piece) {
|
|
4206
|
+
let cut = 0;
|
|
4207
|
+
graphemes ?? (graphemes = new Intl.Segmenter(undefined, {
|
|
4208
|
+
granularity: 'grapheme'
|
|
4209
|
+
}));
|
|
4210
|
+
for (const { index, segment } of graphemes.segment(piece)){
|
|
4211
|
+
if (index + segment.length > MAX_REPLY_LENGTH) break;
|
|
4212
|
+
cut = index + segment.length;
|
|
4213
|
+
}
|
|
4214
|
+
if (cut > 0) return cut;
|
|
4215
|
+
const high = piece.charCodeAt(MAX_REPLY_LENGTH - 1);
|
|
4216
|
+
return high >= 0xd800 && high <= 0xdbff ? MAX_REPLY_LENGTH - 1 : MAX_REPLY_LENGTH;
|
|
4217
|
+
}
|
|
4218
|
+
/**
|
|
4219
|
+
* Splits a reply into Discord-sized messages at line breaks, a line longer than one message cut where it must be. A
|
|
4220
|
+
* message of only whitespace, which Discord refuses as empty, is left out.
|
|
4221
|
+
*/ function splitReply(text) {
|
|
4194
4222
|
const chunks = [];
|
|
4223
|
+
const push = (chunk)=>{
|
|
4224
|
+
if (chunk.trim() !== '') chunks.push(chunk);
|
|
4225
|
+
};
|
|
4195
4226
|
let current = '';
|
|
4196
4227
|
for (const line of text.split('\n')){
|
|
4197
|
-
for(let piece = line;;
|
|
4198
|
-
const
|
|
4228
|
+
for(let piece = line;;){
|
|
4229
|
+
const cut = piece.length <= MAX_REPLY_LENGTH ? piece.length : fittingCut(piece);
|
|
4230
|
+
const part = piece.slice(0, cut);
|
|
4199
4231
|
const joined = current ? `${current}\n${part}` : part;
|
|
4200
4232
|
if (joined.length <= MAX_REPLY_LENGTH) current = joined;
|
|
4201
4233
|
else {
|
|
4202
|
-
|
|
4234
|
+
push(current);
|
|
4203
4235
|
current = part;
|
|
4204
4236
|
}
|
|
4205
|
-
if (piece.length
|
|
4237
|
+
if (cut === piece.length) break;
|
|
4238
|
+
piece = piece.slice(cut);
|
|
4206
4239
|
}
|
|
4207
4240
|
}
|
|
4208
|
-
|
|
4241
|
+
push(current);
|
|
4209
4242
|
return chunks;
|
|
4210
4243
|
}
|
|
4211
4244
|
|
|
@@ -4747,7 +4780,7 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
4747
4780
|
controller_decorator.startupError(new Error(`${where}: the provider for ${name} needs exactly one of useValue, useClass and useFactory.`));
|
|
4748
4781
|
continue;
|
|
4749
4782
|
}
|
|
4750
|
-
if ('useClass' in entry &&
|
|
4783
|
+
if ('useClass' in entry && !controller_decorator.isConstructor(entry.useClass)) {
|
|
4751
4784
|
controller_decorator.startupError(new Error(`${where}: the provider for ${name} has a useClass that is not a class.`));
|
|
4752
4785
|
continue;
|
|
4753
4786
|
}
|
|
@@ -4824,6 +4857,15 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
4824
4857
|
});
|
|
4825
4858
|
for (const dependency of inject)if (isAppClassToken(dependency)) bindClass(dependency);
|
|
4826
4859
|
}
|
|
4860
|
+
/**
|
|
4861
|
+
* Reports each service listed by a string or symbol token that no provider provides, which would otherwise be bound
|
|
4862
|
+
* as if it were a class.
|
|
4863
|
+
*/ function assertProvidedServices(appName, services, providers, where) {
|
|
4864
|
+
for (const service of services){
|
|
4865
|
+
if (typeof service === 'function' || providers.has(service)) continue;
|
|
4866
|
+
controller_decorator.startupError(new Error(`${appName}: @MeoCord({ services }) lists ${tokenName(service)}, which nothing provides: add a provider for it to ${where}.`));
|
|
4867
|
+
}
|
|
4868
|
+
}
|
|
4827
4869
|
/**
|
|
4828
4870
|
* Reports a class or a factory that asks for a string or symbol token nothing binds, naming both, rather than failing
|
|
4829
4871
|
* when the class is first resolved. A class token is bound on demand, so it is never missing.
|
|
@@ -4991,6 +5033,7 @@ exports.appPresenterOf = appPresenterOf;
|
|
|
4991
5033
|
exports.appStages = appStages;
|
|
4992
5034
|
exports.assertObservers = assertObservers;
|
|
4993
5035
|
exports.assertProvided = assertProvided;
|
|
5036
|
+
exports.assertProvidedServices = assertProvidedServices;
|
|
4994
5037
|
exports.assertStageEntries = assertStageEntries;
|
|
4995
5038
|
exports.assertStartupChecked = assertStartupChecked;
|
|
4996
5039
|
exports.assertTypedParameters = assertTypedParameters;
|
package/dist/cjs/_shared/{sharded-cooldown-store-CsJJyCZd.cjs → sharded-cooldown-store-HTUb3Ii_.cjs}
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var node_crypto = require('node:crypto');
|
|
4
|
-
var controller_decorator = require('./controller.decorator-
|
|
4
|
+
var controller_decorator = require('./controller.decorator-U2NBWcoU.cjs');
|
|
5
5
|
|
|
6
6
|
/** The `fatal` code of a shard whose app MeoCord refused, which every shard would refuse alike. */ const REFUSED_CODE = 'Refused';
|
|
7
7
|
/** Whether an IPC message is one of MeoCord's own, rather than one of discord.js's. */ function isShardMessage(message) {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var discord_js = require('discord.js');
|
|
4
|
-
var controller_decorator = require('./controller.decorator-
|
|
5
|
-
var providers = require('./providers-
|
|
4
|
+
var controller_decorator = require('./controller.decorator-U2NBWcoU.cjs');
|
|
5
|
+
var providers = require('./providers-B4MOFBrW.cjs');
|
|
6
6
|
var enum_index = require('../enum/index.cjs');
|
|
7
7
|
var node_async_hooks = require('node:async_hooks');
|
|
8
|
-
var translateError = require('./translate-error-
|
|
8
|
+
var translateError = require('./translate-error-CbBHUu5P.cjs');
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* How a pattern ranks, one character per segment twice over: `L` for literal text or `P` for a param, then how narrow
|
|
@@ -1480,10 +1480,7 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
|
|
|
1480
1480
|
* says whether the handler ran. Its guard wrappers let this call through, so each guard runs once.
|
|
1481
1481
|
*/ async invokeHandler(instance, methodName, args, call, hooks = {}) {
|
|
1482
1482
|
const handler = `${instance.constructor.name}.${methodName}`;
|
|
1483
|
-
const onUnanswered = this.options.warnUnanswered ? (
|
|
1484
|
-
name: returnedBy.interceptor.name,
|
|
1485
|
-
handlerStarted: returnedBy.handlerStarted
|
|
1486
|
-
}) : undefined;
|
|
1483
|
+
const onUnanswered = this.options.warnUnanswered ? (unanswered)=>this.warnUnansweredOnce(handler, unanswered) : undefined;
|
|
1487
1484
|
const outcome = await providers.runHandler(this.container, instance, methodName, args, {
|
|
1488
1485
|
...this.runOptions(call),
|
|
1489
1486
|
onUnanswered,
|
|
@@ -1493,13 +1490,17 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
|
|
|
1493
1490
|
return outcome.ran;
|
|
1494
1491
|
}
|
|
1495
1492
|
/**
|
|
1496
|
-
* Warns, once per handler, that it left its interaction unanswered or deferred without a follow-up,
|
|
1497
|
-
*
|
|
1498
|
-
*/ warnUnansweredOnce(handler, phase,
|
|
1493
|
+
* Warns, once per handler, that it left its interaction or autocomplete unanswered or deferred without a follow-up,
|
|
1494
|
+
* or that an interceptor returned before the handler ran or finished, and the call ended so.
|
|
1495
|
+
*/ warnUnansweredOnce(handler, { type, phase, returnedBy }) {
|
|
1499
1496
|
if (this.warnedUnanswered.has(handler)) return;
|
|
1500
1497
|
this.warnedUnanswered.add(handler);
|
|
1498
|
+
const interceptor = returnedBy && {
|
|
1499
|
+
name: returnedBy.interceptor.name,
|
|
1500
|
+
handlerStarted: returnedBy.handlerStarted
|
|
1501
|
+
};
|
|
1501
1502
|
const returned = interceptor && `${handler}: its interceptor ${interceptor.name} returned before the handler ${interceptor.handlerStarted ? 'finished' : 'ran'}`;
|
|
1502
|
-
const what = interceptor ? phase === 'unanswered' ? `${returned}, without answering the interaction, so the user saw "The application did not respond". Answer it ` + `in ${interceptor.name}, or await next.handle().` : interceptor.handlerStarted ? `${returned}, and the deferred interaction had no follow-up when the call ended, so @Defer's lock was ` + `released while the user still saw it thinking. Follow up in ${interceptor.name} with respond(interaction).send(), ` + 'or await next.handle().' : `${returned}, and the deferred interaction was never followed up, so the user saw it thinking until Discord ` + `gave up. Follow up in ${interceptor.name} with respond(interaction).send(), or await next.handle().` : phase === 'unanswered' ? `${handler} finished without answering its interaction, so the user saw "The application did not respond". ` + 'Answer it with respond(interaction).send(), or acknowledge it first with @Defer().' : `${handler} deferred its interaction and never followed up, so the user saw it thinking until Discord gave up. ` + 'Follow up with respond(interaction).send().';
|
|
1503
|
+
const what = type === 'autocomplete' ? `${handler} finished without answering its autocomplete, so the user saw its options fail to load. Answer it ` + 'with interaction.respond(choices) within 3 seconds.' : interceptor ? phase === 'unanswered' ? `${returned}, without answering the interaction, so the user saw "The application did not respond". Answer it ` + `in ${interceptor.name}, or await next.handle().` : interceptor.handlerStarted ? `${returned}, and the deferred interaction had no follow-up when the call ended, so @Defer's lock was ` + `released while the user still saw it thinking. Follow up in ${interceptor.name} with respond(interaction).send(), ` + 'or await next.handle().' : `${returned}, and the deferred interaction was never followed up, so the user saw it thinking until Discord ` + `gave up. Follow up in ${interceptor.name} with respond(interaction).send(), or await next.handle().` : phase === 'unanswered' ? `${handler} finished without answering its interaction, so the user saw "The application did not respond". ` + 'Answer it with respond(interaction).send(), or acknowledge it first with @Defer().' : `${handler} deferred its interaction and never followed up, so the user saw it thinking until Discord gave up. ` + 'Follow up with respond(interaction).send().';
|
|
1503
1504
|
this.logger.warn(`${what} Shown once per handler; @MeoCord({ warnUnanswered: false }) turns it off.`);
|
|
1504
1505
|
}
|
|
1505
1506
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var controller_decorator = require('./controller.decorator-
|
|
3
|
+
var controller_decorator = require('./controller.decorator-U2NBWcoU.cjs');
|
|
4
4
|
var discord_js = require('discord.js');
|
|
5
5
|
|
|
6
6
|
const logger = new controller_decorator.Logger('Route');
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var controller_decorator = require('../_shared/controller.decorator-
|
|
3
|
+
var controller_decorator = require('../_shared/controller.decorator-U2NBWcoU.cjs');
|
|
4
4
|
require('reflect-metadata');
|
|
5
|
-
var translateError = require('../_shared/translate-error-
|
|
6
|
-
var shardedCooldownStore = require('../_shared/sharded-cooldown-store-
|
|
5
|
+
var translateError = require('../_shared/translate-error-CbBHUu5P.cjs');
|
|
6
|
+
var shardedCooldownStore = require('../_shared/sharded-cooldown-store-HTUb3Ii_.cjs');
|
|
7
7
|
require('../enum/index.cjs');
|
|
8
8
|
require('discord.js');
|
|
9
9
|
require('path');
|
package/dist/cjs/core/index.cjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
require('reflect-metadata');
|
|
4
|
-
var providers = require('../_shared/providers-
|
|
4
|
+
var providers = require('../_shared/providers-B4MOFBrW.cjs');
|
|
5
5
|
var inversify = require('inversify');
|
|
6
6
|
var discord_js = require('discord.js');
|
|
7
|
-
var controller_decorator = require('../_shared/controller.decorator-
|
|
7
|
+
var controller_decorator = require('../_shared/controller.decorator-U2NBWcoU.cjs');
|
|
8
8
|
require('node:crypto');
|
|
9
|
-
var shardedCooldownStore = require('../_shared/sharded-cooldown-store-
|
|
10
|
-
var startupRoots = require('../_shared/startup-roots-
|
|
9
|
+
var shardedCooldownStore = require('../_shared/sharded-cooldown-store-HTUb3Ii_.cjs');
|
|
10
|
+
var startupRoots = require('../_shared/startup-roots-lA5xkBFq.cjs');
|
|
11
11
|
var enum_index = require('../enum/index.cjs');
|
|
12
12
|
var node_fs = require('node:fs');
|
|
13
13
|
var path = require('node:path');
|
|
@@ -23,7 +23,7 @@ require('dayjs/plugin/utc.js');
|
|
|
23
23
|
require('dayjs/plugin/timezone.js');
|
|
24
24
|
require('module');
|
|
25
25
|
require('chalk');
|
|
26
|
-
require('../_shared/translate-error-
|
|
26
|
+
require('../_shared/translate-error-CbBHUu5P.cjs');
|
|
27
27
|
|
|
28
28
|
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
29
29
|
|
|
@@ -1628,7 +1628,9 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1628
1628
|
const providers$1 = controller_decorator.collectStartupErrors(()=>{
|
|
1629
1629
|
declared.forEach(controller_decorator.startupError);
|
|
1630
1630
|
startupRoots.assertDistinctCommands(options.controllers);
|
|
1631
|
-
|
|
1631
|
+
const providers$1 = providers.providerMap(options.providers ?? [], '@MeoCord({ providers })');
|
|
1632
|
+
providers.assertProvidedServices(typeof target === 'function' ? target.name : String(target), options.services ?? [], providers$1, '@MeoCord({ providers })');
|
|
1633
|
+
return providers$1;
|
|
1632
1634
|
});
|
|
1633
1635
|
// A themeFor class is bound on its own, as the cooldown store is, and resolved like a service
|
|
1634
1636
|
const themeResolver = controller_decorator.themeResolverClass(options.themeFor);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
require('reflect-metadata');
|
|
4
|
-
var providers = require('../_shared/providers-
|
|
5
|
-
var controller_decorator = require('../_shared/controller.decorator-
|
|
4
|
+
var providers = require('../_shared/providers-B4MOFBrW.cjs');
|
|
5
|
+
var controller_decorator = require('../_shared/controller.decorator-U2NBWcoU.cjs');
|
|
6
6
|
var inversify = require('inversify');
|
|
7
7
|
require('node:async_hooks');
|
|
8
8
|
require('../enum/index.cjs');
|
|
@@ -447,7 +447,7 @@ require('chalk');
|
|
|
447
447
|
controller_decorator.refuseOnMethod('@Catch', target, propertyKey);
|
|
448
448
|
providers.makeInjectable(target);
|
|
449
449
|
errorTypes.forEach((type, index)=>{
|
|
450
|
-
if (controller_decorator.
|
|
450
|
+
if (controller_decorator.isConstructible(type)) return;
|
|
451
451
|
const entry = ORDINALS[index] ? `${ORDINALS[index]} entry` : `entry ${index + 1}`;
|
|
452
452
|
controller_decorator.warnDeprecatedBehaviour(logger$1, `${target.name}: @Catch's ${entry}, ${controller_decorator.describeValue(type)}, which matches no error,`, 'is refused', 'an error class, such as @Catch(CooldownError),');
|
|
453
453
|
});
|
|
@@ -555,6 +555,35 @@ const ORDINALS = [
|
|
|
555
555
|
if (type.labelKey !== undefined) assertLabelKey(appName, name, type.labelKey, i18n);
|
|
556
556
|
}
|
|
557
557
|
}
|
|
558
|
+
/**
|
|
559
|
+
* Refuses an option that takes a class given anything else, such as an object or an arrow function, where the app is
|
|
560
|
+
* declared: the startup checks read each class's prototype, and would otherwise fail on it without naming the option.
|
|
561
|
+
*/ function assertClassOptions(appName, { presenter, cooldownStore, controllers, services }) {
|
|
562
|
+
for (const [name, value, implementing] of [
|
|
563
|
+
[
|
|
564
|
+
'presenter',
|
|
565
|
+
presenter,
|
|
566
|
+
'ResponsePresenter'
|
|
567
|
+
],
|
|
568
|
+
[
|
|
569
|
+
'cooldownStore',
|
|
570
|
+
cooldownStore,
|
|
571
|
+
'CooldownStore'
|
|
572
|
+
]
|
|
573
|
+
]){
|
|
574
|
+
if (value !== undefined && !controller_decorator.isConstructor(value)) {
|
|
575
|
+
throw controller_decorator.refuse(new TypeError(`${appName}: @MeoCord({ ${name} }) takes a class implementing ${implementing}, not ${controller_decorator.describeValue(value)}.`));
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
for (const controller of controllers ?? []){
|
|
579
|
+
if (!controller_decorator.isConstructor(controller)) throw controller_decorator.refuse(new TypeError(`${appName}: @MeoCord({ controllers }) takes classes, not ${controller_decorator.describeValue(controller)}.`));
|
|
580
|
+
}
|
|
581
|
+
// A string or symbol is a token, which a provider has to provide; that is checked as the app is created
|
|
582
|
+
for (const service of services ?? []){
|
|
583
|
+
if (controller_decorator.isConstructor(service) || typeof service === 'string' || typeof service === 'symbol') continue;
|
|
584
|
+
throw controller_decorator.refuse(new TypeError(`${appName}: @MeoCord({ services }) takes classes, or tokens that providers provide, not ${controller_decorator.describeValue(service)}.`));
|
|
585
|
+
}
|
|
586
|
+
}
|
|
558
587
|
/** Refuses a type's `labelKey` that no message of the app's default catalog has, as it would be shown as the key. */ function assertLabelKey(appName, name, labelKey, i18n) {
|
|
559
588
|
const where = `${appName}: @MeoCord({ messages: { types } }): "${name}" has labelKey`;
|
|
560
589
|
if (typeof labelKey !== 'string') throw controller_decorator.refuse(new TypeError(`${where} ${String(labelKey)}; it takes a message key, such as 'types.${name}'.`));
|
|
@@ -599,6 +628,7 @@ const ORDINALS = [
|
|
|
599
628
|
*/ function MeoCord(options) {
|
|
600
629
|
return controller_decorator.declaring((target, propertyKey)=>{
|
|
601
630
|
if (controller_decorator.deprecatedOnMethod('@MeoCord', target, propertyKey)) return;
|
|
631
|
+
assertClassOptions(target.name, options);
|
|
602
632
|
providers.assertStageEntries('@MeoCord({ guards })', 'guard', target.name, options.guards ?? []);
|
|
603
633
|
providers.assertStageEntries('@MeoCord({ interceptors })', 'interceptor', target.name, options.interceptors ?? []);
|
|
604
634
|
providers.assertStageEntries('@MeoCord({ filters })', 'filter', target.name, options.filters ?? []);
|