@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.
- package/README.md +3 -3
- package/dist/Bus-VJw-QpoY.mjs +986 -0
- package/dist/Bus-VJw-QpoY.mjs.map +1 -0
- package/dist/{CoordinatedShutdown-H5YjZn8A.d.mts → CoordinatedShutdown-fPKLWjc5.d.mts} +13 -4
- package/dist/{CoordinatedStartup-CGnJrqcl.mjs → CoordinatedStartup-Zw_Lk7Zk.mjs} +22 -20
- package/dist/CoordinatedStartup-Zw_Lk7Zk.mjs.map +1 -0
- package/dist/Generated-ByC53_A2.d.mts +26 -0
- package/dist/HmrModuleHandler-Btgu8r96.mjs +165 -0
- package/dist/HmrModuleHandler-Btgu8r96.mjs.map +1 -0
- package/dist/{PluginHost-DB4j8unu.d.mts → PluginHost-DFnkIldj.d.mts} +43 -15
- package/dist/PluginHost-DTtxWRiL.mjs +552 -0
- package/dist/PluginHost-DTtxWRiL.mjs.map +1 -0
- package/dist/{PluginLifecycle-AhQ46NIr.mjs → PluginLifecycle-hx5zUc-P.mjs} +4 -4
- package/dist/PluginLifecycle-hx5zUc-P.mjs.map +1 -0
- package/dist/{ServerHost-DYeh0KTk.d.mts → ServerHost-B5o3ScCF.d.mts} +7 -6
- package/dist/{ServerHost-CtmAhEXn.mjs → ServerHost-CDaEDR4C.mjs} +17 -6
- package/dist/{ServerHost-CtmAhEXn.mjs.map → ServerHost-CDaEDR4C.mjs.map} +1 -1
- package/dist/bindViteEnv-NFzvdqlT.mjs +32 -0
- package/dist/{bindViteEnv-DWq5rftJ.mjs.map → bindViteEnv-NFzvdqlT.mjs.map} +1 -1
- package/dist/env-internal.index.mjs +1 -1
- package/dist/hmr.index.d.mts +20 -20
- package/dist/hmr.index.mjs +1 -1
- package/dist/{index-Du71Xx69.d.mts → index-DGHng1IN.d.mts} +115 -201
- package/dist/index.d.mts +321 -74
- package/dist/index.mjs +323 -102
- package/dist/index.mjs.map +1 -1
- package/dist/internal.index.d.mts +223 -83
- package/dist/internal.index.mjs +573 -74
- package/dist/internal.index.mjs.map +1 -1
- package/dist/node-internal.index.d.mts +8 -4
- package/dist/node-internal.index.mjs +19 -6
- package/dist/node-internal.index.mjs.map +1 -1
- package/dist/node.index.d.mts +3 -3
- package/dist/node.index.mjs +22 -28
- package/dist/node.index.mjs.map +1 -1
- package/dist/{phases-FTM69J3a.d.mts → phases-C2QCSqRu.d.mts} +164 -55
- package/dist/plugin.index.d.mts +3 -2
- package/dist/plugin.index.mjs +1 -1
- package/dist/subscribers-CGuVjbrg.mjs +982 -0
- package/dist/subscribers-CGuVjbrg.mjs.map +1 -0
- package/dist/{bindViteEnv-DWq5rftJ.mjs → usingCtx-Cf2cZi96.mjs} +1 -31
- package/package.json +14 -11
- package/dist/CoordinatedStartup-CGnJrqcl.mjs.map +0 -1
- package/dist/HmrModuleHandler-8ZC-UuZh.mjs +0 -162
- package/dist/HmrModuleHandler-8ZC-UuZh.mjs.map +0 -1
- package/dist/PluginHost-B8kxLOeb.mjs +0 -245
- package/dist/PluginHost-B8kxLOeb.mjs.map +0 -1
- package/dist/PluginLifecycle-AhQ46NIr.mjs.map +0 -1
- package/dist/phases-BauKJOTT.mjs +0 -31
- package/dist/phases-BauKJOTT.mjs.map +0 -1
- package/dist/publishDefault-D3dAyzZ9.mjs +0 -7
- package/dist/publishDefault-D3dAyzZ9.mjs.map +0 -1
- package/dist/subscribers-Ce3SWy23.mjs +0 -1913
- package/dist/subscribers-Ce3SWy23.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as
|
|
1
|
+
import { D as InteractionKind, N as BuilderComponent, P as RowComponent, b as StartupPhase, d as WebhookLog, f as Subscriber, l as WebhookUrl, t as Bus, u as Subscribe, v as MemoryDriver, y as ShutdownPhase } from "./Bus-VJw-QpoY.mjs";
|
|
2
|
+
import { A as RegisterInteractionMiddleware, B as RegisterCommand, C as attemptWrite, D as AckTrace, F as storeComponentRoute, H as Commands, I as storeInteractionRoute, M as contextMenuRouteOf, O as checkAckLegality, S as translateSerializationError, U as ContextMenus, _ as NotInGuild, at as Notice, b as PermissionsUnresolved, c as runCheck, d as HasPresentPermissions, f as MissingPermissions, g as NotInDm, h as NotAllowed, i as Silence, k as sendTarget, m as NeedsAny, n as pageCursor, o as markCommits, ot as NoticeCard, p as MissingRole, r as ResolvedEmoji, s as rollbackCommits, t as PAGE_MAX, u as Fault, v as NotOwner, w as publishResponse, x as serializeReply, y as OnCooldown } from "./subscribers-CGuVjbrg.mjs";
|
|
3
3
|
import "reflect-metadata";
|
|
4
4
|
import { CustomId, decodeFor, prefixOf, setCustomIdErrors, setCustomIdErrors as setCustomIdErrors$1 } from "@seedcord/custom-id";
|
|
5
5
|
import { ActionRowBuilder, ButtonBuilder, TextDisplayBuilder } from "@discordjs/builders";
|
|
6
|
-
import { ApplicationCommandType, ButtonStyle } from "discord-api-types/v10";
|
|
6
|
+
import { ApplicationCommandType, ButtonStyle, PermissionFlagsBits } from "discord-api-types/v10";
|
|
7
7
|
import { SeedcordErrorCode } from "@seedcord/errors";
|
|
8
8
|
import { SeedcordError, SeedcordRangeError, SeedcordTypeError } from "@seedcord/errors/internal";
|
|
9
9
|
import { Logger } from "@seedcord/logger";
|
|
10
|
-
import { parseDuration } from "@seedcord/utils";
|
|
10
|
+
import { parseDuration, prettify } from "@seedcord/utils";
|
|
11
11
|
|
|
12
12
|
//#region src/customId/Errors.ts
|
|
13
13
|
/**
|
|
@@ -47,8 +47,8 @@ setCustomIdErrors$1({
|
|
|
47
47
|
/**
|
|
48
48
|
* Routes one or more slash commands to a `SlashHandler`.
|
|
49
49
|
*
|
|
50
|
-
* Pass the same route string(s) as the handler's generic.
|
|
51
|
-
*
|
|
50
|
+
* Pass the same route string(s) as the handler's generic. On a handler for one command, read
|
|
51
|
+
* `this.options`. On a handler for several, branch with `this.match`. The decorator routes and the generic must match exactly. Listing
|
|
52
52
|
* fewer or more than the handler declares is a compile error. Subcommands use a slash path.
|
|
53
53
|
*
|
|
54
54
|
* @param routes - The route string(s) this handler serves, e.g. `'ban'`, `'ban', 'kick'`, or `'demo/setup'`.
|
|
@@ -72,7 +72,7 @@ function SlashRoute(...routes) {
|
|
|
72
72
|
* Routes one or more commands' autocomplete to an `AutocompleteHandler`.
|
|
73
73
|
*
|
|
74
74
|
* Pass the same command route string(s) as the handler's generic. One command branches with `this.match`
|
|
75
|
-
* over its autocompletable fields, several commands share one handler whose
|
|
75
|
+
* over its autocompletable fields, several commands share one handler whose cases span every command's
|
|
76
76
|
* fields. Listing fewer or more commands than the handler declares is a compile error, since the
|
|
77
77
|
* decorator routes and the generic must match exactly.
|
|
78
78
|
*
|
|
@@ -174,7 +174,7 @@ function ModalRoute(...defs) {
|
|
|
174
174
|
* Routes string select menu interactions to a `StringMenuHandler`.
|
|
175
175
|
*
|
|
176
176
|
* Pass the {@link CustomId} definition(s) this handler decodes and list the same ones in the handler's
|
|
177
|
-
* generic.
|
|
177
|
+
* generic. Read the chosen option values from `this.values`.
|
|
178
178
|
*
|
|
179
179
|
* @param defs - The customId definition(s) this handler decodes, one per route.
|
|
180
180
|
* @decorator
|
|
@@ -289,38 +289,6 @@ function MentionableMenuRoute(...defs) {
|
|
|
289
289
|
};
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
-
//#endregion
|
|
293
|
-
//#region src/dispatch/DispatchContext.ts
|
|
294
|
-
/**
|
|
295
|
-
* A typed bag allocated once per interaction dispatch and handed to the handler. The middleware and i18n
|
|
296
|
-
* work reads and writes it through {@link DispatchState}. Today the bag is empty, so it only carries the
|
|
297
|
-
* dispatched `routeId`.
|
|
298
|
-
*/
|
|
299
|
-
var DispatchContext = class {
|
|
300
|
-
routeId;
|
|
301
|
-
state = Object.create(null);
|
|
302
|
-
id = crypto.randomUUID();
|
|
303
|
-
constructor(routeId) {
|
|
304
|
-
this.routeId = routeId;
|
|
305
|
-
}
|
|
306
|
-
set(key, value) {
|
|
307
|
-
this.state[key] = value;
|
|
308
|
-
}
|
|
309
|
-
get(key) {
|
|
310
|
-
return this.state[key];
|
|
311
|
-
}
|
|
312
|
-
/**
|
|
313
|
-
* Read a key that must be present, with `undefined` stripped from the result.
|
|
314
|
-
*
|
|
315
|
-
* @throws A **SeedcordError** when nothing set the key on this dispatch.
|
|
316
|
-
*/
|
|
317
|
-
require(key) {
|
|
318
|
-
const value = this.state[key];
|
|
319
|
-
if (value === void 0) throw new SeedcordError(SeedcordErrorCode.DispatchStateMissing, [String(key)]);
|
|
320
|
-
return value;
|
|
321
|
-
}
|
|
322
|
-
};
|
|
323
|
-
|
|
324
292
|
//#endregion
|
|
325
293
|
//#region src/handlers/BaseHandler.ts
|
|
326
294
|
/**
|
|
@@ -336,11 +304,14 @@ var BaseHandler = class {
|
|
|
336
304
|
logger;
|
|
337
305
|
/** The bag for this dispatch. Each interaction gets its own. Every handler for one event shares a single bag. */
|
|
338
306
|
dispatch;
|
|
307
|
+
/** The route that started this dispatch. Each handler base narrows it to its own routes. */
|
|
308
|
+
route;
|
|
339
309
|
constructor(event, core, dispatch, channel) {
|
|
340
310
|
this.core = core;
|
|
341
311
|
this.event = event;
|
|
342
312
|
this.logger = new Logger(this.constructor.name, { channel });
|
|
343
313
|
this.dispatch = dispatch;
|
|
314
|
+
this.route = dispatch.route;
|
|
344
315
|
}
|
|
345
316
|
};
|
|
346
317
|
|
|
@@ -348,7 +319,7 @@ var BaseHandler = class {
|
|
|
348
319
|
//#region src/handlers/RepliableHandler.ts
|
|
349
320
|
/**
|
|
350
321
|
* Shared base the repliable handlers on both transports extend. Defines the reply members over one sender.
|
|
351
|
-
*
|
|
322
|
+
* The dispatch run builds that sender and passes it to the handler and each middleware.
|
|
352
323
|
*
|
|
353
324
|
* @typeParam Event - The repliable interaction type this handler processes
|
|
354
325
|
* @typeParam TCore - The transport's Core
|
|
@@ -359,11 +330,9 @@ var BaseHandler = class {
|
|
|
359
330
|
var RepliableHandler = class extends BaseHandler {
|
|
360
331
|
/** The reply surface for this interaction. Pass the handler to reply from outside its class. */
|
|
361
332
|
sender;
|
|
362
|
-
routeId;
|
|
363
333
|
constructor(event, core, dispatch, sender) {
|
|
364
334
|
super(event, core, dispatch, "interactions");
|
|
365
|
-
this.
|
|
366
|
-
this.sender = sender ?? this.buildSender(event, core, dispatch);
|
|
335
|
+
this.sender = sender;
|
|
367
336
|
}
|
|
368
337
|
/** Send the initial response, exactly once. */
|
|
369
338
|
reply(response, opts) {
|
|
@@ -559,44 +528,88 @@ var BaseReplySender = class {
|
|
|
559
528
|
};
|
|
560
529
|
|
|
561
530
|
//#endregion
|
|
562
|
-
//#region src/
|
|
531
|
+
//#region src/gates/Gate.ts
|
|
563
532
|
/**
|
|
564
|
-
*
|
|
533
|
+
* Builds a {@link Gate} from a check. The check refuses by throwing a Notice or a Silence and passes
|
|
534
|
+
* by returning. The required context is inferred from `ctx`, so annotate it as narrowly as the fields
|
|
535
|
+
* the check reads. No annotation defaults to {@link GateContextBase}, an agnostic gate that fits every
|
|
536
|
+
* handler.
|
|
565
537
|
*
|
|
566
|
-
*
|
|
567
|
-
*
|
|
568
|
-
* to show the generic reply without the bus publish. The original error is stored as the standard
|
|
569
|
-
* `cause`, so the real stack reaches the webhook. When the user should see a real message for a fault,
|
|
570
|
-
* subclass {@link Notice} and write your own `render` instead.
|
|
538
|
+
* @typeParam Name - The gate's name, captured as a literal for mismatch errors.
|
|
539
|
+
* @typeParam Ctx - The context the check reads, inferred from the `ctx` annotation.
|
|
571
540
|
*
|
|
572
|
-
*
|
|
573
|
-
*
|
|
541
|
+
* @param name - The gate's name, used in mismatch errors.
|
|
542
|
+
* @param fn - The check, which refuses by throwing a Notice or a Silence and passes by returning.
|
|
543
|
+
*
|
|
544
|
+
* @see {@link defineEffectGate}
|
|
545
|
+
* @see the `@Gated` decorator from your transport package
|
|
574
546
|
*
|
|
575
547
|
* @example
|
|
576
548
|
* ```ts
|
|
577
|
-
*
|
|
549
|
+
* // a gate factory that takes its own option and closes over it
|
|
550
|
+
* function MinRoles(min: number): Gate<GateContextBase, 'MinRoles'> {
|
|
551
|
+
* return defineGate('MinRoles', (ctx) => {
|
|
552
|
+
* if (ctx.memberRoleIds.length < min) throw new NotEnoughRoles(min);
|
|
553
|
+
* });
|
|
554
|
+
* }
|
|
555
|
+
* ```
|
|
578
556
|
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
-
*
|
|
583
|
-
* throw new
|
|
557
|
+
* @example
|
|
558
|
+
* ```ts
|
|
559
|
+
* // agnostic gate (no ctx annotation): fits any handler
|
|
560
|
+
* const Owner = defineGate('owner', (ctx) => {
|
|
561
|
+
* if (!ctx.userId) throw new NotOwner();
|
|
562
|
+
* });
|
|
563
|
+
* ```
|
|
564
|
+
*/
|
|
565
|
+
function defineGate(name, fn) {
|
|
566
|
+
return {
|
|
567
|
+
name,
|
|
568
|
+
check: async (ctx) => fn(ctx)
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Builds an {@link EffectGate} from a `check` and a `commit`. `check` peeks and refuses by throwing, and
|
|
573
|
+
* `commit` applies the side effect, running only once the whole gate set passes so a later refusal never
|
|
574
|
+
* commits. In an `or`, seedcord rolls back the commit a refusing gate queued. This is how {@link Cooldown} peeks
|
|
575
|
+
* in `check` and charges the slot in `commit`.
|
|
576
|
+
*
|
|
577
|
+
* @typeParam Name - The gate's literal name, used to name a mismatch error.
|
|
578
|
+
* @typeParam Ctx - The context both `check` and `commit` read, inferred from the `ctx` annotation.
|
|
579
|
+
*
|
|
580
|
+
* @param name - The gate's name, used in mismatch errors.
|
|
581
|
+
* @param check - Peeks and refuses by throwing, without applying the side effect.
|
|
582
|
+
* @param commit - Applies the side effect, running only after the whole gate set passes.
|
|
584
583
|
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
584
|
+
* @see {@link defineGate}
|
|
585
|
+
* @see the `@Gated` decorator from your transport package
|
|
586
|
+
* @see {@link Cooldown}
|
|
587
|
+
*
|
|
588
|
+
* @example
|
|
589
|
+
* ```ts
|
|
590
|
+
* // an effect-gate factory that takes its own limit and closes over it
|
|
591
|
+
* function UsesPerDay(max: number): EffectGate<GateContextBase, 'UsesPerDay'> {
|
|
592
|
+
* return defineEffectGate(
|
|
593
|
+
* 'UsesPerDay',
|
|
594
|
+
* (ctx) => {
|
|
595
|
+
* // peek and refuse by throwing. do not mutate yet
|
|
596
|
+
* if (usedToday(ctx.userId) >= max) throw new OutOfUsesNotice(max);
|
|
597
|
+
* },
|
|
598
|
+
* (ctx) => {
|
|
599
|
+
* // runs only after the whole gate set passes
|
|
600
|
+
* recordUse(ctx.userId);
|
|
601
|
+
* }
|
|
602
|
+
* );
|
|
587
603
|
* }
|
|
588
604
|
* ```
|
|
589
605
|
*/
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
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] };
|
|
598
|
-
}
|
|
599
|
-
};
|
|
606
|
+
function defineEffectGate(name, check, commit) {
|
|
607
|
+
return {
|
|
608
|
+
name,
|
|
609
|
+
check: async (ctx) => check(ctx),
|
|
610
|
+
commit: async (ctx) => commit(ctx)
|
|
611
|
+
};
|
|
612
|
+
}
|
|
600
613
|
|
|
601
614
|
//#endregion
|
|
602
615
|
//#region src/gates/combinators.ts
|
|
@@ -645,9 +658,9 @@ function or(...args) {
|
|
|
645
658
|
/**
|
|
646
659
|
* Passes only for a user id listed in `config.ownerIds`, else refuses.
|
|
647
660
|
*
|
|
648
|
-
* Agnostic, so it attaches to interaction and event handlers alike. With no `ownerIds` configured it refuses every caller. Pass
|
|
661
|
+
* Agnostic, so it attaches to interaction and event handlers alike. With no `ownerIds` configured it refuses every caller. Pass a Notice class as `notice` to replace the refusal.
|
|
649
662
|
*
|
|
650
|
-
* @param options -
|
|
663
|
+
* @param options - Replace the refusal with a Notice class as `notice`.
|
|
651
664
|
*
|
|
652
665
|
* @see the `@Gated` decorator from your transport package
|
|
653
666
|
*
|
|
@@ -664,15 +677,15 @@ function or(...args) {
|
|
|
664
677
|
*
|
|
665
678
|
* @example
|
|
666
679
|
* ```ts
|
|
667
|
-
* //
|
|
668
|
-
* OwnerOnly({
|
|
680
|
+
* // OwnersOnly is your own Notice class
|
|
681
|
+
* OwnerOnly({ notice: OwnersOnly });
|
|
669
682
|
* ```
|
|
670
683
|
*/
|
|
671
684
|
function OwnerOnly(options) {
|
|
672
685
|
return defineGate("OwnerOnly", (ctx) => {
|
|
673
686
|
const owners = ctx.core.config.ownerIds ?? [];
|
|
674
687
|
if (ctx.userId !== null && owners.includes(ctx.userId)) return;
|
|
675
|
-
throw
|
|
688
|
+
throw new ((options?.notice) ?? NotOwner)();
|
|
676
689
|
});
|
|
677
690
|
}
|
|
678
691
|
/**
|
|
@@ -680,7 +693,7 @@ function OwnerOnly(options) {
|
|
|
680
693
|
*
|
|
681
694
|
* Agnostic. Attaches to any handler kind. Often paired with `RequirePermissions` in an {@link and}.
|
|
682
695
|
*
|
|
683
|
-
* @param options -
|
|
696
|
+
* @param options - Replace the refusal with a Notice class as `notice`.
|
|
684
697
|
*
|
|
685
698
|
* @see the `@Gated` decorator from your transport package
|
|
686
699
|
*
|
|
@@ -700,7 +713,7 @@ function OwnerOnly(options) {
|
|
|
700
713
|
function GuildOnly(options) {
|
|
701
714
|
return defineGate("GuildOnly", (ctx) => {
|
|
702
715
|
if (ctx.guildId) return;
|
|
703
|
-
throw
|
|
716
|
+
throw new ((options?.notice) ?? NotInGuild)();
|
|
704
717
|
});
|
|
705
718
|
}
|
|
706
719
|
/**
|
|
@@ -708,7 +721,7 @@ function GuildOnly(options) {
|
|
|
708
721
|
*
|
|
709
722
|
* Agnostic, fitting any handler kind. The inverse of {@link GuildOnly}, so combining the two in an {@link and} can never pass.
|
|
710
723
|
*
|
|
711
|
-
* @param options -
|
|
724
|
+
* @param options - Replace the refusal with a Notice class as `notice`.
|
|
712
725
|
*
|
|
713
726
|
* @see the `@Gated` decorator from your transport package
|
|
714
727
|
*
|
|
@@ -726,7 +739,7 @@ function GuildOnly(options) {
|
|
|
726
739
|
function DmOnly(options) {
|
|
727
740
|
return defineGate("DmOnly", (ctx) => {
|
|
728
741
|
if (!ctx.guildId) return;
|
|
729
|
-
throw
|
|
742
|
+
throw new ((options?.notice) ?? NotInDm)();
|
|
730
743
|
});
|
|
731
744
|
}
|
|
732
745
|
|
|
@@ -744,10 +757,10 @@ function scopeValue(ctx, per) {
|
|
|
744
757
|
* so a later refusal never burns the cooldown.
|
|
745
758
|
* That split also means two requests racing the same key can both pass before either charges the slot.
|
|
746
759
|
* The key combines the handler's route with the window settings, keeping two different handlers on separate
|
|
747
|
-
* windows.
|
|
760
|
+
* windows. Replace the refusal with {@link CooldownOptions.notice}.
|
|
748
761
|
*
|
|
749
762
|
* @param duration - A number is seconds, and a string is a duration like `30m` or `24h`.
|
|
750
|
-
* @param options - Sets the scope with `per`, the uses per window with `limit`, and the refusal
|
|
763
|
+
* @param options - Sets the scope with `per`, the uses per window with `limit`, and the refusal with `notice`.
|
|
751
764
|
* @throws A **SeedcordTypeError** when `duration` is a string that doesn't parse.
|
|
752
765
|
* @throws A **SeedcordRangeError** when `duration` is zero or negative.
|
|
753
766
|
*
|
|
@@ -799,22 +812,46 @@ function Cooldown(duration, options) {
|
|
|
799
812
|
limit: options.limit
|
|
800
813
|
};
|
|
801
814
|
const anonId = anonSeq++;
|
|
802
|
-
const keyOf = (ctx) => `cooldown:${ctx.declaredRoute ?? `anon${anonId}`}:${per}:w${windowMs}:l${options?.limit ?? 1}:${scopeValue(ctx, per)}`;
|
|
815
|
+
const keyOf = (ctx) => `cooldown:${ctx.dispatch.declaredRoute ?? `anon${anonId}`}:${per}:w${windowMs}:l${options?.limit ?? 1}:${scopeValue(ctx, per)}`;
|
|
803
816
|
return defineEffectGate("Cooldown", async (ctx) => {
|
|
804
817
|
const result = await ctx.core.rateLimiter.peek(keyOf(ctx), window);
|
|
805
818
|
if (!result.limited) return;
|
|
806
|
-
|
|
807
|
-
throw new OnCooldown(result.resetAt, options?.message?.(result.resetAt));
|
|
819
|
+
throw new ((options?.notice) ?? OnCooldown)(result.resetAt);
|
|
808
820
|
}, async (ctx) => {
|
|
809
821
|
await ctx.core.rateLimiter.charge(keyOf(ctx), window);
|
|
810
822
|
});
|
|
811
823
|
}
|
|
812
824
|
|
|
825
|
+
//#endregion
|
|
826
|
+
//#region src/permissions/bits.ts
|
|
827
|
+
const PermissionNames = new Map(Object.entries(PermissionFlagsBits).map(([key, bit]) => [bit, prettify(key)]));
|
|
828
|
+
const ADMINISTRATOR = PermissionFlagsBits.Administrator;
|
|
829
|
+
function nameOf(bit) {
|
|
830
|
+
return PermissionNames.get(bit) ?? String(bit);
|
|
831
|
+
}
|
|
832
|
+
function holds(held, bit) {
|
|
833
|
+
return (held & ADMINISTRATOR) === ADMINISTRATOR || (held & bit) === bit;
|
|
834
|
+
}
|
|
835
|
+
function namesWhere(held, scope, isHeld) {
|
|
836
|
+
return scope.reduce((names, bit) => {
|
|
837
|
+
if (holds(held, bit) === isHeld) names.push(nameOf(bit));
|
|
838
|
+
return names;
|
|
839
|
+
}, []);
|
|
840
|
+
}
|
|
841
|
+
/** @internal */
|
|
842
|
+
function missingNames(held, scope) {
|
|
843
|
+
return namesWhere(held, scope, false);
|
|
844
|
+
}
|
|
845
|
+
/** @internal */
|
|
846
|
+
function presentNames(held, scope) {
|
|
847
|
+
return namesWhere(held, scope, true);
|
|
848
|
+
}
|
|
849
|
+
|
|
813
850
|
//#endregion
|
|
814
851
|
//#region src/permissions/assert.ts
|
|
815
852
|
/**
|
|
816
853
|
* Refuses when the subject is missing a permission in `scope`, or with `inverse` when it holds one.
|
|
817
|
-
* The Administrator bit passes any scope, and
|
|
854
|
+
* The Administrator bit passes any scope, and with `inverse` it reports every bit in `scope`.
|
|
818
855
|
*
|
|
819
856
|
* The caller computes `permissions`, gateway off a discord.js role, member, or channel, and HTTP off the
|
|
820
857
|
* bitfield the interaction payload carries.
|
|
@@ -859,14 +896,12 @@ function Cooldown(duration, options) {
|
|
|
859
896
|
* ```ts
|
|
860
897
|
* import { Notice } from '@seedcord/core';
|
|
861
898
|
*
|
|
862
|
-
* // a replacement notice takes the same three arguments the default one does
|
|
863
899
|
* class CannotBan extends Notice {
|
|
864
900
|
* public constructor(
|
|
865
|
-
*
|
|
866
|
-
* private readonly subject: string,
|
|
901
|
+
* private readonly subject: string | null,
|
|
867
902
|
* private readonly missing: readonly string[]
|
|
868
903
|
* ) {
|
|
869
|
-
* super(
|
|
904
|
+
* super('You cannot ban here.');
|
|
870
905
|
* }
|
|
871
906
|
*
|
|
872
907
|
* public render(): ReplyResponse {
|
|
@@ -878,19 +913,97 @@ function Cooldown(duration, options) {
|
|
|
878
913
|
* subject: `<@${member.id}>`,
|
|
879
914
|
* permissions: member.permissions.bitfield,
|
|
880
915
|
* scope: [PermissionFlagsBits.BanMembers],
|
|
881
|
-
*
|
|
916
|
+
* notice: CannotBan
|
|
882
917
|
* });
|
|
883
918
|
* ```
|
|
884
919
|
*/
|
|
885
|
-
function assertPermissions(
|
|
886
|
-
const
|
|
887
|
-
if (
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
920
|
+
function assertPermissions({ subject, permissions, scope, inverse = false, notice }) {
|
|
921
|
+
const [refused, Default] = inverse ? [presentNames(permissions, scope), HasPresentPermissions] : [missingNames(permissions, scope), MissingPermissions];
|
|
922
|
+
if (refused.length === 0) return;
|
|
923
|
+
throw new (notice ?? Default)(subject, refused);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
//#endregion
|
|
927
|
+
//#region src/gates/catalog/permissions.ts
|
|
928
|
+
const BOT_SUBJECT = "The bot";
|
|
929
|
+
function requireHeld({ guildId, subject, readOrThrow }, scope, options) {
|
|
930
|
+
if (!guildId) throw new ((options?.notInGuild) ?? NotInGuild)();
|
|
931
|
+
const permissions = readOrThrow(guildId);
|
|
932
|
+
assertPermissions({
|
|
933
|
+
subject,
|
|
934
|
+
permissions,
|
|
935
|
+
scope,
|
|
936
|
+
notice: options?.missing
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
function botMemberMissing(guildId) {
|
|
940
|
+
throw new SeedcordError(SeedcordErrorCode.CoreBotMemberMissing, [guildId]);
|
|
941
|
+
}
|
|
942
|
+
function RequirePermissions(scope, options) {
|
|
943
|
+
const resolved = (held) => {
|
|
944
|
+
if (held === null) throw new ((options?.unresolved) ?? PermissionsUnresolved)();
|
|
945
|
+
return held;
|
|
946
|
+
};
|
|
947
|
+
if (options?.in === "guild") return defineGate("RequirePermissions", (ctx) => {
|
|
948
|
+
const readOrThrow = () => resolved(ctx.memberGuildPermissions);
|
|
949
|
+
requireHeld({
|
|
950
|
+
guildId: ctx.guildId,
|
|
951
|
+
subject: null,
|
|
952
|
+
readOrThrow
|
|
953
|
+
}, scope, options);
|
|
954
|
+
});
|
|
955
|
+
return defineGate("RequirePermissions", (ctx) => {
|
|
956
|
+
const readOrThrow = () => resolved(ctx.memberPermissions);
|
|
957
|
+
requireHeld({
|
|
958
|
+
guildId: ctx.guildId,
|
|
959
|
+
subject: null,
|
|
960
|
+
readOrThrow
|
|
961
|
+
}, scope, options);
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
function RequireBotPermissions(scope, options) {
|
|
965
|
+
if (options?.in === "guild") return defineGate("RequireBotPermissions", (ctx) => {
|
|
966
|
+
const readOrThrow = (guildId) => ctx.appGuildPermissions ?? botMemberMissing(guildId);
|
|
967
|
+
requireHeld({
|
|
968
|
+
guildId: ctx.guildId,
|
|
969
|
+
subject: BOT_SUBJECT,
|
|
970
|
+
readOrThrow
|
|
971
|
+
}, scope, options);
|
|
972
|
+
});
|
|
973
|
+
return defineGate("RequireBotPermissions", (ctx) => {
|
|
974
|
+
requireHeld({
|
|
975
|
+
guildId: ctx.guildId,
|
|
976
|
+
subject: BOT_SUBJECT,
|
|
977
|
+
readOrThrow: () => ctx.appPermissions
|
|
978
|
+
}, scope, options);
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Requires the caller to hold `roleId`, read from the member's role ids. Refuses outside a guild first,
|
|
983
|
+
* then when the role is missing. Roles are guild-global, so there is no scope dimension.
|
|
984
|
+
*
|
|
985
|
+
* @param roleId - The role snowflake the caller must hold.
|
|
986
|
+
* @param options - Replace a refusal with a Notice class as `notInGuild` or `missingRole`.
|
|
987
|
+
*
|
|
988
|
+
* @see the `@Gated` decorator from your transport package
|
|
989
|
+
*
|
|
990
|
+
* @example
|
|
991
|
+
* ```ts
|
|
992
|
+
* \@Gated(RequireRole('123456789012345678'))
|
|
993
|
+
* \@SlashRoute('vip')
|
|
994
|
+
* class VipHandler extends SlashHandler<'vip'> {
|
|
995
|
+
* async execute() {
|
|
996
|
+
* // ...
|
|
997
|
+
* }
|
|
998
|
+
* }
|
|
999
|
+
* ```
|
|
1000
|
+
*/
|
|
1001
|
+
function RequireRole(roleId, options) {
|
|
1002
|
+
return defineGate("RequireRole", (ctx) => {
|
|
1003
|
+
if (!ctx.guildId) throw new ((options?.notInGuild) ?? NotInGuild)();
|
|
1004
|
+
if (ctx.memberRoleIds.includes(roleId)) return;
|
|
1005
|
+
throw new ((options?.missingRole) ?? MissingRole)(roleId);
|
|
1006
|
+
});
|
|
894
1007
|
}
|
|
895
1008
|
|
|
896
1009
|
//#endregion
|
|
@@ -1146,11 +1259,119 @@ var CursorSourceBase = class {
|
|
|
1146
1259
|
}
|
|
1147
1260
|
};
|
|
1148
1261
|
|
|
1262
|
+
//#endregion
|
|
1263
|
+
//#region src/kv/KeyvDriver.ts
|
|
1264
|
+
/**
|
|
1265
|
+
* Wraps a Keyv instance for `config.kv`.
|
|
1266
|
+
*
|
|
1267
|
+
* @example
|
|
1268
|
+
* ```ts
|
|
1269
|
+
* new Seedcord({ ...config, kv: new KeyvDriver(new Keyv(new KeyvSqlite('sqlite://./kv.sqlite'))) });
|
|
1270
|
+
* ```
|
|
1271
|
+
*/
|
|
1272
|
+
var KeyvDriver = class {
|
|
1273
|
+
keyv;
|
|
1274
|
+
constructor(keyv) {
|
|
1275
|
+
this.keyv = keyv;
|
|
1276
|
+
}
|
|
1277
|
+
async get(key) {
|
|
1278
|
+
const value = await this.keyv.get(key);
|
|
1279
|
+
return typeof value === "string" ? value : void 0;
|
|
1280
|
+
}
|
|
1281
|
+
async set(key, value, ttlMs) {
|
|
1282
|
+
if (!await this.keyv.set(key, value, ttlMs)) throw new SeedcordError(SeedcordErrorCode.CoreKvDriverFailed, ["write", key]);
|
|
1283
|
+
}
|
|
1284
|
+
async delete(key) {
|
|
1285
|
+
await this.keyv.delete(key);
|
|
1286
|
+
}
|
|
1287
|
+
/** @throws A **SeedcordError** when the Keyv store can't list its keys, like `@keyv/memcache`. */
|
|
1288
|
+
async *entries(prefix) {
|
|
1289
|
+
if (!this.#canListKeys()) throw new SeedcordError(SeedcordErrorCode.CoreKvKeysUnsupported);
|
|
1290
|
+
for await (const [key, value] of this.keyv.iterator()) if (key.startsWith(prefix) && typeof value === "string") yield [key, value];
|
|
1291
|
+
}
|
|
1292
|
+
async [Symbol.asyncDispose]() {
|
|
1293
|
+
await this.keyv.disconnect();
|
|
1294
|
+
}
|
|
1295
|
+
#canListKeys() {
|
|
1296
|
+
const capabilities = this.keyv.store.capabilities;
|
|
1297
|
+
return capabilities?.store === "mapLike" || capabilities?.methods.iterator.exists === true;
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1300
|
+
|
|
1301
|
+
//#endregion
|
|
1302
|
+
//#region src/kv/UnstorageDriver.ts
|
|
1303
|
+
const MIN_TTL_SECONDS = 60;
|
|
1304
|
+
const MS_PER_SECOND = 1e3;
|
|
1305
|
+
function storageKey(key) {
|
|
1306
|
+
return `_${encodeURIComponent(key).replaceAll(".", "%2E")}`;
|
|
1307
|
+
}
|
|
1308
|
+
function originalKey(stored) {
|
|
1309
|
+
return decodeURIComponent(stored.slice(1));
|
|
1310
|
+
}
|
|
1311
|
+
function text(raw) {
|
|
1312
|
+
if (raw === null) return void 0;
|
|
1313
|
+
return typeof raw === "string" ? raw : new TextDecoder().decode(raw);
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Wraps an unstorage `Storage` for `config.kv`.
|
|
1317
|
+
*
|
|
1318
|
+
* @example
|
|
1319
|
+
* ```ts
|
|
1320
|
+
* new Seedcord({ ...config, kv: new UnstorageDriver(createStorage({ driver: fsDriver({ base: './.kv' }) })) });
|
|
1321
|
+
* ```
|
|
1322
|
+
*/
|
|
1323
|
+
var UnstorageDriver = class {
|
|
1324
|
+
storage;
|
|
1325
|
+
constructor(storage) {
|
|
1326
|
+
this.storage = storage;
|
|
1327
|
+
}
|
|
1328
|
+
async get(key) {
|
|
1329
|
+
return text(await this.storage.getItemRaw(storageKey(key)));
|
|
1330
|
+
}
|
|
1331
|
+
async set(key, value, ttlMs) {
|
|
1332
|
+
const ttl = ttlMs === void 0 ? {} : { ttl: Math.max(MIN_TTL_SECONDS, Math.ceil(ttlMs / MS_PER_SECOND)) };
|
|
1333
|
+
await this.storage.setItemRaw(storageKey(key), value, ttl);
|
|
1334
|
+
}
|
|
1335
|
+
async delete(key) {
|
|
1336
|
+
await this.storage.removeItem(storageKey(key));
|
|
1337
|
+
}
|
|
1338
|
+
async *entries(prefix) {
|
|
1339
|
+
const storedPrefix = storageKey(prefix);
|
|
1340
|
+
for (const key of await this.storage.getKeys()) {
|
|
1341
|
+
if (!key.startsWith(storedPrefix)) continue;
|
|
1342
|
+
const value = text(await this.storage.getItemRaw(key));
|
|
1343
|
+
if (value !== void 0) yield [originalKey(key), value];
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
async [Symbol.asyncDispose]() {
|
|
1347
|
+
await this.storage.dispose();
|
|
1348
|
+
}
|
|
1349
|
+
};
|
|
1350
|
+
|
|
1351
|
+
//#endregion
|
|
1352
|
+
//#region src/rateLimiter/buildKey.ts
|
|
1353
|
+
/**
|
|
1354
|
+
* Builds a rate-limit key from a prefix and scope parts, joined with `:`. A null or undefined part
|
|
1355
|
+
* becomes `'global'`. A key built from a DM's guild id reads `<prefix>:global` for every DM caller.
|
|
1356
|
+
*
|
|
1357
|
+
* @param prefix - The bucket family, e.g. `'cooldown'`.
|
|
1358
|
+
* @param parts - Scope values, e.g. a user or guild id.
|
|
1359
|
+
*
|
|
1360
|
+
* @example
|
|
1361
|
+
* ```ts
|
|
1362
|
+
* buildKey('report', interaction.guildId); // 'report:1234...' or 'report:global' in a DM
|
|
1363
|
+
* ```
|
|
1364
|
+
*/
|
|
1365
|
+
function buildKey(prefix, ...parts) {
|
|
1366
|
+
if (parts.length === 0) return prefix;
|
|
1367
|
+
return [prefix, ...parts.map((part) => part ?? "global")].join(":");
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1149
1370
|
//#endregion
|
|
1150
1371
|
//#region src/index.ts
|
|
1151
1372
|
/** Package version */
|
|
1152
|
-
const version = "0.
|
|
1373
|
+
const version = "0.12.0-next.2";
|
|
1153
1374
|
|
|
1154
1375
|
//#endregion
|
|
1155
|
-
export { ArraySourceBase, AutocompleteRoute, BaseHandler, BaseReplySender, BuilderComponent, Bus, ButtonRoute, ChannelMenuRoute, Commands, ContextMenus, Cooldown, CursorSourceBase, CustomId,
|
|
1376
|
+
export { ArraySourceBase, AutocompleteRoute, BaseHandler, BaseReplySender, BuilderComponent, Bus, ButtonRoute, ChannelMenuRoute, Commands, ContextMenus, Cooldown, CursorSourceBase, CustomId, DmOnly, Fault, GuildOnly, InteractionKind, KeyvDriver, MemoryDriver, MentionableMenuRoute, MessageContextMenuRoute, ModalRoute, Notice, OwnerOnly, PaginatorBase, RegisterCommand, RegisterInteractionMiddleware, RepliableHandler, RequireBotPermissions, RequirePermissions, RequireRole, ResolvedEmoji, RoleMenuRoute, RowComponent, ShutdownPhase, Silence, SlashRoute, StartupPhase, StringMenuRoute, Subscribe, Subscriber, UnstorageDriver, UserContextMenuRoute, UserMenuRoute, WebhookLog, WebhookUrl, and, assertPermissions, buildKey, decodeFor, defineEffectGate, defineGate, mergeRoles, or, paginate, prefixOf, setCustomIdErrors, version };
|
|
1156
1377
|
//# sourceMappingURL=index.mjs.map
|