@seedcord/core 0.1.0-next.1 → 0.1.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/dist/{CustomId-XgZ2KDgT.mjs → CustomId-Bq6WagiA.mjs} +196 -88
- package/dist/CustomId-Bq6WagiA.mjs.map +1 -0
- package/dist/{CustomId-CqiuEllp.cjs → CustomId-DQ6ZH4s-.cjs} +321 -87
- package/dist/CustomId-DQ6ZH4s-.cjs.map +1 -0
- package/dist/CustomId-Ddwg48VY.d.mts +654 -0
- package/dist/index.cjs +323 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +201 -41
- package/dist/index.mjs +316 -4
- package/dist/index.mjs.map +1 -1
- package/dist/internal.index.cjs +39 -1
- package/dist/internal.index.cjs.map +1 -1
- package/dist/internal.index.d.mts +60 -41
- package/dist/internal.index.mjs +22 -2
- package/dist/internal.index.mjs.map +1 -1
- package/package.json +7 -3
- package/dist/CustomId-BJF6mbJs.d.mts +0 -317
- package/dist/CustomId-CqiuEllp.cjs.map +0 -1
- package/dist/CustomId-XgZ2KDgT.mjs.map +0 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { C as BuilderComponent, E as RowType, S as RegisterCommand, T as BuilderType, _ as RequiredOf, b as CoreBase, d as JoinNames, f as TwoOrMore, g as GateContextBase, h as Gate, m as EffectGate, n as CustomId, p as Notice, s as GateNoticeOptions, u as IntersectRequired, v as defineEffectGate, w as RowComponent, y as defineGate } from "./CustomId-Ddwg48VY.mjs";
|
|
2
|
+
import { ValidDuration } from "@seedcord/utils";
|
|
3
|
+
import { EpochMs, RenderContext, ReplyResponse } from "@seedcord/types";
|
|
3
4
|
|
|
4
5
|
//#region src/registries/ContextMenuRegistry.d.ts
|
|
5
6
|
/**
|
|
@@ -59,71 +60,230 @@ interface SlashOption {
|
|
|
59
60
|
*/
|
|
60
61
|
interface SlashOptionRegistry {}
|
|
61
62
|
//#endregion
|
|
62
|
-
//#region src/
|
|
63
|
+
//#region src/gates/combinators.d.ts
|
|
63
64
|
/**
|
|
64
|
-
*
|
|
65
|
+
* Runs each gate in order and refuses on the first refusal. Takes two or more arms. The required
|
|
66
|
+
* context is the intersection of the arms, so an event-only and an interaction-only gate cannot be
|
|
67
|
+
* combined.
|
|
65
68
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
69
|
+
* @typeParam Gates - The tuple of two or more gate arms, run left to right.
|
|
70
|
+
* @param gates - The gate arms to run in order, all refused on the first refusal.
|
|
71
|
+
*
|
|
72
|
+
* @see {@link or}
|
|
70
73
|
*
|
|
71
74
|
* @example
|
|
72
75
|
* ```ts
|
|
73
|
-
* import {
|
|
74
|
-
* import { TextDisplayBuilder } from '@discordjs/builders';
|
|
76
|
+
* import { PermissionFlagsBits } from 'discord.js';
|
|
75
77
|
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* new TextDisplayBuilder().setContent(`### Insufficient balance\nYou need more than ${balance} coins.`)
|
|
82
|
-
* );
|
|
78
|
+
* \@Gated(and(GuildOnly(), RequirePermissions([PermissionFlagsBits.BanMembers])))
|
|
79
|
+
* \@SlashRoute('ban')
|
|
80
|
+
* class BanHandler extends SlashHandler<'ban'> {
|
|
81
|
+
* async execute() {
|
|
82
|
+
* // ...
|
|
83
83
|
* }
|
|
84
84
|
* }
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
declare function and<Gates extends TwoOrMore<Gate<GateContextBase>>>(...gates: Gates): Gate<IntersectRequired<Gates> & GateContextBase, JoinNames<Gates, ' & '>>;
|
|
88
|
+
interface OrOptions {
|
|
89
|
+
fail: Notice | ((ctx: GateContextBase) => Notice);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Runs each gate in order and passes on the first arm that passes. Takes two or more arms. The
|
|
93
|
+
* required context is the union of the arms, so a handler that matches any one arm fits. When every
|
|
94
|
+
* arm refuses it throws the trailing {@link OrOptions} `fail` if given, else an auto list of the
|
|
95
|
+
* arms derived from the summary field, else a default refusal. The trailing options object does not
|
|
96
|
+
* count as an arm.
|
|
97
|
+
*
|
|
98
|
+
* @typeParam Gates - The tuple of two or more gate arms, tried left to right.
|
|
99
|
+
* @param gates - The gate arms to try in order, with an optional trailing {@link OrOptions} object.
|
|
85
100
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
101
|
+
* @see {@link and}
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* \@Gated(or(OwnerOnly(), RequireRole('123456789012345678')))
|
|
106
|
+
* \@SlashRoute('admin')
|
|
107
|
+
* class AdminHandler extends SlashHandler<'admin'> {
|
|
108
|
+
* async execute() {
|
|
109
|
+
* // ...
|
|
89
110
|
* }
|
|
111
|
+
* }
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
declare function or<Gates extends TwoOrMore<Gate<GateContextBase>>>(...gates: Gates): Gate<RequiredOf<Gates[number]>, JoinNames<Gates, ' | '>>;
|
|
115
|
+
/**
|
|
116
|
+
* @example
|
|
117
|
+
* ```ts
|
|
118
|
+
* // custom refusal when every arm refuses
|
|
119
|
+
* or(OwnerOnly(), RequireRole('123456789012345678'), { fail: new NotAllowedNotice() });
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
declare function or<Gates extends TwoOrMore<Gate<GateContextBase>>>(...args: [...Gates, OrOptions]): Gate<RequiredOf<Gates[number]>, JoinNames<Gates, ' | '>>;
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/gates/catalog/access.d.ts
|
|
125
|
+
/**
|
|
126
|
+
* Passes only for a user id listed in `config.ownerIds`, else refuses.
|
|
127
|
+
*
|
|
128
|
+
* Agnostic, so it attaches to interaction and event handlers alike. With no `ownerIds` configured it refuses every caller. Pass {@link GateNoticeOptions} to reword or replace the refusal.
|
|
129
|
+
*
|
|
130
|
+
* @param options - Reword the default refusal with `message`, or replace it with `notice`.
|
|
90
131
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
132
|
+
* @see the `@Gated` decorator from `seedcord`
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* \@Gated(OwnerOnly())
|
|
137
|
+
* \@SlashRoute('shutdown')
|
|
138
|
+
* class ShutdownHandler extends SlashHandler<'shutdown'> {
|
|
139
|
+
* async execute() {
|
|
140
|
+
* // ...
|
|
93
141
|
* }
|
|
94
142
|
* }
|
|
143
|
+
* ```
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```ts
|
|
147
|
+
* // reword the refusal, keeping the embed styling
|
|
148
|
+
* OwnerOnly({ message: 'Owners only.' });
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
declare function OwnerOnly(options?: GateNoticeOptions): Gate<GateContextBase, 'OwnerOnly'>;
|
|
152
|
+
/**
|
|
153
|
+
* Passes inside a guild, else refuses.
|
|
95
154
|
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
155
|
+
* Agnostic, so it attaches to any handler kind. Often paired with `RequirePermissions` in an {@link and}.
|
|
156
|
+
*
|
|
157
|
+
* @param options - Reword the default refusal with `message`, or replace it with `notice`.
|
|
158
|
+
*
|
|
159
|
+
* @see the `@Gated` decorator from `seedcord`
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```ts
|
|
163
|
+
* import { PermissionFlagsBits } from 'discord.js';
|
|
164
|
+
*
|
|
165
|
+
* \@Gated(GuildOnly(), RequirePermissions([PermissionFlagsBits.Administrator]))
|
|
166
|
+
* \@SlashRoute('maintenance')
|
|
167
|
+
* class Maintenance extends SlashHandler<'maintenance'> {
|
|
168
|
+
* async execute() {
|
|
169
|
+
* // ...
|
|
170
|
+
* }
|
|
171
|
+
* }
|
|
98
172
|
* ```
|
|
99
173
|
*/
|
|
100
|
-
declare
|
|
174
|
+
declare function GuildOnly(options?: GateNoticeOptions): Gate<GateContextBase, 'GuildOnly'>;
|
|
175
|
+
/**
|
|
176
|
+
* Passes in a direct message, else refuses.
|
|
177
|
+
*
|
|
178
|
+
* Agnostic, so it attaches to any handler kind. The inverse of {@link GuildOnly}, so combining the two in an {@link and} can never pass.
|
|
179
|
+
*
|
|
180
|
+
* @param options - Reword the default refusal with `message`, or replace it with `notice`.
|
|
181
|
+
*
|
|
182
|
+
* @see the `@Gated` decorator from `seedcord`
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```ts
|
|
186
|
+
* \@Gated(DmOnly())
|
|
187
|
+
* \@SlashRoute('verify')
|
|
188
|
+
* class VerifyHandler extends SlashHandler<'verify'> {
|
|
189
|
+
* async execute() {
|
|
190
|
+
* // ...
|
|
191
|
+
* }
|
|
192
|
+
* }
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
declare function DmOnly(options?: GateNoticeOptions): Gate<GateContextBase, 'DmOnly'>;
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region src/gates/catalog/Cooldown.d.ts
|
|
198
|
+
/**
|
|
199
|
+
* Options for {@link Cooldown}. `per` sets the bucket the window applies to and `limit` the uses allowed per
|
|
200
|
+
* window. `message` rewords the refusal and `notice` replaces it, both receiving the epoch ms the key frees
|
|
201
|
+
* up so the refusal can show the retry time.
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* ```ts
|
|
205
|
+
* // one use per channel, rewording the refusal with the retry time
|
|
206
|
+
* Cooldown('10s', {
|
|
207
|
+
* per: 'channel',
|
|
208
|
+
* message: (resetAt) => `Slow down. Try again <t:${Math.round(resetAt / 1000)}:R>.`
|
|
209
|
+
* });
|
|
210
|
+
* ```
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* ```ts
|
|
214
|
+
* // five uses per minute, per user
|
|
215
|
+
* Cooldown('1m', { limit: 5 });
|
|
216
|
+
* ```
|
|
217
|
+
*/
|
|
218
|
+
interface CooldownOptions {
|
|
101
219
|
/**
|
|
102
|
-
*
|
|
103
|
-
* bus. The user always sees {@link Notice.render} either way.
|
|
220
|
+
* The bucket the cooldown window applies to. 'user' scopes by user ID, falls back to global when the source carries no user id. 'guild' scopes by guild ID (falls back to global if no guild), and 'channel' scopes by channel ID (falls back to global if no channel).
|
|
104
221
|
*
|
|
105
|
-
* @defaultValue `
|
|
222
|
+
* @defaultValue `'user'`
|
|
106
223
|
*/
|
|
107
|
-
|
|
224
|
+
per?: 'user' | 'guild' | 'channel';
|
|
108
225
|
/**
|
|
109
|
-
*
|
|
110
|
-
* whole channel should see.
|
|
226
|
+
* Uses allowed inside one window before the gate refuses.
|
|
111
227
|
*
|
|
112
|
-
* @defaultValue `
|
|
228
|
+
* @defaultValue `1`
|
|
113
229
|
*/
|
|
114
|
-
|
|
230
|
+
limit?: number;
|
|
115
231
|
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
232
|
+
* Reword the refusal, keeping the standard notice card. Receives the epoch ms the key frees up, so the
|
|
233
|
+
* text can include the retry time with `<t:${Math.round(resetAt / 1000)}:R>`.
|
|
118
234
|
*/
|
|
119
|
-
|
|
120
|
-
protected constructor(message: string, options?: ErrorOptions);
|
|
235
|
+
message?: (resetAt: EpochMs) => string;
|
|
121
236
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
237
|
+
* Replace the refusal Notice entirely, for full control or a translated copy. Receives the epoch ms the
|
|
238
|
+
* key frees up.
|
|
124
239
|
*/
|
|
125
|
-
|
|
240
|
+
notice?: (resetAt: EpochMs) => Notice;
|
|
126
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Allows `limit` uses per window, scoped by `per`. A number `duration` is **seconds**, a string is
|
|
244
|
+
* a duration like `30m` or `24h`. An unparseable string throws a **SeedcordTypeError** at construction. The
|
|
245
|
+
* slot is charged in commit, only after the whole gate set passes, so a later refusal never burns the cooldown.
|
|
246
|
+
* That split also means two requests racing the same key can both pass before either charges the slot.
|
|
247
|
+
* Each call gets its own bucket, so two handlers never share a window. Reword the refusal with {@link CooldownOptions.message}
|
|
248
|
+
* or replace it with {@link CooldownOptions.notice}.
|
|
249
|
+
*
|
|
250
|
+
* @param duration - A number is seconds, a string is a duration like `30m` or `24h`. An unparseable string throws a **SeedcordTypeError**.
|
|
251
|
+
* @param options - Sets the scope with `per`, the uses per window with `limit`, and the refusal text with `message` or `notice`.
|
|
252
|
+
*
|
|
253
|
+
* @see the `@Gated` decorator from `seedcord`
|
|
254
|
+
* @see {@link IRateLimiter}
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* ```ts
|
|
258
|
+
* // a number argument is SECONDS
|
|
259
|
+
* \@Gated(Cooldown(5))
|
|
260
|
+
* \@SlashRoute('daily')
|
|
261
|
+
* class DailyHandler extends SlashHandler<'daily'> {
|
|
262
|
+
* async execute() {
|
|
263
|
+
* // ...
|
|
264
|
+
* }
|
|
265
|
+
* }
|
|
266
|
+
* ```
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* ```ts
|
|
270
|
+
* // a string is a duration, here scoped per guild (the default is per user)
|
|
271
|
+
* Cooldown('30m', { per: 'guild' });
|
|
272
|
+
* ```
|
|
273
|
+
*
|
|
274
|
+
* @example
|
|
275
|
+
* ```ts
|
|
276
|
+
* // a burst, 3 uses per 10 minutes per user, then it refuses until a slot frees
|
|
277
|
+
* \@Gated(Cooldown('10m', { limit: 3 }))
|
|
278
|
+
* \@SlashRoute('report')
|
|
279
|
+
* class ReportHandler extends SlashHandler<'report'> {
|
|
280
|
+
* async execute() {
|
|
281
|
+
* // ...
|
|
282
|
+
* }
|
|
283
|
+
* }
|
|
284
|
+
* ```
|
|
285
|
+
*/
|
|
286
|
+
declare function Cooldown(duration: number | ValidDuration, options?: CooldownOptions): EffectGate<GateContextBase, 'Cooldown'>;
|
|
127
287
|
//#endregion
|
|
128
288
|
//#region src/stops/Fault.d.ts
|
|
129
289
|
/**
|
|
@@ -218,5 +378,5 @@ declare function paginate<Item>(items: readonly Item[], page: number, perPage: n
|
|
|
218
378
|
/** Package version */
|
|
219
379
|
declare const version: string;
|
|
220
380
|
//#endregion
|
|
221
|
-
export { BuilderComponent, type BuilderType, CustomId, Fault, type MessageContextMenuRegistry, Notice, type OptionKind, type PageView, RowComponent, type RowType, Silence, type SlashOption, type SlashOptionRegistry, type UserContextMenuRegistry, paginate, version };
|
|
381
|
+
export { BuilderComponent, type BuilderType, Cooldown, type CooldownOptions, type CoreBase, CustomId, DmOnly, type EffectGate, Fault, type Gate, type GateContextBase, type GateNoticeOptions, GuildOnly, type MessageContextMenuRegistry, Notice, type OptionKind, OwnerOnly, type PageView, RegisterCommand, type RequiredOf, RowComponent, type RowType, Silence, type SlashOption, type SlashOptionRegistry, type UserContextMenuRegistry, and, defineEffectGate, defineGate, or, paginate, version };
|
|
222
382
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,113 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { S as CommandMetadataKey, _ as NoticeCard, c as runCheck, d as NeedsAny, f as NotAllowed, g as OnCooldown, h as NotOwner, i as pickNotice, m as NotInGuild, o as markCommits, p as NotInDm, s as rollbackCommits, t as CustomId, v as BuilderComponent, x as Notice, y as RowComponent } from "./CustomId-Bq6WagiA.mjs";
|
|
2
|
+
import "reflect-metadata";
|
|
2
3
|
import { SeedcordErrorCode } from "@seedcord/errors";
|
|
3
|
-
import { SeedcordRangeError } from "@seedcord/errors/internal";
|
|
4
|
+
import { SeedcordError, SeedcordRangeError, SeedcordTypeError } from "@seedcord/errors/internal";
|
|
5
|
+
import { parseDuration } from "@seedcord/utils";
|
|
4
6
|
|
|
7
|
+
//#region src/decorators/Command.ts
|
|
8
|
+
function RegisterCommand(scope, guilds = []) {
|
|
9
|
+
return (ctor) => {
|
|
10
|
+
const existingMeta = Reflect.getOwnMetadata(CommandMetadataKey, ctor);
|
|
11
|
+
if (existingMeta) throw new SeedcordError(SeedcordErrorCode.DecoratorCommandAlreadyRegistered, [
|
|
12
|
+
ctor.name,
|
|
13
|
+
existingMeta.scope,
|
|
14
|
+
scope
|
|
15
|
+
]);
|
|
16
|
+
if (scope === "global" && guilds.length > 0) throw new SeedcordError(SeedcordErrorCode.DecoratorCommandGlobalWithGuilds);
|
|
17
|
+
if (scope === "guild" && (!Array.isArray(guilds) || guilds.length === 0)) throw new SeedcordError(SeedcordErrorCode.DecoratorCommandGuildWithoutGuilds);
|
|
18
|
+
const meta = scope === "global" ? { scope } : {
|
|
19
|
+
scope,
|
|
20
|
+
guilds
|
|
21
|
+
};
|
|
22
|
+
Reflect.defineMetadata(CommandMetadataKey, meta, ctor);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/gates/Gate.ts
|
|
28
|
+
/**
|
|
29
|
+
* Builds a {@link Gate} from a check. The check refuses by throwing a Notice or a Silence and passes
|
|
30
|
+
* by returning. The required context is inferred from `ctx`, so annotate it as narrowly as the fields
|
|
31
|
+
* the check reads. No annotation defaults to {@link GateContextBase}, an agnostic gate that fits every
|
|
32
|
+
* handler.
|
|
33
|
+
*
|
|
34
|
+
* @typeParam Name - The gate's name captured as a literal, so a mismatch error can name the gate.
|
|
35
|
+
* @typeParam Ctx - The context the check reads, inferred from the `ctx` annotation.
|
|
36
|
+
*
|
|
37
|
+
* @param name - The gate's name, used in mismatch errors.
|
|
38
|
+
* @param fn - The check, which refuses by throwing a Notice or a Silence and passes by returning.
|
|
39
|
+
*
|
|
40
|
+
* @see {@link defineEffectGate}
|
|
41
|
+
* @see the `@Gated` decorator from `seedcord`
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* // a gate factory that takes its own option and closes over it
|
|
46
|
+
* function MinRoles(min: number): Gate<GateContextBase, 'MinRoles'> {
|
|
47
|
+
* return defineGate('MinRoles', (ctx) => {
|
|
48
|
+
* if (ctx.memberRoleIds.length < min) throw new NotEnoughRoles(min);
|
|
49
|
+
* });
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* // agnostic gate (no ctx annotation): fits any handler
|
|
56
|
+
* const Owner = defineGate('owner', (ctx) => {
|
|
57
|
+
* if (!ctx.userId) throw new NotOwner();
|
|
58
|
+
* });
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
function defineGate(name, fn) {
|
|
62
|
+
return {
|
|
63
|
+
name,
|
|
64
|
+
check: async (ctx) => fn(ctx)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Builds an {@link EffectGate} from a `check` and a `commit`. `check` peeks and refuses by throwing, and
|
|
69
|
+
* `commit` applies the side effect, running only once the whole gate set passes so a later refusal never
|
|
70
|
+
* commits. In an `or`, a refusing arm's queued commit is rolled back. This is how {@link Cooldown} peeks
|
|
71
|
+
* in `check` and charges the slot in `commit`.
|
|
72
|
+
*
|
|
73
|
+
* @typeParam Name - The gate's name captured as a literal, so a mismatch error can name the gate.
|
|
74
|
+
* @typeParam Ctx - The context both `check` and `commit` read, inferred from the `ctx` annotation.
|
|
75
|
+
*
|
|
76
|
+
* @param name - The gate's name, used in mismatch errors.
|
|
77
|
+
* @param check - Peeks and refuses by throwing, without applying the side effect.
|
|
78
|
+
* @param commit - Applies the side effect, running only after the whole gate set passes.
|
|
79
|
+
*
|
|
80
|
+
* @see {@link defineGate}
|
|
81
|
+
* @see the `@Gated` decorator from `seedcord`
|
|
82
|
+
* @see {@link Cooldown}
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* // an effect-gate factory that takes its own limit and closes over it
|
|
87
|
+
* function UsesPerDay(max: number): EffectGate<GateContextBase, 'UsesPerDay'> {
|
|
88
|
+
* return defineEffectGate(
|
|
89
|
+
* 'UsesPerDay',
|
|
90
|
+
* (ctx) => {
|
|
91
|
+
* // peek and refuse by throwing, do not mutate yet
|
|
92
|
+
* if (usedToday(ctx.userId) >= max) throw new OutOfUsesNotice(max);
|
|
93
|
+
* },
|
|
94
|
+
* (ctx) => {
|
|
95
|
+
* // runs only after the whole gate set passes
|
|
96
|
+
* recordUse(ctx.userId);
|
|
97
|
+
* }
|
|
98
|
+
* );
|
|
99
|
+
* }
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
function defineEffectGate(name, check, commit) {
|
|
103
|
+
return {
|
|
104
|
+
name,
|
|
105
|
+
check: async (ctx) => check(ctx),
|
|
106
|
+
commit: async (ctx) => commit(ctx)
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
//#endregion
|
|
5
111
|
//#region src/stops/Fault.ts
|
|
6
112
|
/**
|
|
7
113
|
* A generic fault you throw after catching an error you do not have a specific message for.
|
|
@@ -40,6 +146,212 @@ var Fault = class extends Notice {
|
|
|
40
146
|
}
|
|
41
147
|
};
|
|
42
148
|
|
|
149
|
+
//#endregion
|
|
150
|
+
//#region src/gates/combinators.ts
|
|
151
|
+
function and(...gates) {
|
|
152
|
+
return defineGate(gates.map((gate) => gate.name).join(" & "), async (ctx) => {
|
|
153
|
+
for (const gate of gates) await runCheck(gate, ctx);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function isOrOptions(arg) {
|
|
157
|
+
return !("check" in arg);
|
|
158
|
+
}
|
|
159
|
+
function or(...args) {
|
|
160
|
+
const last = args.at(-1);
|
|
161
|
+
const options = last !== void 0 && isOrOptions(last) ? last : void 0;
|
|
162
|
+
const gates = options ? args.slice(0, -1) : args;
|
|
163
|
+
return defineGate(gates.map((gate) => gate.name).join(" | "), async (ctx) => {
|
|
164
|
+
const summaries = [];
|
|
165
|
+
let everyArmHasSummary = true;
|
|
166
|
+
for (const gate of gates) {
|
|
167
|
+
const mark = markCommits(ctx);
|
|
168
|
+
try {
|
|
169
|
+
await runCheck(gate, ctx);
|
|
170
|
+
return;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (error instanceof Notice && !(error instanceof Fault) && !error.report) {
|
|
173
|
+
rollbackCommits(ctx, mark);
|
|
174
|
+
if (error.summary === void 0) everyArmHasSummary = false;
|
|
175
|
+
else summaries.push(error.summary);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (options) throw typeof options.fail === "function" ? options.fail(ctx) : options.fail;
|
|
182
|
+
if (everyArmHasSummary && summaries.length > 0) throw new NeedsAny(summaries);
|
|
183
|
+
throw new NotAllowed();
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region src/gates/catalog/access.ts
|
|
189
|
+
/**
|
|
190
|
+
* Passes only for a user id listed in `config.ownerIds`, else refuses.
|
|
191
|
+
*
|
|
192
|
+
* Agnostic, so it attaches to interaction and event handlers alike. With no `ownerIds` configured it refuses every caller. Pass {@link GateNoticeOptions} to reword or replace the refusal.
|
|
193
|
+
*
|
|
194
|
+
* @param options - Reword the default refusal with `message`, or replace it with `notice`.
|
|
195
|
+
*
|
|
196
|
+
* @see the `@Gated` decorator from `seedcord`
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```ts
|
|
200
|
+
* \@Gated(OwnerOnly())
|
|
201
|
+
* \@SlashRoute('shutdown')
|
|
202
|
+
* class ShutdownHandler extends SlashHandler<'shutdown'> {
|
|
203
|
+
* async execute() {
|
|
204
|
+
* // ...
|
|
205
|
+
* }
|
|
206
|
+
* }
|
|
207
|
+
* ```
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* ```ts
|
|
211
|
+
* // reword the refusal, keeping the embed styling
|
|
212
|
+
* OwnerOnly({ message: 'Owners only.' });
|
|
213
|
+
* ```
|
|
214
|
+
*/
|
|
215
|
+
function OwnerOnly(options) {
|
|
216
|
+
return defineGate("OwnerOnly", (ctx) => {
|
|
217
|
+
const owners = ctx.core.config.ownerIds ?? [];
|
|
218
|
+
if (ctx.userId !== null && owners.includes(ctx.userId)) return;
|
|
219
|
+
throw pickNotice(options, (message) => new NotOwner(message));
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Passes inside a guild, else refuses.
|
|
224
|
+
*
|
|
225
|
+
* Agnostic, so it attaches to any handler kind. Often paired with `RequirePermissions` in an {@link and}.
|
|
226
|
+
*
|
|
227
|
+
* @param options - Reword the default refusal with `message`, or replace it with `notice`.
|
|
228
|
+
*
|
|
229
|
+
* @see the `@Gated` decorator from `seedcord`
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* ```ts
|
|
233
|
+
* import { PermissionFlagsBits } from 'discord.js';
|
|
234
|
+
*
|
|
235
|
+
* \@Gated(GuildOnly(), RequirePermissions([PermissionFlagsBits.Administrator]))
|
|
236
|
+
* \@SlashRoute('maintenance')
|
|
237
|
+
* class Maintenance extends SlashHandler<'maintenance'> {
|
|
238
|
+
* async execute() {
|
|
239
|
+
* // ...
|
|
240
|
+
* }
|
|
241
|
+
* }
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
function GuildOnly(options) {
|
|
245
|
+
return defineGate("GuildOnly", (ctx) => {
|
|
246
|
+
if (ctx.guildId) return;
|
|
247
|
+
throw pickNotice(options, (message) => new NotInGuild(message));
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Passes in a direct message, else refuses.
|
|
252
|
+
*
|
|
253
|
+
* Agnostic, so it attaches to any handler kind. The inverse of {@link GuildOnly}, so combining the two in an {@link and} can never pass.
|
|
254
|
+
*
|
|
255
|
+
* @param options - Reword the default refusal with `message`, or replace it with `notice`.
|
|
256
|
+
*
|
|
257
|
+
* @see the `@Gated` decorator from `seedcord`
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* ```ts
|
|
261
|
+
* \@Gated(DmOnly())
|
|
262
|
+
* \@SlashRoute('verify')
|
|
263
|
+
* class VerifyHandler extends SlashHandler<'verify'> {
|
|
264
|
+
* async execute() {
|
|
265
|
+
* // ...
|
|
266
|
+
* }
|
|
267
|
+
* }
|
|
268
|
+
* ```
|
|
269
|
+
*/
|
|
270
|
+
function DmOnly(options) {
|
|
271
|
+
return defineGate("DmOnly", (ctx) => {
|
|
272
|
+
if (!ctx.guildId) return;
|
|
273
|
+
throw pickNotice(options, (message) => new NotInDm(message));
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
//#endregion
|
|
278
|
+
//#region src/gates/catalog/Cooldown.ts
|
|
279
|
+
let bucketSeq = 0;
|
|
280
|
+
function scopeValue(ctx, per) {
|
|
281
|
+
if (per === "guild") return ctx.guildId ?? "global";
|
|
282
|
+
if (per === "channel") return ctx.channelId ?? "global";
|
|
283
|
+
return ctx.userId ?? "global";
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Allows `limit` uses per window, scoped by `per`. A number `duration` is **seconds**, a string is
|
|
287
|
+
* a duration like `30m` or `24h`. An unparseable string throws a **SeedcordTypeError** at construction. The
|
|
288
|
+
* slot is charged in commit, only after the whole gate set passes, so a later refusal never burns the cooldown.
|
|
289
|
+
* That split also means two requests racing the same key can both pass before either charges the slot.
|
|
290
|
+
* Each call gets its own bucket, so two handlers never share a window. Reword the refusal with {@link CooldownOptions.message}
|
|
291
|
+
* or replace it with {@link CooldownOptions.notice}.
|
|
292
|
+
*
|
|
293
|
+
* @param duration - A number is seconds, a string is a duration like `30m` or `24h`. An unparseable string throws a **SeedcordTypeError**.
|
|
294
|
+
* @param options - Sets the scope with `per`, the uses per window with `limit`, and the refusal text with `message` or `notice`.
|
|
295
|
+
*
|
|
296
|
+
* @see the `@Gated` decorator from `seedcord`
|
|
297
|
+
* @see {@link IRateLimiter}
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* ```ts
|
|
301
|
+
* // a number argument is SECONDS
|
|
302
|
+
* \@Gated(Cooldown(5))
|
|
303
|
+
* \@SlashRoute('daily')
|
|
304
|
+
* class DailyHandler extends SlashHandler<'daily'> {
|
|
305
|
+
* async execute() {
|
|
306
|
+
* // ...
|
|
307
|
+
* }
|
|
308
|
+
* }
|
|
309
|
+
* ```
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* ```ts
|
|
313
|
+
* // a string is a duration, here scoped per guild (the default is per user)
|
|
314
|
+
* Cooldown('30m', { per: 'guild' });
|
|
315
|
+
* ```
|
|
316
|
+
*
|
|
317
|
+
* @example
|
|
318
|
+
* ```ts
|
|
319
|
+
* // a burst, 3 uses per 10 minutes per user, then it refuses until a slot frees
|
|
320
|
+
* \@Gated(Cooldown('10m', { limit: 3 }))
|
|
321
|
+
* \@SlashRoute('report')
|
|
322
|
+
* class ReportHandler extends SlashHandler<'report'> {
|
|
323
|
+
* async execute() {
|
|
324
|
+
* // ...
|
|
325
|
+
* }
|
|
326
|
+
* }
|
|
327
|
+
* ```
|
|
328
|
+
*/
|
|
329
|
+
function Cooldown(duration, options) {
|
|
330
|
+
let windowMs;
|
|
331
|
+
if (typeof duration === "number") windowMs = duration * 1e3;
|
|
332
|
+
else {
|
|
333
|
+
const parsed = parseDuration(duration);
|
|
334
|
+
if (parsed === null) throw new SeedcordTypeError(SeedcordErrorCode.GateInvalidCooldownDuration, [duration]);
|
|
335
|
+
windowMs = parsed;
|
|
336
|
+
}
|
|
337
|
+
if (windowMs <= 0) throw new SeedcordTypeError(SeedcordErrorCode.GateInvalidCooldownDuration, [String(duration)]);
|
|
338
|
+
const bucket = `cooldown:${bucketSeq++}`;
|
|
339
|
+
const per = options?.per ?? "user";
|
|
340
|
+
const window = options?.limit === void 0 ? { windowMs } : {
|
|
341
|
+
windowMs,
|
|
342
|
+
limit: options.limit
|
|
343
|
+
};
|
|
344
|
+
const keyOf = (ctx) => `${bucket}:${scopeValue(ctx, per)}`;
|
|
345
|
+
return defineEffectGate("Cooldown", async (ctx) => {
|
|
346
|
+
const result = await ctx.core.rateLimiter.peek(keyOf(ctx), window);
|
|
347
|
+
if (!result.limited) return;
|
|
348
|
+
if (options?.notice) throw options.notice(result.resetAt);
|
|
349
|
+
throw new OnCooldown(result.resetAt, options?.message?.(result.resetAt));
|
|
350
|
+
}, async (ctx) => {
|
|
351
|
+
await ctx.core.rateLimiter.charge(keyOf(ctx), window);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
43
355
|
//#endregion
|
|
44
356
|
//#region src/stops/Silence.ts
|
|
45
357
|
/**
|
|
@@ -98,8 +410,8 @@ function paginate(items, page, perPage) {
|
|
|
98
410
|
//#endregion
|
|
99
411
|
//#region src/index.ts
|
|
100
412
|
/** Package version */
|
|
101
|
-
const version = "0.1.0-next.
|
|
413
|
+
const version = "0.1.0-next.2";
|
|
102
414
|
|
|
103
415
|
//#endregion
|
|
104
|
-
export { BuilderComponent, CustomId, Fault, Notice, RowComponent, Silence, paginate, version };
|
|
416
|
+
export { BuilderComponent, Cooldown, CustomId, DmOnly, Fault, GuildOnly, Notice, OwnerOnly, RegisterCommand, RowComponent, Silence, and, defineEffectGate, defineGate, or, paginate, version };
|
|
105
417
|
//# sourceMappingURL=index.mjs.map
|