@seedcord/core 0.11.0 → 0.12.0-next.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.
Files changed (54) hide show
  1. package/README.md +3 -3
  2. package/dist/Bus-VJw-QpoY.mjs +986 -0
  3. package/dist/Bus-VJw-QpoY.mjs.map +1 -0
  4. package/dist/{CoordinatedShutdown-H5YjZn8A.d.mts → CoordinatedShutdown-fPKLWjc5.d.mts} +13 -4
  5. package/dist/{CoordinatedStartup-CGnJrqcl.mjs → CoordinatedStartup-Zw_Lk7Zk.mjs} +22 -20
  6. package/dist/CoordinatedStartup-Zw_Lk7Zk.mjs.map +1 -0
  7. package/dist/Generated-ByC53_A2.d.mts +26 -0
  8. package/dist/HmrModuleHandler-Btgu8r96.mjs +165 -0
  9. package/dist/HmrModuleHandler-Btgu8r96.mjs.map +1 -0
  10. package/dist/{PluginHost-DB4j8unu.d.mts → PluginHost-DFnkIldj.d.mts} +43 -15
  11. package/dist/PluginHost-DTtxWRiL.mjs +552 -0
  12. package/dist/PluginHost-DTtxWRiL.mjs.map +1 -0
  13. package/dist/{PluginLifecycle-AhQ46NIr.mjs → PluginLifecycle-hx5zUc-P.mjs} +4 -4
  14. package/dist/PluginLifecycle-hx5zUc-P.mjs.map +1 -0
  15. package/dist/{ServerHost-DYeh0KTk.d.mts → ServerHost-B5o3ScCF.d.mts} +7 -6
  16. package/dist/{ServerHost-CtmAhEXn.mjs → ServerHost-CDaEDR4C.mjs} +17 -6
  17. package/dist/{ServerHost-CtmAhEXn.mjs.map → ServerHost-CDaEDR4C.mjs.map} +1 -1
  18. package/dist/bindViteEnv-NFzvdqlT.mjs +32 -0
  19. package/dist/{bindViteEnv-DWq5rftJ.mjs.map → bindViteEnv-NFzvdqlT.mjs.map} +1 -1
  20. package/dist/env-internal.index.mjs +1 -1
  21. package/dist/hmr.index.d.mts +20 -20
  22. package/dist/hmr.index.mjs +1 -1
  23. package/dist/{index-Du71Xx69.d.mts → index-DGHng1IN.d.mts} +115 -201
  24. package/dist/index.d.mts +321 -74
  25. package/dist/index.mjs +323 -102
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/internal.index.d.mts +223 -83
  28. package/dist/internal.index.mjs +573 -74
  29. package/dist/internal.index.mjs.map +1 -1
  30. package/dist/node-internal.index.d.mts +8 -4
  31. package/dist/node-internal.index.mjs +19 -6
  32. package/dist/node-internal.index.mjs.map +1 -1
  33. package/dist/node.index.d.mts +3 -3
  34. package/dist/node.index.mjs +22 -28
  35. package/dist/node.index.mjs.map +1 -1
  36. package/dist/{phases-FTM69J3a.d.mts → phases-C2QCSqRu.d.mts} +164 -55
  37. package/dist/plugin.index.d.mts +3 -2
  38. package/dist/plugin.index.mjs +1 -1
  39. package/dist/subscribers-CGuVjbrg.mjs +982 -0
  40. package/dist/subscribers-CGuVjbrg.mjs.map +1 -0
  41. package/dist/{bindViteEnv-DWq5rftJ.mjs → usingCtx-Cf2cZi96.mjs} +1 -31
  42. package/package.json +14 -11
  43. package/dist/CoordinatedStartup-CGnJrqcl.mjs.map +0 -1
  44. package/dist/HmrModuleHandler-8ZC-UuZh.mjs +0 -162
  45. package/dist/HmrModuleHandler-8ZC-UuZh.mjs.map +0 -1
  46. package/dist/PluginHost-B8kxLOeb.mjs +0 -245
  47. package/dist/PluginHost-B8kxLOeb.mjs.map +0 -1
  48. package/dist/PluginLifecycle-AhQ46NIr.mjs.map +0 -1
  49. package/dist/phases-BauKJOTT.mjs +0 -31
  50. package/dist/phases-BauKJOTT.mjs.map +0 -1
  51. package/dist/publishDefault-D3dAyzZ9.mjs +0 -7
  52. package/dist/publishDefault-D3dAyzZ9.mjs.map +0 -1
  53. package/dist/subscribers-Ce3SWy23.mjs +0 -1913
  54. package/dist/subscribers-Ce3SWy23.mjs.map +0 -1
