meocord 4.2.2 → 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 +130 -0
- package/dist/cjs/_shared/{controller.decorator-U2NBWcoU.cjs → controller.decorator-hp5xdJU4.cjs} +512 -183
- package/dist/cjs/_shared/{startup-roots-lA5xkBFq.cjs → event-sources-DzqTODXg.cjs} +265 -80
- package/dist/cjs/_shared/{providers-B4MOFBrW.cjs → providers-BBvbnmLP.cjs} +780 -281
- package/dist/cjs/_shared/{sharded-cooldown-store-HTUb3Ii_.cjs → sharded-cooldown-store-B62g5O88.cjs} +11 -9
- package/dist/cjs/_shared/{translate-error-CbBHUu5P.cjs → translate-error-wsgfsUET.cjs} +1 -1
- package/dist/cjs/common/index.cjs +4 -104
- package/dist/cjs/core/index.cjs +432 -181
- package/dist/cjs/decorator/index.cjs +85 -120
- package/dist/cjs/testing/index.cjs +1664 -784
- 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 +27 -5
- package/dist/esm/common/sharded-cooldown-store.js +11 -9
- package/dist/esm/common/theme.js +3 -1
- package/dist/esm/common/translator.js +17 -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 +83 -58
- 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 +4 -4
- 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 +44 -28
- 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 +80 -53
- package/dist/esm/core/meocord.app.js +174 -89
- package/dist/esm/core/message-help.js +5 -9
- 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 +2 -1
- package/dist/esm/core/process-state.js +19 -0
- package/dist/esm/core/providers.js +28 -3
- 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/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 +27 -29
- package/dist/esm/core/theme-validation.js +1 -1
- package/dist/esm/decorator/app.decorator.js +6 -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 +2 -2
- 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 +29 -13
- package/dist/esm/testing/cooldown-store-suite.js +3 -3
- package/dist/esm/testing/discord-defaults.js +449 -355
- package/dist/esm/testing/inspect-handler.js +91 -10
- package/dist/esm/testing/meocord-testing-module.js +195 -77
- package/dist/esm/testing/mock-fn.js +8 -2
- package/dist/esm/testing/mock-interaction.js +639 -113
- 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 +1 -0
- 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 +15 -4
- package/dist/types/common/index.d.cts +13 -9
- package/dist/types/common/index.d.ts +13 -9
- 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 +137 -54
- package/dist/types/testing/index.d.ts +137 -54
- 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,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { hasCustomId, describeInteraction,
|
|
1
|
+
import { getReactionHandlers, getMessageHandlers, matchesEmoji } from '../decorator/controller.decorator.js';
|
|
2
|
+
import { hasCustomId, describeInteraction, resolveOptionParams, matchesCommandType } from '../util/interaction.util.js';
|
|
3
|
+
import { buildAutocompleteRoutes, matchAutocompleteRoute } from './autocomplete-routes.js';
|
|
3
4
|
import { buildComponentRoutes, matchComponentRoute } from './component-routes.js';
|
|
4
5
|
import { handleUnroutedError, observeUnclaimed, runHandler, runInAppTheme, globalStagesOf, appPresenterOf } from './handler-pipeline.js';
|
|
5
6
|
import { helpWords, isListable, matchHelpRequest, computeMessageHelp, helpInvocation, renderMessageHelp, splitReply } from './message-help.js';
|
|
@@ -12,7 +13,7 @@ import { handlerInput } from './handler-input.js';
|
|
|
12
13
|
import { matchCommandRoute } from './command-routes.js';
|
|
13
14
|
import { runGuards } from './guard-runner.js';
|
|
14
15
|
import { HandlerRegistry, shareMessageRoutes } from './handler-registry.js';
|
|
15
|
-
import { matchMessageRoute, matchMessageCommand, messageStartsFor, usesAppPrefix, matchMessageSubcommands, commandWordsOf, messageStarts, buildMessageRoutes } from './message-routes.js';
|
|
16
|
+
import { matchMessageRoute, matchMessageCommand, hasMessageText, messageStartsFor, usesAppPrefix, matchMessageSubcommands, commandWordsOf, messageStarts, buildMessageRoutes, messageRouteWarnings } from './message-routes.js';
|
|
16
17
|
import { subcommandUsageError, messageCommandHooks } from './message-params.js';
|
|
17
18
|
import { CommandNotFoundError } from '../common/errors.js';
|
|
18
19
|
import { existingResponse } from '../common/response/response-state.js';
|
|
@@ -83,13 +84,19 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
83
84
|
this.logger.warn(`messages.help is on, but ${route.controllerClass.name}.${route.method} (${JSON.stringify(route.pattern)}) takes "${shown}", ` + `which runs it instead, so the built-in help never answers. Turn help off, or give the handler another word.`);
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* The call for one event, stamped as it is made, so every handler it runs reports one start and a duration that
|
|
89
|
+
* includes routing and any handler that ran before it; the fallback is told of each error that reaches it when there
|
|
90
|
+
* is a recorder.
|
|
91
|
+
*/ callFor(record) {
|
|
87
92
|
const fallback = record ? async (error, context)=>{
|
|
88
93
|
record.unhandled(error);
|
|
89
|
-
|
|
94
|
+
const failure = await this.fallback(error, context);
|
|
95
|
+
if (failure !== undefined) record.unanswered(error, failure);
|
|
96
|
+
return failure;
|
|
90
97
|
} : this.fallback;
|
|
91
98
|
return {
|
|
92
|
-
startedAt,
|
|
99
|
+
startedAt: performance.now(),
|
|
93
100
|
fallback,
|
|
94
101
|
record
|
|
95
102
|
};
|
|
@@ -100,32 +107,35 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
100
107
|
}
|
|
101
108
|
return this.controllerInstancesCache.get(controllerClass);
|
|
102
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Runs `run` on the controller's instance, resolved as its handler starts. A controller that cannot be made fails
|
|
112
|
+
* only its own handlers: the error goes to the global filters, then the fallback, and the call goes on without it.
|
|
113
|
+
*/ async withInstance(controllerClass, args, call, run, unresolved = new Set()) {
|
|
114
|
+
// A controller that failed to resolve for this event is reported once, however many of its handlers it reaches
|
|
115
|
+
if (unresolved.has(controllerClass)) return;
|
|
116
|
+
let instance;
|
|
117
|
+
try {
|
|
118
|
+
instance = this.getInstance(controllerClass);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
unresolved.add(controllerClass);
|
|
121
|
+
await handleUnroutedError(this.container, args, error, this.runOptions(call));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
await run(instance);
|
|
125
|
+
}
|
|
103
126
|
/** The component routes, built on first use; the app calls it as it is created. */ getComponentRoutes() {
|
|
104
127
|
return this.componentRoutes ?? (this.componentRoutes = buildComponentRoutes([
|
|
105
128
|
...this.controllerClasses
|
|
106
129
|
]));
|
|
107
130
|
}
|
|
108
131
|
getAutocompleteRoutes() {
|
|
109
|
-
|
|
110
|
-
const routes = [];
|
|
111
|
-
for (const controllerClass of this.controllerClasses){
|
|
112
|
-
for (const meta of getAutocompleteHandlers(this.getInstance(controllerClass))){
|
|
113
|
-
routes.push({
|
|
114
|
-
controllerClass,
|
|
115
|
-
meta
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
routes.sort((a, b)=>Number(Boolean(b.meta.optionName)) - Number(Boolean(a.meta.optionName)));
|
|
120
|
-
this.autocompleteRoutes = routes;
|
|
121
|
-
return routes;
|
|
132
|
+
return this.autocompleteRoutes ?? (this.autocompleteRoutes = buildAutocompleteRoutes(this.controllerClasses));
|
|
122
133
|
}
|
|
123
134
|
/**
|
|
124
135
|
* Dispatches an interaction. A failure outside any handler, such as no route matching or resolving
|
|
125
136
|
* the controller, goes to the global filters, then the fallback.
|
|
126
137
|
*/ async interaction(interaction, record) {
|
|
127
|
-
|
|
128
|
-
const call = this.callFor(record, performance.now());
|
|
138
|
+
const call = this.callFor(record);
|
|
129
139
|
try {
|
|
130
140
|
await this.dispatchInteraction(interaction, call);
|
|
131
141
|
} catch (error) {
|
|
@@ -151,14 +161,14 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
151
161
|
if (matched) {
|
|
152
162
|
const { route, params } = matched;
|
|
153
163
|
interaction.dynamicParams = params;
|
|
154
|
-
await this.executeCommand(
|
|
164
|
+
await this.executeCommand(route.controllerClass, route.meta, interaction, call);
|
|
155
165
|
return;
|
|
156
166
|
}
|
|
157
167
|
}
|
|
158
168
|
// The same match a test's invoke checks a handler against, so the two cannot disagree
|
|
159
169
|
const command = matchCommandRoute(this.controllerClasses, interaction);
|
|
160
170
|
if (command) {
|
|
161
|
-
await this.executeCommand(
|
|
171
|
+
await this.executeCommand(command.controllerClass, command.meta, interaction, call);
|
|
162
172
|
return;
|
|
163
173
|
}
|
|
164
174
|
// A component or modal no route takes may be a collector's: with another listener on the client, it has the
|
|
@@ -181,21 +191,17 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
181
191
|
* Answers an autocomplete interaction from the `@Autocomplete` handler that claims it. An unclaimed
|
|
182
192
|
* option gets an empty list and a warning, rather than a menu stuck loading until Discord times out.
|
|
183
193
|
*/ async handleAutocomplete(interaction, call) {
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
], call);
|
|
196
|
-
if (!ran) await closeAutocomplete(interaction, this.logger);
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
194
|
+
const route = matchAutocompleteRoute(this.getAutocompleteRoutes(), interaction);
|
|
195
|
+
if (route) {
|
|
196
|
+
const { controllerClass, meta } = route;
|
|
197
|
+
this.logger.log('[AUTOCOMPLETE]', `[${meta.commandPath}]`, `[${meta.methodName}]`);
|
|
198
|
+
const params = resolveOptionParams(interaction);
|
|
199
|
+
const ran = await this.invokeHandler(this.getInstance(controllerClass), meta.methodName, [
|
|
200
|
+
interaction,
|
|
201
|
+
params
|
|
202
|
+
], call);
|
|
203
|
+
if (!ran) await closeAutocomplete(interaction, this.logger);
|
|
204
|
+
return;
|
|
199
205
|
}
|
|
200
206
|
this.logger.warn(`No handler matched ${describeInteraction(interaction)}. Declare an @Autocomplete handler for it, ` + `or drop setAutocomplete(true) from the option.`);
|
|
201
207
|
await closeAutocomplete(interaction, this.logger);
|
|
@@ -203,19 +209,22 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
203
209
|
interaction
|
|
204
210
|
], this.runOptions(call));
|
|
205
211
|
}
|
|
206
|
-
/** Warns, in development, that a modal field or select menu choice is hidden by a customId param of the same name. */ warnCollisions(methodName, names) {
|
|
212
|
+
/** Warns, in development, that a modal field or select menu choice is hidden by a customId param of the same name. */ warnCollisions(controllerClass, methodName, names) {
|
|
207
213
|
if (process.env.NODE_ENV !== 'development') return;
|
|
214
|
+
let warned = this.warnedCollisions.get(controllerClass);
|
|
215
|
+
if (!warned) this.warnedCollisions.set(controllerClass, warned = new Set());
|
|
208
216
|
for (const name of names){
|
|
209
217
|
const key = `${methodName}:${name}`;
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
this.logger.warn(`"${name}" is both a customId param and a modal field or select menu choice of ${methodName}; the handler receives the customId
|
|
218
|
+
if (warned.has(key)) continue;
|
|
219
|
+
warned.add(key);
|
|
220
|
+
this.logger.warn(`"${name}" is both a customId param and a modal field or select menu choice of ${controllerClass.name}.${methodName}; the handler ` + `receives the customId param. Rename one to receive both.`);
|
|
213
221
|
}
|
|
214
222
|
}
|
|
215
223
|
/**
|
|
216
224
|
* Runs a resolved command, shared by both dispatch paths so a pattern-matched
|
|
217
225
|
* component and a named slash command behave identically once the route is chosen.
|
|
218
|
-
*/ async executeCommand(
|
|
226
|
+
*/ async executeCommand(controllerClass, commandMetadata, interaction, call) {
|
|
227
|
+
const controllerInstance = this.getInstance(controllerClass);
|
|
219
228
|
const { methodName, type } = commandMetadata;
|
|
220
229
|
// No interaction-type check here: both callers pick the route with
|
|
221
230
|
// `matchesCommandType` before getting this far, and `@Command` re-checks the
|
|
@@ -223,7 +232,7 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
223
232
|
this.logger.log('[INTERACTION]', `[${type}]`, `[${methodName}]`);
|
|
224
233
|
const routeParams = interaction.dynamicParams;
|
|
225
234
|
const { params, collisions } = handlerInput(interaction, routeParams);
|
|
226
|
-
this.warnCollisions(methodName, collisions);
|
|
235
|
+
this.warnCollisions(controllerClass, methodName, collisions);
|
|
227
236
|
await this.invokeHandler(controllerInstance, methodName, [
|
|
228
237
|
interaction,
|
|
229
238
|
params
|
|
@@ -270,7 +279,7 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
270
279
|
* usage through that handler's filters. A bare parent's unguarded subcommands' usage, once the app's guards allow
|
|
271
280
|
* it, and a failure to read the prefixes, go through the global filters, then the fallback; the listeners still run.
|
|
272
281
|
*/ async message(message, record) {
|
|
273
|
-
if (message.author.bot || !message.content
|
|
282
|
+
if (message.author.bot || !hasMessageText(message.content)) return;
|
|
274
283
|
const call = this.callFor(record);
|
|
275
284
|
let target;
|
|
276
285
|
try {
|
|
@@ -297,24 +306,29 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
297
306
|
}
|
|
298
307
|
// The matched handler, then every listener, each resolved from the container only as it starts
|
|
299
308
|
const runs = [];
|
|
309
|
+
const unresolved = new Set();
|
|
300
310
|
if (target) {
|
|
301
311
|
const { route, params, start, given } = target;
|
|
302
312
|
noteInvocation(message, `${start}${commandWordsOf(route.tokens).join(' ')}`);
|
|
303
313
|
const hooks = messageCommandHooks(route, params, message, start, given, this.messageOptions.types);
|
|
304
314
|
runs.push({
|
|
305
315
|
name: `${route.controllerClass.name}.${route.method}`,
|
|
306
|
-
run: (at)=>this.
|
|
307
|
-
message
|
|
308
|
-
|
|
309
|
-
|
|
316
|
+
run: (at)=>this.withInstance(route.controllerClass, [
|
|
317
|
+
message
|
|
318
|
+
], at, (instance)=>this.invokeHandler(instance, route.method, [
|
|
319
|
+
message,
|
|
320
|
+
params
|
|
321
|
+
], at, hooks), unresolved)
|
|
310
322
|
});
|
|
311
323
|
}
|
|
312
324
|
for (const { controllerClass, method } of this.messageListeners){
|
|
313
325
|
runs.push({
|
|
314
326
|
name: `${controllerClass.name}.${method}`,
|
|
315
|
-
run: (at)=>this.
|
|
327
|
+
run: (at)=>this.withInstance(controllerClass, [
|
|
316
328
|
message
|
|
317
|
-
], at)
|
|
329
|
+
], at, (instance)=>this.invokeHandler(instance, method, [
|
|
330
|
+
message
|
|
331
|
+
], at), unresolved)
|
|
318
332
|
});
|
|
319
333
|
}
|
|
320
334
|
if (this.messageOptions.handlers === 'concurrent') await this.runTogether(runs, call);
|
|
@@ -339,7 +353,8 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
339
353
|
...call,
|
|
340
354
|
record: {
|
|
341
355
|
settled: (...report)=>reports[index].push(report),
|
|
342
|
-
unhandled: (error)=>record.unhandled(error)
|
|
356
|
+
unhandled: (error)=>record.unhandled(error),
|
|
357
|
+
unanswered: (error, failure)=>record.unanswered(error, failure)
|
|
343
358
|
}
|
|
344
359
|
} : call)));
|
|
345
360
|
for (const report of reports.flat())record?.settled(...report);
|
|
@@ -419,7 +434,7 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
419
434
|
const matching = this.controllerClasses.map((controllerClass)=>({
|
|
420
435
|
controllerClass,
|
|
421
436
|
// Handlers for this emoji first, then those for every emoji
|
|
422
|
-
handlers: getReactionHandlers(
|
|
437
|
+
handlers: getReactionHandlers(controllerClass.prototype).filter(forEmoji).sort((a, b)=>Number(!a.emoji) - Number(!b.emoji))
|
|
423
438
|
})).filter(({ handlers })=>handlers.length > 0);
|
|
424
439
|
if (matching.length === 0) return;
|
|
425
440
|
// Only asked when a matching handler leaves bots out, since a partial user costs a fetch
|
|
@@ -443,16 +458,21 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
443
458
|
return;
|
|
444
459
|
}
|
|
445
460
|
for (const { controllerClass, handlers } of runs){
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
461
|
+
await this.withInstance(controllerClass, [
|
|
462
|
+
reaction,
|
|
463
|
+
{
|
|
464
|
+
user,
|
|
465
|
+
action
|
|
466
|
+
}
|
|
467
|
+
], call, async (instance)=>{
|
|
468
|
+
for (const { method } of handlers)await this.invokeHandler(instance, method, [
|
|
449
469
|
reaction,
|
|
450
470
|
{
|
|
451
471
|
user,
|
|
452
472
|
action
|
|
453
473
|
}
|
|
454
474
|
], call);
|
|
455
|
-
}
|
|
475
|
+
});
|
|
456
476
|
}
|
|
457
477
|
}
|
|
458
478
|
/**
|
|
@@ -471,7 +491,10 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
471
491
|
constructor(options){
|
|
472
492
|
this.options = options;
|
|
473
493
|
this.controllerInstancesCache = new Map();
|
|
474
|
-
/**
|
|
494
|
+
/**
|
|
495
|
+
* Method and name pairs already warned about, by controller class, so a collision warns once per handler, not on
|
|
496
|
+
* every submit or selection. Keyed by the class itself, so two controllers of one name stay apart.
|
|
497
|
+
*/ this.warnedCollisions = new WeakMap();
|
|
475
498
|
/** The handlers already warned about, so each is named once however often it runs. */ this.warnedUnanswered = new Set();
|
|
476
499
|
/** The message handlers already warned about as slow, so each is named once. */ this.warnedSlow = new Set();
|
|
477
500
|
this.container = options.container;
|
|
@@ -486,6 +509,8 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
486
509
|
// The app's own help command reads this table too, so it lists the commands dispatch reaches
|
|
487
510
|
if (this.container.isBound(HandlerRegistry)) shareMessageRoutes(this.container.get(HandlerRegistry), this.messageRoutes);
|
|
488
511
|
this.warnUnreachableHelp();
|
|
512
|
+
// Here, where the routes are built once, rather than in buildMessageRoutes, which startup runs twice
|
|
513
|
+
for (const warning of messageRouteWarnings(this.messageRoutes, this.messageOptions))this.logger.warn(warning);
|
|
489
514
|
this.messageListeners = this.controllerClasses.flatMap((controllerClass)=>getMessageHandlers(controllerClass.prototype).filter((handler)=>handler.pattern === undefined).map(({ method })=>({
|
|
490
515
|
controllerClass,
|
|
491
516
|
method
|
|
@@ -13,6 +13,22 @@ const REACTION_PARTIALS = [
|
|
|
13
13
|
Partials.Message,
|
|
14
14
|
Partials.Reaction
|
|
15
15
|
];
|
|
16
|
+
const POLLS = [
|
|
17
|
+
GatewayIntentBits.GuildMessagePolls,
|
|
18
|
+
GatewayIntentBits.DirectMessagePolls
|
|
19
|
+
];
|
|
20
|
+
// discord.js drops a vote on a message it has not cached unless it may build the message, its poll and the answer
|
|
21
|
+
const POLL_PARTIALS = [
|
|
22
|
+
Partials.Message,
|
|
23
|
+
Partials.Poll,
|
|
24
|
+
Partials.PollAnswer
|
|
25
|
+
];
|
|
26
|
+
// The partials whose warning names members and polls; the others keep the wording for messages, reactions and users
|
|
27
|
+
const CACHED_ELSEWHERE = new Set([
|
|
28
|
+
Partials.GuildMember,
|
|
29
|
+
Partials.Poll,
|
|
30
|
+
Partials.PollAnswer
|
|
31
|
+
]);
|
|
16
32
|
/** The intents Discord only sends once they are also enabled for the app in the developer portal. */ const PRIVILEGED = new Set([
|
|
17
33
|
GatewayIntentBits.GuildMembers,
|
|
18
34
|
GatewayIntentBits.GuildPresences,
|
|
@@ -50,10 +66,9 @@ const REACTION_PARTIALS = [
|
|
|
50
66
|
]
|
|
51
67
|
}
|
|
52
68
|
])),
|
|
69
|
+
// A member that joins arrives whole; one that leaves or changes reaches its handler only if cached or with this partial
|
|
53
70
|
...Object.fromEntries([
|
|
54
71
|
'guildMemberAdd',
|
|
55
|
-
'guildMemberRemove',
|
|
56
|
-
'guildMemberUpdate',
|
|
57
72
|
'guildMemberAvailable',
|
|
58
73
|
'threadMembersUpdate'
|
|
59
74
|
].map((event)=>[
|
|
@@ -66,6 +81,26 @@ const REACTION_PARTIALS = [
|
|
|
66
81
|
]
|
|
67
82
|
}
|
|
68
83
|
])),
|
|
84
|
+
guildMemberRemove: {
|
|
85
|
+
intents: [
|
|
86
|
+
[
|
|
87
|
+
GatewayIntentBits.GuildMembers
|
|
88
|
+
]
|
|
89
|
+
],
|
|
90
|
+
partials: [
|
|
91
|
+
Partials.GuildMember
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
guildMemberUpdate: {
|
|
95
|
+
intents: [
|
|
96
|
+
[
|
|
97
|
+
GatewayIntentBits.GuildMembers
|
|
98
|
+
]
|
|
99
|
+
],
|
|
100
|
+
partials: [
|
|
101
|
+
Partials.GuildMember
|
|
102
|
+
]
|
|
103
|
+
},
|
|
69
104
|
guildAuditLogEntryCreate: {
|
|
70
105
|
intents: [
|
|
71
106
|
[
|
|
@@ -198,9 +233,7 @@ const REACTION_PARTIALS = [
|
|
|
198
233
|
intents: [
|
|
199
234
|
REACTIONS
|
|
200
235
|
],
|
|
201
|
-
partials:
|
|
202
|
-
Partials.Reaction
|
|
203
|
-
]
|
|
236
|
+
partials: REACTION_PARTIALS
|
|
204
237
|
},
|
|
205
238
|
typingStart: {
|
|
206
239
|
intents: [
|
|
@@ -212,26 +245,20 @@ const REACTION_PARTIALS = [
|
|
|
212
245
|
},
|
|
213
246
|
messagePollVoteAdd: {
|
|
214
247
|
intents: [
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
]
|
|
219
|
-
]
|
|
248
|
+
POLLS
|
|
249
|
+
],
|
|
250
|
+
partials: POLL_PARTIALS
|
|
220
251
|
},
|
|
221
252
|
messagePollVoteRemove: {
|
|
222
253
|
intents: [
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
]
|
|
227
|
-
]
|
|
254
|
+
POLLS
|
|
255
|
+
],
|
|
256
|
+
partials: POLL_PARTIALS
|
|
228
257
|
},
|
|
229
258
|
...Object.fromEntries([
|
|
230
259
|
'guildScheduledEventCreate',
|
|
231
260
|
'guildScheduledEventDelete',
|
|
232
|
-
'guildScheduledEventUpdate'
|
|
233
|
-
'guildScheduledEventUserAdd',
|
|
234
|
-
'guildScheduledEventUserRemove'
|
|
261
|
+
'guildScheduledEventUpdate'
|
|
235
262
|
].map((event)=>[
|
|
236
263
|
event,
|
|
237
264
|
{
|
|
@@ -242,6 +269,27 @@ const REACTION_PARTIALS = [
|
|
|
242
269
|
]
|
|
243
270
|
}
|
|
244
271
|
])),
|
|
272
|
+
// Each names its user by id alone, which discord.js reads from the cache unless this partial is on
|
|
273
|
+
guildScheduledEventUserAdd: {
|
|
274
|
+
intents: [
|
|
275
|
+
[
|
|
276
|
+
GatewayIntentBits.GuildScheduledEvents
|
|
277
|
+
]
|
|
278
|
+
],
|
|
279
|
+
partials: [
|
|
280
|
+
Partials.User
|
|
281
|
+
]
|
|
282
|
+
},
|
|
283
|
+
guildScheduledEventUserRemove: {
|
|
284
|
+
intents: [
|
|
285
|
+
[
|
|
286
|
+
GatewayIntentBits.GuildScheduledEvents
|
|
287
|
+
]
|
|
288
|
+
],
|
|
289
|
+
partials: [
|
|
290
|
+
Partials.User
|
|
291
|
+
]
|
|
292
|
+
},
|
|
245
293
|
autoModerationRuleCreate: {
|
|
246
294
|
intents: [
|
|
247
295
|
[
|
|
@@ -355,7 +403,7 @@ function intentName(bit) {
|
|
|
355
403
|
(labels.some((label)=>label.startsWith("@MessageHandler('")) ? "; messages: { mention: 'only' } needs none." : '.') : ''));
|
|
356
404
|
}
|
|
357
405
|
for (const [partial, labels] of missingPartials){
|
|
358
|
-
warnings.push(`Partials.${Partials[partial]} is not in clientOptions.partials, so ${labels.join(', ')} will miss ` + (partial === Partials.Channel ? 'direct messages: no DM channel is cached after the bot starts, and discord.js drops a message from a ' + 'channel it has not cached.' : 'events about messages, reactions or users the bot has not cached, such as messages sent before it started.'));
|
|
406
|
+
warnings.push(`Partials.${Partials[partial]} is not in clientOptions.partials, so ${labels.join(', ')} will miss ` + (partial === Partials.Channel ? 'direct messages: no DM channel is cached after the bot starts, and discord.js drops a message from a ' + 'channel it has not cached.' : CACHED_ELSEWHERE.has(partial) ? 'events about members or polls the bot has not cached, such as members who joined before it started.' : 'events about messages, reactions or users the bot has not cached, such as messages sent before it started.'));
|
|
359
407
|
}
|
|
360
408
|
return warnings;
|
|
361
409
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { getEventHandlers } from '../decorator/event.decorator.js';
|
|
2
|
+
import { prepareEventHandler } from './handler-pipeline.js';
|
|
3
|
+
import { isAppClassToken } from './lifecycle-order.js';
|
|
4
|
+
import { isClassProvider } from './providers.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One source for each instance whose lifecycle hooks run, the cooldown store aside, in lifecycle order; read once the
|
|
8
|
+
* providers are made, as a value's or a factory's class is its instance's. A class `prepared` lacks has its handlers
|
|
9
|
+
* prepared here, and one a controller's would be refused for is left out, with `warn` naming the refusal.
|
|
10
|
+
*/ function eventSources(container, providers, units, prepared, warn) {
|
|
11
|
+
// The tokens of each class the container makes, bound as itself or given as a provider's `useClass`, the cooldown
|
|
12
|
+
// store's included: no event reaches it, nor its instance through a factory
|
|
13
|
+
const classTokens = new Map();
|
|
14
|
+
for (const { token } of units){
|
|
15
|
+
const provider = providers.get(token);
|
|
16
|
+
if (provider && !isClassProvider(provider) || !provider && !isAppClassToken(token)) continue;
|
|
17
|
+
const cls = provider ? provider.useClass : token;
|
|
18
|
+
classTokens.set(cls, [
|
|
19
|
+
...classTokens.get(cls) ?? [],
|
|
20
|
+
token
|
|
21
|
+
]);
|
|
22
|
+
}
|
|
23
|
+
const names = new Map(prepared.map((cls)=>[
|
|
24
|
+
cls.name,
|
|
25
|
+
cls
|
|
26
|
+
]));
|
|
27
|
+
const handlers = new Map();
|
|
28
|
+
const handlersOf = (cls)=>{
|
|
29
|
+
let found = handlers.get(cls);
|
|
30
|
+
if (!found) {
|
|
31
|
+
found = prepared.includes(cls) ? getEventHandlers(cls.prototype) : prepareClass(container, cls, names, warn);
|
|
32
|
+
handlers.set(cls, found);
|
|
33
|
+
}
|
|
34
|
+
return found;
|
|
35
|
+
};
|
|
36
|
+
const sources = [];
|
|
37
|
+
const given = new Set();
|
|
38
|
+
for (const { token, cooldownStore } of units){
|
|
39
|
+
// The cooldown store is the app's infrastructure, never one of its classes, so no event reaches it
|
|
40
|
+
if (cooldownStore) continue;
|
|
41
|
+
const provider = providers.get(token);
|
|
42
|
+
if (!provider || isClassProvider(provider)) {
|
|
43
|
+
const cls = provider ? provider.useClass : token;
|
|
44
|
+
const found = handlersOf(cls);
|
|
45
|
+
if (found.length > 0) sources.push({
|
|
46
|
+
cls,
|
|
47
|
+
handlers: found,
|
|
48
|
+
resolve: ()=>container.get(token)
|
|
49
|
+
});
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
// A value, or a factory's, made by now; one instance under several tokens is one source
|
|
53
|
+
const instance = container.get(token);
|
|
54
|
+
if (typeof instance !== 'object' || instance === null || given.has(instance)) continue;
|
|
55
|
+
given.add(instance);
|
|
56
|
+
const cls = instance.constructor;
|
|
57
|
+
if (typeof cls !== 'function') continue;
|
|
58
|
+
const found = handlersOf(cls);
|
|
59
|
+
if (found.length === 0) continue;
|
|
60
|
+
// A factory may return the instance the container made for a class unit, whose own source runs it; only units of
|
|
61
|
+
// the instance's class are resolved, and each resolves on the event through its own source anyway
|
|
62
|
+
const tokens = classTokens.get(cls) ?? [];
|
|
63
|
+
const owned = ()=>tokens.some((token)=>container.get(token) === instance);
|
|
64
|
+
sources.push({
|
|
65
|
+
cls,
|
|
66
|
+
handlers: found,
|
|
67
|
+
resolve: ()=>owned() ? undefined : instance
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return sources;
|
|
71
|
+
}
|
|
72
|
+
/** The handlers of `cls` that pass a controller's checks, warning of each other one and why it does not run. */ function prepareClass(container, cls, names, warn) {
|
|
73
|
+
const declared = getEventHandlers(cls.prototype);
|
|
74
|
+
if (declared.length === 0) return [];
|
|
75
|
+
const other = names.get(cls.name);
|
|
76
|
+
if (!other) names.set(cls.name, cls);
|
|
77
|
+
// @Once tells classes apart by name, so this class's would share another's
|
|
78
|
+
const sharesName = other !== undefined && other !== cls && declared.some((handler)=>handler.once);
|
|
79
|
+
if (sharesName) {
|
|
80
|
+
refused(warn, cls, `${cls.name}: two classes have this name; @Cooldown and @Once tell classes apart by name, so they would share their counts. Rename one of them.`, declared.filter((handler)=>handler.once));
|
|
81
|
+
}
|
|
82
|
+
const errors = new Map();
|
|
83
|
+
return declared.filter((handler)=>{
|
|
84
|
+
if (sharesName && handler.once) return false;
|
|
85
|
+
let found = errors.get(handler.method);
|
|
86
|
+
if (!found) {
|
|
87
|
+
found = prepareEventHandler(container, cls, handler.method);
|
|
88
|
+
errors.set(handler.method, found);
|
|
89
|
+
const onMethod = declared.filter((other)=>other.method === handler.method && !(sharesName && other.once));
|
|
90
|
+
for (const error of found)refused(warn, cls, error.message, onMethod);
|
|
91
|
+
}
|
|
92
|
+
return found.length === 0;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function refused(warn, cls, reason, handlers) {
|
|
96
|
+
const labels = handlers.map(({ event, method, once })=>`@${once ? 'Once' : 'On'}('${event}') in ${cls.name}.${method}`);
|
|
97
|
+
warn(`${reason} Until the next major version (5.0) refuses it at startup, ${labels.join(', ')} does not run.`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export { eventSources };
|
|
@@ -9,6 +9,7 @@ import { interactionLocale, translatorOfClient, messageLocale, renderText } from
|
|
|
9
9
|
import { logFailedSend } from '../common/response/send-failure.js';
|
|
10
10
|
import { presenterFor, fitText, TEXT_LIMITS, withSendableFiles, DEFAULT_ATTACHMENT_SIZE_LIMIT, attachmentsOf, renderEmbed, withoutFiles, isTooLarge, REFUSED_AS_TOO_LARGE } from '../common/response/presenter.js';
|
|
11
11
|
import { escapeForLog, quoteForLog } from '../util/user-text.util.js';
|
|
12
|
+
import { timerKeepsSeconds } from '../util/timer-limit.util.js';
|
|
12
13
|
|
|
13
14
|
const UNKNOWN_INTERACTION = 10062;
|
|
14
15
|
/** Discord's refusal of a direct message: the member's are closed, or they blocked the bot. */ const CANNOT_MESSAGE_USER = 50007;
|
|
@@ -280,7 +281,8 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
|
|
|
280
281
|
parse: []
|
|
281
282
|
}
|
|
282
283
|
}));
|
|
283
|
-
|
|
284
|
+
// Longer than a timer can wait keeps it, as 0 does; the timer would fire at once
|
|
285
|
+
if (seconds > 0 && timerKeepsSeconds(seconds)) {
|
|
284
286
|
setTimeout(()=>{
|
|
285
287
|
reply.delete().catch((failure)=>logFailedSend(logger, 'delete a reply to a command', failure));
|
|
286
288
|
}, seconds * 1000).unref?.();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
|
-
import { perHandler, sourcePrototype, stageClasses } from './guard-runner.js';
|
|
2
|
+
import { perHandler, sourcePrototype, stageClasses, stageOwners } from './guard-runner.js';
|
|
3
3
|
import { bindShared } from './interceptor-runner.js';
|
|
4
4
|
import { startupError } from '../util/refusal.util.js';
|
|
5
5
|
import { isConstructible } from '../util/value.util.js';
|
|
@@ -26,13 +26,13 @@ import { META } from '../util/metadata-keys.js';
|
|
|
26
26
|
];
|
|
27
27
|
}
|
|
28
28
|
/** A handler's method and class filters, the classes innermost first, resolved once. */ const ownFilterLevels = perHandler((prototype, methodName)=>{
|
|
29
|
-
|
|
30
|
-
if (!source) return {
|
|
29
|
+
if (!sourcePrototype(prototype, methodName)) return {
|
|
31
30
|
method: [],
|
|
32
31
|
classes: []
|
|
33
32
|
};
|
|
34
33
|
return {
|
|
35
|
-
|
|
34
|
+
// An override's filters before those of the methods it overrides, innermost first as the classes are
|
|
35
|
+
method: stageOwners(prototype, methodName).reverse().flatMap((owner)=>Reflect.getOwnMetadata(META.methodFilters, owner, methodName) ?? []),
|
|
36
36
|
classes: [
|
|
37
37
|
...stageClasses(prototype, methodName)
|
|
38
38
|
].reverse().flatMap((cls)=>Reflect.getOwnMetadata(META.classFilters, cls) ?? [])
|