meocord 4.2.0 → 4.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +322 -0
- package/THIRD_PARTY_NOTICES.md +14 -13
- package/dist/cjs/_shared/{controller.decorator-CRn9kWob.cjs → controller.decorator-U2NBWcoU.cjs} +416 -108
- package/dist/cjs/_shared/{providers-ChvMqnLd.cjs → providers-B4MOFBrW.cjs} +202 -50
- package/dist/cjs/_shared/{sharded-cooldown-store-BdAUTdYs.cjs → sharded-cooldown-store-HTUb3Ii_.cjs} +10 -7
- package/dist/cjs/_shared/{startup-roots-CQo8mSP8.cjs → startup-roots-lA5xkBFq.cjs} +228 -71
- package/dist/cjs/_shared/{translate-error-DhDVbT5M.cjs → translate-error-CbBHUu5P.cjs} +10 -5
- package/dist/cjs/common/index.cjs +3 -3
- package/dist/cjs/core/index.cjs +188 -169
- package/dist/cjs/decorator/index.cjs +52 -7
- package/dist/cjs/testing/index.cjs +1854 -311
- package/dist/cli.json +1 -1
- package/dist/esm/bin/generator.js +4 -5
- package/dist/esm/bin/helper/filter-generator.helper.js +4 -5
- package/dist/esm/bin/helper/guard-generator.helper.js +4 -5
- package/dist/esm/bin/helper/interceptor-generator.helper.js +4 -5
- package/dist/esm/bin/helper/observer-generator.helper.js +4 -5
- package/dist/esm/bin/helper/pipe-generator.helper.js +4 -5
- package/dist/esm/bin/helper/service-generator.helper.js +4 -5
- package/dist/esm/bin/meocord.js +84 -25
- package/dist/esm/build/load-config.pre-entry.js +13 -3
- package/dist/esm/build/rsbuild-config.js +16 -12
- package/dist/esm/build/typed-externals.js +36 -0
- package/dist/esm/common/cooldown-store.js +10 -2
- package/dist/esm/common/errors.js +25 -0
- package/dist/esm/common/meocord-text.js +11 -8
- package/dist/esm/common/response/call-order.js +9 -4
- package/dist/esm/common/response/presenter.js +33 -6
- package/dist/esm/common/response/response-state.js +89 -15
- package/dist/esm/common/route.js +11 -4
- package/dist/esm/common/sharded-cooldown-store.js +1 -1
- package/dist/esm/common/translator.js +52 -11
- package/dist/esm/core/command-registration.js +93 -24
- package/dist/esm/core/command-routes.js +2 -2
- package/dist/esm/core/cooldown-runner.js +29 -8
- package/dist/esm/core/dispatcher.js +9 -8
- package/dist/esm/core/fallback.js +20 -11
- package/dist/esm/core/filter-runner.js +2 -2
- package/dist/esm/core/guard-runner.js +16 -3
- package/dist/esm/core/handler-input.js +4 -3
- package/dist/esm/core/handler-pipeline.js +10 -4
- package/dist/esm/core/lifecycle-hooks.js +134 -47
- package/dist/esm/core/meocord-factory.js +59 -34
- package/dist/esm/core/meocord.app.js +116 -75
- package/dist/esm/core/message-help.js +32 -6
- package/dist/esm/core/observer-runner.js +2 -1
- package/dist/esm/core/providers.js +19 -4
- package/dist/esm/core/running-calls.js +43 -0
- package/dist/esm/core/shard-context.js +5 -6
- package/dist/esm/core/shard-manager.js +16 -10
- package/dist/esm/core/stage-scope.js +2 -1
- package/dist/esm/core/startup-roots.js +21 -1
- package/dist/esm/core/theme-resolvers.js +4 -1
- package/dist/esm/core/theme-scope.js +7 -2
- package/dist/esm/core/theme-validation.js +17 -10
- package/dist/esm/decorator/app.decorator.js +31 -0
- package/dist/esm/decorator/controller.decorator.js +56 -20
- package/dist/esm/decorator/cooldown.decorator.js +17 -0
- package/dist/esm/decorator/filter.decorator.js +2 -2
- package/dist/esm/decorator/guard.decorator.js +3 -2
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/catalog.js +20 -13
- package/dist/esm/testing/cooldown-store-suite.js +67 -5
- package/dist/esm/testing/discord-defaults.js +225 -30
- package/dist/esm/testing/meocord-testing-module.js +158 -44
- package/dist/esm/testing/mock-fn.js +107 -26
- package/dist/esm/testing/mock-interaction.js +1250 -182
- package/dist/esm/testing/snowflake.js +9 -2
- package/dist/esm/testing/strict-mocks.js +40 -17
- package/dist/esm/util/common.util.js +47 -19
- package/dist/esm/util/dev-runner.util.js +8 -8
- package/dist/esm/util/generator-cli.util.js +4 -5
- package/dist/esm/util/interaction.util.js +2 -2
- package/dist/esm/util/meocord-config-loader.util.js +10 -2
- package/dist/esm/util/meocord-config-validation.util.js +3 -5
- package/dist/esm/util/meocord-source-config.util.js +65 -20
- package/dist/esm/util/method-wrapper.util.js +16 -0
- package/dist/esm/util/platform.util.js +10 -10
- package/dist/esm/util/shard-process.util.js +4 -1
- package/dist/esm/util/sharding-mode.util.js +5 -7
- package/dist/esm/util/shared-state.util.js +21 -0
- package/dist/esm/util/tsconfig-paths.util.js +29 -0
- package/dist/esm/util/tsconfig.util.js +10 -10
- package/dist/esm/util/value.util.js +30 -2
- package/dist/types/common/index.d.cts +10 -7
- package/dist/types/common/index.d.ts +10 -7
- package/dist/types/core/index.d.cts +3 -2
- package/dist/types/core/index.d.ts +3 -2
- package/dist/types/decorator/index.d.cts +1 -0
- package/dist/types/decorator/index.d.ts +1 -0
- package/dist/types/{index-CdXsFgjE.d.ts → index-DKJclIi-.d.ts} +45 -21
- package/dist/types/{index-nn5p7OoX.d.cts → index-Dlf0ZZtS.d.cts} +45 -21
- package/dist/types/interface/index.d.cts +1 -1
- package/dist/types/interface/index.d.ts +1 -1
- package/dist/types/{route-BZgFoNeB.d.cts → route-Bh1U13yy.d.cts} +3 -2
- package/dist/types/{route-BZgFoNeB.d.ts → route-Bh1U13yy.d.ts} +3 -2
- package/dist/types/testing/index.d.cts +198 -62
- package/dist/types/testing/index.d.ts +198 -62
- package/dist/types/{theme-resolvers-UKr3-6Ys.d.cts → theme-resolvers-CyEz9pnv.d.cts} +1 -1
- package/dist/types/{theme-resolvers-BcOJVzR9.d.ts → theme-resolvers-DA204O-q.d.ts} +1 -1
- package/dist/types/{translator-DHMkkCpg.d.cts → translator-Bg7hwxDF.d.cts} +21 -8
- package/dist/types/{translator-BViWKtLJ.d.ts → translator-CeP4bCzp.d.ts} +21 -8
- package/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var controller_decorator = require('./controller.decorator-
|
|
3
|
+
var controller_decorator = require('./controller.decorator-U2NBWcoU.cjs');
|
|
4
4
|
require('reflect-metadata');
|
|
5
5
|
var node_async_hooks = require('node:async_hooks');
|
|
6
6
|
var inversify = require('inversify');
|
|
@@ -90,7 +90,8 @@ var Table__default = /*#__PURE__*/_interopDefaultCompat(Table);
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
/** The class of a stage entry. */ function stageClass(entry) {
|
|
93
|
-
|
|
93
|
+
// Entries were checked as they were declared, so a function is the class itself
|
|
94
|
+
return typeof entry === 'function' ? entry : entry.provide;
|
|
94
95
|
}
|
|
95
96
|
/** The context types a stage declared, or `undefined` when it runs for every type. */ function stageTypes(entry) {
|
|
96
97
|
return Reflect.getMetadata(controller_decorator.META.stageTypes, stageClass(entry));
|
|
@@ -505,14 +506,27 @@ const wrapperCount = perHandler((prototype, methodName)=>{
|
|
|
505
506
|
else marks.delete(methodName);
|
|
506
507
|
return true;
|
|
507
508
|
}
|
|
509
|
+
/** The container that made each instance, so a direct call runs in its own app or testing module. */ const instanceContainers = new WeakMap();
|
|
510
|
+
/**
|
|
511
|
+
* Records each instance `container` makes of `classes` as its own. Two testing modules made from one class each run a
|
|
512
|
+
* direct call on their own instance in themselves, where the class's metadata names only the last.
|
|
513
|
+
*/ function recordInstanceContainers(container, classes) {
|
|
514
|
+
for (const cls of classes){
|
|
515
|
+
container.onActivation(cls, (_context, instance)=>{
|
|
516
|
+
if (typeof instance === 'object' && instance !== null) instanceContainers.set(instance, container);
|
|
517
|
+
return instance;
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
508
521
|
/**
|
|
509
522
|
* Runs a direct call to a guarded handler: the handler's guards, as dispatch resolves them, then the
|
|
510
523
|
* call with a pass for each wrapper inside the one entered, so no guard runs twice.
|
|
511
524
|
*/ async function runDirectCall(instance, methodName, args, call) {
|
|
512
525
|
const prototype = Object.getPrototypeOf(instance);
|
|
513
526
|
const controller = instance.constructor;
|
|
514
|
-
// The app that made the class
|
|
515
|
-
|
|
527
|
+
// The container that made the instance, or else the app that made the class or a class it extends: a subclass the
|
|
528
|
+
// app's code makes runs as its base does
|
|
529
|
+
const container = instanceContainers.get(instance) ?? Reflect.getMetadata(controller_decorator.META.container, controller);
|
|
516
530
|
if (!container) {
|
|
517
531
|
throw new Error(`${controller.name}.${methodName}: @UseGuard runs its guards through the app that made ${controller.name}, and this ` + `instance was made outside one. Inject ${controller.name}, or call it on an instance the app made.`);
|
|
518
532
|
}
|
|
@@ -767,19 +781,37 @@ const logger$3 = new controller_decorator.Logger('Cooldown');
|
|
|
767
781
|
const value = by ? await by(context, params) : undefined;
|
|
768
782
|
// Encoded, so a value holding a colon cannot count under another value's key. Without a value, `:by` alone keeps
|
|
769
783
|
// the call apart from a cooldown with no `by` over the same window.
|
|
770
|
-
const suffix = !by ? '' : value === undefined ? ':by' : `:by:${
|
|
784
|
+
const suffix = !by ? '' : value === undefined ? ':by' : `:by:${encodeKeyPart(String(value))}`;
|
|
785
|
+
const scope = scopeId(per, first);
|
|
786
|
+
// The caller, for a notice of a refusal under a scope that counts more than one caller
|
|
787
|
+
const user = scope.startsWith('user:') ? undefined : caller(first).user;
|
|
771
788
|
counted.push({
|
|
772
|
-
key: `${controller.name}.${methodName}#${id}:${per}:${
|
|
789
|
+
key: `${controller.name}.${methodName}#${id}:${per}:${scope}${suffix}`,
|
|
773
790
|
windowMs,
|
|
774
791
|
uses,
|
|
775
|
-
per
|
|
792
|
+
per,
|
|
793
|
+
user
|
|
776
794
|
});
|
|
777
795
|
}
|
|
778
796
|
return counted;
|
|
779
797
|
}
|
|
798
|
+
// A surrogate with no partner beside it, which encodeURIComponent refuses
|
|
799
|
+
const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
|
|
780
800
|
/**
|
|
781
|
-
*
|
|
782
|
-
*
|
|
801
|
+
* A value as part of a store key: each well-formed run as `encodeURIComponent` writes it, and each lone surrogate as `%u`
|
|
802
|
+
* and its four hex digits, which no other value encodes to, since `encodeURIComponent` writes `%` only before two.
|
|
803
|
+
*/ function encodeKeyPart(value) {
|
|
804
|
+
let encoded = '';
|
|
805
|
+
let from = 0;
|
|
806
|
+
for (const { 0: surrogate, index } of value.matchAll(LONE_SURROGATE)){
|
|
807
|
+
encoded += `${encodeURIComponent(value.slice(from, index))}%u${surrogate.charCodeAt(0).toString(16).toUpperCase()}`;
|
|
808
|
+
from = index + 1;
|
|
809
|
+
}
|
|
810
|
+
return encoded + encodeURIComponent(value.slice(from));
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* The store key and window of the cooldown each refusal came from, when its wait ends by the store's clock if the
|
|
814
|
+
* store said, and the refused caller when the key counts more than one, for {@link claimCooldownNotice}.
|
|
783
815
|
*/ const refusalKeys = new WeakMap();
|
|
784
816
|
/** Asks the store about the call under the app's policy, and throws what the answer means for it. */ async function ask(container, counted, peek, call) {
|
|
785
817
|
const store = cooldownStoreOf(container);
|
|
@@ -812,7 +844,8 @@ const logger$3 = new controller_decorator.Logger('Cooldown');
|
|
|
812
844
|
refusalKeys.set(refusal, {
|
|
813
845
|
key: blocking.key,
|
|
814
846
|
windowMs: blocking.windowMs,
|
|
815
|
-
endsAt: verdict.retryTimestamp
|
|
847
|
+
endsAt: verdict.retryTimestamp,
|
|
848
|
+
user: blocking.user
|
|
816
849
|
});
|
|
817
850
|
throw refusal;
|
|
818
851
|
}
|
|
@@ -835,16 +868,18 @@ const logger$3 = new controller_decorator.Logger('Cooldown');
|
|
|
835
868
|
// retry landing a moment late is refused on the earlier, which its wait holds, before taking the later, which a wait
|
|
836
869
|
// ending then needs for its own notice.
|
|
837
870
|
const bucket = Math.floor(refusal.retryAt.getTime() / NOTICE_BUCKET_MS);
|
|
871
|
+
// A key counting a server, a channel or everyone is shared by every caller it refuses, each told once
|
|
872
|
+
const key = refused.user === undefined ? refused.key : `${refused.key}:user:${refused.user}`;
|
|
838
873
|
const notices = refused.endsAt !== undefined ? [
|
|
839
874
|
{
|
|
840
|
-
key: `${
|
|
875
|
+
key: `${key}:notice:at:${refused.endsAt}`,
|
|
841
876
|
limit
|
|
842
877
|
}
|
|
843
878
|
] : [
|
|
844
879
|
bucket - 1,
|
|
845
880
|
bucket
|
|
846
881
|
].map((at)=>({
|
|
847
|
-
key: `${
|
|
882
|
+
key: `${key}:notice:${at}`,
|
|
848
883
|
limit
|
|
849
884
|
}));
|
|
850
885
|
try {
|
|
@@ -1066,19 +1101,35 @@ const scopeKey = (scope)=>scope === 'global' ? 'global' : `guild-${scope.guild}`
|
|
|
1066
1101
|
const guilds = options.config?.guilds ?? [];
|
|
1067
1102
|
return !everythingTarget(options) && guilds.length > 0 && ids(guilds).length === 0;
|
|
1068
1103
|
}
|
|
1104
|
+
/** Whether a command is an activity's primary entry point, which Discord takes only globally. */ const isEntryPoint = (command)=>command.body.type === discord_js.ApplicationCommandType.PrimaryEntryPoint;
|
|
1105
|
+
/** What a blank guild to register every command to says, as an unset variable gives one. */ const BLANK_GUILD = 'No commands were registered: the guild to register to is blank. Give a guild id, or leave it out to register where commands.* says.';
|
|
1069
1106
|
/**
|
|
1070
1107
|
* Where each command is sent: one bulk update per scope. A scope's update replaces everything the application has
|
|
1071
1108
|
* there, so the default scope is included even when it ends up empty, unless `commands.guilds` lists no guild id.
|
|
1109
|
+
*
|
|
1110
|
+
* A primary entry point command goes in the global update, as Discord refuses a guild update that holds one. A run
|
|
1111
|
+
* that sends everything to one guild sends nothing global, so it leaves the entry point out.
|
|
1072
1112
|
*/ function planTargets(commands, options, logger) {
|
|
1113
|
+
// Registering to the default scopes instead could publish everywhere
|
|
1114
|
+
if (options.onlyGuild !== undefined && !options.onlyGuild.trim()) {
|
|
1115
|
+
logger.error(BLANK_GUILD);
|
|
1116
|
+
return [];
|
|
1117
|
+
}
|
|
1073
1118
|
const everythingTo = everythingTarget(options);
|
|
1074
|
-
if (everythingTo)
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
},
|
|
1079
|
-
commands
|
|
1119
|
+
if (everythingTo) {
|
|
1120
|
+
// A global update from here would hold only what this run sends, deleting the application's other global commands
|
|
1121
|
+
for (const command of commands.filter(isEntryPoint)){
|
|
1122
|
+
logger.warn(`Entry point command "${command.name}" is registered globally only; a production run or \`meocord register\` registers it.`);
|
|
1080
1123
|
}
|
|
1081
|
-
|
|
1124
|
+
return [
|
|
1125
|
+
{
|
|
1126
|
+
scope: {
|
|
1127
|
+
guild: everythingTo
|
|
1128
|
+
},
|
|
1129
|
+
commands: commands.filter((command)=>!isEntryPoint(command))
|
|
1130
|
+
}
|
|
1131
|
+
];
|
|
1132
|
+
}
|
|
1082
1133
|
const defaultGuilds = ids(options.config?.guilds);
|
|
1083
1134
|
const nowhere = defaultGuildsEmpty(options);
|
|
1084
1135
|
const targets = new Map();
|
|
@@ -1096,7 +1147,12 @@ const scopeKey = (scope)=>scope === 'global' ? 'global' : `guild-${scope.guild}`
|
|
|
1096
1147
|
});
|
|
1097
1148
|
const unregistered = [];
|
|
1098
1149
|
for (const command of commands){
|
|
1099
|
-
if (command
|
|
1150
|
+
if (isEntryPoint(command)) {
|
|
1151
|
+
if (command.guilds) {
|
|
1152
|
+
logger.warn(`Entry point command "${command.name}" can only be registered globally, so the guilds its builder lists are ignored.`);
|
|
1153
|
+
}
|
|
1154
|
+
add('global', command);
|
|
1155
|
+
} else if (command.guilds) {
|
|
1100
1156
|
const own = ids(command.guilds);
|
|
1101
1157
|
// Falling back to the default scope could publish a staff-only command everywhere.
|
|
1102
1158
|
if (own.length === 0) logger.warn(`Command "${command.name}" lists no guild ids in its builder, so it is not registered.`);
|
|
@@ -1114,7 +1170,7 @@ const scopeKey = (scope)=>scope === 'global' ? 'global' : `guild-${scope.guild}`
|
|
|
1114
1170
|
}
|
|
1115
1171
|
}
|
|
1116
1172
|
if (nowhere) {
|
|
1117
|
-
const outcome = unregistered.length === 0 ? 'nothing is registered globally' : `${unregistered.join(', ')} ${unregistered.length === 1 ? 'is' : 'are'} not registered, rather than registered globally`;
|
|
1173
|
+
const outcome = unregistered.length === 0 ? targets.has('global') ? 'only the entry point command is registered globally' : 'nothing is registered globally' : `${unregistered.join(', ')} ${unregistered.length === 1 ? 'is' : 'are'} not registered, rather than registered globally`;
|
|
1118
1174
|
logger.warn(`commands.guilds lists no guild id, as an unset environment variable leaves it, so ${outcome}. ` + `Set the guild ids, or remove commands.guilds to register globally.`);
|
|
1119
1175
|
}
|
|
1120
1176
|
return [
|
|
@@ -1143,28 +1199,57 @@ const describeScope = (scope)=>scope === 'global' ? 'globally' : `to guild ${sco
|
|
|
1143
1199
|
return path__default.default.join(nodeModules, '.cache', 'meocord', `commands-${applicationId}-${scopeKey(scope)}.json`);
|
|
1144
1200
|
}
|
|
1145
1201
|
const hashOf = (commands)=>crypto.createHash('sha256').update(JSON.stringify(commands.map(({ body })=>body))).digest('hex');
|
|
1146
|
-
function
|
|
1202
|
+
function readRecord(file, logger) {
|
|
1147
1203
|
if (!file || !fs.existsSync(file)) return undefined;
|
|
1148
1204
|
try {
|
|
1149
|
-
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
1205
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
1150
1206
|
} catch (error) {
|
|
1151
1207
|
logger.debug(`Could not read the registered commands, so they are sent again: ${String(error)}`);
|
|
1152
1208
|
return undefined;
|
|
1153
1209
|
}
|
|
1154
1210
|
}
|
|
1155
|
-
function
|
|
1211
|
+
function writeRecord(file, record, logger) {
|
|
1156
1212
|
if (!file) return;
|
|
1157
1213
|
try {
|
|
1158
1214
|
fs.mkdirSync(path__default.default.dirname(file), {
|
|
1159
1215
|
recursive: true
|
|
1160
1216
|
});
|
|
1161
|
-
fs.writeFileSync(file, `${JSON.stringify(
|
|
1162
|
-
hash
|
|
1163
|
-
})}\n`);
|
|
1217
|
+
fs.writeFileSync(file, `${JSON.stringify(record)}\n`);
|
|
1164
1218
|
} catch (error) {
|
|
1165
1219
|
logger.debug(`Could not record the registered commands: ${String(error)}`);
|
|
1166
1220
|
}
|
|
1167
1221
|
}
|
|
1222
|
+
/** Forgets what development last sent to a scope, once that scope is written some other way. */ function forgetRecord(file, logger) {
|
|
1223
|
+
if (!file || !fs.existsSync(file)) return;
|
|
1224
|
+
try {
|
|
1225
|
+
fs.rmSync(file);
|
|
1226
|
+
} catch (error) {
|
|
1227
|
+
logger.debug(`Could not forget the registered commands: ${String(error)}`);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Each command's `id:version`, sorted, from what Discord lists or answers a bulk update with; `undefined` for anything
|
|
1232
|
+
* else. Discord gives a command a new version whenever it is updated, so the set changes with any edit, removal or
|
|
1233
|
+
* re-registration, from this checkout or any other.
|
|
1234
|
+
*/ function versionsOf(commands) {
|
|
1235
|
+
if (!Array.isArray(commands)) return undefined;
|
|
1236
|
+
const versions = commands.map((command)=>{
|
|
1237
|
+
const { id, version } = command ?? {};
|
|
1238
|
+
return typeof id === 'string' && typeof version === 'string' ? `${id}:${version}` : undefined;
|
|
1239
|
+
});
|
|
1240
|
+
return versions.every((version)=>version !== undefined) ? versions.sort() : undefined;
|
|
1241
|
+
}
|
|
1242
|
+
/** Whether Discord still holds a scope as development last sent it, by each command's version; a failed listing doesn't. */ async function stillAsSent(rest, route, record, scope, logger) {
|
|
1243
|
+
if (!record.versions) return false;
|
|
1244
|
+
let registered;
|
|
1245
|
+
try {
|
|
1246
|
+
registered = await rest.get(route);
|
|
1247
|
+
} catch (error) {
|
|
1248
|
+
logger.debug(`Could not list the commands registered ${describeScope(scope)}, so they are sent again: ${String(error)}`);
|
|
1249
|
+
return false;
|
|
1250
|
+
}
|
|
1251
|
+
return versionsOf(registered)?.join('\n') === record.versions.join('\n');
|
|
1252
|
+
}
|
|
1168
1253
|
function logRegistered(target, logger) {
|
|
1169
1254
|
const table = new Table__default.default({
|
|
1170
1255
|
head: [
|
|
@@ -1187,22 +1272,29 @@ function logRegistered(target, logger) {
|
|
|
1187
1272
|
};
|
|
1188
1273
|
for (const { name, body } of target.commands){
|
|
1189
1274
|
const options = Array.isArray(body.options) ? body.options : [];
|
|
1275
|
+
// A subcommand or a subcommand group, not a plain option
|
|
1276
|
+
const subcommands = options.filter((option)=>option.type === 1 || option.type === 2);
|
|
1190
1277
|
table.push([
|
|
1191
1278
|
name,
|
|
1192
1279
|
typeNames[typeof body.type === 'number' ? body.type : 1] ?? 'Command',
|
|
1193
|
-
|
|
1280
|
+
subcommands.map((option)=>option.name).join(', ')
|
|
1194
1281
|
]);
|
|
1195
1282
|
}
|
|
1196
1283
|
logger.log(`Registered ${target.commands.length} bot commands ${describeScope(target.scope)}:\n${table.toString()}`);
|
|
1197
1284
|
}
|
|
1198
1285
|
/**
|
|
1199
|
-
* Registers the commands, one bulk update per scope; development skips a scope unchanged since its last send
|
|
1200
|
-
* `force`. Once one is sent, and not to `onlyGuild`, the other scopes the configuration names are checked for
|
|
1286
|
+
* Registers the commands, one bulk update per scope; development skips a scope unchanged since its last send and still
|
|
1287
|
+
* registered as sent, which one listing per skipped scope checks by each command's version, unless `force`. A blank `onlyGuild` registers nothing. Once one is sent, and not to `onlyGuild`, the other scopes the configuration names are checked for
|
|
1201
1288
|
* leftovers, which `clearOther` removes; a development-guild run or a `commands.guilds` with no id only warns. Never
|
|
1202
1289
|
* throws.
|
|
1203
1290
|
* @returns Whether every scope was sent or unchanged, and `commands.guilds`, if set, lists a guild id.
|
|
1204
1291
|
*/ async function registerCommands(options) {
|
|
1205
1292
|
const { rest, applicationId, controllerClasses, logger, config, development, onlyGuild, force = false } = options;
|
|
1293
|
+
// An unset variable passed as the guild; registering to the default scopes instead could publish everywhere
|
|
1294
|
+
if (onlyGuild !== undefined && !onlyGuild.trim()) {
|
|
1295
|
+
logger.error(BLANK_GUILD);
|
|
1296
|
+
return false;
|
|
1297
|
+
}
|
|
1206
1298
|
const commands = collectCommands(controllerClasses, logger);
|
|
1207
1299
|
if (!commands) return false;
|
|
1208
1300
|
const targets = planTargets(commands, {
|
|
@@ -1219,18 +1311,26 @@ function logRegistered(target, logger) {
|
|
|
1219
1311
|
let succeeded = !guildsEmpty;
|
|
1220
1312
|
let sent = false;
|
|
1221
1313
|
for (const target of targets){
|
|
1222
|
-
const file =
|
|
1314
|
+
const file = cacheFile(applicationId, target.scope);
|
|
1223
1315
|
const hash = hashOf(target.commands);
|
|
1224
|
-
|
|
1316
|
+
const route = routeFor(applicationId, target.scope);
|
|
1317
|
+
// Unchanged since development last sent it, and still what Discord holds there
|
|
1318
|
+
const record = development && !force ? readRecord(file, logger) : undefined;
|
|
1319
|
+
if (record?.hash === hash && await stillAsSent(rest, route, record, target.scope, logger)) {
|
|
1225
1320
|
logger.log(`Commands ${describeScope(target.scope)} are unchanged; not registering them again.`);
|
|
1226
1321
|
continue;
|
|
1227
1322
|
}
|
|
1228
1323
|
try {
|
|
1229
|
-
await rest.put(
|
|
1324
|
+
const answered = await rest.put(route, {
|
|
1230
1325
|
body: target.commands.map(({ body })=>body)
|
|
1231
1326
|
});
|
|
1232
1327
|
sent = true;
|
|
1233
|
-
|
|
1328
|
+
// Outside development the record would be stale, so the next development start sends again
|
|
1329
|
+
if (development) writeRecord(file, {
|
|
1330
|
+
hash,
|
|
1331
|
+
versions: versionsOf(answered)
|
|
1332
|
+
}, logger);
|
|
1333
|
+
else forgetRecord(file, logger);
|
|
1234
1334
|
logRegistered(target, logger);
|
|
1235
1335
|
} catch (error) {
|
|
1236
1336
|
succeeded = false;
|
|
@@ -1259,13 +1359,17 @@ function logRegistered(target, logger) {
|
|
|
1259
1359
|
if (!Array.isArray(existing) || existing.length === 0) continue;
|
|
1260
1360
|
const names = existing.map(({ name })=>name).join(', ');
|
|
1261
1361
|
if (!config?.clearOther || keptBecause) {
|
|
1262
|
-
|
|
1362
|
+
// Advising clearOther when it would not apply sends the reader to a setting that changes nothing
|
|
1363
|
+
const advice = !keptBecause ? 'Set commands.clearOther to remove them.' : config?.clearOther ? `clearOther is on, but they are not removed ${keptBecause}.` : `They are not removed ${keptBecause}, even with commands.clearOther.`;
|
|
1364
|
+
logger.warn(`${existing.length} command(s) are still registered ${describeScope(scope)} (${names}), which this ` + `configuration does not register to, so Discord keeps showing them there. ${advice}`);
|
|
1263
1365
|
continue;
|
|
1264
1366
|
}
|
|
1265
1367
|
try {
|
|
1266
1368
|
await rest.put(routeFor(applicationId, scope), {
|
|
1267
1369
|
body: []
|
|
1268
1370
|
});
|
|
1371
|
+
// What development last sent there is gone, so its next start sends again
|
|
1372
|
+
forgetRecord(cacheFile(applicationId, scope), logger);
|
|
1269
1373
|
logger.log(`Removed ${existing.length} command(s) left registered ${describeScope(scope)}: ${names}`);
|
|
1270
1374
|
} catch (error) {
|
|
1271
1375
|
logger.error(`Error removing the commands left registered ${describeScope(scope)}:`, error);
|
|
@@ -1581,7 +1685,7 @@ function isInterceptorWithParams(entry) {
|
|
|
1581
1685
|
// Every filter reaching here was checked for @Catch at startup, so its types are always recorded
|
|
1582
1686
|
const types = Reflect.getOwnMetadata(controller_decorator.META.catchTypes, filterClass(entry));
|
|
1583
1687
|
// Only classes match: anything else given to @Catch, warned of as it applied, matches no error
|
|
1584
|
-
if (types.length === 0 || types.some((type)=>controller_decorator.
|
|
1688
|
+
if (types.length === 0 || types.some((type)=>controller_decorator.isConstructible(type) && error instanceof type)) return entry;
|
|
1585
1689
|
}
|
|
1586
1690
|
}
|
|
1587
1691
|
return undefined;
|
|
@@ -1686,7 +1790,7 @@ const logger$1 = new controller_decorator.Logger('Observer');
|
|
|
1686
1790
|
/** Reports each entry that is not an `@Observer` class, naming where it was listed. */ function assertObservers(where, observers) {
|
|
1687
1791
|
for (const observer of observers){
|
|
1688
1792
|
if (typeof observer !== 'function' || !Reflect.getMetadata(controller_decorator.META.observerClass, observer)) {
|
|
1689
|
-
const name = typeof observer === 'function' ?
|
|
1793
|
+
const name = controller_decorator.isConstructor(observer) ? observer.name || 'an anonymous class' : typeof observer === 'function' ? 'a function' : String(observer);
|
|
1690
1794
|
controller_decorator.startupError(new Error(`${where} takes classes decorated with @Observer(), not ${name}.`));
|
|
1691
1795
|
continue;
|
|
1692
1796
|
}
|
|
@@ -2229,7 +2333,9 @@ const logger = new controller_decorator.Logger('ExceptionFilter');
|
|
|
2229
2333
|
denial
|
|
2230
2334
|
})) {
|
|
2231
2335
|
outcome = 'denied';
|
|
2232
|
-
|
|
2336
|
+
// Without @Defer too, so a denied component is acknowledged rather than left to fail
|
|
2337
|
+
const denied = response ?? (type === 'interaction' && first?.isRepliable?.() ? controller_decorator.responseOf(first) : undefined);
|
|
2338
|
+
await denied?.abandon();
|
|
2233
2339
|
return {
|
|
2234
2340
|
ran: false
|
|
2235
2341
|
};
|
|
@@ -2285,14 +2391,18 @@ const logger = new controller_decorator.Logger('ExceptionFilter');
|
|
|
2285
2391
|
};
|
|
2286
2392
|
} finally{
|
|
2287
2393
|
await response?.release();
|
|
2288
|
-
if (options.onUnanswered && (ran || returnedEarly) && outcome === 'ran' && type === 'interaction') {
|
|
2394
|
+
if (options.onUnanswered && (ran || returnedEarly) && outcome === 'ran' && (type === 'interaction' || type === 'autocomplete')) {
|
|
2289
2395
|
const phase = responsePhaseOf(contextOf());
|
|
2290
2396
|
// The handler is to blame only once it has finished; until then, the interceptor that ended the call is
|
|
2291
|
-
const
|
|
2397
|
+
const returnedBy = !finished && returnedEarly ? {
|
|
2292
2398
|
interceptor: returnedEarly,
|
|
2293
2399
|
handlerStarted: ran
|
|
2294
2400
|
} : undefined;
|
|
2295
|
-
if (phase === 'unanswered' || phase === 'deferred') options.onUnanswered(
|
|
2401
|
+
if (phase === 'unanswered' || phase === 'deferred') options.onUnanswered({
|
|
2402
|
+
type,
|
|
2403
|
+
phase,
|
|
2404
|
+
returnedBy
|
|
2405
|
+
});
|
|
2296
2406
|
}
|
|
2297
2407
|
if (options.awaitObservers) await starting;
|
|
2298
2408
|
// After the answer and the release, so the duration covers the whole call
|
|
@@ -4087,22 +4197,48 @@ const sortedEntries = (entries)=>entries.sort((a, b)=>a.usage < b.usage ? -1 : a
|
|
|
4087
4197
|
scope
|
|
4088
4198
|
].filter((line)=>line !== undefined).join('\n');
|
|
4089
4199
|
}
|
|
4090
|
-
|
|
4200
|
+
// Built on first use, for a line longer than a message
|
|
4201
|
+
let graphemes;
|
|
4202
|
+
/**
|
|
4203
|
+
* Where a line longer than one message is cut: after the last character a reader sees as one, such as an emoji, a flag
|
|
4204
|
+
* or a letter with its accent, that still fits. A single such character longer than a message is cut between code points.
|
|
4205
|
+
*/ function fittingCut(piece) {
|
|
4206
|
+
let cut = 0;
|
|
4207
|
+
graphemes ?? (graphemes = new Intl.Segmenter(undefined, {
|
|
4208
|
+
granularity: 'grapheme'
|
|
4209
|
+
}));
|
|
4210
|
+
for (const { index, segment } of graphemes.segment(piece)){
|
|
4211
|
+
if (index + segment.length > MAX_REPLY_LENGTH) break;
|
|
4212
|
+
cut = index + segment.length;
|
|
4213
|
+
}
|
|
4214
|
+
if (cut > 0) return cut;
|
|
4215
|
+
const high = piece.charCodeAt(MAX_REPLY_LENGTH - 1);
|
|
4216
|
+
return high >= 0xd800 && high <= 0xdbff ? MAX_REPLY_LENGTH - 1 : MAX_REPLY_LENGTH;
|
|
4217
|
+
}
|
|
4218
|
+
/**
|
|
4219
|
+
* Splits a reply into Discord-sized messages at line breaks, a line longer than one message cut where it must be. A
|
|
4220
|
+
* message of only whitespace, which Discord refuses as empty, is left out.
|
|
4221
|
+
*/ function splitReply(text) {
|
|
4091
4222
|
const chunks = [];
|
|
4223
|
+
const push = (chunk)=>{
|
|
4224
|
+
if (chunk.trim() !== '') chunks.push(chunk);
|
|
4225
|
+
};
|
|
4092
4226
|
let current = '';
|
|
4093
4227
|
for (const line of text.split('\n')){
|
|
4094
|
-
for(let piece = line;;
|
|
4095
|
-
const
|
|
4228
|
+
for(let piece = line;;){
|
|
4229
|
+
const cut = piece.length <= MAX_REPLY_LENGTH ? piece.length : fittingCut(piece);
|
|
4230
|
+
const part = piece.slice(0, cut);
|
|
4096
4231
|
const joined = current ? `${current}\n${part}` : part;
|
|
4097
4232
|
if (joined.length <= MAX_REPLY_LENGTH) current = joined;
|
|
4098
4233
|
else {
|
|
4099
|
-
|
|
4234
|
+
push(current);
|
|
4100
4235
|
current = part;
|
|
4101
4236
|
}
|
|
4102
|
-
if (piece.length
|
|
4237
|
+
if (cut === piece.length) break;
|
|
4238
|
+
piece = piece.slice(cut);
|
|
4103
4239
|
}
|
|
4104
4240
|
}
|
|
4105
|
-
|
|
4241
|
+
push(current);
|
|
4106
4242
|
return chunks;
|
|
4107
4243
|
}
|
|
4108
4244
|
|
|
@@ -4644,7 +4780,7 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
4644
4780
|
controller_decorator.startupError(new Error(`${where}: the provider for ${name} needs exactly one of useValue, useClass and useFactory.`));
|
|
4645
4781
|
continue;
|
|
4646
4782
|
}
|
|
4647
|
-
if ('useClass' in entry &&
|
|
4783
|
+
if ('useClass' in entry && !controller_decorator.isConstructor(entry.useClass)) {
|
|
4648
4784
|
controller_decorator.startupError(new Error(`${where}: the provider for ${name} has a useClass that is not a class.`));
|
|
4649
4785
|
continue;
|
|
4650
4786
|
}
|
|
@@ -4721,6 +4857,15 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
4721
4857
|
});
|
|
4722
4858
|
for (const dependency of inject)if (isAppClassToken(dependency)) bindClass(dependency);
|
|
4723
4859
|
}
|
|
4860
|
+
/**
|
|
4861
|
+
* Reports each service listed by a string or symbol token that no provider provides, which would otherwise be bound
|
|
4862
|
+
* as if it were a class.
|
|
4863
|
+
*/ function assertProvidedServices(appName, services, providers, where) {
|
|
4864
|
+
for (const service of services){
|
|
4865
|
+
if (typeof service === 'function' || providers.has(service)) continue;
|
|
4866
|
+
controller_decorator.startupError(new Error(`${appName}: @MeoCord({ services }) lists ${tokenName(service)}, which nothing provides: add a provider for it to ${where}.`));
|
|
4867
|
+
}
|
|
4868
|
+
}
|
|
4724
4869
|
/**
|
|
4725
4870
|
* Reports a class or a factory that asks for a string or symbol token nothing binds, naming both, rather than failing
|
|
4726
4871
|
* when the class is first resolved. A class token is bound on demand, so it is never missing.
|
|
@@ -4854,15 +4999,20 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
4854
4999
|
}
|
|
4855
5000
|
/**
|
|
4856
5001
|
* Runs every factory in `order`, awaiting those that return a promise, so each value is made before
|
|
4857
|
-
* anything that injects it is resolved. Rejects with {@link providerFailure} for the first that fails.
|
|
4858
|
-
|
|
5002
|
+
* anything that injects it is resolved. Rejects with {@link providerFailure} for the first that fails. With `control`,
|
|
5003
|
+
* it makes no further value once the app is stopping.
|
|
5004
|
+
*/ async function resolveProviders(container, providers, order, control) {
|
|
4859
5005
|
for (const token of order){
|
|
4860
5006
|
const provider = providers.get(token);
|
|
4861
5007
|
if (!provider || !isFactoryProvider(provider)) continue;
|
|
5008
|
+
if (control?.stopped()) return;
|
|
5009
|
+
control?.pending(tokenName(token));
|
|
4862
5010
|
try {
|
|
4863
5011
|
await container.getAsync(token);
|
|
4864
5012
|
} catch (error) {
|
|
4865
5013
|
throw providerFailure(token, error);
|
|
5014
|
+
} finally{
|
|
5015
|
+
control?.pending(undefined);
|
|
4866
5016
|
}
|
|
4867
5017
|
}
|
|
4868
5018
|
}
|
|
@@ -4883,6 +5033,7 @@ exports.appPresenterOf = appPresenterOf;
|
|
|
4883
5033
|
exports.appStages = appStages;
|
|
4884
5034
|
exports.assertObservers = assertObservers;
|
|
4885
5035
|
exports.assertProvided = assertProvided;
|
|
5036
|
+
exports.assertProvidedServices = assertProvidedServices;
|
|
4886
5037
|
exports.assertStageEntries = assertStageEntries;
|
|
4887
5038
|
exports.assertStartupChecked = assertStartupChecked;
|
|
4888
5039
|
exports.assertTypedParameters = assertTypedParameters;
|
|
@@ -4941,6 +5092,7 @@ exports.prepareHandlerStages = prepareHandlerStages;
|
|
|
4941
5092
|
exports.prepareThemes = prepareThemes;
|
|
4942
5093
|
exports.providerMap = providerMap;
|
|
4943
5094
|
exports.reachableClasses = reachableClasses;
|
|
5095
|
+
exports.recordInstanceContainers = recordInstanceContainers;
|
|
4944
5096
|
exports.registerClientTheme = registerClientTheme;
|
|
4945
5097
|
exports.registerCommands = registerCommands;
|
|
4946
5098
|
exports.registrationKey = registrationKey;
|
package/dist/cjs/_shared/{sharded-cooldown-store-BdAUTdYs.cjs → sharded-cooldown-store-HTUb3Ii_.cjs}
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var node_crypto = require('node:crypto');
|
|
4
|
-
var controller_decorator = require('./controller.decorator-
|
|
4
|
+
var controller_decorator = require('./controller.decorator-U2NBWcoU.cjs');
|
|
5
5
|
|
|
6
6
|
/** The `fatal` code of a shard whose app MeoCord refused, which every shard would refuse alike. */ const REFUSED_CODE = 'Refused';
|
|
7
7
|
/** Whether an IPC message is one of MeoCord's own, rather than one of discord.js's. */ function isShardMessage(message) {
|
|
@@ -9,7 +9,10 @@ var controller_decorator = require('./controller.decorator-CRn9kWob.cjs');
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
/** Set by discord.js in every process its ShardingManager spawns. */ const SHARDING_MANAGER_ENV = 'SHARDING_MANAGER';
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Whether a process with `env` was spawned as a shard by a sharding manager. Read before anything builds a
|
|
14
|
+
* `ShardingManager`, whose constructor writes the variable into its own process too.
|
|
15
|
+
*/ function isShardProcess(env) {
|
|
13
16
|
return env[SHARDING_MANAGER_ENV] !== undefined;
|
|
14
17
|
}
|
|
15
18
|
|
|
@@ -21,10 +24,10 @@ var controller_decorator = require('./controller.decorator-CRn9kWob.cjs');
|
|
|
21
24
|
return env.NODE_ENV !== 'development' || config.sharding.development === true;
|
|
22
25
|
}
|
|
23
26
|
/**
|
|
24
|
-
* The role of this process: a shard when a manager spawned it, the manager when process sharding is
|
|
25
|
-
* and otherwise a single process running every shard it has.
|
|
26
|
-
*/ function shardingRole(config, env = process.env) {
|
|
27
|
-
if (
|
|
27
|
+
* The role of this process: a shard when a manager spawned it, as `shard` says, the manager when process sharding is
|
|
28
|
+
* on, and otherwise a single process running every shard it has.
|
|
29
|
+
*/ function shardingRole(config, shard, env = process.env) {
|
|
30
|
+
if (shard) return 'shard';
|
|
28
31
|
return processShardingEnabled(config, env) ? 'manager' : 'single';
|
|
29
32
|
}
|
|
30
33
|
/**
|
|
@@ -68,7 +71,7 @@ var controller_decorator = require('./controller.decorator-CRn9kWob.cjs');
|
|
|
68
71
|
* never held forever. MeoCord itself fails the call after `cooldownStoreTimeoutMs`, a second by default.
|
|
69
72
|
*/ const SHARDED_COOLDOWN_ABANDON_MS = 30000;
|
|
70
73
|
/** A shard's channel to its manager, or none outside process sharding. */ function managerChannel() {
|
|
71
|
-
return isShardProcess() && typeof process.send === 'function' ? channelOver(process) : undefined;
|
|
74
|
+
return isShardProcess(process.env) && typeof process.send === 'function' ? channelOver(process) : undefined;
|
|
72
75
|
}
|
|
73
76
|
/**
|
|
74
77
|
* The channel over a process's IPC. A send on a closed channel does not throw: without a callback, Node raises an
|