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,12 +1,13 @@
|
|
|
1
1
|
import { REST, Routes, Message } from 'discord.js';
|
|
2
2
|
import { Logger, hideInLogs } from '../common/logger.js';
|
|
3
|
-
import
|
|
4
|
-
import './theme-scope.js';
|
|
5
|
-
import './theme-validation.js';
|
|
3
|
+
import '../common/theme.js';
|
|
6
4
|
import '../common/decorator.js';
|
|
7
5
|
import 'reflect-metadata';
|
|
6
|
+
import { warnDeprecatedBehaviour } from '../common/deprecation.js';
|
|
8
7
|
import '../common/response/response-state.js';
|
|
8
|
+
import './theme-scope.js';
|
|
9
9
|
import { GuardDeniedError, UserError } from '../common/errors.js';
|
|
10
|
+
import '../common/cooldown-store.js';
|
|
10
11
|
import 'node:crypto';
|
|
11
12
|
import { getMessageHandlers, getReactionHandlers } from '../decorator/controller.decorator.js';
|
|
12
13
|
import './scalar-types.js';
|
|
@@ -15,18 +16,17 @@ import { managerGone } from '../util/sharding-mode.util.js';
|
|
|
15
16
|
import '../common/translator.js';
|
|
16
17
|
import '../common/meocord-text.js';
|
|
17
18
|
import { markExplained } from '../common/explained-error.js';
|
|
19
|
+
import './theme-resolvers.js';
|
|
18
20
|
import { messageDecorator, reactionDecorator } from './command-conflicts.js';
|
|
19
21
|
import { ReactionHandlerAction } from '../enum/controller.enum.js';
|
|
20
22
|
import { globalStagesOf, runHandler } from './handler-pipeline.js';
|
|
21
23
|
import { createFallback, replyWithUserError } from './fallback.js';
|
|
22
24
|
import { waitForCooldownStore, claimStoreDownNotice, claimCooldownNotice } from './cooldown-runner.js';
|
|
23
25
|
import { stageTypes, stageClass } from './stage-scope.js';
|
|
24
|
-
import { getEventHandlers } from '../decorator/event.decorator.js';
|
|
25
26
|
import { eventRequirements, messageHandlerRequirements, REACTION_HANDLER_REQUIREMENTS, missingRequirementWarnings } from './event-requirements.js';
|
|
26
|
-
import {
|
|
27
|
-
import { runReadyHooks, runShutdownSequence, ReadyPass } from './lifecycle-hooks.js';
|
|
27
|
+
import { runShutdownSequence, runReadyHooks, lifecycleEntry, ReadyPass } from './lifecycle-hooks.js';
|
|
28
28
|
import { RunningCalls } from './running-calls.js';
|
|
29
|
-
import {
|
|
29
|
+
import { processState } from './process-state.js';
|
|
30
30
|
import { tellDevRunner, onDevRunnerStop } from '../util/dev-runner.util.js';
|
|
31
31
|
import { fatalLoginCode, explainLoginFailure, tokenMessage, isRefusedToken } from './login-failure.js';
|
|
32
32
|
import { endFailedShard, tellManager } from './shard-exit.js';
|
|
@@ -36,21 +36,18 @@ import { undoFailedLogin } from './failed-login.js';
|
|
|
36
36
|
import { ownInteractionListener, Dispatcher } from './dispatcher.js';
|
|
37
37
|
import { deliverUncachedDmReactions } from './dm-reactions.js';
|
|
38
38
|
import { loadMeoCordConfig } from '../util/meocord-config-loader.util.js';
|
|
39
|
-
import { isRegisterOnly, FORCE_REGISTER_ENV, REGISTER_GUILD_ENV } from '../util/registration-mode.util.js';
|
|
39
|
+
import { isRegisterOnly, FORCE_REGISTER_ENV, registersAsDevelopment, REGISTER_GUILD_ENV } from '../util/registration-mode.util.js';
|
|
40
40
|
import { shutdownTimeoutOf } from '../util/shutdown-timeout.util.js';
|
|
41
41
|
import { escapeForLog } from '../util/user-text.util.js';
|
|
42
42
|
|
|
43
43
|
/** How long an `onReady` hook runs before a warning says the hooks after it are waiting. */ const SLOW_READY_HOOK_MS = 10000;
|
|
44
|
-
|
|
45
|
-
const stopRequest = stopRequests();
|
|
46
|
-
let signalHandlersInstalled = false;
|
|
47
|
-
let shuttingDown;
|
|
44
|
+
const { runningApps } = processState;
|
|
48
45
|
/**
|
|
49
|
-
* Shuts every started app down and exits, with
|
|
50
|
-
* it, and in a shard its manager's request or its going. A call within `REPEAT_SIGNAL_WINDOW_MS` of the first is the
|
|
46
|
+
* Shuts every started app down and exits, with the code set when the shutdown began or 0, and 1 if an app failed to
|
|
47
|
+
* close. Signals call it, and in a shard its manager's request or its going. A call within `REPEAT_SIGNAL_WINDOW_MS` of the first is the
|
|
51
48
|
* same request; one after it forces exit 1, except in a shard, whose manager forces it.
|
|
52
49
|
*/ async function shutdownAndExit(shard) {
|
|
53
|
-
const request = stopRequest();
|
|
50
|
+
const request = processState.stopRequest();
|
|
54
51
|
if (request !== 'first') {
|
|
55
52
|
// A shard hears Ctrl+C both directly and from its manager, which owns forcing it; so it waits
|
|
56
53
|
if (request === 'duplicate' || shard) return;
|
|
@@ -60,23 +57,28 @@ let shuttingDown;
|
|
|
60
57
|
return shutDown();
|
|
61
58
|
}
|
|
62
59
|
/** The one shutdown of the process, however many ask for it: a stop request joins one already under way. */ function shutDown() {
|
|
63
|
-
|
|
60
|
+
var _processState;
|
|
61
|
+
(_processState = processState).shuttingDown ?? (_processState.shuttingDown = (async ()=>{
|
|
62
|
+
// A clean stop keeps a code already set, such as the 1 a failed login set, so a supervisor still sees it. Read as
|
|
63
|
+
// the shutdown begins: a code main.ts sets as it hears of the stop is the stop's, not a failure.
|
|
64
|
+
const exitCode = Number(process.exitCode ?? 0) || 0;
|
|
64
65
|
const closed = await Promise.all([
|
|
65
66
|
...runningApps
|
|
66
67
|
].map((close)=>close()));
|
|
67
|
-
|
|
68
|
-
process.exit(closed.every(Boolean) ? Number(process.exitCode ?? 0) || 0 : 1);
|
|
68
|
+
process.exit(closed.every(Boolean) ? exitCode : 1);
|
|
69
69
|
})());
|
|
70
|
-
return shuttingDown;
|
|
70
|
+
return processState.shuttingDown;
|
|
71
71
|
}
|
|
72
|
+
/** The errors {@link stoppedBeforeOnline} made, which end a start without being its failure. */ const stops = new WeakSet();
|
|
72
73
|
/** What `start()` rejects with when shutdown begins before the bot came online, which shutdown has logged. */ function stoppedBeforeOnline() {
|
|
73
74
|
const error = new Error('The bot was stopped before it came online.');
|
|
74
75
|
markExplained(error);
|
|
76
|
+
stops.add(error);
|
|
75
77
|
return error;
|
|
76
78
|
}
|
|
77
79
|
/** One pair of signal listeners for the process, however many apps it starts, for the role the first decided. */ function installSignalHandlers(shard) {
|
|
78
|
-
if (signalHandlersInstalled) return;
|
|
79
|
-
signalHandlersInstalled = true;
|
|
80
|
+
if (processState.signalHandlersInstalled) return;
|
|
81
|
+
processState.signalHandlersInstalled = true;
|
|
80
82
|
process.on('SIGINT', ()=>void shutdownAndExit(shard));
|
|
81
83
|
process.on('SIGTERM', ()=>void shutdownAndExit(shard));
|
|
82
84
|
// How `meocord start --dev` stops the application to restart it, a signal being no graceful stop on Windows. It is
|
|
@@ -129,13 +131,21 @@ class MeoCordApp {
|
|
|
129
131
|
}
|
|
130
132
|
}
|
|
131
133
|
/**
|
|
132
|
-
* Logs the bot in, as {@link MeoCordApplication.start} describes. The
|
|
133
|
-
*
|
|
134
|
+
* Logs the bot in, as {@link MeoCordApplication.start} describes. The event handlers are attached once; the providers
|
|
135
|
+
* and services are made for each start, as a failed one, of a factory or the login, shuts them down and drops them.
|
|
134
136
|
*/ async start() {
|
|
135
137
|
if (isRegisterOnly()) return this.registerOnly();
|
|
136
138
|
if (this.stopped) throw new Error('This app was stopped; use MeoCordFactory.create to make a new one.');
|
|
137
139
|
if (this.online) return;
|
|
138
|
-
this.starting ?? (this.starting = this.startOnce().catch((error)=>{
|
|
140
|
+
this.starting ?? (this.starting = this.startOnce().catch(async (error)=>{
|
|
141
|
+
// A failed start stays among the running apps while it shuts down what it made, which a stop meanwhile waits
|
|
142
|
+
// for, and then while it holds a value it was given open, which a signal closes as stop() does. One that holds
|
|
143
|
+
// nothing leaves, so a signal later stops only the apps that run; a stopped start stays until its close settles
|
|
144
|
+
if (!this.closing) {
|
|
145
|
+
this.undoing = this.undoStart().finally(()=>this.undoing = undefined);
|
|
146
|
+
await this.undoing;
|
|
147
|
+
if (!this.closing && !this.holdsOpen()) runningApps.delete(this.close);
|
|
148
|
+
}
|
|
139
149
|
// A shard ends, for its manager to restart, or to stop all if told it is fatal; a stopped start is the stop's
|
|
140
150
|
if (this.shard && !this.closing) endFailedShard(error);
|
|
141
151
|
throw error;
|
|
@@ -156,6 +166,30 @@ class MeoCordApp {
|
|
|
156
166
|
// As a signal's shutdown exits 1 for a client that failed to close, a code another failure set aside
|
|
157
167
|
if (!await this.close() && !process.exitCode) process.exitCode = 1;
|
|
158
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Shuts down what a failed start made, by the rule a stop follows, and drops it from the container, so the process
|
|
171
|
+
* holds nothing of it open and a retry makes it afresh.
|
|
172
|
+
*/ async undoStart() {
|
|
173
|
+
if (!this.attempt || !this.begun) return;
|
|
174
|
+
this.begun = false;
|
|
175
|
+
const { given, constructed } = this.attempt;
|
|
176
|
+
const entries = this.shutdownEntries().filter(({ token })=>!given.has(token));
|
|
177
|
+
// A value the app gave stays constructed, so a stop with no retry still closes it
|
|
178
|
+
for (const token of constructed)if (!given.has(token)) constructed.delete(token);
|
|
179
|
+
this.readySettled.clear();
|
|
180
|
+
this.attempt.drop();
|
|
181
|
+
this.made = false;
|
|
182
|
+
// Nothing came online, so no call runs and no listener needs removing
|
|
183
|
+
await runShutdownSequence(this.container, ()=>entries, {
|
|
184
|
+
drainCalls: ()=>this.calls.settled(),
|
|
185
|
+
timeoutMs: this.shutdownTimeout,
|
|
186
|
+
hookFailed: (name, error)=>this.logger.error(`onShutdown failed in ${name}:`, error),
|
|
187
|
+
warn: (message)=>this.logger.warn(message)
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
/** Whether a shutdown has an `onShutdown` to run, as for a value the app gave once a failed start is undone. */ holdsOpen() {
|
|
191
|
+
return this.shutdownEntries().some(({ instance })=>typeof instance.onShutdown === 'function');
|
|
192
|
+
}
|
|
159
193
|
async startOnce() {
|
|
160
194
|
this.logger.log('Starting bot...');
|
|
161
195
|
// Before the providers are made, so a signal, or a shard's manager, can stop a start that is slow to come online
|
|
@@ -166,7 +200,9 @@ class MeoCordApp {
|
|
|
166
200
|
// A bot runs one app, whose theme code outside any call reads, its providers' factories included; claimed for
|
|
167
201
|
// each start, as a failed one gives it up
|
|
168
202
|
claimAmbientAppTheme(this.container);
|
|
169
|
-
if (!this.
|
|
203
|
+
if (!this.made) {
|
|
204
|
+
this.attempt?.begin();
|
|
205
|
+
this.begun = true;
|
|
170
206
|
// Kept while it runs, so a stop meanwhile waits for it
|
|
171
207
|
this.preparing = this.prepare();
|
|
172
208
|
try {
|
|
@@ -174,7 +210,11 @@ class MeoCordApp {
|
|
|
174
210
|
} finally{
|
|
175
211
|
this.preparing = undefined;
|
|
176
212
|
}
|
|
177
|
-
this.
|
|
213
|
+
this.made = true;
|
|
214
|
+
}
|
|
215
|
+
if (!this.attached) {
|
|
216
|
+
this.attachHandlers();
|
|
217
|
+
this.attached = true;
|
|
178
218
|
}
|
|
179
219
|
if (this.loginFailed) {
|
|
180
220
|
warnDeprecatedBehaviour(this.logger, 'Retrying start() after a failed login', 'rejects', 'MeoCordFactory.create to make a new app');
|
|
@@ -182,7 +222,7 @@ class MeoCordApp {
|
|
|
182
222
|
}
|
|
183
223
|
await this.login();
|
|
184
224
|
}
|
|
185
|
-
/** Makes the provided values and listed services
|
|
225
|
+
/** Makes the provided values and listed services. */ async prepare() {
|
|
186
226
|
// Every provided value is made before anything that injects it is resolved, and before login
|
|
187
227
|
if (this.startup) {
|
|
188
228
|
try {
|
|
@@ -194,7 +234,7 @@ class MeoCordApp {
|
|
|
194
234
|
releaseAmbientAppTheme(this.container);
|
|
195
235
|
if (process.exitCode === undefined || process.exitCode === 0) {
|
|
196
236
|
process.exitCode = 1;
|
|
197
|
-
|
|
237
|
+
processState.startFailureSetExitCode = true;
|
|
198
238
|
}
|
|
199
239
|
throw error;
|
|
200
240
|
}
|
|
@@ -207,6 +247,8 @@ class MeoCordApp {
|
|
|
207
247
|
this.storeReady = Promise.withResolvers();
|
|
208
248
|
waitForCooldownStore(this.container, this.storeReady.promise);
|
|
209
249
|
}
|
|
250
|
+
}
|
|
251
|
+
/** Attaches the Discord event handlers. */ attachHandlers() {
|
|
210
252
|
this.bot.on('clientReady', (readyClient)=>this.runListener('clientReady', async ()=>{
|
|
211
253
|
// Only for activities the app listed: without them, a status it sets itself stays as it set it
|
|
212
254
|
if (this.activities?.length) {
|
|
@@ -231,9 +273,10 @@ class MeoCordApp {
|
|
|
231
273
|
})));
|
|
232
274
|
// discord.js drops a reaction in a DM channel it has not cached; this fetches the channel and delivers it
|
|
233
275
|
deliverUncachedDmReactions(this.bot, this.logger);
|
|
234
|
-
this.
|
|
235
|
-
this.
|
|
236
|
-
this.
|
|
276
|
+
const sources = this.eventSources();
|
|
277
|
+
this.attachEventHandlers(sources);
|
|
278
|
+
this.warnAboutMissingRequirements(sources);
|
|
279
|
+
this.noteGlobalStagesOnEvents(sources);
|
|
237
280
|
}
|
|
238
281
|
/** Logs the bot in, setting the exit code when that fails and clearing it when a later attempt succeeds. */ async login() {
|
|
239
282
|
// Checked here as well, after the last await before the login: from here to setting abortLogin runs in one turn,
|
|
@@ -254,7 +297,6 @@ class MeoCordApp {
|
|
|
254
297
|
]);
|
|
255
298
|
} catch (error) {
|
|
256
299
|
if (this.closing) throw error;
|
|
257
|
-
runningApps.delete(this.close);
|
|
258
300
|
// A bot that never came online is not the app whose theme is read outside calls
|
|
259
301
|
releaseAmbientAppTheme(this.container);
|
|
260
302
|
const fatal = fatalLoginCode(error);
|
|
@@ -269,12 +311,12 @@ class MeoCordApp {
|
|
|
269
311
|
if (fatal) await reportFatalLogin(this.shard, fatal, explanation ?? (error instanceof Error ? error.message : String(error)));
|
|
270
312
|
if (process.exitCode === undefined || process.exitCode === 0) {
|
|
271
313
|
process.exitCode = 1;
|
|
272
|
-
|
|
314
|
+
processState.startFailureSetExitCode = true;
|
|
273
315
|
}
|
|
274
316
|
await tellDevRunner({
|
|
275
317
|
meocord: 'login-failed'
|
|
276
318
|
}, this.shard);
|
|
277
|
-
|
|
319
|
+
processState.toldDevRunnerLoginFailed = true;
|
|
278
320
|
this.loginFailed = true;
|
|
279
321
|
throw error;
|
|
280
322
|
} finally{
|
|
@@ -283,12 +325,12 @@ class MeoCordApp {
|
|
|
283
325
|
// A shutdown that began as the login completed has the client to close; the bot is not reported online
|
|
284
326
|
if (this.closing) throw stoppedBeforeOnline();
|
|
285
327
|
// 0 rather than undefined, which Bun ignores
|
|
286
|
-
if (
|
|
328
|
+
if (processState.startFailureSetExitCode && process.exitCode === 1) {
|
|
287
329
|
process.exitCode = 0;
|
|
288
|
-
|
|
330
|
+
processState.startFailureSetExitCode = false;
|
|
289
331
|
}
|
|
290
|
-
if (
|
|
291
|
-
|
|
332
|
+
if (processState.toldDevRunnerLoginFailed) {
|
|
333
|
+
processState.toldDevRunnerLoginFailed = false;
|
|
292
334
|
await tellDevRunner({
|
|
293
335
|
meocord: 'online'
|
|
294
336
|
}, this.shard);
|
|
@@ -298,11 +340,16 @@ class MeoCordApp {
|
|
|
298
340
|
}
|
|
299
341
|
/**
|
|
300
342
|
* Registers the application's commands with Discord, where `meocord.config.ts`'s `commands` says. Runs once the bot
|
|
301
|
-
* is ready, except in a shard, whose manager registers.
|
|
343
|
+
* is ready, except in a shard, whose manager registers. Called before the bot is online, it logs a warning and
|
|
344
|
+
* registers nothing, as the application comes from the login. It never throws: a failure is logged and the bot stays
|
|
302
345
|
* online.
|
|
303
346
|
*/ async registerCommands() {
|
|
347
|
+
// discord.js sets the application when the gateway's READY arrives
|
|
304
348
|
const applicationId = this.bot.application?.id;
|
|
305
|
-
if (!applicationId)
|
|
349
|
+
if (!applicationId) {
|
|
350
|
+
this.logger.warn('registerCommands() registers nothing before the bot is online, as Discord names the application at login. ' + 'The bot registers once it is online; to register without logging in, run `meocord register`.');
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
306
353
|
const config = loadMeoCordConfig()?.commands;
|
|
307
354
|
if (config?.register === false) {
|
|
308
355
|
this.logger.log('Command registration is off (commands.register: false); run `meocord register` to register.');
|
|
@@ -314,7 +361,7 @@ class MeoCordApp {
|
|
|
314
361
|
controllerClasses: this.controllerClasses,
|
|
315
362
|
logger: this.logger,
|
|
316
363
|
config,
|
|
317
|
-
development:
|
|
364
|
+
development: registersAsDevelopment(),
|
|
318
365
|
force: process.env[FORCE_REGISTER_ENV] === '1'
|
|
319
366
|
});
|
|
320
367
|
}
|
|
@@ -345,24 +392,25 @@ class MeoCordApp {
|
|
|
345
392
|
controllerClasses: this.controllerClasses,
|
|
346
393
|
logger: this.logger,
|
|
347
394
|
config: loadMeoCordConfig()?.commands,
|
|
348
|
-
development:
|
|
395
|
+
development: registersAsDevelopment(),
|
|
349
396
|
onlyGuild: process.env[REGISTER_GUILD_ENV],
|
|
350
397
|
force: true
|
|
351
398
|
});
|
|
352
399
|
process.exit(registered ? 0 : 1);
|
|
353
400
|
}
|
|
354
401
|
/**
|
|
355
|
-
* Adds a client listener for every `@On` and `@Once` handler on
|
|
356
|
-
*
|
|
357
|
-
* handler, and the next listener still runs.
|
|
358
|
-
*/ attachEventHandlers() {
|
|
359
|
-
for (const
|
|
360
|
-
|
|
361
|
-
|
|
402
|
+
* Adds a client listener for every `@On` and `@Once` handler on each instance whose lifecycle hooks run, in lifecycle
|
|
403
|
+
* order, each event run on the instance the container holds. Each call is isolated: an error is logged against the
|
|
404
|
+
* event and the handler, and the next listener still runs.
|
|
405
|
+
*/ attachEventHandlers(sources) {
|
|
406
|
+
for (const source of sources){
|
|
407
|
+
const { name } = source.cls;
|
|
408
|
+
for (const { event, method, once } of source.handlers){
|
|
409
|
+
const logError = (error)=>this.logger.error(`Error handling event "${event}" in ${name}.${method}:`, error);
|
|
362
410
|
// An error no filter handles is logged, with the handler. A guard denying an event only filters which
|
|
363
411
|
// events the handler takes, and a UserError is meant for the sender of the event's message, if it has one
|
|
364
412
|
const fallback = async (error, context)=>{
|
|
365
|
-
const where = `event "${event}" in ${
|
|
413
|
+
const where = `event "${event}" in ${name}.${method}`;
|
|
366
414
|
if (error instanceof GuardDeniedError) this.logger.debug(`Denied ${where}: ${escapeForLog(error.message)}`);
|
|
367
415
|
else if (error instanceof UserError) {
|
|
368
416
|
this.logger.debug(`Refused ${where}: ${escapeForLog(error.message)}`);
|
|
@@ -374,12 +422,12 @@ class MeoCordApp {
|
|
|
374
422
|
} else logError(error);
|
|
375
423
|
return undefined;
|
|
376
424
|
};
|
|
377
|
-
const label = `@${once ? 'Once' : 'On'}('${event}') in ${
|
|
425
|
+
const label = `@${once ? 'Once' : 'On'}('${event}') in ${name}.${method}`;
|
|
378
426
|
// A call, as a dispatched one is, which shutdown waits for
|
|
379
427
|
const listener = (...args)=>this.calls.run(label, async ()=>{
|
|
380
428
|
try {
|
|
381
|
-
const instance =
|
|
382
|
-
await runHandler(this.container, instance, method, args, {
|
|
429
|
+
const instance = source.resolve();
|
|
430
|
+
if (instance) await runHandler(this.container, instance, method, args, {
|
|
383
431
|
fallback,
|
|
384
432
|
type: 'event'
|
|
385
433
|
});
|
|
@@ -396,8 +444,8 @@ class MeoCordApp {
|
|
|
396
444
|
/**
|
|
397
445
|
* Says, once per stage, which global guards and interceptors declare no `types` and so also run
|
|
398
446
|
* before `@On` and `@Once` handlers, when the app has any.
|
|
399
|
-
*/ noteGlobalStagesOnEvents() {
|
|
400
|
-
if (
|
|
447
|
+
*/ noteGlobalStagesOnEvents(sources) {
|
|
448
|
+
if (sources.length === 0) return;
|
|
401
449
|
const { guards, interceptors } = globalStagesOf(this.container);
|
|
402
450
|
for (const [label, decorator, entries] of [
|
|
403
451
|
[
|
|
@@ -420,29 +468,29 @@ class MeoCordApp {
|
|
|
420
468
|
/**
|
|
421
469
|
* Warns about handlers whose events the client options will not deliver, once per missing intent or partial. Only a
|
|
422
470
|
* controller's message and reaction handlers are counted, as only those run.
|
|
423
|
-
*/ warnAboutMissingRequirements() {
|
|
471
|
+
*/ warnAboutMissingRequirements(sources) {
|
|
424
472
|
const options = this.bot.options;
|
|
425
473
|
if (!options?.intents) return;
|
|
426
|
-
const controllers = new Set(this.controllerClasses);
|
|
427
474
|
const handlers = [];
|
|
428
|
-
for (const
|
|
429
|
-
const
|
|
430
|
-
for (const { event, method, once } of getEventHandlers(prototype)){
|
|
475
|
+
for (const { cls, handlers: events } of sources){
|
|
476
|
+
for (const { event, method, once } of events){
|
|
431
477
|
handlers.push({
|
|
432
|
-
label: `@${once ? 'Once' : 'On'}('${event}') in ${
|
|
478
|
+
label: `@${once ? 'Once' : 'On'}('${event}') in ${cls.name}.${method}`,
|
|
433
479
|
requirements: eventRequirements(event)
|
|
434
480
|
});
|
|
435
481
|
}
|
|
436
|
-
|
|
482
|
+
}
|
|
483
|
+
for (const controller of this.controllerClasses){
|
|
484
|
+
const prototype = controller.prototype;
|
|
437
485
|
for (const handler of getMessageHandlers(prototype)){
|
|
438
486
|
handlers.push({
|
|
439
|
-
label: `${messageDecorator(handler.pattern)} in ${
|
|
487
|
+
label: `${messageDecorator(handler.pattern)} in ${controller.name}.${handler.method}`,
|
|
440
488
|
requirements: messageHandlerRequirements(handler, this.messageOptions ?? {})
|
|
441
489
|
});
|
|
442
490
|
}
|
|
443
491
|
for (const { emoji, method } of getReactionHandlers(prototype)){
|
|
444
492
|
handlers.push({
|
|
445
|
-
label: `${reactionDecorator(emoji)} in ${
|
|
493
|
+
label: `${reactionDecorator(emoji)} in ${controller.name}.${method}`,
|
|
446
494
|
requirements: REACTION_HANDLER_REQUIREMENTS
|
|
447
495
|
});
|
|
448
496
|
}
|
|
@@ -454,11 +502,9 @@ class MeoCordApp {
|
|
|
454
502
|
* at a time, in dependency order. A failure is logged and the next one still runs, with a warning for each hook
|
|
455
503
|
* whose dependencies failed. Once shutdown begins, no further hook starts.
|
|
456
504
|
*/ async runReadyHooks(client) {
|
|
457
|
-
const entries = [];
|
|
458
|
-
this.lifecycleEntries = entries;
|
|
459
505
|
await this.readyPass.run(()=>runReadyHooks(this.container, this.lifecycleUnits, client, {
|
|
460
506
|
primary: client.shard ? client.shard.ids.includes(0) : true
|
|
461
|
-
},
|
|
507
|
+
}, {
|
|
462
508
|
resolveFailed: (unit, error)=>this.logger.error(`Could not resolve ${unit.name} to run its lifecycle hooks:`, error),
|
|
463
509
|
hookFailed: (unit, error)=>this.logger.error(`onReady failed in ${unit.name}:`, error),
|
|
464
510
|
dependsOnFailed: (unit, failed)=>this.logger.warn(`Running onReady in ${unit.name} although it depends on ${failed.map(({ name })=>name).join(', ')}, which failed.`),
|
|
@@ -467,15 +513,37 @@ class MeoCordApp {
|
|
|
467
513
|
{
|
|
468
514
|
stopped: ()=>this.closing,
|
|
469
515
|
slowAfterMs: SLOW_READY_HOOK_MS,
|
|
470
|
-
settled: (unit)=>
|
|
516
|
+
settled: (unit)=>{
|
|
517
|
+
this.readySettled.add(unit.token);
|
|
518
|
+
if (unit.cooldownStore) this.storeReady?.resolve();
|
|
519
|
+
},
|
|
471
520
|
runOnReady: this.readyPass.runOnReady
|
|
472
521
|
}));
|
|
473
522
|
// Also when shutdown stopped the hooks before the store's, so no call waits on it
|
|
474
523
|
this.storeReady?.resolve();
|
|
475
524
|
}
|
|
476
|
-
/**
|
|
477
|
-
|
|
478
|
-
|
|
525
|
+
/**
|
|
526
|
+
* The units a shutdown closes: each one the container constructed whose `onReady`, and that of every unit it depends
|
|
527
|
+
* on, has settled or doesn't exist, in dependency order. A unit whose `onReady` never ran gets no `onShutdown`, so a
|
|
528
|
+
* hook that undoes `onReady` never runs without it, and neither does anything that depends on it.
|
|
529
|
+
*/ shutdownEntries() {
|
|
530
|
+
const units = new Set(this.lifecycleUnits.map(({ token })=>token));
|
|
531
|
+
const closes = new Set();
|
|
532
|
+
const entries = [];
|
|
533
|
+
for (const unit of this.lifecycleUnits){
|
|
534
|
+
if (!this.attempt?.constructed.has(unit.token)) continue;
|
|
535
|
+
// Already made, so this returns the instance; a provided value may be anything, null included
|
|
536
|
+
const instance = this.container.get(unit.token) ?? {};
|
|
537
|
+
const ready = typeof instance.onReady !== 'function' || this.readySettled.has(unit.token);
|
|
538
|
+
if (!ready || unit.dependencies.some((dependency)=>units.has(dependency) && !closes.has(dependency))) continue;
|
|
539
|
+
closes.add(unit.token);
|
|
540
|
+
entries.push(lifecycleEntry(unit, instance));
|
|
541
|
+
}
|
|
542
|
+
return entries;
|
|
543
|
+
}
|
|
544
|
+
/** Runs the `onShutdown` hooks through the shutdown sequence, within `timeoutMs`: the stop's share of `shutdownTimeout`. */ async runShutdownHooks(timeoutMs) {
|
|
545
|
+
// Read once the onReady in progress settled
|
|
546
|
+
await runShutdownSequence(this.container, ()=>this.shutdownEntries(), {
|
|
479
547
|
// No new call starts once the listeners are gone; an error the client emits meanwhile still has its listeners
|
|
480
548
|
drainCalls: ()=>{
|
|
481
549
|
const emitter = this.bot;
|
|
@@ -485,7 +553,7 @@ class MeoCordApp {
|
|
|
485
553
|
drainAlways: true,
|
|
486
554
|
runningCalls: ()=>this.calls.running(),
|
|
487
555
|
readyPass: this.readyPass,
|
|
488
|
-
timeoutMs
|
|
556
|
+
timeoutMs,
|
|
489
557
|
hookFailed: (name, error)=>this.logger.error(`onShutdown failed in ${name}:`, error),
|
|
490
558
|
warn: (message)=>this.logger.warn(message)
|
|
491
559
|
});
|
|
@@ -493,7 +561,8 @@ class MeoCordApp {
|
|
|
493
561
|
/**
|
|
494
562
|
* Runs the shutdown hooks, if the ready hooks ran, then destroys the client; a stop during login ends the login.
|
|
495
563
|
*
|
|
496
|
-
* @returns Whether the client closed cleanly, which an ended login counts as
|
|
564
|
+
* @returns Whether the client closed cleanly, which an ended login counts as, and the startup it waited for, if any,
|
|
565
|
+
* did not fail.
|
|
497
566
|
*/ async closeClient() {
|
|
498
567
|
this.closing = true;
|
|
499
568
|
try {
|
|
@@ -506,56 +575,71 @@ class MeoCordApp {
|
|
|
506
575
|
/**
|
|
507
576
|
* Waits for the startup under way to stop making providers and services, at most `shutdownTimeout`; past it, warns
|
|
508
577
|
* with the factory it still awaits, which then makes nothing more.
|
|
578
|
+
*
|
|
579
|
+
* @returns Whether the startup failed meanwhile, as a provider's factory that rejects does, rather than stopping.
|
|
509
580
|
*/ async waitForStartup(preparing) {
|
|
510
581
|
let timer;
|
|
511
582
|
const outlasted = new Promise((resolve)=>{
|
|
512
|
-
timer = setTimeout(()=>resolve(
|
|
583
|
+
timer = setTimeout(()=>resolve('outlasted'), this.shutdownTimeout);
|
|
513
584
|
});
|
|
514
|
-
const settled = preparing.then(()=>
|
|
515
|
-
|
|
585
|
+
const settled = preparing.then(()=>'stopped', (error)=>error instanceof Error && stops.has(error) ? 'stopped' : 'failed');
|
|
586
|
+
const outcome = await Promise.race([
|
|
516
587
|
settled,
|
|
517
588
|
outlasted
|
|
518
|
-
])
|
|
589
|
+
]);
|
|
590
|
+
clearTimeout(timer);
|
|
591
|
+
if (outcome === 'outlasted') {
|
|
519
592
|
const what = this.pendingProvider ? `the factory providing ${this.pendingProvider}` : 'its providers';
|
|
520
593
|
this.logger.warn(`The startup still waits for ${what} after ${this.shutdownTimeout} ms; shutting down without it.`);
|
|
521
594
|
}
|
|
522
|
-
|
|
595
|
+
return outcome === 'failed';
|
|
523
596
|
}
|
|
524
597
|
async shutDownClient() {
|
|
525
598
|
this.logger.log('Shutting down bot...');
|
|
526
|
-
//
|
|
527
|
-
|
|
528
|
-
|
|
599
|
+
// A failed start's shutdown under way finishes first, so its hooks run in full, before those of what it left open.
|
|
600
|
+
// `shutdownTimeout` bounds the whole stop, as a shard's manager waits only that long, so the rest gets what is left
|
|
601
|
+
let timeoutMs = this.shutdownTimeout;
|
|
602
|
+
if (this.undoing) {
|
|
603
|
+
const waitedFrom = Date.now();
|
|
604
|
+
await this.undoing;
|
|
605
|
+
timeoutMs = Math.max(0, timeoutMs - (Date.now() - waitedFrom));
|
|
606
|
+
}
|
|
607
|
+
// So nothing is made after the bot has shut down; a factory that fails meanwhile is a failure of the stop too
|
|
608
|
+
const startupFailed = this.preparing !== undefined && await this.waitForStartup(this.preparing);
|
|
609
|
+
// Nothing came online, so no onReady ran; the listeners go first, so none of them runs, then what the start made
|
|
610
|
+
// without an onReady to wait for shuts down
|
|
529
611
|
if (this.abortLogin) {
|
|
530
612
|
this.logger.log('The bot was still logging in, so it stops without coming online');
|
|
531
613
|
this.bot.removeAllListeners();
|
|
532
614
|
this.abortLogin();
|
|
615
|
+
await this.runShutdownHooks(timeoutMs);
|
|
533
616
|
this.logger.log('Bot has shut down');
|
|
534
617
|
return true;
|
|
535
618
|
}
|
|
536
619
|
if (this.activityInterval) clearInterval(this.activityInterval);
|
|
537
620
|
// The calls drain on every shutdown, and the onReady in progress settles first
|
|
538
|
-
await this.runShutdownHooks();
|
|
621
|
+
await this.runShutdownHooks(timeoutMs);
|
|
539
622
|
try {
|
|
540
623
|
this.bot.removeAllListeners();
|
|
541
624
|
await this.bot.destroy();
|
|
542
625
|
this.logger.log('Bot has shut down');
|
|
543
|
-
return
|
|
626
|
+
return !startupFailed;
|
|
544
627
|
} catch (error) {
|
|
545
628
|
this.logger.error('Error during shutdown:', error);
|
|
546
629
|
return false;
|
|
547
630
|
}
|
|
548
631
|
}
|
|
549
|
-
constructor(controllerClasses, container, discordClient, discordToken, activities,
|
|
632
|
+
constructor(controllerClasses, container, discordClient, discordToken, activities, /** The instances whose `@On` and `@Once` handlers run, read once the providers are made. */ eventSources = ()=>[], shutdownTimeout, startup, lifecycleUnits, messageOptions = {}, warnUnanswered = false, /** Whether a manager spawned this process, as `MeoCordFactory.create` decided before anything could change it. */ shard = false, /** What each start makes; without one, as an app built by hand in a spec, nothing is shut down before ready. */ attempt){
|
|
550
633
|
this.controllerClasses = controllerClasses;
|
|
551
634
|
this.container = container;
|
|
552
635
|
this.discordClient = discordClient;
|
|
553
636
|
this.discordToken = discordToken;
|
|
554
637
|
this.activities = activities;
|
|
555
|
-
this.
|
|
638
|
+
this.eventSources = eventSources;
|
|
556
639
|
this.startup = startup;
|
|
557
640
|
this.messageOptions = messageOptions;
|
|
558
641
|
this.shard = shard;
|
|
642
|
+
this.attempt = attempt;
|
|
559
643
|
this.logger = new Logger(MeoCordApp.name);
|
|
560
644
|
this.fallback = createFallback(this.logger, ()=>this.messageOptions, {
|
|
561
645
|
cooldownNotice: (refusal)=>claimCooldownNotice(this.container, refusal),
|
|
@@ -564,18 +648,21 @@ class MeoCordApp {
|
|
|
564
648
|
this.activityInterval = null;
|
|
565
649
|
/** Where the activities' rotation is: the index of the next one shown. */ this.nextActivity = 0;
|
|
566
650
|
/** Whether shutdown has begun, so the ready hooks start no more. */ this.closing = false;
|
|
651
|
+
/** The tokens of the units whose `onReady` the ready pass has settled, or found they have none. */ this.readySettled = new Set();
|
|
567
652
|
/**
|
|
568
653
|
* Closes the app once, however many stops and signals ask: whether the client was destroyed cleanly. The app stays
|
|
569
654
|
* among the running ones until the close settles, so a signal or a manager's request meanwhile waits for it.
|
|
570
655
|
*/ this.close = ()=>this.stopped ?? (this.stopped = this.closeClient().finally(()=>runningApps.delete(this.close)));
|
|
571
656
|
/** The calls under way, which shutdown waits for, less one that asked the app to stop. */ this.calls = new RunningCalls();
|
|
572
657
|
/** The ready hooks' pass, whose `onReady` in progress shutdown waits for, less one that asked the app to stop. */ this.readyPass = new ReadyPass();
|
|
573
|
-
/** Whether
|
|
658
|
+
/** Whether a start has marked where it began and not been undone, so a failed one drops what it made exactly once. */ this.begun = false;
|
|
659
|
+
/** Whether this start's providers and services are made; a failed start drops them, so a retry makes them again. */ this.made = false;
|
|
660
|
+
/** Whether the Discord event handlers are attached, once for the app: they resolve what they run as each event comes. */ this.attached = false;
|
|
574
661
|
/** Whether the bot logged in, after which `start()` has nothing to do. */ this.online = false;
|
|
575
662
|
/** Whether a login of this app failed, so a retry restores the client discord.js destroyed. */ this.loginFailed = false;
|
|
576
663
|
// First, so nothing logged from here on, before login or after, prints it
|
|
577
664
|
hideInLogs(discordToken);
|
|
578
|
-
this.lifecycleUnits = lifecycleUnits ??
|
|
665
|
+
this.lifecycleUnits = lifecycleUnits ?? [];
|
|
579
666
|
this.bot = this.discordClient;
|
|
580
667
|
// Built now, so a pattern that cannot be read or two that match the same messages or customIds stop the bot as it
|
|
581
668
|
// is created, before start() attaches anything
|
|
@@ -593,7 +680,5 @@ class MeoCordApp {
|
|
|
593
680
|
this.shutdownTimeout = shutdownTimeoutOf(shutdownTimeout, (message)=>this.logger.warn(message));
|
|
594
681
|
}
|
|
595
682
|
}
|
|
596
|
-
/** Whether a failed login set the process exit code, so a later successful one knows to clear it. */ MeoCordApp.failedLoginSetExitCode = false;
|
|
597
|
-
/** Whether `meocord start --dev` was told the bot could not log in, so a later login tells it the bot is online. */ MeoCordApp.toldDevRunnerLoginFailed = false;
|
|
598
683
|
|
|
599
684
|
export { MeoCordApp, SLOW_READY_HOOK_MS, shutdownAndExit };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fitsScope, commandWordsOf, afterStart } from './message-routes.js';
|
|
2
|
-
import {
|
|
2
|
+
import { effectiveScope, primaryFirst, usageOf, paramTypeLabel } from './message-params.js';
|
|
3
3
|
import { splitWords } from './message-words.js';
|
|
4
4
|
import { usageHeading } from '../common/errors.js';
|
|
5
5
|
import { textRenderer } from '../common/meocord-text.js';
|
|
@@ -31,12 +31,13 @@ import { userWords } from '../util/user-text.util.js';
|
|
|
31
31
|
const text = content.trim();
|
|
32
32
|
const rest = afterStart(text, starts.prefixes.filter((prefix)=>prefix !== ''), starts.mention, caseSensitive);
|
|
33
33
|
if (!rest) return undefined;
|
|
34
|
-
const [first] = splitWords(rest);
|
|
34
|
+
const [first, second] = splitWords(rest);
|
|
35
35
|
const key = (word)=>caseSensitive ? word : word.toLowerCase();
|
|
36
36
|
if (!first || !words.some((word)=>key(word) === key(first.value))) return undefined;
|
|
37
|
+
// From where the next word starts, as the help word may have been written in quotes
|
|
37
38
|
return {
|
|
38
39
|
start: text.slice(0, text.length - rest.length),
|
|
39
|
-
query: rest.slice(
|
|
40
|
+
query: second ? rest.slice(second.start).trim() : ''
|
|
40
41
|
};
|
|
41
42
|
}
|
|
42
43
|
/** 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) {
|
|
@@ -45,11 +46,6 @@ import { userWords } from '../util/user-text.util.js';
|
|
|
45
46
|
if (route.prefix !== undefined) return route.prefix[0] ?? '';
|
|
46
47
|
return start;
|
|
47
48
|
}
|
|
48
|
-
/** Where a route works: its scope, narrowed to servers by a `member`, `role` or `channel` param or flag. */ function effectiveScope(route) {
|
|
49
|
-
if (route.scope !== 'any') return route.scope;
|
|
50
|
-
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));
|
|
51
|
-
return guildOnly ? 'guild' : 'any';
|
|
52
|
-
}
|
|
53
49
|
/** A route's params and flags, each with what it takes in words. */ function paramsOf(route, types, render) {
|
|
54
50
|
const params = route.tokens.flatMap((token)=>{
|
|
55
51
|
if (!('param' in token)) return [];
|
|
@@ -294,4 +290,4 @@ let graphemes;
|
|
|
294
290
|
return chunks;
|
|
295
291
|
}
|
|
296
292
|
|
|
297
|
-
export { computeMessageHelp,
|
|
293
|
+
export { computeMessageHelp, helpInvocation, helpWords, isListable, matchHelpRequest, renderMessageHelp, splitReply };
|