@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
|
@@ -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
|