meocord 4.2.1 → 4.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +188 -0
- package/dist/cjs/_shared/{controller.decorator-XKMlcUZz.cjs → controller.decorator-hp5xdJU4.cjs} +550 -189
- package/dist/cjs/_shared/{startup-roots-CgfPQrNl.cjs → event-sources-DzqTODXg.cjs} +274 -88
- package/dist/cjs/_shared/{providers-BZgJyfZS.cjs → providers-BBvbnmLP.cjs} +837 -295
- package/dist/cjs/_shared/{sharded-cooldown-store-CsJJyCZd.cjs → sharded-cooldown-store-B62g5O88.cjs} +11 -9
- package/dist/cjs/_shared/{translate-error-BUAR1WYG.cjs → translate-error-wsgfsUET.cjs} +1 -1
- package/dist/cjs/common/index.cjs +4 -104
- package/dist/cjs/core/index.cjs +435 -182
- package/dist/cjs/decorator/index.cjs +116 -121
- package/dist/cjs/testing/index.cjs +1923 -808
- package/dist/cli.json +1 -1
- package/dist/esm/bin/app-template/package.json.template +9 -9
- package/dist/esm/bin/app-template/src/main.ts.template +3 -2
- package/dist/esm/bin/generator.js +6 -4
- package/dist/esm/bin/helper/filter-generator.helper.js +6 -4
- package/dist/esm/bin/helper/guard-generator.helper.js +6 -4
- package/dist/esm/bin/helper/interceptor-generator.helper.js +6 -4
- package/dist/esm/bin/helper/observer-generator.helper.js +6 -4
- package/dist/esm/bin/helper/pipe-generator.helper.js +6 -4
- package/dist/esm/bin/helper/service-generator.helper.js +6 -4
- package/dist/esm/bin/meocord.js +106 -48
- package/dist/esm/build/native-addons.js +75 -19
- package/dist/esm/build/rsbuild-config.js +11 -3
- package/dist/esm/build/typed-externals.js +15 -2
- package/dist/esm/common/cooldown-store.js +38 -14
- package/dist/esm/common/deprecation.js +4 -1
- package/dist/esm/common/execution-context.js +3 -1
- package/dist/esm/common/explained-error.js +7 -3
- package/dist/esm/common/log-level.js +13 -9
- package/dist/esm/common/logger.js +4 -2
- package/dist/esm/common/meocord-text.js +11 -6
- package/dist/esm/common/redis-cooldown-store.js +3 -7
- package/dist/esm/common/response/flags.js +17 -1
- package/dist/esm/common/response/install-context.js +27 -16
- package/dist/esm/common/response/presenter.js +7 -3
- package/dist/esm/common/response/response-state.js +32 -10
- package/dist/esm/common/sharded-cooldown-store.js +11 -9
- package/dist/esm/common/theme.js +3 -1
- package/dist/esm/common/translator.js +27 -8
- package/dist/esm/core/autocomplete-routes.js +26 -0
- package/dist/esm/core/command-conflicts.js +15 -7
- package/dist/esm/core/command-registration.js +15 -1
- package/dist/esm/core/cooldown-runner.js +7 -9
- package/dist/esm/core/defer.js +2 -5
- package/dist/esm/core/dispatcher.js +92 -66
- package/dist/esm/core/event-requirements.js +67 -19
- package/dist/esm/core/event-sources.js +100 -0
- package/dist/esm/core/fallback.js +3 -1
- package/dist/esm/core/filter-runner.js +6 -6
- package/dist/esm/core/guard-runner.js +173 -52
- package/dist/esm/core/handler-input.js +24 -3
- package/dist/esm/core/handler-pipeline.js +54 -32
- package/dist/esm/core/handler-registry.js +51 -21
- package/dist/esm/core/input-runner.js +7 -8
- package/dist/esm/core/interceptor-runner.js +4 -5
- package/dist/esm/core/lifecycle-hooks.js +3 -8
- package/dist/esm/core/lifecycle-order.js +1 -15
- package/dist/esm/core/meocord-classes.js +14 -1
- package/dist/esm/core/meocord-factory.js +83 -54
- package/dist/esm/core/meocord.app.js +174 -89
- package/dist/esm/core/message-help.js +37 -15
- package/dist/esm/core/message-params.js +126 -46
- package/dist/esm/core/message-routes.js +225 -45
- package/dist/esm/core/message-words.js +30 -2
- package/dist/esm/core/observer-runner.js +4 -2
- package/dist/esm/core/process-state.js +19 -0
- package/dist/esm/core/providers.js +39 -4
- package/dist/esm/core/scalar-types.js +57 -16
- package/dist/esm/core/shard-context.js +11 -10
- package/dist/esm/core/shard-exit.js +3 -3
- package/dist/esm/core/shard-manager.js +16 -6
- package/dist/esm/core/stage-scope.js +2 -1
- package/dist/esm/core/startup-roots.js +28 -8
- package/dist/esm/core/theme-resolvers.js +26 -11
- package/dist/esm/core/theme-runtime.js +23 -13
- package/dist/esm/core/theme-scope.js +31 -30
- package/dist/esm/core/theme-validation.js +1 -1
- package/dist/esm/decorator/app.decorator.js +37 -1
- package/dist/esm/decorator/command-builder.decorator.js +14 -0
- package/dist/esm/decorator/controller-class.decorator.js +1 -1
- package/dist/esm/decorator/controller.decorator.js +48 -19
- package/dist/esm/decorator/defer.decorator.js +2 -2
- package/dist/esm/decorator/event.decorator.js +4 -5
- package/dist/esm/decorator/filter.decorator.js +4 -4
- package/dist/esm/decorator/guard.decorator.js +4 -61
- package/dist/esm/decorator/inject.decorator.js +4 -1
- package/dist/esm/decorator/interceptor.decorator.js +2 -2
- package/dist/esm/decorator/theme.decorator.js +4 -3
- package/dist/esm/decorator/validation.decorator.js +5 -5
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/catalog.js +32 -25
- package/dist/esm/testing/cooldown-store-suite.js +3 -3
- package/dist/esm/testing/discord-defaults.js +449 -335
- package/dist/esm/testing/inspect-handler.js +91 -10
- package/dist/esm/testing/meocord-testing-module.js +217 -91
- package/dist/esm/testing/mock-fn.js +8 -2
- package/dist/esm/testing/mock-interaction.js +864 -124
- package/dist/esm/testing/response-log.js +8 -0
- package/dist/esm/testing/response.js +7 -5
- package/dist/esm/testing/routing.js +48 -6
- package/dist/esm/testing/strict-mocks.js +8 -4
- package/dist/esm/testing/theme.js +3 -2
- package/dist/esm/util/common.util.js +30 -9
- package/dist/esm/util/generator-cli.util.js +6 -4
- package/dist/esm/util/inherited-env.util.js +72 -13
- package/dist/esm/util/json.util.js +10 -3
- package/dist/esm/util/meocord-cli.util.js +3 -2
- package/dist/esm/util/meocord-config-loader.util.js +14 -1
- package/dist/esm/util/meocord-config-validation.util.js +13 -3
- package/dist/esm/util/meocord-source-config.util.js +13 -3
- package/dist/esm/util/platform.util.js +3 -2
- package/dist/esm/util/refusal.util.js +29 -11
- package/dist/esm/util/registration-mode.util.js +14 -1
- package/dist/esm/util/timer-limit.util.js +4 -1
- package/dist/esm/util/tsconfig-paths.util.js +38 -7
- package/dist/esm/util/tsconfig.util.js +16 -58
- package/dist/esm/util/user-text.util.js +8 -4
- package/dist/esm/util/value.util.js +33 -5
- package/dist/types/common/index.d.cts +17 -10
- package/dist/types/common/index.d.ts +17 -10
- package/dist/types/core/index.d.cts +21 -11
- package/dist/types/core/index.d.ts +21 -11
- package/dist/types/decorator/index.d.cts +63 -63
- package/dist/types/decorator/index.d.ts +63 -63
- package/dist/types/{index-DKJclIi-.d.ts → index-Brkd6QGL.d.ts} +81 -35
- package/dist/types/{index-Dlf0ZZtS.d.cts → index-CqXImwF_.d.cts} +81 -35
- package/dist/types/interface/index.d.cts +1 -1
- package/dist/types/interface/index.d.ts +1 -1
- package/dist/types/{route-Bh1U13yy.d.ts → route-IQrK-MN2.d.cts} +11 -3
- package/dist/types/{route-Bh1U13yy.d.cts → route-bI-okq7g.d.ts} +11 -3
- package/dist/types/testing/index.d.cts +158 -61
- package/dist/types/testing/index.d.ts +158 -61
- package/dist/types/{theme-resolvers-CyEz9pnv.d.cts → theme-resolvers-AtgeaYqF.d.cts} +1 -1
- package/dist/types/{theme-resolvers-DA204O-q.d.ts → theme-resolvers-BgqysV8d.d.ts} +1 -1
- package/dist/types/{translator-Bg7hwxDF.d.cts → translator-D8aydlCE.d.ts} +12 -11
- package/dist/types/{translator-CeP4bCzp.d.ts → translator-iJhDlgwL.d.cts} +12 -11
- package/package.json +5 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { existsSync, realpathSync, cpSync,
|
|
1
|
+
import { existsSync, realpathSync, cpSync, readdirSync } from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { currentPlatform } from '../util/platform.util.js';
|
|
3
|
+
import { currentPlatform, LIBC_PLATFORM } from '../util/platform.util.js';
|
|
4
|
+
import { readJsonFile } from '../util/json.util.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* The package a resolved module file belongs to, and the directory it was installed in.
|
|
@@ -85,7 +86,7 @@ import { currentPlatform } from '../util/platform.util.js';
|
|
|
85
86
|
}
|
|
86
87
|
/** A package's declared dependencies, runtime and optional, or none if it cannot be read. */ function readDependencies(dir) {
|
|
87
88
|
try {
|
|
88
|
-
const pkg =
|
|
89
|
+
const pkg = readJsonFile(path.join(dir, 'package.json'));
|
|
89
90
|
return {
|
|
90
91
|
dependencies: Object.keys(pkg.dependencies ?? {}),
|
|
91
92
|
optional: Object.keys(pkg.optionalDependencies ?? {})
|
|
@@ -160,12 +161,31 @@ import { currentPlatform } from '../util/platform.util.js';
|
|
|
160
161
|
* native package it keeps out is recorded in `found`, so the build can copy it into the output.
|
|
161
162
|
*/ function createNativeExternals(root) {
|
|
162
163
|
const found = new Map();
|
|
164
|
+
const otherCopies = new Map();
|
|
163
165
|
const plain = new Set();
|
|
166
|
+
// Where each importing directory finds a native package, read once per directory
|
|
167
|
+
const resolved = new Map();
|
|
168
|
+
const resolveFrom = (name, from)=>{
|
|
169
|
+
const key = `${name}\0${from}`;
|
|
170
|
+
if (!resolved.has(key)) resolved.set(key, resolvePackageDir(name, from, root));
|
|
171
|
+
return resolved.get(key);
|
|
172
|
+
};
|
|
164
173
|
const externals = ({ request, context }, callback)=>{
|
|
165
174
|
const name = request ? packageNameOfRequest(request) : undefined;
|
|
166
175
|
if (!name || plain.has(name)) return callback();
|
|
167
|
-
|
|
168
|
-
const
|
|
176
|
+
const dir = resolveFrom(name, context ?? root);
|
|
177
|
+
const known = found.get(name);
|
|
178
|
+
if (known) {
|
|
179
|
+
// The bundle loads one copy of it for every importer, so code that needs another installed copy gets this one
|
|
180
|
+
const others = otherCopies.get(name) ?? [];
|
|
181
|
+
if (dir && !samePackageDir(dir, known.dir) && !others.some((other)=>samePackageDir(other, dir))) {
|
|
182
|
+
otherCopies.set(name, [
|
|
183
|
+
...others,
|
|
184
|
+
dir
|
|
185
|
+
]);
|
|
186
|
+
}
|
|
187
|
+
return callback(undefined, request);
|
|
188
|
+
}
|
|
169
189
|
const carrier = dir ? nativeCarrier(dir, name, root) : undefined;
|
|
170
190
|
if (!dir || !carrier) {
|
|
171
191
|
plain.add(name);
|
|
@@ -179,34 +199,70 @@ import { currentPlatform } from '../util/platform.util.js';
|
|
|
179
199
|
};
|
|
180
200
|
return {
|
|
181
201
|
externals,
|
|
182
|
-
found
|
|
202
|
+
found,
|
|
203
|
+
otherCopies
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/** Whether two directories are one installed package, as pnpm links one from its store under several names. */ function samePackageDir(a, b) {
|
|
207
|
+
const real = (dir)=>{
|
|
208
|
+
try {
|
|
209
|
+
return realpathSync(dir);
|
|
210
|
+
} catch {
|
|
211
|
+
return dir;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
return real(a) === real(b);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* The warning for a native package bundled code imports from more than one installed copy: each copy's version and
|
|
218
|
+
* place, and that the one packed is what every importer loads.
|
|
219
|
+
*/ function otherCopiesWarning(name, packed, others, root) {
|
|
220
|
+
const where = (dir)=>{
|
|
221
|
+
let version = 'an unknown version';
|
|
222
|
+
try {
|
|
223
|
+
version = String(readJsonFile(path.join(dir, 'package.json')).version ?? version);
|
|
224
|
+
} catch {
|
|
225
|
+
// Unreadable: named by its place alone
|
|
226
|
+
}
|
|
227
|
+
return `${version} in ${path.relative(root, dir) || '.'}`;
|
|
183
228
|
};
|
|
229
|
+
return `${name} is installed more than once for bundled code: ${[
|
|
230
|
+
packed,
|
|
231
|
+
...others
|
|
232
|
+
].map(where).join(', ')}. A self-contained ` + `build packs one copy of a native package, ${where(packed)}, and every importer loads it, so code that needs another ` + `version gets this one. Install one version of ${name}, for example with \`npm dedupe\` or an \`overrides\` entry. ` + 'The next major version (5.0) stops the build here.';
|
|
184
233
|
}
|
|
234
|
+
/** The sole entry of an `os`, `cpu` or `libc` list that admits every value. */ const ANY_PLATFORM = 'any';
|
|
185
235
|
/**
|
|
186
|
-
* Whether a value passes a package.json `os`, `cpu` or `libc`
|
|
187
|
-
* entry excludes its value, and a list with
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
236
|
+
* Whether a value passes a package.json `os`, `cpu` or `libc` field, the way npm reads one (npm-install-checks'
|
|
237
|
+
* `checkList`): a string is a list of one, `['any']` admits every value, a `!` entry excludes its value, and a list with
|
|
238
|
+
* plain entries admits only those.
|
|
239
|
+
*/ function allowedBy(field, value) {
|
|
240
|
+
if (!field || typeof field !== 'string' && !Array.isArray(field)) return true;
|
|
241
|
+
const list = typeof field === 'string' ? [
|
|
242
|
+
field
|
|
243
|
+
] : field.filter((entry)=>typeof entry === 'string');
|
|
244
|
+
if (list.length === 1 && list[0] === ANY_PLATFORM) return true;
|
|
245
|
+
const excluded = list.filter((entry)=>entry.startsWith('!'));
|
|
246
|
+
if (excluded.includes(`!${value}`)) return false;
|
|
247
|
+
return excluded.length === list.length || list.includes(value);
|
|
194
248
|
}
|
|
195
249
|
/**
|
|
196
250
|
* Whether an installed package is built for the platform, going by the `os`, `cpu` and `libc` its
|
|
197
251
|
* package.json declares. A package that declares none of them runs anywhere.
|
|
198
252
|
*
|
|
199
|
-
* `libc` is compared only when the build platform's C library is known, so a runtime that
|
|
200
|
-
* report it keeps the package rather than dropping it on a guess.
|
|
253
|
+
* `libc` is compared only when the build platform's C library is known, so a Linux runtime that
|
|
254
|
+
* cannot report it keeps the package rather than dropping it on a guess. A platform with no C
|
|
255
|
+
* library to choose, such as macOS or Windows, takes no package that names one, as npm reads it.
|
|
201
256
|
*/ function isBuiltFor(dir, platform) {
|
|
202
257
|
let pkg;
|
|
203
258
|
try {
|
|
204
|
-
pkg =
|
|
259
|
+
pkg = readJsonFile(path.join(dir, 'package.json'));
|
|
205
260
|
} catch {
|
|
206
261
|
return true;
|
|
207
262
|
}
|
|
208
263
|
if (!allowedBy(pkg.os, platform.platform) || !allowedBy(pkg.cpu, platform.arch)) return false;
|
|
209
|
-
|
|
264
|
+
if (platform.libc) return allowedBy(pkg.libc, platform.libc);
|
|
265
|
+
return platform.platform === LIBC_PLATFORM || !pkg.libc;
|
|
210
266
|
}
|
|
211
267
|
/**
|
|
212
268
|
* Copies packages and their runtime dependencies into `<outDir>/node_modules`, skipping `@types` and other platforms'
|
|
@@ -287,4 +343,4 @@ import { currentPlatform } from '../util/platform.util.js';
|
|
|
287
343
|
throw new Error(`bundleDependencies bundled ${found.size === 1 ? 'a package that loads' : 'packages that load'} a native addon:\n` + `${lines.join('\n')}\n` + 'A native addon is a compiled binary for one platform and cannot travel inside the bundle. The build ' + 'would run on this machine and fail in production the first time the addon loads. Keep ' + `${found.size === 1 ? 'it' : 'them'} out of the bundle and install ${found.size === 1 ? 'it' : 'them'} ` + `on the server:\n\n externals: [${names}]`);
|
|
288
344
|
}
|
|
289
345
|
|
|
290
|
-
export { assertNoBundledNativeAddons, bundledModuleFiles, copyPackagesInto, createNativeExternals, findBundledNativeAddons, installedPackages, isBuiltFor, nativeCarrier, packageFromPath, packageNameOfRequest };
|
|
346
|
+
export { assertNoBundledNativeAddons, bundledModuleFiles, copyPackagesInto, createNativeExternals, findBundledNativeAddons, installedPackages, isBuiltFor, nativeCarrier, otherCopiesWarning, packageFromPath, packageNameOfRequest };
|
|
@@ -2,8 +2,9 @@ import path from 'path';
|
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
3
|
import { OptionalProbesPlugin } from './optional-probes.js';
|
|
4
4
|
import { RunnableBundlePlugin } from './runnable-bundle.js';
|
|
5
|
-
import { typeExternals } from './typed-externals.js';
|
|
5
|
+
import { typeExternals, externalMatcher } from './typed-externals.js';
|
|
6
6
|
import { prepareModifiedTsConfig } from '../util/tsconfig.util.js';
|
|
7
|
+
import { WEBPACK_HOOK_REFUSAL } from '../util/meocord-config-validation.util.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* The module bundled ahead of the application's entry to load `dist/meocord.config.mjs` first. Shipped
|
|
@@ -33,7 +34,8 @@ import { prepareModifiedTsConfig } from '../util/tsconfig.util.js';
|
|
|
33
34
|
* The `optionalExternals` also listed in `externals`, where an ESM import of each runs as its importer loads and
|
|
34
35
|
* throws when the package is missing, defeating the point of listing it as optional; a require stays one either way.
|
|
35
36
|
*/ function optionalExternalConflicts(optionalExternals = [], externals = []) {
|
|
36
|
-
|
|
37
|
+
const matchers = externals.map(externalMatcher);
|
|
38
|
+
return optionalExternals.filter((name)=>matchers.some((matches)=>matches(name)));
|
|
37
39
|
}
|
|
38
40
|
/**
|
|
39
41
|
* The prefix an asset import is joined to at runtime: the output directory, with forward slashes.
|
|
@@ -117,6 +119,12 @@ import { prepareModifiedTsConfig } from '../util/tsconfig.util.js';
|
|
|
117
119
|
]));
|
|
118
120
|
// Says a missing package a dependency only probes for is harmless, and how to silence it
|
|
119
121
|
chain.plugin('meocord-optional-probes').use(OptionalProbesPlugin);
|
|
122
|
+
// Project references off, where Rsbuild sets 'auto': with them, Rspack resolves the root project's own files
|
|
123
|
+
// without its paths and baseUrl, which tsc applies, so a bot that references another project couldn't build. A
|
|
124
|
+
// file inside a referenced project then resolves through the root config's paths, not its own
|
|
125
|
+
if (chain.resolve.get('tsConfig')) chain.resolve.tsConfig({
|
|
126
|
+
configFile: tsconfigPath
|
|
127
|
+
});
|
|
120
128
|
},
|
|
121
129
|
swc: {
|
|
122
130
|
jsc: {
|
|
@@ -231,7 +239,7 @@ import { prepareModifiedTsConfig } from '../util/tsconfig.util.js';
|
|
|
231
239
|
* the default export's properties, but `in` and `Object.keys` only see `default`.
|
|
232
240
|
*/ function assertNoWebpackHook(config) {
|
|
233
241
|
if (config?.webpack === undefined) return;
|
|
234
|
-
throw new Error(
|
|
242
|
+
throw new Error(WEBPACK_HOOK_REFUSAL);
|
|
235
243
|
}
|
|
236
244
|
|
|
237
245
|
export { CONFIG_PRE_ENTRY, DISCORD_OPTIONAL_NATIVES, assertNoWebpackHook, assetPrefixFor, createRsbuildConfig, optionalExternalConflicts, optionalExternalNames };
|
|
@@ -6,6 +6,19 @@
|
|
|
6
6
|
*/ function externalType(request, dependencyType) {
|
|
7
7
|
return `${dependencyType === 'commonjs' ? 'node-commonjs' : 'import'} ${request}`;
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Whether a request matches an `externals` entry: a name it equals, or a pattern. A pattern is tested through a copy of
|
|
11
|
+
* its own, from the start of each request: with the `g` or `y` flag, `test` would otherwise resume where the last match
|
|
12
|
+
* ended and miss the next request for the same package. A `y` pattern still matches only at the start, as Rsbuild
|
|
13
|
+
* tests it.
|
|
14
|
+
*/ function externalMatcher(entry) {
|
|
15
|
+
if (typeof entry === 'string') return (request)=>request === entry;
|
|
16
|
+
const pattern = new RegExp(entry);
|
|
17
|
+
return (request)=>{
|
|
18
|
+
pattern.lastIndex = 0;
|
|
19
|
+
return pattern.test(request);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
9
22
|
/** Whether an external's result names its own type, as `node-commonjs zlib-sync` does. */ const typed = (result)=>/\s/.test(result);
|
|
10
23
|
/**
|
|
11
24
|
* The externals with every untyped entry given its type by {@link externalType}: a string or pattern, as `autoExternal`
|
|
@@ -13,7 +26,7 @@
|
|
|
13
26
|
*/ function typeExternals(items) {
|
|
14
27
|
return items.map((item)=>{
|
|
15
28
|
if (typeof item === 'string' || item instanceof RegExp) {
|
|
16
|
-
const matches = (
|
|
29
|
+
const matches = externalMatcher(item);
|
|
17
30
|
return (data, callback)=>{
|
|
18
31
|
const { request, dependencyType } = data;
|
|
19
32
|
if (request && matches(request)) return callback(null, externalType(request, dependencyType));
|
|
@@ -33,4 +46,4 @@
|
|
|
33
46
|
});
|
|
34
47
|
}
|
|
35
48
|
|
|
36
|
-
export { externalType, typeExternals };
|
|
49
|
+
export { externalMatcher, externalType, typeExternals };
|
|
@@ -1,14 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* @group Types
|
|
5
|
-
*/ /** `verdict` with `release` added as a non-enumerable property, as the built-in stores give it. */ function withRelease(verdict, release) {
|
|
1
|
+
import { sharedKey } from '../util/shared-state.util.js';
|
|
2
|
+
|
|
3
|
+
/** `verdict` with `release` added as a non-enumerable property, as the built-in stores give it. */ function withRelease(verdict, release) {
|
|
6
4
|
return Object.defineProperty(verdict, 'release', {
|
|
7
5
|
value: release,
|
|
8
6
|
enumerable: false,
|
|
9
7
|
configurable: true
|
|
10
8
|
});
|
|
11
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* `verdict` with `retryTimestamp` added as a non-enumerable property, unless it holds the field already, so a refusal
|
|
12
|
+
* that gains it compares and serialises as it did.
|
|
13
|
+
*/ function withRetryTimestamp(verdict, retryTimestamp) {
|
|
14
|
+
if (retryTimestamp === undefined || Object.hasOwn(verdict, 'retryTimestamp')) return verdict;
|
|
15
|
+
return Object.defineProperty(verdict, 'retryTimestamp', {
|
|
16
|
+
value: retryTimestamp,
|
|
17
|
+
enumerable: false,
|
|
18
|
+
configurable: true
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/** A batch verdict for one key, as the built-in stores' `consume` gives it: `allowed`, `retryAfterMs`, and the wait's end. */ function singleVerdict({ allowed, retryAfterMs, retryTimestamp }) {
|
|
22
|
+
return withRetryTimestamp({
|
|
23
|
+
allowed,
|
|
24
|
+
retryAfterMs
|
|
25
|
+
}, retryTimestamp);
|
|
26
|
+
}
|
|
12
27
|
/**
|
|
13
28
|
* Where `@Cooldown` counts calls.
|
|
14
29
|
*
|
|
@@ -59,10 +74,11 @@
|
|
|
59
74
|
*/ async consumeMany(entries) {
|
|
60
75
|
for (const [index, { key, limit }] of entries.entries()){
|
|
61
76
|
const verdict = await this.consume(key, limit);
|
|
62
|
-
|
|
77
|
+
// The wait's end by name, as a built-in store's consume gives it non-enumerable, which a spread leaves out
|
|
78
|
+
if (!verdict.allowed) return withRetryTimestamp({
|
|
63
79
|
...verdict,
|
|
64
80
|
blocked: index
|
|
65
|
-
};
|
|
81
|
+
}, verdict.retryTimestamp);
|
|
66
82
|
}
|
|
67
83
|
return {
|
|
68
84
|
allowed: true,
|
|
@@ -88,6 +104,18 @@
|
|
|
88
104
|
});
|
|
89
105
|
}
|
|
90
106
|
}
|
|
107
|
+
// CooldownStore's own consumeMany and peekMany carry a mark both builds share, so a check can tell a store's
|
|
108
|
+
// override from either build's default, as a store that extends one build's class can be checked by the other's
|
|
109
|
+
const DEFAULT_BATCH_METHOD = sharedKey('cooldown-store:default-batch-method');
|
|
110
|
+
for (const method of [
|
|
111
|
+
CooldownStore.prototype.consumeMany,
|
|
112
|
+
CooldownStore.prototype.peekMany
|
|
113
|
+
]){
|
|
114
|
+
Object.defineProperty(method, DEFAULT_BATCH_METHOD, {
|
|
115
|
+
value: true
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/** Whether `method` is CooldownStore's own consumeMany or peekMany, from either build of this version. */ const isDefaultBatchMethod = (method)=>typeof method === 'function' && DEFAULT_BATCH_METHOD in method;
|
|
91
119
|
/**
|
|
92
120
|
* Of several refusals, the one a caller waits on: the call runs only once every entry allows it, so the
|
|
93
121
|
* longest wait. Undefined when nothing refused.
|
|
@@ -178,16 +206,12 @@
|
|
|
178
206
|
* @see {@link CooldownStore}
|
|
179
207
|
*/ class MemoryCooldownStore extends CooldownStore {
|
|
180
208
|
async consume(key, limit) {
|
|
181
|
-
|
|
209
|
+
return singleVerdict(await this.consumeMany([
|
|
182
210
|
{
|
|
183
211
|
key,
|
|
184
212
|
limit
|
|
185
213
|
}
|
|
186
|
-
]);
|
|
187
|
-
return {
|
|
188
|
-
allowed,
|
|
189
|
-
retryAfterMs
|
|
190
|
-
};
|
|
214
|
+
]));
|
|
191
215
|
}
|
|
192
216
|
/**
|
|
193
217
|
* Checks every entry, and records the call against all of them only if all allow it.
|
|
@@ -274,4 +298,4 @@
|
|
|
274
298
|
}
|
|
275
299
|
}
|
|
276
300
|
|
|
277
|
-
export { CooldownStore, MemoryCooldownStore, forgetRecorded, longestRefusal, recordedOf, withRelease };
|
|
301
|
+
export { CooldownStore, MemoryCooldownStore, forgetRecorded, isDefaultBatchMethod, longestRefusal, recordedOf, singleVerdict, withRelease };
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import { shared } from '../util/shared-state.util.js';
|
|
2
|
+
|
|
3
|
+
// One per process for this meocord version, so a warning both builds give prints once
|
|
4
|
+
const warned = shared('deprecationWarnings', ()=>new Set());
|
|
2
5
|
/** Warns once per run that `old` goes in 5.0, naming what to use instead. */ function warnDeprecated(logger, old, replacement) {
|
|
3
6
|
warnOnce(logger, `${old} is deprecated and will be removed in the next major version (5.0). Use ${replacement} instead.`);
|
|
4
7
|
}
|
|
@@ -38,8 +38,10 @@ import { useTheme } from '../core/theme-scope.js';
|
|
|
38
38
|
/**
|
|
39
39
|
* The response state of the interaction being handled, the same one `respond(interaction)` returns,
|
|
40
40
|
* or `undefined` for anything that cannot be answered: a message, a reaction, an event or
|
|
41
|
-
* autocomplete.
|
|
41
|
+
* autocomplete. An `@On('interactionCreate')` handler's call has none either, as MeoCord answers that interaction
|
|
42
|
+
* through the handler it dispatches it to.
|
|
42
43
|
*/ get response() {
|
|
44
|
+
if (this.getType() === 'event') return undefined;
|
|
43
45
|
const interaction = this.getInteraction();
|
|
44
46
|
return interaction?.isRepliable() ? respond(interaction) : undefined;
|
|
45
47
|
}
|
|
@@ -12,7 +12,10 @@ const EXPLAINED = Symbol.for('meocord.explained');
|
|
|
12
12
|
*
|
|
13
13
|
* Use it where a bot logs a startup failure, so an error MeoCord explained is not logged twice: an app
|
|
14
14
|
* `MeoCordFactory.create()` refuses, such as two handlers for one command, or Discord refusing the bot token or a
|
|
15
|
-
* privileged intent at login.
|
|
15
|
+
* privileged intent at login. A built bot has `process.exitCode` set for such an error already, and a start that a stop
|
|
16
|
+
* ended, which is explained too, leaves it as it was, so the process exits 0. Outside a build, as a script run by hand,
|
|
17
|
+
* `MeoCordFactory.create()` sets no code for an app it refuses, so a catch that returns early for an explained error,
|
|
18
|
+
* as below, sets `process.exitCode` itself there. The error is otherwise unchanged.
|
|
16
19
|
*
|
|
17
20
|
* @param error - An error `MeoCordFactory.create()` threw or `app.start()` rejected with.
|
|
18
21
|
* @returns `true` when MeoCord explained the error.
|
|
@@ -28,9 +31,10 @@ const EXPLAINED = Symbol.for('meocord.explained');
|
|
|
28
31
|
* await app.start()
|
|
29
32
|
* }
|
|
30
33
|
*
|
|
31
|
-
* // An error MeoCord explained, such as an app it refuses or a token Discord refused, is already logged
|
|
34
|
+
* // An error MeoCord explained, such as an app it refuses or a token Discord refused, is already logged, with the exit code set
|
|
32
35
|
* bootstrap().catch(error => {
|
|
33
|
-
* if (
|
|
36
|
+
* if (isExplainedError(error)) return
|
|
37
|
+
* logger.error('Error during startup:', error)
|
|
34
38
|
* process.exitCode = 1
|
|
35
39
|
* })
|
|
36
40
|
* ```
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { loadMeoCordConfig } from '../util/meocord-config-loader.util.js';
|
|
2
2
|
import { isBuiltApplication } from '../util/bundle-entry.util.js';
|
|
3
|
+
import { shared } from '../util/shared-state.util.js';
|
|
3
4
|
|
|
4
5
|
/** The environment variable that sets the level for one run, over `logLevel` in meocord.config.ts. */ const LOG_LEVEL_ENV = 'MEOCORD_LOG_LEVEL';
|
|
5
6
|
/** Each level's rank: a line prints when its rank is at least the level's. `silent` outranks every line. */ const LOG_LEVEL_RANK = {
|
|
@@ -10,32 +11,35 @@ import { isBuiltApplication } from '../util/bundle-entry.util.js';
|
|
|
10
11
|
silent: 4
|
|
11
12
|
};
|
|
12
13
|
const isLogLevel = (value)=>typeof value === 'string' && Object.hasOwn(LOG_LEVEL_RANK, value);
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
// One per process for this meocord version, so both builds print at one level and warn about a bad variable once
|
|
15
|
+
const level = shared('logLevel', ()=>({
|
|
16
|
+
threshold: undefined,
|
|
17
|
+
rejectedEnv: undefined
|
|
18
|
+
}));
|
|
15
19
|
/**
|
|
16
20
|
* The rank a line needs to print. Resolved on first use and kept, so logging reads neither the
|
|
17
21
|
* environment nor the config again: `MEOCORD_LOG_LEVEL`, then `logLevel`, then `debug` in development
|
|
18
22
|
* and `log` otherwise. `logLevel` applies to the built bot only; the CLI and tests would otherwise read
|
|
19
23
|
* it from whatever `dist` a previous build left.
|
|
20
24
|
*/ function logThreshold() {
|
|
21
|
-
if (threshold === undefined) {
|
|
25
|
+
if (level.threshold === undefined) {
|
|
22
26
|
// The config first: loading it runs its dotenv call, which may set the variable
|
|
23
27
|
const configured = isBuiltApplication() ? loadMeoCordConfig()?.logLevel : undefined;
|
|
24
28
|
const raw = process.env[LOG_LEVEL_ENV];
|
|
25
29
|
// An environment variable is often written in capitals, DEBUG for debug
|
|
26
30
|
const fromEnv = raw?.toLowerCase();
|
|
27
|
-
rejectedEnv = raw && !isLogLevel(fromEnv) ? raw : undefined;
|
|
28
|
-
const
|
|
29
|
-
threshold = LOG_LEVEL_RANK[
|
|
31
|
+
level.rejectedEnv = raw && !isLogLevel(fromEnv) ? raw : undefined;
|
|
32
|
+
const chosen = isLogLevel(fromEnv) ? fromEnv : isLogLevel(configured) ? configured : process.env.NODE_ENV === 'development' ? 'debug' : 'log';
|
|
33
|
+
level.threshold = LOG_LEVEL_RANK[chosen];
|
|
30
34
|
}
|
|
31
|
-
return threshold;
|
|
35
|
+
return level.threshold;
|
|
32
36
|
}
|
|
33
37
|
/**
|
|
34
38
|
* An unknown `MEOCORD_LOG_LEVEL` the last resolution ignored, handed out once so the logger can warn
|
|
35
39
|
* about it; undefined afterwards.
|
|
36
40
|
*/ function takeRejectedLogLevel() {
|
|
37
|
-
const rejected = rejectedEnv;
|
|
38
|
-
rejectedEnv = undefined;
|
|
41
|
+
const rejected = level.rejectedEnv;
|
|
42
|
+
level.rejectedEnv = undefined;
|
|
39
43
|
return rejected;
|
|
40
44
|
}
|
|
41
45
|
|
|
@@ -5,7 +5,8 @@ import timezone from 'dayjs/plugin/timezone.js';
|
|
|
5
5
|
import { loadMeoCordConfig } from '../util/meocord-config-loader.util.js';
|
|
6
6
|
import { isBuiltApplication } from '../util/bundle-entry.util.js';
|
|
7
7
|
import chalk, { chalkStderr } from 'chalk';
|
|
8
|
-
import { logThreshold, LOG_LEVEL_ENV, LOG_LEVEL_RANK
|
|
8
|
+
import { logThreshold, takeRejectedLogLevel, LOG_LEVEL_ENV, LOG_LEVEL_RANK } from './log-level.js';
|
|
9
|
+
import { shared } from '../util/shared-state.util.js';
|
|
9
10
|
|
|
10
11
|
dayjs.extend(utc);
|
|
11
12
|
dayjs.extend(timezone);
|
|
@@ -31,7 +32,8 @@ const tagColours = TAG_COLOURS;
|
|
|
31
32
|
* The shortest value registered: a bot's credential is far longer, and a short value, such as a test's placeholder,
|
|
32
33
|
* would replace ordinary words in every line.
|
|
33
34
|
*/ const MIN_HIDDEN_LENGTH = 16;
|
|
34
|
-
|
|
35
|
+
// One per process for this meocord version, so a value hidden through one build stays out of the other's lines
|
|
36
|
+
const hiddenValues = shared('hiddenValues', ()=>new Set());
|
|
35
37
|
/**
|
|
36
38
|
* Keeps `value`, such as the bot's credential, out of every line Logger prints from now on, whatever object, string
|
|
37
39
|
* or error carries it: trimmed, and without the `Bot ` or `Bearer ` prefix discord.js strips.
|
|
@@ -7,8 +7,9 @@ const ENGLISH = 'en-US';
|
|
|
7
7
|
/**
|
|
8
8
|
* The message a text is in for `locale`, and whether it is the app's: the app's catalogs down the locale's chain, with
|
|
9
9
|
* MeoCord's English as the English catalog. An English request reads it after the app's English catalogs and before a
|
|
10
|
-
* default in another language; any other request reads it last.
|
|
11
|
-
|
|
10
|
+
* default in another language; any other request reads it last. A message whose form for `count` is empty, as tools
|
|
11
|
+
* export an untranslated string, counts as missing, as it does for the app's own texts.
|
|
12
|
+
*/ function findText(translator, locale, key, count) {
|
|
12
13
|
const english = ()=>{
|
|
13
14
|
const message = lookup(MEOCORD_MESSAGES, key);
|
|
14
15
|
return message === undefined ? undefined : {
|
|
@@ -23,7 +24,10 @@ const ENGLISH = 'en-US';
|
|
|
23
24
|
const beforeDefault = englishFirst && served === translator.defaultLocale ? english() : undefined;
|
|
24
25
|
if (beforeDefault) return beforeDefault;
|
|
25
26
|
const message = lookup(translator[CATALOGS][served], key);
|
|
26
|
-
if (message !== undefined
|
|
27
|
+
if (message !== undefined && pluralForm({
|
|
28
|
+
message,
|
|
29
|
+
locale: served
|
|
30
|
+
}, count) !== '') return {
|
|
27
31
|
message,
|
|
28
32
|
locale: served,
|
|
29
33
|
own: true
|
|
@@ -44,9 +48,9 @@ const lists = new Map();
|
|
|
44
48
|
* that has it; any other list is joined with its `joiner`, a comma unless given, as Intl's `unit` lists join with
|
|
45
49
|
* nothing in zh-CN and with a space in ja.
|
|
46
50
|
*/ function renderText(translator, locale, text) {
|
|
47
|
-
const found = findText(translator, locale, text.key);
|
|
48
|
-
if (!found) return text.fallback ?? text.key;
|
|
49
51
|
const params = text.params ?? {};
|
|
52
|
+
const found = findText(translator, locale, text.key, params.count);
|
|
53
|
+
if (!found) return text.fallback ?? text.key;
|
|
50
54
|
return fillPlaceholders(pluralForm(found, params.count), (name, whole)=>{
|
|
51
55
|
if (!Object.hasOwn(params, name)) return whole;
|
|
52
56
|
const value = params[name];
|
|
@@ -72,7 +76,8 @@ const translators = shared('clientTranslators', ()=>new WeakMap());
|
|
|
72
76
|
/** A text for an interaction, in its user's language through its app's translator. */ function textFor(interaction, text) {
|
|
73
77
|
return renderText(translatorOfClient(interaction.client), interactionLocale(interaction), text);
|
|
74
78
|
}
|
|
75
|
-
|
|
79
|
+
// One per process for this meocord version, so an issue made by one build is translated by the other
|
|
80
|
+
const issueTexts = shared('issueTexts', ()=>new WeakMap());
|
|
76
81
|
/** An issue of a usage error, whose message is the text in English, keeping the text to translate it later. */ function usageIssue(text, param) {
|
|
77
82
|
const issue = {
|
|
78
83
|
...param !== undefined && {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID, createHash } from 'node:crypto';
|
|
2
|
-
import { CooldownStore, withRelease, longestRefusal } from './cooldown-store.js';
|
|
2
|
+
import { CooldownStore, singleVerdict, withRelease, longestRefusal } from './cooldown-store.js';
|
|
3
3
|
import { Logger } from './logger.js';
|
|
4
4
|
|
|
5
5
|
const logger = new Logger('RedisCooldownStore');
|
|
@@ -138,16 +138,12 @@ const DEFAULT_PREFIX = 'meocord:cooldown:';
|
|
|
138
138
|
* @param limit - The calls allowed, and the window they are counted over.
|
|
139
139
|
* @returns Whether this call was recorded, and if not, how long until one can be.
|
|
140
140
|
*/ async consume(key, limit) {
|
|
141
|
-
|
|
141
|
+
return singleVerdict(await this.consumeMany([
|
|
142
142
|
{
|
|
143
143
|
key,
|
|
144
144
|
limit
|
|
145
145
|
}
|
|
146
|
-
]);
|
|
147
|
-
return {
|
|
148
|
-
allowed,
|
|
149
|
-
retryAfterMs
|
|
150
|
-
};
|
|
146
|
+
]));
|
|
151
147
|
}
|
|
152
148
|
/**
|
|
153
149
|
* Records a call against every entry if all allow it, as one script: one round trip, however many
|
|
@@ -31,5 +31,21 @@ const { Ephemeral, SuppressEmbeds, SuppressNotifications, IsComponentsV2 } = Mes
|
|
|
31
31
|
/** The names of the flags set in a value, for warnings. */ function flagNames(flags) {
|
|
32
32
|
return new MessageFlagsBitField(flags).toArray();
|
|
33
33
|
}
|
|
34
|
+
/** Each call as a warning names it, its article included. */ const STEP_NAMES = {
|
|
35
|
+
deferReply: 'a deferred reply',
|
|
36
|
+
reply: 'a reply',
|
|
37
|
+
update: 'an update',
|
|
38
|
+
edit: 'an edit',
|
|
39
|
+
followUp: 'a follow-up'
|
|
40
|
+
};
|
|
41
|
+
/** What an update or an edit does with `Ephemeral` instead, and how to answer only the member. */ const PRIVATE_HINT = {
|
|
42
|
+
update: 'An update changes the message the member clicked, for everyone; to answer only them, use respond(interaction).followUp() with MessageFlags.Ephemeral.',
|
|
43
|
+
edit: 'An edit keeps the message as public or private as it was; to answer only the member, use respond(interaction).followUp() with MessageFlags.Ephemeral.'
|
|
44
|
+
};
|
|
45
|
+
/** The development warning for the flags a call dropped, with how to answer privately when `Ephemeral` is one of them. */ function droppedFlagsWarning(step, dropped) {
|
|
46
|
+
const text = `Dropped flags ${flagNames(dropped).join(', ')}, which ${STEP_NAMES[step]} cannot take.`;
|
|
47
|
+
const hint = hasEphemeral(dropped) ? PRIVATE_HINT[step] : undefined;
|
|
48
|
+
return hint ? `${text} ${hint}` : text;
|
|
49
|
+
}
|
|
34
50
|
|
|
35
|
-
export { flagNames, hasComponentsV2, hasEphemeral, resolveFlags };
|
|
51
|
+
export { droppedFlagsWarning, flagNames, hasComponentsV2, hasEphemeral, resolveFlags };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { InteractionContextType, ApplicationIntegrationType } from 'discord.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Reports where an interaction happened and whether the bot is present there.
|
|
@@ -26,36 +26,47 @@ import { ApplicationIntegrationType, InteractionContextType } from 'discord.js';
|
|
|
26
26
|
*
|
|
27
27
|
* @group Utilities
|
|
28
28
|
*/ function getInstallContext(interaction) {
|
|
29
|
-
const
|
|
30
|
-
const guildInstalled = owners[ApplicationIntegrationType.GuildInstall] !== undefined;
|
|
31
|
-
const userInstalled = owners[ApplicationIntegrationType.UserInstall] !== undefined;
|
|
29
|
+
const botInstalled = isBotInstalled(interaction);
|
|
32
30
|
switch(interaction.context){
|
|
33
31
|
case InteractionContextType.BotDM:
|
|
34
32
|
return {
|
|
35
33
|
where: 'bot-dm',
|
|
36
|
-
botInstalled
|
|
34
|
+
botInstalled
|
|
37
35
|
};
|
|
38
36
|
case InteractionContextType.PrivateChannel:
|
|
39
37
|
return {
|
|
40
38
|
where: 'private-channel',
|
|
41
|
-
botInstalled
|
|
39
|
+
botInstalled
|
|
42
40
|
};
|
|
43
41
|
case InteractionContextType.Guild:
|
|
44
|
-
// With no owners reported, the interaction came through the bot, as every interaction did before user installs.
|
|
45
42
|
return {
|
|
46
43
|
where: 'guild',
|
|
47
|
-
botInstalled
|
|
44
|
+
botInstalled
|
|
48
45
|
};
|
|
49
46
|
default:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
botInstalled: true
|
|
54
|
-
} : {
|
|
55
|
-
where: 'bot-dm',
|
|
56
|
-
botInstalled: true
|
|
47
|
+
return {
|
|
48
|
+
where: interaction.guildId ? 'guild' : 'bot-dm',
|
|
49
|
+
botInstalled
|
|
57
50
|
};
|
|
58
51
|
}
|
|
59
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Whether the bot is present where an interaction happened, as {@link getInstallContext} reports it, read from Discord's
|
|
55
|
+
* `context` and the integrations that authorized it alone: no `guildId`, which only `where` needs.
|
|
56
|
+
*/ function isBotInstalled(interaction) {
|
|
57
|
+
switch(interaction.context){
|
|
58
|
+
case InteractionContextType.PrivateChannel:
|
|
59
|
+
return false;
|
|
60
|
+
case InteractionContextType.Guild:
|
|
61
|
+
{
|
|
62
|
+
const owners = interaction.authorizingIntegrationOwners ?? {};
|
|
63
|
+
// With no owners reported, the interaction came through the bot, as every interaction did before user installs
|
|
64
|
+
return owners[ApplicationIntegrationType.GuildInstall] !== undefined || owners[ApplicationIntegrationType.UserInstall] === undefined;
|
|
65
|
+
}
|
|
66
|
+
default:
|
|
67
|
+
// A DM with the bot, or an interaction from before contexts existed, which reached only the bot
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
60
71
|
|
|
61
|
-
export { getInstallContext };
|
|
72
|
+
export { getInstallContext, isBotInstalled };
|
|
@@ -24,8 +24,12 @@ import { shared } from '../../util/shared-state.util.js';
|
|
|
24
24
|
/** The id MeoCord gives the containers it renders, so a view left behind can be found again. */ const RENDERED_CONTAINER_ID = 0x4d43;
|
|
25
25
|
// Shared by both builds of this version, so either answers with the app's presenter
|
|
26
26
|
const presenters = shared('clientPresenters', ()=>new WeakMap());
|
|
27
|
-
/** Sets the presenter for the interactions a client receives. */ function setPresenter(client, presenter) {
|
|
28
|
-
presenters.set(client, presenter);
|
|
27
|
+
/** Sets the presenter for the interactions a client receives; without one, they take the default. */ function setPresenter(client, presenter) {
|
|
28
|
+
if (presenter) presenters.set(client, presenter);
|
|
29
|
+
else presenters.delete(client);
|
|
30
|
+
}
|
|
31
|
+
/** The presenter set on a client, if any. */ function clientPresenter(client) {
|
|
32
|
+
return presenters.get(client);
|
|
29
33
|
}
|
|
30
34
|
/** The presenter for an interaction's client, or the default one. */ function presenterFor(client) {
|
|
31
35
|
return client && presenters.get(client) || defaultPresenter;
|
|
@@ -185,4 +189,4 @@ const IMAGE = /\.(png|jpe?g|gif|webp)$/i;
|
|
|
185
189
|
};
|
|
186
190
|
}
|
|
187
191
|
|
|
188
|
-
export { ATTACHMENT_LIMIT, DEFAULT_ATTACHMENT_SIZE_LIMIT, REFUSED_AS_TOO_LARGE, RENDERED_CONTAINER_ID, TEXT_LIMITS, attachmentsOf, defaultPresenter, filesProblem, fitText, isTooLarge, presentedMessage, presenterFor, renderContainer, renderEmbed, setPresenter, withSendableFiles, withoutFiles };
|
|
192
|
+
export { ATTACHMENT_LIMIT, DEFAULT_ATTACHMENT_SIZE_LIMIT, REFUSED_AS_TOO_LARGE, RENDERED_CONTAINER_ID, TEXT_LIMITS, attachmentsOf, clientPresenter, defaultPresenter, filesProblem, fitText, isTooLarge, presentedMessage, presenterFor, renderContainer, renderEmbed, setPresenter, withSendableFiles, withoutFiles };
|