@@ -0,0 +1,982 @@
1
+ import { A as InteractionRouteKeys, C as CommandMetadataKey, D as InteractionKind, N as BuilderComponent, O as InteractionMetadataKey, S as assertUniqueKeys, k as InteractionMiddlewareMetadataKey, x as PublishDefault } from "./Bus-VJw-QpoY.mjs";
2
+ import "reflect-metadata";
3
+ import { CustomId, decodeFor } from "@seedcord/custom-id";
4
+ import { TextDisplayBuilder } from "@discordjs/builders";
5
+ import { ApplicationCommandType, Routes } from "discord-api-types/v10";
6
+ import { SeedcordErrorCode, paint } from "@seedcord/errors";
7
+ import { SeedcordError, SeedcordTypeError } from "@seedcord/errors/internal";
8
+ import { Logger } from "@seedcord/logger";
9
+ import { asError, isStringArray, pluralize, routeLeavesOf } from "@seedcord/utils/internal";
10
+ import { toEpochSeconds } from "@seedcord/utils";
11
+
12
+ //#region src/stops/NoticeCard.ts
13
+ var NoticeCard = class extends BuilderComponent {
14
+ constructor(description, title = "Cannot Proceed") {
15
+ super("container");
16
+ this.instance.addTextDisplayComponents(new TextDisplayBuilder().setContent(`### ${title}\n${description}`));
17
+ }
18
+ };
19
+
20
+ //#endregion
21
+ //#region src/stops/Notice.ts
22
+ /**
23
+ * Base class for a user-facing refusal or a reported fault.
24
+ *
25
+ * Throw a `Notice` to stop a handler and reply to the user. The framework catches it at the controller
26
+ * boundary and renders {@link Notice.render}, which always decides what the user sees. With `report`
27
+ * false that render is all that happens. With `report` true the framework also logs the fault and
28
+ * publishes it to the `handledException` bus. A raw, non-Notice throw shows the generic message.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * import { Notice, BuilderComponent, type RenderContext, type ReplyResponse } from '@seedcord/gateway';
33
+ * import { TextDisplayBuilder } from '@discordjs/builders';
34
+ *
35
+ * // reading `.component` applies the configured bot color to the container accent
36
+ * class TooPoorCard extends BuilderComponent<'container'> {
37
+ * constructor(balance: number) {
38
+ * super('container');
39
+ * this.instance.addTextDisplayComponents(
40
+ * new TextDisplayBuilder().setContent(`### Insufficient balance\nYou need more than ${balance} coins.`)
41
+ * );
42
+ * }
43
+ * }
44
+ *
45
+ * class TooPoor extends Notice {
46
+ * constructor(private readonly balance: number) {
47
+ * super(`balance ${balance} is below the cost`);
48
+ * }
49
+ *
50
+ * override render(_ctx: RenderContext): ReplyResponse {
51
+ * return { components: [new TooPoorCard(this.balance).component] };
52
+ * }
53
+ * }
54
+ *
55
+ * // in a handler, throwing stops the handler and replies with render(ctx)
56
+ * if (wallet.balance < cost) throw new TooPoor(wallet.balance);
57
+ * ```
58
+ */
59
+ var Notice = class extends Error {
60
+ /**
61
+ * Whether this denial is a reported fault. True also logs it and publishes it to the `handledException`
62
+ * bus. The user always sees {@link Notice.render} either way.
63
+ *
64
+ * @defaultValue `false`
65
+ */
66
+ report = false;
67
+ /**
68
+ * Whether the reply is ephemeral, seen only by the invoking user. Set it false for a refusal the
69
+ * whole channel should see.
70
+ *
71
+ * @defaultValue `true`
72
+ */
73
+ ephemeral = true;
74
+ /**
75
+ * A short one-line reason. When every gate in an {@link or} refuses and each one sets this, `or`
76
+ * throws a refusal listing them under `You need to meet at least one of these:`. Write a verb phrase that
77
+ * finishes that line, for example, "be the bot owner".
78
+ */
79
+ summary;
80
+ constructor(message, options) {
81
+ super(message, options);
82
+ this.name = new.target.name;
83
+ Error.captureStackTrace(this, this.constructor);
84
+ }
85
+ /**
86
+ * Builds what the user sees. Called fresh each time the denial is shown, so the builders are new
87
+ * and the bot color resolves at render time. By default it returns seedcord's refusal card with the message as
88
+ * its text. Override it to show your own components.
89
+ */
90
+ render(_ctx) {
91
+ return { components: [new NoticeCard(this.message).component] };
92
+ }
93
+ };
94
+
95
+ //#endregion
96
+ //#region src/commands/contextMenuLeaves.ts
97
+ function contextMenuLeaves(commands) {
98
+ const user = /* @__PURE__ */ new Set();
99
+ const message = /* @__PURE__ */ new Set();
100
+ for (const command of commands) if (command.type === ApplicationCommandType.User) user.add(command.name);
101
+ else if (command.type === ApplicationCommandType.Message) message.add(command.name);
102
+ return {
103
+ user,
104
+ message
105
+ };
106
+ }
107
+
108
+ //#endregion
109
+ //#region src/commands/scopes.ts
110
+ const GLOBAL_ID = "global";
111
+ const allOf$1 = new Intl.ListFormat("en", { type: "conjunction" });
112
+ function commandsRoute(applicationId, scope) {
113
+ return scope === "global" ? Routes.applicationCommands(applicationId) : Routes.applicationGuildCommands(applicationId, scope);
114
+ }
115
+ function globalFirst(scopes) {
116
+ return [...scopes].toSorted(([a], [b]) => Number(b === GLOBAL_ID) - Number(a === GLOBAL_ID));
117
+ }
118
+ function scopeIdsOf(target) {
119
+ return target.scope === "global" ? [GLOBAL_ID] : target.guilds;
120
+ }
121
+ function skippedNote(skipped, guildLabel) {
122
+ const guilds = skipped.filter((scope) => scope !== GLOBAL_ID).map(guildLabel);
123
+ const parts = [...skipped.includes("global") ? ["the global commands"] : [], ...guilds.length > 0 ? [`the commands in ${allOf$1.format(guilds)}`] : []];
124
+ return `Skipped ${allOf$1.format(parts)} because they match the last deploy.`;
125
+ }
126
+
127
+ //#endregion
128
+ //#region src/miscellaneous/guarded.ts
129
+ function accessorStore() {
130
+ return Object.create(null);
131
+ }
132
+ function clearStore(store) {
133
+ for (const key of Object.keys(store)) Reflect.deleteProperty(store, key);
134
+ }
135
+ function guardedAccessor(accessor, storage) {
136
+ return new Proxy(storage, { get(target, key, receiver) {
137
+ if (typeof key !== "string" || Object.hasOwn(target, key)) return Reflect.get(target, key, receiver);
138
+ throw new SeedcordError(SeedcordErrorCode.CoreAccessorUnresolved, [accessor, key]);
139
+ } });
140
+ }
141
+
142
+ //#endregion
143
+ //#region src/commands/linkCommands.ts
144
+ const MULTI_GUILD = Symbol("seedcord:commands:multi-guild");
145
+ function commandRefOf({ name, type }) {
146
+ return {
147
+ name,
148
+ type: type ?? ApplicationCommandType.ChatInput
149
+ };
150
+ }
151
+ function commandKey(command) {
152
+ const { type, name } = commandRefOf(command);
153
+ return `${type}:${name}`;
154
+ }
155
+ function indexById(commands) {
156
+ return new Map(commands.map((command) => [command.id, command]));
157
+ }
158
+ function isSlash(json) {
159
+ return commandRefOf(json).type === ApplicationCommandType.ChatInput;
160
+ }
161
+ function uniqueRefs(refs) {
162
+ return [...new Map(refs.map((ref) => [commandKey(ref), ref])).values()];
163
+ }
164
+ function deployedIds(deploy) {
165
+ const ids = /* @__PURE__ */ new Map();
166
+ for (const [scope, collection] of deploy) {
167
+ if (scope === "global") continue;
168
+ for (const command of collection.values()) {
169
+ const key = commandKey(command);
170
+ const existing = ids.get(key);
171
+ if (existing === void 0) ids.set(key, command.id);
172
+ else if (existing !== command.id) ids.set(key, MULTI_GUILD);
173
+ }
174
+ }
175
+ for (const command of deploy.get("global")?.values() ?? []) ids.set(commandKey(command), command.id);
176
+ return ids;
177
+ }
178
+ function allDeployed(deploy) {
179
+ return [...deploy.values()].flatMap((collection) => [...collection.values()]);
180
+ }
181
+ function mentionOf(route, id) {
182
+ const spaced = route.replaceAll("/", " ");
183
+ return id ? `</${spaced}:${id}>` : `/${spaced}`;
184
+ }
185
+ var CommandLinker = class {
186
+ deploy;
187
+ ids;
188
+ constructor(deploy) {
189
+ this.deploy = deploy;
190
+ this.ids = deployedIds(deploy);
191
+ }
192
+ link(all) {
193
+ const slash = all.filter(isSlash);
194
+ const leaves = contextMenuLeaves(all);
195
+ const inCode = uniqueRefs([
196
+ ...slash.map(commandRefOf),
197
+ ...[...leaves.user].map((name) => ({
198
+ name,
199
+ type: ApplicationCommandType.User
200
+ })),
201
+ ...[...leaves.message].map((name) => ({
202
+ name,
203
+ type: ApplicationCommandType.Message
204
+ }))
205
+ ]);
206
+ return {
207
+ commands: this.slashRoutes(slash),
208
+ contextMenus: {
209
+ user: this.menus(leaves.user, ApplicationCommandType.User),
210
+ message: this.menus(leaves.message, ApplicationCommandType.Message)
211
+ },
212
+ multiGuild: inCode.filter((ref) => this.ids.get(commandKey(ref)) === MULTI_GUILD),
213
+ notInCode: this.notInCode(inCode)
214
+ };
215
+ }
216
+ slashRoutes(slash) {
217
+ const commands = accessorStore();
218
+ for (const command of slash) {
219
+ const id = this.idOf(commandRefOf(command));
220
+ for (const { route, description } of routeLeavesOf(command)) commands[route] = {
221
+ mention: mentionOf(route, id),
222
+ ...id && { id },
223
+ route,
224
+ description
225
+ };
226
+ }
227
+ return commands;
228
+ }
229
+ menus(names, kind) {
230
+ const menus = accessorStore();
231
+ for (const name of names) {
232
+ const id = this.idOf({
233
+ name,
234
+ type: kind
235
+ });
236
+ menus[name] = {
237
+ ...id && { id },
238
+ name,
239
+ kind
240
+ };
241
+ }
242
+ return menus;
243
+ }
244
+ idOf(ref) {
245
+ const id = this.ids.get(commandKey(ref));
246
+ return id === MULTI_GUILD ? void 0 : id;
247
+ }
248
+ notInCode(inCode) {
249
+ const inCodeKeys = new Set(inCode.map(commandKey));
250
+ const extra = /* @__PURE__ */ new Map();
251
+ for (const command of allDeployed(this.deploy)) {
252
+ const key = commandKey(command);
253
+ if (!inCodeKeys.has(key)) extra.set(key, commandRefOf(command));
254
+ }
255
+ return [...extra.values()];
256
+ }
257
+ };
258
+ function linkCommands(deploy, all) {
259
+ return new CommandLinker(deploy).link(all);
260
+ }
261
+
262
+ //#endregion
263
+ //#region src/commands/CommandInjector.ts
264
+ const commandStorage = accessorStore();
265
+ const userMenuStorage = accessorStore();
266
+ const messageMenuStorage = accessorStore();
267
+ /**
268
+ * The bot's deployed slash commands, keyed by {@link SlashRegistry}. A server bot fills these after
269
+ * each deploy. An edge bot fills them on its first request, from the ids `seedcord build` wrote into
270
+ * the worker.
271
+ *
272
+ * @throws A **SeedcordError** when you read a command before the bot fills it.
273
+ *
274
+ * @example
275
+ * ```ts
276
+ * import { Commands } from '@seedcord/gateway';
277
+ *
278
+ * await this.reply(`Try ${Commands['config/set'].mention}`);
279
+ *
280
+ * // a help listing
281
+ * const lines = Object.values(Commands).map((c) => `${c.mention} ${c.description}`);
282
+ * ```
283
+ */
284
+ const Commands = guardedAccessor("Commands", commandStorage);
285
+ /**
286
+ * The bot's deployed context-menu commands. A server bot fills these after each deploy. An edge bot
287
+ * fills them on its first request, from the ids `seedcord build` wrote into the worker.
288
+ *
289
+ * @throws A **SeedcordError** when you read a command before the bot fills it.
290
+ *
291
+ * @example
292
+ * ```ts
293
+ * import { ContextMenus } from '@seedcord/gateway';
294
+ *
295
+ * const id = ContextMenus.user['View Profile'].id;
296
+ * ```
297
+ */
298
+ const ContextMenus = {
299
+ user: guardedAccessor("ContextMenus.user", userMenuStorage),
300
+ message: guardedAccessor("ContextMenus.message", messageMenuStorage)
301
+ };
302
+ function installLinks({ commands, contextMenus }) {
303
+ const pairs = [
304
+ [commandStorage, commands],
305
+ [userMenuStorage, contextMenus.user],
306
+ [messageMenuStorage, contextMenus.message]
307
+ ];
308
+ for (const [store, values] of pairs) {
309
+ clearStore(store);
310
+ Object.assign(store, values);
311
+ }
312
+ }
313
+ const KIND_LABELS = {
314
+ [ApplicationCommandType.ChatInput]: "slash command",
315
+ [ApplicationCommandType.User]: "user menu",
316
+ [ApplicationCommandType.Message]: "message menu",
317
+ [ApplicationCommandType.PrimaryEntryPoint]: "entry point command"
318
+ };
319
+ function multiGuildWarning({ name, type }) {
320
+ const consequence = type === ApplicationCommandType.ChatInput ? "so its mention stays plain text (not clickable)" : "so its id stays unset";
321
+ return `${paint.sky.bold(name)} is deployed to multiple guilds, ${consequence}.`;
322
+ }
323
+ function warnLinks(logger, { multiGuild, notInCode }) {
324
+ for (const ref of multiGuild) logger.warn(multiGuildWarning(ref));
325
+ for (const { name, type } of notInCode) logger.warn(`The ${KIND_LABELS[type]} ${paint.sky.bold(name)} is deployed on Discord with no command for it in your code.`);
326
+ }
327
+ /** @internal */
328
+ var CommandInjector = class {
329
+ logger = new Logger("Commands", { channel: "bot" });
330
+ inject(deploy, all) {
331
+ const linked = linkCommands(deploy, all.map((command) => command.toJSON()));
332
+ installLinks(linked);
333
+ warnLinks(this.logger, linked);
334
+ const routes = Object.values(linked.commands);
335
+ const clickable = routes.filter((command) => command.id !== void 0).length;
336
+ this.logger.utils.summary("Linked commands", {
337
+ clickable,
338
+ plain: routes.length - clickable,
339
+ "context menus": Object.keys(linked.contextMenus.user).length + Object.keys(linked.contextMenus.message).length
340
+ });
341
+ }
342
+ };
343
+
344
+ //#endregion
345
+ //#region src/decorators/Command.ts
346
+ const ConfigGuilds = "config";
347
+ function RegisterCommand(scope = "config", guilds = []) {
348
+ return (ctor) => {
349
+ const existingMeta = Reflect.getOwnMetadata(CommandMetadataKey, ctor);
350
+ if (existingMeta) throw new SeedcordTypeError(SeedcordErrorCode.DecoratorCommandAlreadyRegistered, [
351
+ ctor.name,
352
+ decoratorCall(existingMeta.scope),
353
+ decoratorCall(scope)
354
+ ]);
355
+ if (scope === "global" && guilds.length > 0) throw new SeedcordTypeError(SeedcordErrorCode.DecoratorCommandGlobalWithGuilds, [ctor.name]);
356
+ if (scope === "guild" && (!Array.isArray(guilds) || guilds.length === 0)) throw new SeedcordTypeError(SeedcordErrorCode.DecoratorCommandGuildWithoutGuilds, [ctor.name]);
357
+ const meta = scope === "guild" ? {
358
+ scope,
359
+ guilds
360
+ } : { scope };
361
+ Reflect.defineMetadata(CommandMetadataKey, meta, ctor);
362
+ };
363
+ }
364
+ function decoratorCall(scope) {
365
+ if (scope === "global") return "@RegisterCommand('global')";
366
+ if (scope === "guild") return "@RegisterCommand('guild', [...])";
367
+ return "@RegisterCommand()";
368
+ }
369
+
370
+ //#endregion
371
+ //#region src/customId/routing.ts
372
+ /**
373
+ * route decorators store a handler's customId defs here, so the component base can decode
374
+ * against them at runtime.
375
+ *
376
+ * @internal
377
+ */
378
+ const ComponentDefsKey = Symbol.for("seedcord:custom-id:component-defs");
379
+ /**
380
+ * throws `CustomIdHandlerRouteMissing` when the class has no stored defs. a drifted or corrupt wire
381
+ * throws `StaleCustomId` or `InvalidCustomId`.
382
+ *
383
+ * @internal
384
+ */
385
+ function decodeComponentRoute(handlerClass, wire) {
386
+ const defs = Reflect.getMetadata(ComponentDefsKey, handlerClass);
387
+ if (!defs) throw new SeedcordTypeError(SeedcordErrorCode.CustomIdHandlerRouteMissing, [handlerClass.name]);
388
+ return decodeFor(defs, wire);
389
+ }
390
+
391
+ //#endregion
392
+ //#region src/decorators/interactionRoutes.ts
393
+ function interactionRoutesOf(constructor) {
394
+ const pairs = [];
395
+ for (const route of Object.values(InteractionKind)) {
396
+ const meta = Reflect.getMetadata(InteractionRouteKeys[route], constructor);
397
+ if (isStringArray(meta)) pairs.push([route, meta]);
398
+ }
399
+ return pairs;
400
+ }
401
+ function routeDecoratorOf(kind) {
402
+ return `@${kind.charAt(0).toUpperCase()}${kind.slice(1)}Route`;
403
+ }
404
+ function storeInteractionRoute(route, routes, constructor, fix = "Remove the repeat.") {
405
+ const key = InteractionRouteKeys[route];
406
+ const saved = Reflect.getMetadata(key, constructor);
407
+ const existing = isStringArray(saved) ? saved : [];
408
+ const toStore = typeof routes === "string" ? [routes] : routes;
409
+ const own = Reflect.getOwnMetadata(key, constructor);
410
+ assertUniqueKeys(routeDecoratorOf(route), constructor, [...isStringArray(own) ? own : [], ...toStore], fix);
411
+ Reflect.defineMetadata(key, [...existing, ...toStore], constructor);
412
+ Reflect.defineMetadata(InteractionMetadataKey, true, constructor);
413
+ }
414
+ function storeComponentRoute(route, defs, constructor) {
415
+ storeInteractionRoute(route, defs.map((def) => def.prefix), constructor, "Remove the repeat, or give each customId its own prefix.");
416
+ Reflect.defineMetadata(ComponentDefsKey, defs, constructor);
417
+ }
418
+ function contextMenuRouteOf(kind) {
419
+ return kind === ApplicationCommandType.User ? "userContextMenu" : "messageContextMenu";
420
+ }
421
+
422
+ //#endregion
423
+ //#region src/decorators/middleware.ts
424
+ /**
425
+ * Registers an interaction middleware. It runs after the dispatcher builds the handler and before its
426
+ * gates, over that handler's reply surface.
427
+ *
428
+ * @param options - The kinds this middleware runs on and its ordering.
429
+ * @decorator
430
+ *
431
+ * @example
432
+ * ```ts
433
+ * \@RegisterInteractionMiddleware({ kinds: [InteractionKind.Button], priority: 10 })
434
+ * class Audit extends InteractionMiddleware<InteractionKind.Button> {
435
+ * async execute() {
436
+ * this.logger.info(this.dispatch.routeId);
437
+ * }
438
+ * }
439
+ * ```
440
+ *
441
+ * @throws A **SeedcordTypeError** when `priority` is not a finite number.
442
+ */
443
+ function RegisterInteractionMiddleware(options = {}) {
444
+ return function(ctor) {
445
+ const priority = Number(options.priority ?? 0);
446
+ if (!Number.isFinite(priority)) throw new SeedcordTypeError(SeedcordErrorCode.DecoratorInvalidMiddlewarePriority, [ctor.name, String(options.priority)]);
447
+ if (options.kinds?.length === 0) throw new SeedcordTypeError(SeedcordErrorCode.DecoratorEmptyMiddlewareFilter, [ctor.name, "kinds"]);
448
+ const metadata = {
449
+ priority,
450
+ ...options.kinds && { kinds: options.kinds }
451
+ };
452
+ Reflect.defineMetadata(InteractionMiddlewareMetadataKey, metadata, ctor);
453
+ };
454
+ }
455
+ /** @internal */
456
+ function interactionMiddlewareMetaOf(constructor) {
457
+ return Reflect.getMetadata(InteractionMiddlewareMetadataKey, constructor);
458
+ }
459
+
460
+ //#endregion
461
+ //#region src/reply/ackLegality.ts
462
+ const showModalReason = {
463
+ reason: "this interaction was already acknowledged",
464
+ alternative: "showModal() must be the initial response."
465
+ };
466
+ function deferReason(what) {
467
+ return {
468
+ reason: `this interaction was already acknowledged with ${what}`,
469
+ alternative: "A deferral can only be the initial response."
470
+ };
471
+ }
472
+ const ILLEGAL = {
473
+ reply: {
474
+ "deferred-reply": {
475
+ reason: "this interaction was already deferred",
476
+ alternative: "Use edit() to fill the deferred reply or followUp() for a new message."
477
+ },
478
+ "deferred-update": {
479
+ reason: "this interaction was already acknowledged with a deferred update",
480
+ alternative: "Use followUp() for a new message."
481
+ },
482
+ replied: {
483
+ reason: "this interaction was already replied to",
484
+ alternative: "Use followUp() for a new message or edit() to rewrite the reply."
485
+ }
486
+ },
487
+ defer: {
488
+ "deferred-reply": deferReason("a deferred reply"),
489
+ "deferred-update": deferReason("a deferred update"),
490
+ replied: deferReason("a reply")
491
+ },
492
+ deferUpdate: {
493
+ "deferred-reply": deferReason("a deferred reply"),
494
+ "deferred-update": deferReason("a deferred update"),
495
+ replied: deferReason("a reply")
496
+ },
497
+ update: {
498
+ "deferred-reply": {
499
+ reason: "this interaction was deferred as a reply",
500
+ alternative: "update() only rewrites a source message. Use edit() to fill the placeholder."
501
+ },
502
+ replied: {
503
+ reason: "this interaction was already replied to",
504
+ alternative: "update() rewrites a source message. Use edit() or followUp() instead."
505
+ }
506
+ },
507
+ followUp: { unacked: {
508
+ reason: "this interaction has not been acknowledged yet",
509
+ alternative: "Ack first with reply() or defer()."
510
+ } },
511
+ edit: { unacked: {
512
+ reason: "nothing has been sent yet",
513
+ alternative: "Send with reply() or defer() first. After that, edit() rewrites it."
514
+ } },
515
+ delete: { unacked: {
516
+ reason: "nothing has been sent yet",
517
+ alternative: "Acknowledge first with reply() or defer(). After that, delete() removes the reply."
518
+ } },
519
+ showModal: {
520
+ "deferred-reply": showModalReason,
521
+ "deferred-update": showModalReason,
522
+ replied: showModalReason
523
+ }
524
+ };
525
+ const SEND_TARGET = {
526
+ unacked: "reply",
527
+ "deferred-reply": "edit",
528
+ "deferred-update": "followUp",
529
+ replied: "followUp"
530
+ };
531
+ function checkAckLegality(method, state, routeId, ackedBy) {
532
+ if (method === "send") return;
533
+ const illegal = ILLEGAL[method][state];
534
+ if (!illegal) return;
535
+ throw new SeedcordError(SeedcordErrorCode.ReplyIllegalAckState, [
536
+ method,
537
+ illegal.reason,
538
+ illegal.alternative,
539
+ routeId
540
+ ], { cause: ackedBy });
541
+ }
542
+ function sendTarget(state) {
543
+ return SEND_TARGET[state];
544
+ }
545
+
546
+ //#endregion
547
+ //#region src/reply/AckTrace.ts
548
+ var AckTrace = class extends Error {
549
+ constructor(method) {
550
+ super(`${method}() acknowledged this interaction`);
551
+ this.name = "AckTrace";
552
+ }
553
+ };
554
+
555
+ //#endregion
556
+ //#region src/miscellaneous/lazyLogger.ts
557
+ function lazyLogger(label, options) {
558
+ let logger;
559
+ return () => logger ??= new Logger(label, options);
560
+ }
561
+
562
+ //#endregion
563
+ //#region src/reply/responseReport.ts
564
+ const logger = lazyLogger("Reply", { channel: "interactions" });
565
+ /** @internal */
566
+ function publishResponse(telemetry, report) {
567
+ const durationMs = performance.now() - report.startedAt;
568
+ const { routeId } = telemetry.dispatch;
569
+ const write = {
570
+ dispatchId: telemetry.dispatch.id,
571
+ routeId,
572
+ interactionId: telemetry.interactionId,
573
+ method: report.method,
574
+ durationMs
575
+ };
576
+ telemetry.bus[PublishDefault]("responseAttempted", report.outcome === "failed" ? {
577
+ ...write,
578
+ outcome: "failed",
579
+ messageId: null,
580
+ error: report.error
581
+ } : {
582
+ ...write,
583
+ outcome: "sent",
584
+ messageId: report.messageId
585
+ });
586
+ logger().trace(`${paint.sky.bold(routeId)} ${report.method} ${report.outcome} ${paint.mute("in")} ${Math.round(durationMs)}ms`);
587
+ }
588
+ async function reportedWrite(telemetry, method, write) {
589
+ const startedAt = performance.now();
590
+ const result = await attemptWrite(telemetry, method, startedAt, write);
591
+ publishResponse(telemetry, {
592
+ method,
593
+ startedAt,
594
+ outcome: "sent",
595
+ messageId: null
596
+ });
597
+ return result;
598
+ }
599
+ async function attemptWrite(telemetry, method, startedAt, write) {
600
+ try {
601
+ return await write();
602
+ } catch (caught) {
603
+ publishResponse(telemetry, {
604
+ method,
605
+ startedAt,
606
+ outcome: "failed",
607
+ error: asError(caught)
608
+ });
609
+ throw caught;
610
+ }
611
+ }
612
+
613
+ //#endregion
614
+ //#region src/reply/translateSerialization.ts
615
+ const allOf = new Intl.ListFormat("en", { type: "conjunction" });
616
+ const MAX_DEPTH = 8;
617
+ const OPERATOR_WORD = {
618
+ "<=": "at most",
619
+ ">=": "at least",
620
+ "<": "fewer than",
621
+ ">": "more than"
622
+ };
623
+ const KIND_WORD = {
624
+ string: "a string",
625
+ number: "a number",
626
+ boolean: "a boolean",
627
+ bigint: "a bigint",
628
+ array: "a list",
629
+ tuple: "a list",
630
+ date: "a date",
631
+ object: "an object"
632
+ };
633
+ function asRecord(value) {
634
+ return typeof value === "object" && value !== null ? value : null;
635
+ }
636
+ function firstLine(message) {
637
+ if (typeof message !== "string") return "unknown validation failure";
638
+ return message.split("\n")[0] ?? message;
639
+ }
640
+ const GIVEN_PREVIEW_LIMIT = 40;
641
+ function gotPhrase(given) {
642
+ if (given === void 0) return "nothing";
643
+ if (given === null) return "null";
644
+ if (typeof given === "string") {
645
+ if (given.length === 0) return "an empty string";
646
+ if (given.length > GIVEN_PREVIEW_LIMIT) return `${given.length} characters`;
647
+ return JSON.stringify(given);
648
+ }
649
+ if (Array.isArray(given)) return given.length === 0 ? "an empty list" : `${given.length} items`;
650
+ if (typeof given === "number" || typeof given === "bigint" || typeof given === "boolean") return String(given);
651
+ if (typeof given === "object") return given.constructor?.name ?? "an object";
652
+ return typeof given;
653
+ }
654
+ function needPhrase(expected, given) {
655
+ const bounds = [...expected.matchAll(/expected(\.length)?\s*(<=|>=|<|>)\s*(\d+)/g)];
656
+ if (bounds.length === 0) return null;
657
+ const phrases = bounds.map(([, lengthPart, operator, limit]) => {
658
+ const word = OPERATOR_WORD[operator ?? ""] ?? operator ?? "";
659
+ if (!lengthPart) return `${word} ${limit}`;
660
+ const unit = typeof given === "string" ? "character" : "item";
661
+ return `${word} ${limit} ${pluralize(Number(limit), unit)}`;
662
+ });
663
+ return allOf.format(phrases);
664
+ }
665
+ function instanceName(expected) {
666
+ if (typeof expected === "function" && expected.name) return expected.name;
667
+ return "specific class instance";
668
+ }
669
+ function describeLeaf(record) {
670
+ if (typeof record.constraint === "string") {
671
+ const need = typeof record.expected === "string" ? needPhrase(record.expected, record.given) : null;
672
+ if (need) return `need ${need}, got ${gotPhrase(record.given)}`;
673
+ return `${firstLine(record.message)}${"given" in record ? ` (got ${gotPhrase(record.given)})` : ""}`;
674
+ }
675
+ if (typeof record.expected === "function") return `need a ${instanceName(record.expected)}, got ${gotPhrase(record.given)}`;
676
+ if (typeof record.validator === "string") {
677
+ const kind = KIND_WORD[record.validator.replace(/^s\./, "").replace(/\(.*$/, "")];
678
+ if (kind) return `need ${kind}, got ${gotPhrase(record.given)}`;
679
+ return `${firstLine(record.message)}, got ${gotPhrase(record.given)}`;
680
+ }
681
+ return firstLine(record.message);
682
+ }
683
+ function describeUnion(branches, depth) {
684
+ const records = [];
685
+ for (const branch of branches) {
686
+ const record = asRecord(branch);
687
+ if (record) records.push(record);
688
+ }
689
+ const bounded = records.find((record) => typeof record.constraint === "string");
690
+ if (bounded) return describeLeaf(bounded);
691
+ const names = [];
692
+ let allInstances = records.length > 0;
693
+ for (const record of records) if (typeof record.expected === "function") names.push(instanceName(record.expected));
694
+ else allInstances = false;
695
+ if (allInstances) return `need a ${[...new Set(names)].join(" or ")}, got ${gotPhrase(records[0]?.given)}`;
696
+ return detailOf(branches[0], depth + 1);
697
+ }
698
+ function detailOf(value, depth) {
699
+ const record = asRecord(value);
700
+ if (!record || depth >= MAX_DEPTH) return firstLine(record?.message);
701
+ if (Array.isArray(record.errors) && record.errors.length > 0) {
702
+ const errors = record.errors;
703
+ const [first] = errors;
704
+ if (Array.isArray(first)) {
705
+ const [key, inner] = first;
706
+ return `${String(key)}: ${detailOf(inner, depth + 1)}`;
707
+ }
708
+ return describeUnion(errors, depth);
709
+ }
710
+ return describeLeaf(record);
711
+ }
712
+ function translateSerializationError(error, componentClass, index, routeId) {
713
+ const detail = detailOf(error, 0);
714
+ return new SeedcordError(SeedcordErrorCode.ReplyComponentSerialization, [
715
+ componentClass,
716
+ index,
717
+ detail,
718
+ routeId
719
+ ], { cause: error });
720
+ }
721
+
722
+ //#endregion
723
+ //#region src/reply/serializeReply.ts
724
+ function serializeReply(response, routeId) {
725
+ if (typeof response === "string") try {
726
+ return { components: [new TextDisplayBuilder().setContent(response).toJSON()] };
727
+ } catch (error) {
728
+ throw translateSerializationError(error, "TextDisplayBuilder", 0, routeId);
729
+ }
730
+ return {
731
+ components: response.components.map((component, index) => {
732
+ try {
733
+ return "toJSON" in component ? component.toJSON() : component;
734
+ } catch (error) {
735
+ const name = component?.constructor?.name;
736
+ throw translateSerializationError(error, !name || name === "Object" ? "component" : name, index, routeId);
737
+ }
738
+ }),
739
+ ...response.allowedMentions && { allowedMentions: response.allowedMentions },
740
+ ...response.files && { files: response.files }
741
+ };
742
+ }
743
+
744
+ //#endregion
745
+ //#region src/notices/index.ts
746
+ var NotOwner = class extends Notice {
747
+ constructor() {
748
+ super("Only the bot owner can use this.");
749
+ this.summary = "be the bot owner";
750
+ }
751
+ };
752
+ var NotInGuild = class extends Notice {
753
+ constructor() {
754
+ super("This can only be used in a server.");
755
+ this.summary = "be in a server";
756
+ }
757
+ };
758
+ var NotInDm = class extends Notice {
759
+ constructor() {
760
+ super("This can only be used in a direct message.");
761
+ this.summary = "be in a direct message";
762
+ }
763
+ };
764
+ function retryAt(resetAt) {
765
+ return `<t:${toEpochSeconds(resetAt)}:R>`;
766
+ }
767
+ var OnCooldown = class extends Notice {
768
+ resetAt;
769
+ constructor(resetAt) {
770
+ super(`You are doing that too fast. Try again ${retryAt(resetAt)}.`);
771
+ this.resetAt = resetAt;
772
+ this.summary = `try again ${retryAt(resetAt)}`;
773
+ }
774
+ };
775
+ var NotAllowed = class extends Notice {
776
+ constructor() {
777
+ super("not allowed");
778
+ }
779
+ render() {
780
+ return { components: [new NoticeCard("You are not allowed to use this command.").component] };
781
+ }
782
+ };
783
+ var NeedsAny = class extends Notice {
784
+ summaries;
785
+ constructor(summaries) {
786
+ super("not allowed");
787
+ this.summaries = [...new Set(summaries)];
788
+ }
789
+ render() {
790
+ const bullets = this.summaries.map((summary) => `• ${summary}`).join("\n");
791
+ return { components: [new NoticeCard(`You need to meet at least one of these:\n${bullets}`).component] };
792
+ }
793
+ };
794
+ var MissingRole = class extends Notice {
795
+ constructor(roleId) {
796
+ super(`You need the <@&${roleId}> role to use this.`);
797
+ this.summary = `hold the <@&${roleId}> role`;
798
+ }
799
+ };
800
+ var PermissionsUnresolved = class extends Notice {
801
+ constructor() {
802
+ super("Your server member data could not be resolved. Try again.");
803
+ }
804
+ };
805
+ var PermissionListNotice = class extends Notice {
806
+ permissions;
807
+ lead;
808
+ constructor(message, subject, permissions, wording) {
809
+ super(message);
810
+ this.permissions = permissions;
811
+ const [prefix, words] = subject === null ? ["", wording.caller] : [`${subject} `, wording.subject];
812
+ this.lead = `${prefix}${words.lead}`;
813
+ this.summary = `${prefix}${words.summary} ${permissions.join(", ")}`;
814
+ }
815
+ render() {
816
+ const bullets = this.permissions.map((perm) => `• ${perm}`).join("\n");
817
+ return { components: [new NoticeCard(`${this.lead}\n${bullets}`).component] };
818
+ }
819
+ };
820
+ var MissingPermissions = class extends PermissionListNotice {
821
+ constructor(subject, permissions) {
822
+ super("A required permission is missing.", subject, permissions, {
823
+ caller: {
824
+ lead: "You are missing the following permission entries:",
825
+ summary: "hold"
826
+ },
827
+ subject: {
828
+ lead: "is missing the following permission entries:",
829
+ summary: "needs"
830
+ }
831
+ });
832
+ }
833
+ };
834
+ var HasPresentPermissions = class extends PermissionListNotice {
835
+ constructor(subject, permissions) {
836
+ super("A refused permission is held.", subject, permissions, {
837
+ caller: {
838
+ lead: "You hold permissions that aren't allowed here:",
839
+ summary: "not hold"
840
+ },
841
+ subject: {
842
+ lead: "holds permissions that aren't allowed here:",
843
+ summary: "must not hold"
844
+ }
845
+ });
846
+ }
847
+ };
848
+
849
+ //#endregion
850
+ //#region src/stops/Fault.ts
851
+ /**
852
+ * A generic fault you throw after catching an error you do not have a specific message for.
853
+ *
854
+ * The user sees a fixed generic reply with the tracking uuid, never the cause. `report` defaults to
855
+ * `true`, so the framework logs it and publishes it to the `handledException` bus. Pass `report: false`
856
+ * to show the generic reply without the bus publish. The original error is stored as the standard
857
+ * `cause`, so the real stack reaches the webhook. When the user should see a real message for a fault,
858
+ * subclass {@link Notice} and write your own `render` instead.
859
+ *
860
+ * The framework also renders this for an unhandled throw, where it points the user at
861
+ * `ctx.developerUsername`.
862
+ *
863
+ * @example
864
+ * ```ts
865
+ * import { Fault } from '@seedcord/gateway';
866
+ *
867
+ * try {
868
+ * await db.write(record);
869
+ * } catch (cause) {
870
+ * // user sees the generic reply with the uuid, and the real error rides along as cause for the webhook
871
+ * throw new Fault({ cause });
872
+ *
873
+ * // pass report: false to show the same reply without publishing to the handledException bus
874
+ * // throw new Fault({ cause, report: false });
875
+ * }
876
+ * ```
877
+ */
878
+ var Fault = class extends Notice {
879
+ constructor(options) {
880
+ super("A fault occurred", options?.cause === void 0 ? void 0 : { cause: options.cause });
881
+ this.report = options?.report ?? true;
882
+ }
883
+ render(ctx) {
884
+ const contact = ctx.developerUsername ?? "the developer";
885
+ return { components: [new NoticeCard(`Something went wrong. Please reach out to ${contact} with a way to reproduce the error and the following:\n### UUID: \`${ctx.uuid}\``, "Error").component] };
886
+ }
887
+ };
888
+
889
+ //#endregion
890
+ //#region src/gates/effects.ts
891
+ const commitQueues = /* @__PURE__ */ new WeakMap();
892
+ function isEffectGate(gate) {
893
+ return typeof gate.commit === "function";
894
+ }
895
+ async function runCheck(gate, ctx) {
896
+ await gate.check(ctx);
897
+ if (!isEffectGate(gate)) return;
898
+ let queue = commitQueues.get(ctx);
899
+ if (!queue) {
900
+ queue = [];
901
+ commitQueues.set(ctx, queue);
902
+ }
903
+ queue.push(() => gate.commit(ctx));
904
+ }
905
+ async function runCommits(ctx) {
906
+ const queue = commitQueues.get(ctx);
907
+ if (!queue) return;
908
+ for (const commit of queue) await commit();
909
+ }
910
+ function discardCommits(ctx) {
911
+ commitQueues.delete(ctx);
912
+ }
913
+ function markCommits(ctx) {
914
+ return commitQueues.get(ctx)?.length ?? 0;
915
+ }
916
+ function rollbackCommits(ctx, mark) {
917
+ const queue = commitQueues.get(ctx);
918
+ if (queue && queue.length > mark) queue.length = mark;
919
+ }
920
+
921
+ //#endregion
922
+ //#region src/stops/Silence.ts
923
+ /**
924
+ * Throw to stop a handler with no reply and no report.
925
+ *
926
+ * The framework boundary catches `Silence` before {@link Notice}, makes zero Discord calls, and stops.
927
+ * Ideally you'd only throw this in `EventHandlers` (or `Gates` for those), because it doesn't make sense to
928
+ * leave the user with no reply for an interaction.
929
+ *
930
+ * @example
931
+ * ```ts
932
+ * import { Silence } from '@seedcord/gateway';
933
+ *
934
+ * // a helper under a guildMemberAdd handler, stopping when this guild configured nothing to send
935
+ * const template = await settings.welcome(member.guild.id);
936
+ * if (!template) throw new Silence('no welcome message set');
937
+ * ```
938
+ */
939
+ var Silence = class extends Error {
940
+ reason;
941
+ /**
942
+ * @param reason - Optional note written only to a debug log, never shown to the user or reported.
943
+ */
944
+ constructor(reason) {
945
+ super(reason ?? "Silence");
946
+ this.reason = reason;
947
+ Error.captureStackTrace(this, this.constructor);
948
+ }
949
+ };
950
+
951
+ //#endregion
952
+ //#region src/miscellaneous/ResolvedEmoji.ts
953
+ /**
954
+ * A resolved custom emoji. Renders as `<:name:id>` in message content, or `<a:name:id>` when animated,
955
+ * and passes straight to a builder's `setEmoji`.
956
+ */
957
+ var ResolvedEmoji = class {
958
+ name;
959
+ id;
960
+ animated;
961
+ constructor(name, id, animated = false) {
962
+ this.name = name;
963
+ this.id = id;
964
+ this.animated = animated;
965
+ }
966
+ toString() {
967
+ return `<${this.animated ? "a" : ""}:${this.name}:${this.id}>`;
968
+ }
969
+ };
970
+
971
+ //#endregion
972
+ //#region src/pagination/cursor.ts
973
+ const PAGE_MAX = 1e6;
974
+ const SLOT_MAX = 4;
975
+ /** Build the page cursor for a paginator. */
976
+ function pageCursor(prefix) {
977
+ return new CustomId(prefix).int("page", 0, PAGE_MAX).int("slot", 0, SLOT_MAX);
978
+ }
979
+
980
+ //#endregion
981
+ export { GLOBAL_ID as $, RegisterInteractionMiddleware as A, RegisterCommand as B, attemptWrite as C, AckTrace as D, lazyLogger as E, storeComponentRoute as F, warnLinks as G, Commands as H, storeInteractionRoute as I, indexById as J, commandKey as K, ComponentDefsKey as L, contextMenuRouteOf as M, interactionRoutesOf as N, checkAckLegality as O, routeDecoratorOf as P, guardedAccessor as Q, decodeComponentRoute as R, translateSerializationError as S, reportedWrite as T, ContextMenus as U, CommandInjector as V, installLinks as W, accessorStore as X, linkCommands as Y, clearStore as Z, NotInGuild as _, discardCommits as a, Notice as at, PermissionsUnresolved as b, runCheck as c, HasPresentPermissions as d, commandsRoute as et, MissingPermissions as f, NotInDm as g, NotAllowed as h, Silence as i, contextMenuLeaves as it, interactionMiddlewareMetaOf as j, sendTarget as k, runCommits as l, NeedsAny as m, pageCursor as n, scopeIdsOf as nt, markCommits as o, NoticeCard as ot, MissingRole as p, commandRefOf as q, ResolvedEmoji as r, skippedNote as rt, rollbackCommits as s, PAGE_MAX as t, globalFirst as tt, Fault as u, NotOwner as v, publishResponse as w, serializeReply as x, OnCooldown as y, ConfigGuilds as z };
982
+ //# sourceMappingURL=subscribers-CGuVjbrg.mjs.map