@seedcord/gateway 0.1.0-next.2 → 0.1.0-next.4
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/index.cjs +875 -1107
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +397 -768
- package/dist/index.mjs +871 -1081
- package/dist/index.mjs.map +1 -1
- package/package.json +15 -13
package/dist/index.cjs
CHANGED
|
@@ -28,28 +28,30 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
//#endregion
|
|
29
29
|
require("reflect-metadata");
|
|
30
30
|
let _seedcord_logger_node = require("@seedcord/logger/node");
|
|
31
|
-
let _seedcord_core = require("@seedcord/core");
|
|
32
31
|
let _seedcord_core_internal = require("@seedcord/core/internal");
|
|
33
|
-
let discord_js = require("discord.js");
|
|
34
32
|
let _seedcord_errors = require("@seedcord/errors");
|
|
35
33
|
let _seedcord_errors_internal = require("@seedcord/errors/internal");
|
|
36
|
-
let
|
|
34
|
+
let _seedcord_core = require("@seedcord/core");
|
|
37
35
|
let _seedcord_logger = require("@seedcord/logger");
|
|
38
36
|
let _discordjs_builders = require("@discordjs/builders");
|
|
39
37
|
let _seedcord_utils_internal = require("@seedcord/utils/internal");
|
|
40
|
-
let
|
|
41
|
-
let
|
|
42
|
-
|
|
43
|
-
let _seedcord_types_internal = require("@seedcord/types/internal");
|
|
44
|
-
let envapt = require("envapt");
|
|
38
|
+
let discord_js = require("discord.js");
|
|
39
|
+
let node_crypto = require("node:crypto");
|
|
40
|
+
node_crypto = __toESM(node_crypto, 1);
|
|
45
41
|
let chalk = require("chalk");
|
|
46
42
|
chalk = __toESM(chalk, 1);
|
|
43
|
+
let envapt = require("envapt");
|
|
44
|
+
let discord_api_types_v10 = require("discord-api-types/v10");
|
|
45
|
+
let _seedcord_utils = require("@seedcord/utils");
|
|
46
|
+
let _discordjs_rest = require("@discordjs/rest");
|
|
47
|
+
let _seedcord_core_node_internal = require("@seedcord/core/node/internal");
|
|
48
|
+
let _seedcord_rate_limiter = require("@seedcord/rate-limiter");
|
|
49
|
+
let _seedcord_types_internal = require("@seedcord/types/internal");
|
|
50
|
+
let _seedcord_event_emitter = require("@seedcord/event-emitter");
|
|
47
51
|
let envapt_legacy = require("envapt/legacy");
|
|
48
52
|
let node_path = require("node:path");
|
|
49
53
|
let _seedcord_core_hmr = require("@seedcord/core/hmr");
|
|
50
54
|
let _seedcord_utils_node = require("@seedcord/utils/node");
|
|
51
|
-
let node_crypto = require("node:crypto");
|
|
52
|
-
node_crypto = __toESM(node_crypto, 1);
|
|
53
55
|
|
|
54
56
|
//#region src/logger-bootstrap.ts
|
|
55
57
|
(0, _seedcord_logger_node.installNodeDefaults)();
|
|
@@ -147,190 +149,6 @@ function Gated(...gates) {
|
|
|
147
149
|
};
|
|
148
150
|
}
|
|
149
151
|
|
|
150
|
-
//#endregion
|
|
151
|
-
//#region src/miscellaneous/areRoutes.ts
|
|
152
|
-
/**
|
|
153
|
-
* Checks if the provided value is an array of strings.
|
|
154
|
-
*
|
|
155
|
-
* @internal
|
|
156
|
-
*/
|
|
157
|
-
function areRoutes(routes) {
|
|
158
|
-
return Array.isArray(routes) && routes.every((r) => typeof r === "string");
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
//#endregion
|
|
162
|
-
//#region src/bot/decorators/Interactions.ts
|
|
163
|
-
/**
|
|
164
|
-
* Types of select menus supported for routing
|
|
165
|
-
*/
|
|
166
|
-
let SelectMenuKind = /* @__PURE__ */ function(SelectMenuKind) {
|
|
167
|
-
SelectMenuKind["String"] = "string";
|
|
168
|
-
SelectMenuKind["User"] = "user";
|
|
169
|
-
SelectMenuKind["Role"] = "role";
|
|
170
|
-
SelectMenuKind["Channel"] = "channel";
|
|
171
|
-
SelectMenuKind["Mentionable"] = "mentionable";
|
|
172
|
-
return SelectMenuKind;
|
|
173
|
-
}({});
|
|
174
|
-
/**
|
|
175
|
-
* Routes one or more slash commands to a {@link SlashHandler}.
|
|
176
|
-
*
|
|
177
|
-
* Pass the same route string(s) as the handler's generic. A single command reads `this.options`, several
|
|
178
|
-
* commands branch with `this.match`. The decorator routes and the generic must match exactly, so listing
|
|
179
|
-
* fewer or more than the handler declares is a compile error. Subcommands use a slash path.
|
|
180
|
-
*
|
|
181
|
-
* @param routes - The route string(s) this handler serves, e.g. `'ban'`, `'ban', 'kick'`, or `'demo/setup'`.
|
|
182
|
-
* @decorator
|
|
183
|
-
* @example
|
|
184
|
-
* ```ts
|
|
185
|
-
* \@SlashRoute('ban')
|
|
186
|
-
* class BanHandler extends SlashHandler<'ban'> {
|
|
187
|
-
* async execute() {
|
|
188
|
-
* const target = this.options.getUser('target');
|
|
189
|
-
* }
|
|
190
|
-
* }
|
|
191
|
-
* ```
|
|
192
|
-
*
|
|
193
|
-
* @example
|
|
194
|
-
* ```ts
|
|
195
|
-
* \@SlashRoute('ban', 'kick')
|
|
196
|
-
* class ModerationHandler extends SlashHandler<'ban' | 'kick'> {
|
|
197
|
-
* async execute() {
|
|
198
|
-
* await this.match({ ban: (o) => o.getUser('target'), kick: (o) => o.getUser('member') });
|
|
199
|
-
* }
|
|
200
|
-
* }
|
|
201
|
-
* ```
|
|
202
|
-
*/
|
|
203
|
-
function SlashRoute(...routes) {
|
|
204
|
-
return function(constructor) {
|
|
205
|
-
storeMetadata(_seedcord_core_internal.InteractionRoutes.Slash, routes, constructor);
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Routes button interactions to handler classes.
|
|
210
|
-
*
|
|
211
|
-
* Pass the {@link CustomId} definition(s) this handler decodes and list the same ones in the handler's
|
|
212
|
-
* generic. Passing different definitions to the decorator and the generic is a compile error. Routing
|
|
213
|
-
* matches the stable prefix, so a wire minted from an older shape still reaches the handler, where
|
|
214
|
-
* reading this.params throws StaleCustomId and the controller boundary turns it into a reply.
|
|
215
|
-
*
|
|
216
|
-
* @param defs - The customId definition(s) this handler decodes, one per route.
|
|
217
|
-
* @decorator
|
|
218
|
-
*/
|
|
219
|
-
function ButtonRoute(...defs) {
|
|
220
|
-
return function(constructor) {
|
|
221
|
-
storeComponentRoute(_seedcord_core_internal.InteractionRoutes.Button, defs, constructor);
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* Routes modal submissions to handler classes.
|
|
226
|
-
*
|
|
227
|
-
* Pass the {@link CustomId} definition(s) this handler decodes and list the same ones in the handler's
|
|
228
|
-
* generic. Passing different definitions to the decorator and the generic is a compile error. Routing
|
|
229
|
-
* matches the stable prefix, so an older-shape wire still reaches the handler and throws StaleCustomId
|
|
230
|
-
* on read, the same as a button route.
|
|
231
|
-
*
|
|
232
|
-
* @param defs - The customId definition(s) this handler decodes, one per route.
|
|
233
|
-
* @decorator
|
|
234
|
-
*/
|
|
235
|
-
function ModalRoute(...defs) {
|
|
236
|
-
return function(constructor) {
|
|
237
|
-
storeComponentRoute(_seedcord_core_internal.InteractionRoutes.Modal, defs, constructor);
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* Routes one or more context-menu commands to a {@link ContextMenuHandler}.
|
|
242
|
-
*
|
|
243
|
-
* Pass the kind (`ApplicationCommandType.User` or `ApplicationCommandType.Message`) and the command name(s),
|
|
244
|
-
* each checked against that kind's registry, so a typo is a compile error. The same kind must sit on the
|
|
245
|
-
* handler's generic, cross-checked both directions. Context menus carry no options, so a multi-name handler
|
|
246
|
-
* reads `this.target` uniformly with no branch.
|
|
247
|
-
*
|
|
248
|
-
* @param kind - The context-menu kind, `ApplicationCommandType.User` or `ApplicationCommandType.Message`.
|
|
249
|
-
* @param names - The command name(s) this handler serves, keyed off the matching registry.
|
|
250
|
-
* @decorator
|
|
251
|
-
* @example
|
|
252
|
-
* ```ts
|
|
253
|
-
* \@ContextMenuRoute(ApplicationCommandType.User, 'View Profile')
|
|
254
|
-
* class ViewProfile extends ContextMenuHandler<ApplicationCommandType.User> {
|
|
255
|
-
* async execute() {
|
|
256
|
-
* const user = this.target;
|
|
257
|
-
* }
|
|
258
|
-
* }
|
|
259
|
-
* ```
|
|
260
|
-
*/
|
|
261
|
-
function ContextMenuRoute(kind, ...names) {
|
|
262
|
-
return function(constructor) {
|
|
263
|
-
storeMetadata(kind === discord_js.ApplicationCommandType.User ? _seedcord_core_internal.InteractionRoutes.UserContextMenu : _seedcord_core_internal.InteractionRoutes.MessageContextMenu, names, constructor);
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
/**
|
|
267
|
-
* Routes one or more commands' autocomplete to an {@link AutocompleteHandler}.
|
|
268
|
-
*
|
|
269
|
-
* Pass the same command route string(s) as the handler's generic. One command branches with `this.match`
|
|
270
|
-
* over its autocompletable fields, several commands share one handler whose arms span every command's
|
|
271
|
-
* fields. The decorator routes and the generic must match exactly, so listing fewer or more than the
|
|
272
|
-
* handler declares is a compile error.
|
|
273
|
-
*
|
|
274
|
-
* @param routes - The command route string(s) this handler serves, e.g. `'search'` or `'search', 'find'`.
|
|
275
|
-
* @decorator
|
|
276
|
-
* @example
|
|
277
|
-
* ```ts
|
|
278
|
-
* \@AutocompleteRoute('search')
|
|
279
|
-
* class SearchAutocomplete extends AutocompleteHandler<'search'> {
|
|
280
|
-
* async execute() {
|
|
281
|
-
* await this.match({ query: (value, respond) => respond([{ name: value, value }]) });
|
|
282
|
-
* }
|
|
283
|
-
* }
|
|
284
|
-
* ```
|
|
285
|
-
*/
|
|
286
|
-
function AutocompleteRoute(...routes) {
|
|
287
|
-
return function(constructor) {
|
|
288
|
-
storeMetadata(_seedcord_core_internal.InteractionRoutes.Autocomplete, routes, constructor);
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Routes select menu interactions to handler classes.
|
|
293
|
-
*
|
|
294
|
-
* Pass the select kind and the {@link CustomId} definition(s) this handler decodes. The handler's
|
|
295
|
-
* generic must list the same definitions, and its second generic argument must be the matching select
|
|
296
|
-
* interaction type, or it is a compile error.
|
|
297
|
-
*
|
|
298
|
-
* @param type - Select menu kind from {@link SelectMenuKind}.
|
|
299
|
-
* @param defs - The customId definition(s) this handler decodes, one per route.
|
|
300
|
-
* @decorator
|
|
301
|
-
*
|
|
302
|
-
* @example
|
|
303
|
-
* ```typescript
|
|
304
|
-
* \@SelectMenuRoute(SelectMenuKind.User, AssignId)
|
|
305
|
-
* class AssignSelect extends SelectMenuHandler<SelectMenuKind.User, [typeof AssignId]> {
|
|
306
|
-
* // handles user select menus minted from AssignId
|
|
307
|
-
* }
|
|
308
|
-
* ```
|
|
309
|
-
*/
|
|
310
|
-
function SelectMenuRoute(type, ...defs) {
|
|
311
|
-
return function(constructor) {
|
|
312
|
-
storeComponentRoute({
|
|
313
|
-
["string"]: _seedcord_core_internal.InteractionRoutes.StringMenu,
|
|
314
|
-
["user"]: _seedcord_core_internal.InteractionRoutes.UserMenu,
|
|
315
|
-
["role"]: _seedcord_core_internal.InteractionRoutes.RoleMenu,
|
|
316
|
-
["channel"]: _seedcord_core_internal.InteractionRoutes.ChannelMenu,
|
|
317
|
-
["mentionable"]: _seedcord_core_internal.InteractionRoutes.MentionableMenu
|
|
318
|
-
}[type], defs, constructor);
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
function storeComponentRoute(route, defs, constructor) {
|
|
322
|
-
storeMetadata(route, defs.map((def) => def.prefix), constructor);
|
|
323
|
-
Reflect.defineMetadata(_seedcord_core_internal.ComponentDefsKey, defs, constructor);
|
|
324
|
-
}
|
|
325
|
-
function storeMetadata(route, routes, constructor) {
|
|
326
|
-
const key = _seedcord_core_internal.InteractionRouteKeys[route];
|
|
327
|
-
const savedRoutes = Reflect.getMetadata(key, constructor);
|
|
328
|
-
const existing = areRoutes(savedRoutes) ? savedRoutes : [];
|
|
329
|
-
const toStore = Array.isArray(routes) ? routes : [routes];
|
|
330
|
-
Reflect.defineMetadata(key, [...existing, ...toStore], constructor);
|
|
331
|
-
Reflect.defineMetadata(_seedcord_core_internal.InteractionMetadataKey, true, constructor);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
152
|
//#endregion
|
|
335
153
|
//#region src/bot/decorators/Middlewares.ts
|
|
336
154
|
/**
|
|
@@ -399,21 +217,6 @@ const IgnoreBots = (0, _seedcord_core.defineGate)("IgnoreBots", (ctx) => {
|
|
|
399
217
|
if (ctx.user?.bot) throw new _seedcord_core.Silence("actor is a bot");
|
|
400
218
|
});
|
|
401
219
|
|
|
402
|
-
//#endregion
|
|
403
|
-
//#region src/bot/notices/utils.ts
|
|
404
|
-
function mentionFor(subject) {
|
|
405
|
-
if (subject instanceof discord_js.Role) return `<@&${subject.id}>`;
|
|
406
|
-
if (subject instanceof discord_js.TextChannel) return `<#${subject.id}>`;
|
|
407
|
-
if (subject instanceof discord_js.GuildMember) return `<@${subject.id}>`;
|
|
408
|
-
return `\`${subject.name}\``;
|
|
409
|
-
}
|
|
410
|
-
function labelFor(subject) {
|
|
411
|
-
if (subject instanceof discord_js.Role) return "role";
|
|
412
|
-
if (subject instanceof discord_js.TextChannel) return "channel";
|
|
413
|
-
if (subject instanceof discord_js.GuildMember) return "member";
|
|
414
|
-
return "guild";
|
|
415
|
-
}
|
|
416
|
-
|
|
417
220
|
//#endregion
|
|
418
221
|
//#region src/bot/notices/index.ts
|
|
419
222
|
var NotNsfw = class extends _seedcord_core_internal.GateNotice {
|
|
@@ -421,13 +224,6 @@ var NotNsfw = class extends _seedcord_core_internal.GateNotice {
|
|
|
421
224
|
super(message);
|
|
422
225
|
}
|
|
423
226
|
};
|
|
424
|
-
var MissingRole = class extends _seedcord_core_internal.GateNotice {
|
|
425
|
-
role;
|
|
426
|
-
constructor(message, role) {
|
|
427
|
-
super(message ?? (role ? `You need the ${role.name} role to use this.` : "You do not have the required role."));
|
|
428
|
-
this.role = role;
|
|
429
|
-
}
|
|
430
|
-
};
|
|
431
227
|
var RoleHigherThanMe = class extends _seedcord_core.Notice {
|
|
432
228
|
role;
|
|
433
229
|
botRole;
|
|
@@ -448,30 +244,19 @@ var CannotAssignBotRole = class extends _seedcord_core.Notice {
|
|
|
448
244
|
return { components: [new _seedcord_core_internal.NoticeCard("I cannot assign a managed role.").component] };
|
|
449
245
|
}
|
|
450
246
|
};
|
|
451
|
-
var MissingPermissions = class extends _seedcord_core.Notice {
|
|
452
|
-
where;
|
|
453
|
-
missingPerms;
|
|
454
|
-
constructor(message, where, missingPerms) {
|
|
455
|
-
super(message);
|
|
456
|
-
this.where = where;
|
|
457
|
-
this.missingPerms = missingPerms;
|
|
458
|
-
}
|
|
459
|
-
render() {
|
|
460
|
-
const bullets = this.missingPerms.map((perm) => `• ${perm}`).join("\n");
|
|
461
|
-
return { components: [new _seedcord_core_internal.NoticeCard(`The ${labelFor(this.where)} ${mentionFor(this.where)} is missing the following permission entries:\n\n${bullets}`).component] };
|
|
462
|
-
}
|
|
463
|
-
};
|
|
464
247
|
var HasDangerousPermissions = class extends _seedcord_core.Notice {
|
|
465
|
-
|
|
248
|
+
subject;
|
|
466
249
|
dangerousPerms;
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
250
|
+
customLead;
|
|
251
|
+
constructor(message, subject, dangerousPerms) {
|
|
252
|
+
super(message ?? "A dangerous permission is enabled.");
|
|
253
|
+
this.subject = subject;
|
|
470
254
|
this.dangerousPerms = dangerousPerms;
|
|
255
|
+
this.customLead = message;
|
|
471
256
|
}
|
|
472
257
|
render() {
|
|
473
258
|
const bullets = this.dangerousPerms.map((perm) => `• ${perm}`).join("\n");
|
|
474
|
-
return { components: [new _seedcord_core_internal.NoticeCard(
|
|
259
|
+
return { components: [new _seedcord_core_internal.NoticeCard(`${this.customLead ?? `${this.subject} has the following permission entries that must not be enabled:`}\n\n${bullets}`).component] };
|
|
475
260
|
}
|
|
476
261
|
};
|
|
477
262
|
var UserNotFound = class extends _seedcord_core.Notice {
|
|
@@ -547,129 +332,6 @@ function Nsfw(options) {
|
|
|
547
332
|
});
|
|
548
333
|
}
|
|
549
334
|
|
|
550
|
-
//#endregion
|
|
551
|
-
//#region src/bot/utilities/permissions/checkPermissions.ts
|
|
552
|
-
/**
|
|
553
|
-
* Map of permission bits to their prettified human readable names.
|
|
554
|
-
*/
|
|
555
|
-
const PermissionNames = new Map(Object.entries(discord_js.PermissionFlagsBits).map(([key, bit]) => [bit, (0, _seedcord_utils.prettify)(key)]));
|
|
556
|
-
function checkPermissions(a, b, c, d, e) {
|
|
557
|
-
const { for: pFor, in: pIn, scope, inverse = false, missing: missingCtor, dangerous: dangerousCtor } = a instanceof discord_js.Role || a instanceof discord_js.GuildMember ? {
|
|
558
|
-
for: a,
|
|
559
|
-
in: b,
|
|
560
|
-
scope: c,
|
|
561
|
-
inverse: d ?? false,
|
|
562
|
-
...e
|
|
563
|
-
} : a;
|
|
564
|
-
const Missing = missingCtor ?? MissingPermissions;
|
|
565
|
-
const Dangerous = dangerousCtor ?? HasDangerousPermissions;
|
|
566
|
-
const perms = pIn instanceof discord_js.Guild ? pFor.permissions : pIn.permissionsFor(pFor, true);
|
|
567
|
-
const names = (bits) => bits.map((bit) => PermissionNames.get(bit) ?? String(bit));
|
|
568
|
-
const present = scope.filter((bit) => perms.has(bit, true));
|
|
569
|
-
const presentNames = names(present);
|
|
570
|
-
if (inverse) {
|
|
571
|
-
if (present.length > 0) throw new Dangerous(`${pFor instanceof discord_js.Role ? "Role" : "Member"} has dangerous permissions in this ${pIn instanceof discord_js.Guild ? "guild" : "channel"}`, pFor, presentNames);
|
|
572
|
-
return;
|
|
573
|
-
}
|
|
574
|
-
const missingBits = scope.filter((bit) => !perms.has(bit, true));
|
|
575
|
-
if (missingBits.length > 0) throw new Missing("Missing Any/All/No Permissions", pFor, names(missingBits));
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
//#endregion
|
|
579
|
-
//#region src/bot/gates/catalog/permissions.ts
|
|
580
|
-
const UNCACHED_MEMBER = "Your server member data could not be resolved. Try again.";
|
|
581
|
-
/**
|
|
582
|
-
* Requires the caller to hold every permission in `scope`, via `checkPermissions`. Refuses outside a guild.
|
|
583
|
-
*
|
|
584
|
-
* Interaction-only and excludes ModalSubmit, since a modal lacks a reliable cached caller member.
|
|
585
|
-
*
|
|
586
|
-
* @param scope - The permission flag bits the caller must all hold.
|
|
587
|
-
* @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-permissions one with `missing`.
|
|
588
|
-
*
|
|
589
|
-
* @see {@link Gated}
|
|
590
|
-
*
|
|
591
|
-
* @example
|
|
592
|
-
* ```ts
|
|
593
|
-
* import { PermissionFlagsBits } from 'discord.js';
|
|
594
|
-
*
|
|
595
|
-
* \@Gated(RequirePermissions([PermissionFlagsBits.BanMembers]))
|
|
596
|
-
* \@SlashRoute('ban')
|
|
597
|
-
* class BanHandler extends SlashHandler<'ban'> {
|
|
598
|
-
* async execute() {
|
|
599
|
-
* const target = this.options.getUser('target');
|
|
600
|
-
* }
|
|
601
|
-
* }
|
|
602
|
-
* ```
|
|
603
|
-
*/
|
|
604
|
-
function RequirePermissions(scope, options) {
|
|
605
|
-
return (0, _seedcord_core.defineGate)("RequirePermissions", (ctx) => {
|
|
606
|
-
if (!ctx.guild) throw (0, _seedcord_core_internal.pickNotice)(options?.notInGuild, (message) => new _seedcord_core_internal.NotInGuild(message));
|
|
607
|
-
if (!ctx.member) throw (0, _seedcord_core_internal.pickNotice)(options?.notInGuild, (message) => new _seedcord_core_internal.NotInGuild(message ?? UNCACHED_MEMBER));
|
|
608
|
-
checkPermissions(ctx.member, ctx.guild, scope, false, options?.missing ? { missing: options.missing } : void 0);
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
/**
|
|
612
|
-
* Requires the bot to hold every permission in `scope`, via `checkPermissions`. Refuses outside a guild.
|
|
613
|
-
*
|
|
614
|
-
* It reads the bot's own member, so it attaches to modal handlers as well.
|
|
615
|
-
*
|
|
616
|
-
* @param scope - The permission flag bits the bot must all hold.
|
|
617
|
-
* @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-permissions one with `missing`.
|
|
618
|
-
*
|
|
619
|
-
* @see {@link Gated}
|
|
620
|
-
*
|
|
621
|
-
* @example
|
|
622
|
-
* ```ts
|
|
623
|
-
* import { PermissionFlagsBits } from 'discord.js';
|
|
624
|
-
*
|
|
625
|
-
* \@Gated(RequireBotPermissions([PermissionFlagsBits.BanMembers]))
|
|
626
|
-
* \@ModalRoute(MyModalId)
|
|
627
|
-
* class MyModalHandler extends ModalHandler<[typeof MyModalId]> {
|
|
628
|
-
* async execute() {
|
|
629
|
-
* // ...
|
|
630
|
-
* }
|
|
631
|
-
* }
|
|
632
|
-
* ```
|
|
633
|
-
*/
|
|
634
|
-
function RequireBotPermissions(scope, options) {
|
|
635
|
-
return (0, _seedcord_core.defineGate)("RequireBotPermissions", (ctx) => {
|
|
636
|
-
const botMember = ctx.guild?.members.me;
|
|
637
|
-
if (!ctx.guild || !botMember) throw (0, _seedcord_core_internal.pickNotice)(options?.notInGuild, (message) => new _seedcord_core_internal.NotInGuild(message));
|
|
638
|
-
checkPermissions(botMember, ctx.guild, scope, false, options?.missing ? { missing: options.missing } : void 0);
|
|
639
|
-
});
|
|
640
|
-
}
|
|
641
|
-
/**
|
|
642
|
-
* Requires the caller to have `roleId`, read from the member's role cache. Refuses outside a guild first, then
|
|
643
|
-
* when the role is missing.
|
|
644
|
-
*
|
|
645
|
-
* Interaction-only and excludes ModalSubmit.
|
|
646
|
-
*
|
|
647
|
-
* @param roleId - The role snowflake the caller must hold, read from the member's role cache.
|
|
648
|
-
* @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-role one with `missingRole`.
|
|
649
|
-
*
|
|
650
|
-
* @see {@link Gated}
|
|
651
|
-
*
|
|
652
|
-
* @example
|
|
653
|
-
* ```ts
|
|
654
|
-
* \@Gated(RequireRole('123456789012345678'))
|
|
655
|
-
* \@SlashRoute('vip')
|
|
656
|
-
* class VipHandler extends SlashHandler<'vip'> {
|
|
657
|
-
* async execute() {
|
|
658
|
-
* // ...
|
|
659
|
-
* }
|
|
660
|
-
* }
|
|
661
|
-
* ```
|
|
662
|
-
*/
|
|
663
|
-
function RequireRole(roleId, options) {
|
|
664
|
-
return (0, _seedcord_core.defineGate)("RequireRole", (ctx) => {
|
|
665
|
-
if (!ctx.guild) throw (0, _seedcord_core_internal.pickNotice)(options?.notInGuild, (message) => new _seedcord_core_internal.NotInGuild(message));
|
|
666
|
-
if (!ctx.member) throw (0, _seedcord_core_internal.pickNotice)(options?.notInGuild, (message) => new _seedcord_core_internal.NotInGuild(message ?? UNCACHED_MEMBER));
|
|
667
|
-
if (ctx.member.roles.cache.has(roleId)) return;
|
|
668
|
-
const role = ctx.guild.roles.cache.get(roleId) ?? null;
|
|
669
|
-
throw (0, _seedcord_core_internal.pickNotice)(options?.missingRole, (message) => new MissingRole(message, role));
|
|
670
|
-
});
|
|
671
|
-
}
|
|
672
|
-
|
|
673
335
|
//#endregion
|
|
674
336
|
//#region src/bot/injectors/EmojiInjector.ts
|
|
675
337
|
const emojiStorage = {};
|
|
@@ -858,6 +520,37 @@ async function fetchText(client, channelId) {
|
|
|
858
520
|
throw new CouldNotFindChannel("Channel not found or not a text channel", channelId);
|
|
859
521
|
}
|
|
860
522
|
|
|
523
|
+
//#endregion
|
|
524
|
+
//#region src/bot/notices/utils.ts
|
|
525
|
+
function mentionFor(subject) {
|
|
526
|
+
if (subject instanceof discord_js.Role) return `<@&${subject.id}>`;
|
|
527
|
+
return `<@${subject.id}>`;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
//#endregion
|
|
531
|
+
//#region src/bot/utilities/permissions/checkPermissions.ts
|
|
532
|
+
function checkPermissions(a, b, c, d, e) {
|
|
533
|
+
const { for: pFor, in: pIn, scope, inverse = false, missingNotice: missingCtor, dangerousNotice: dangerousCtor } = a instanceof discord_js.Role || a instanceof discord_js.GuildMember ? {
|
|
534
|
+
for: a,
|
|
535
|
+
in: b,
|
|
536
|
+
scope: c,
|
|
537
|
+
inverse: d ?? false,
|
|
538
|
+
...e
|
|
539
|
+
} : a;
|
|
540
|
+
const Missing = missingCtor ?? _seedcord_core_internal.MissingPermissions;
|
|
541
|
+
const Dangerous = dangerousCtor ?? HasDangerousPermissions;
|
|
542
|
+
const subject = mentionFor(pFor);
|
|
543
|
+
const perms = pIn instanceof discord_js.Guild ? pFor.permissions : pIn.permissionsFor(pFor, true);
|
|
544
|
+
const names = (bits) => bits.map((bit) => _seedcord_core_internal.PermissionNames.get(bit) ?? String(bit));
|
|
545
|
+
if (inverse) {
|
|
546
|
+
const present = scope.filter((bit) => perms.has(bit, true));
|
|
547
|
+
if (present.length > 0) throw new Dangerous(void 0, subject, names(present));
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const missingBits = scope.filter((bit) => !perms.has(bit, true));
|
|
551
|
+
if (missingBits.length > 0) throw new Missing(void 0, subject, names(missingBits));
|
|
552
|
+
}
|
|
553
|
+
|
|
861
554
|
//#endregion
|
|
862
555
|
//#region src/bot/utilities/permissions/checkBotPermissions.ts
|
|
863
556
|
/**
|
|
@@ -885,8 +578,8 @@ function checkBotPermissions(target, scope, inverse = false, errors) {
|
|
|
885
578
|
if (target instanceof discord_js.Guild) {
|
|
886
579
|
const me = target.members.me;
|
|
887
580
|
if (!me) {
|
|
888
|
-
const names = scope.map((bit) => PermissionNames.get(bit) ?? String(bit));
|
|
889
|
-
throw new ((errors?.
|
|
581
|
+
const names = scope.map((bit) => _seedcord_core_internal.PermissionNames.get(bit) ?? String(bit));
|
|
582
|
+
throw new ((errors?.missingNotice) ?? _seedcord_core_internal.MissingPermissions)(void 0, "The bot", names);
|
|
890
583
|
}
|
|
891
584
|
checkPermissions(me, target, scope, inverse, errors);
|
|
892
585
|
return;
|
|
@@ -894,8 +587,8 @@ function checkBotPermissions(target, scope, inverse = false, errors) {
|
|
|
894
587
|
const me = target.guild.members.me;
|
|
895
588
|
if (!me) {
|
|
896
589
|
new _seedcord_logger.Logger("checkBotPermissions").warn(`Bot member is unavailable in guild ${target.guild.id} while checking permissions in channel ${target.id}`);
|
|
897
|
-
const names = scope.map((bit) => PermissionNames.get(bit) ?? String(bit));
|
|
898
|
-
throw new ((errors?.
|
|
590
|
+
const names = scope.map((bit) => _seedcord_core_internal.PermissionNames.get(bit) ?? String(bit));
|
|
591
|
+
throw new ((errors?.missingNotice) ?? _seedcord_core_internal.MissingPermissions)(void 0, "The bot", names);
|
|
899
592
|
}
|
|
900
593
|
checkPermissions(me, target, scope, inverse, errors);
|
|
901
594
|
}
|
|
@@ -932,26 +625,26 @@ function getBotRole(guild) {
|
|
|
932
625
|
* // Check with custom error handling
|
|
933
626
|
* hasPermsToAssign({
|
|
934
627
|
* targetRole: role,
|
|
935
|
-
*
|
|
936
|
-
*
|
|
937
|
-
*
|
|
938
|
-
*
|
|
628
|
+
* noticeOverrides: {
|
|
629
|
+
* higherNotice: CustomHigherNotice,
|
|
630
|
+
* managedNotice: CustomManagedNotice,
|
|
631
|
+
* missingNotice: CustomMissingPermissionsNotice
|
|
939
632
|
* }
|
|
940
633
|
* });
|
|
941
634
|
* ```
|
|
942
635
|
*/
|
|
943
636
|
function hasPermsToAssign(roleOrOptions) {
|
|
944
|
-
const { targetRole, errors } = roleOrOptions instanceof discord_js.Role ? {
|
|
637
|
+
const { targetRole, noticeOverrides: errors } = roleOrOptions instanceof discord_js.Role ? {
|
|
945
638
|
targetRole: roleOrOptions,
|
|
946
|
-
|
|
639
|
+
noticeOverrides: void 0
|
|
947
640
|
} : roleOrOptions;
|
|
948
|
-
const
|
|
949
|
-
const
|
|
950
|
-
const
|
|
641
|
+
const HigherNotice = errors?.higherNotice ?? RoleHigherThanMe;
|
|
642
|
+
const ManagedNotice = errors?.managedNotice ?? CannotAssignBotRole;
|
|
643
|
+
const MissingNotice = errors?.missingNotice ?? _seedcord_core_internal.MissingPermissions;
|
|
951
644
|
const botRole = getBotRole(targetRole.guild);
|
|
952
|
-
if (targetRole.comparePositionTo(botRole) >= 0) throw new
|
|
953
|
-
if (targetRole.managed) throw new
|
|
954
|
-
checkBotPermissions(targetRole.guild, [discord_js.PermissionFlagsBits.ManageRoles], false, {
|
|
645
|
+
if (targetRole.comparePositionTo(botRole) >= 0) throw new HigherNotice("Role is higher than me", targetRole, botRole);
|
|
646
|
+
if (targetRole.managed) throw new ManagedNotice(`Cannot assign managed role ${targetRole.name}`);
|
|
647
|
+
checkBotPermissions(targetRole.guild, [discord_js.PermissionFlagsBits.ManageRoles], false, { missingNotice: MissingNotice });
|
|
955
648
|
}
|
|
956
649
|
|
|
957
650
|
//#endregion
|
|
@@ -1078,115 +771,310 @@ async function updateMemberRoles(rolesToAdd, rolesToRemove, member) {
|
|
|
1078
771
|
await member.roles.set([...updated]);
|
|
1079
772
|
}
|
|
1080
773
|
|
|
1081
|
-
//#endregion
|
|
1082
|
-
//#region src/bot/harmlessApiCodes.ts
|
|
1083
|
-
/**
|
|
1084
|
-
* Discord API error codes that mean a dead end on an interaction, a dead token, a double ack, or a gone
|
|
1085
|
-
* message. There is nothing to show the user and no bug to report. The controller boundary drops them on
|
|
1086
|
-
* the original throw, and {@link ReplySender} drops them on its own send.
|
|
1087
|
-
*
|
|
1088
|
-
* @internal
|
|
1089
|
-
*/
|
|
1090
|
-
const HARMLESS_API_CODES = new Set([
|
|
1091
|
-
discord_js.RESTJSONErrorCodes.UnknownInteraction,
|
|
1092
|
-
discord_js.RESTJSONErrorCodes.InteractionHasAlreadyBeenAcknowledged,
|
|
1093
|
-
discord_js.RESTJSONErrorCodes.UnknownMessage
|
|
1094
|
-
]);
|
|
1095
|
-
|
|
1096
|
-
//#endregion
|
|
1097
|
-
//#region src/miscellaneous/flagsFor.ts
|
|
1098
|
-
/**
|
|
1099
|
-
* Builds the `flags` bitfield for a framework reply. Every framework reply is ComponentsV2, so the
|
|
1100
|
-
* IsComponentsV2 flag is always set.
|
|
1101
|
-
*
|
|
1102
|
-
* @param ephemeral - Whether the reply is ephemeral.
|
|
1103
|
-
* @returns The OR'd `MessageFlags` value as a plain number.
|
|
1104
|
-
*
|
|
1105
|
-
* @internal
|
|
1106
|
-
*/
|
|
1107
|
-
function flagsFor(ephemeral) {
|
|
1108
|
-
let flags = discord_js.MessageFlags.IsComponentsV2;
|
|
1109
|
-
if (ephemeral) flags |= discord_js.MessageFlags.Ephemeral;
|
|
1110
|
-
return flags;
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
774
|
//#endregion
|
|
1114
775
|
//#region src/bot/ReplySender.ts
|
|
776
|
+
function seedState(interaction) {
|
|
777
|
+
if (interaction.deferred && interaction.ephemeral === null) return "deferred-update";
|
|
778
|
+
if (interaction.replied) return "replied";
|
|
779
|
+
if (interaction.deferred) return "deferred-reply";
|
|
780
|
+
return "unacked";
|
|
781
|
+
}
|
|
1115
782
|
/**
|
|
1116
|
-
*
|
|
1117
|
-
*
|
|
1118
|
-
* controller.
|
|
783
|
+
* Writes interaction responses through the discord.js interaction. Construction is internal to the repliable
|
|
784
|
+
* handler bases and the dispatcher.
|
|
1119
785
|
*/
|
|
1120
|
-
var ReplySender = class {
|
|
786
|
+
var ReplySender = class extends _seedcord_core_internal.BaseReplySender {
|
|
1121
787
|
interaction;
|
|
1122
|
-
|
|
1123
|
-
|
|
788
|
+
constructor(interaction, routeId) {
|
|
789
|
+
super(routeId, seedState(interaction));
|
|
1124
790
|
this.interaction = interaction;
|
|
1125
791
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
* @returns The sent {@link Message}, or `undefined` when the send was swallowed.
|
|
1133
|
-
*/
|
|
1134
|
-
async send(response, ephemeral = true) {
|
|
1135
|
-
try {
|
|
1136
|
-
return await this.dispatch(response, ephemeral);
|
|
1137
|
-
} catch (error) {
|
|
1138
|
-
this.logSwallowed("send", error);
|
|
1139
|
-
return;
|
|
1140
|
-
}
|
|
792
|
+
async writeReply(response, opts) {
|
|
793
|
+
const result = await this.interaction.reply({
|
|
794
|
+
...this.replyOptions(response, (0, _seedcord_core_internal.sendFlags)(opts)),
|
|
795
|
+
withResponse: true
|
|
796
|
+
});
|
|
797
|
+
return this.createdMessage(result);
|
|
1141
798
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
* its outcome. Never throws, a failed edit is logged and dropped.
|
|
1145
|
-
*
|
|
1146
|
-
* @param message - The message to edit (the handle a prior {@link send} returned).
|
|
1147
|
-
* @param response - The reply to replace it with.
|
|
1148
|
-
*/
|
|
1149
|
-
async edit(message, response) {
|
|
1150
|
-
try {
|
|
1151
|
-
await this.interaction.webhook.editMessage(message, this.editBody(response));
|
|
1152
|
-
} catch (error) {
|
|
1153
|
-
this.logSwallowed("edit", error);
|
|
1154
|
-
}
|
|
799
|
+
async writeDefer(opts) {
|
|
800
|
+
await this.interaction.deferReply({ flags: (0, _seedcord_core_internal.deferFlags)(opts) });
|
|
1155
801
|
}
|
|
1156
|
-
async
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
802
|
+
async writeDeferUpdate() {
|
|
803
|
+
await this.sourceInteraction("deferUpdate").deferUpdate();
|
|
804
|
+
}
|
|
805
|
+
async writeUpdate(response) {
|
|
806
|
+
const result = await this.sourceInteraction("update").update({
|
|
807
|
+
...this.updateOptions(response),
|
|
808
|
+
withResponse: true
|
|
809
|
+
});
|
|
810
|
+
return this.createdMessage(result);
|
|
811
|
+
}
|
|
812
|
+
async writeFollowUp(response, opts) {
|
|
813
|
+
return await this.interaction.followUp(this.replyOptions(response, (0, _seedcord_core_internal.sendFlags)(opts)));
|
|
1164
814
|
}
|
|
1165
|
-
|
|
815
|
+
async writeEditOriginal(response) {
|
|
816
|
+
return await this.interaction.editReply(this.editOptions(response));
|
|
817
|
+
}
|
|
818
|
+
async writeEditTarget(targetId, response) {
|
|
819
|
+
return await this.interaction.webhook.editMessage(targetId, this.editOptions(response));
|
|
820
|
+
}
|
|
821
|
+
async writeDeleteOriginal() {
|
|
822
|
+
await this.interaction.deleteReply();
|
|
823
|
+
}
|
|
824
|
+
async writeDeleteTarget(targetId) {
|
|
825
|
+
await this.interaction.deleteReply(targetId);
|
|
826
|
+
}
|
|
827
|
+
async writeModal(data) {
|
|
828
|
+
const { interaction } = this;
|
|
829
|
+
if (interaction.isModalSubmit()) this.rejectModalSubmit();
|
|
830
|
+
await interaction.showModal(data);
|
|
831
|
+
}
|
|
832
|
+
guardModalSource() {
|
|
833
|
+
if (this.interaction.isModalSubmit()) this.rejectModalSubmit();
|
|
834
|
+
}
|
|
835
|
+
rejectModalSubmit() {
|
|
836
|
+
throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ReplyIllegalAckState, [
|
|
837
|
+
"showModal",
|
|
838
|
+
"this interaction is a modal submit",
|
|
839
|
+
"A modal cannot open another modal.",
|
|
840
|
+
this.routeId
|
|
841
|
+
]);
|
|
842
|
+
}
|
|
843
|
+
sourceInteraction(method) {
|
|
844
|
+
const { interaction } = this;
|
|
845
|
+
if (interaction.isMessageComponent()) return interaction;
|
|
846
|
+
if (interaction.isModalSubmit() && interaction.isFromMessage()) return interaction;
|
|
847
|
+
throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ReplyUpdateWithoutSource, [method, this.routeId]);
|
|
848
|
+
}
|
|
849
|
+
replyOptions(response, flags) {
|
|
850
|
+
const reply = this.serialize(response);
|
|
1166
851
|
return {
|
|
1167
|
-
components:
|
|
1168
|
-
flags
|
|
1169
|
-
...
|
|
1170
|
-
...
|
|
852
|
+
components: reply.components,
|
|
853
|
+
flags,
|
|
854
|
+
...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
|
|
855
|
+
...reply.files && { files: [...reply.files] }
|
|
1171
856
|
};
|
|
1172
857
|
}
|
|
1173
|
-
|
|
858
|
+
editOptions(response) {
|
|
859
|
+
const reply = this.serialize(response);
|
|
1174
860
|
return {
|
|
1175
|
-
components:
|
|
861
|
+
components: reply.components,
|
|
1176
862
|
flags: discord_js.MessageFlags.IsComponentsV2,
|
|
1177
|
-
...
|
|
1178
|
-
...
|
|
863
|
+
...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
|
|
864
|
+
...reply.files && { files: [...reply.files] }
|
|
1179
865
|
};
|
|
1180
866
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
867
|
+
updateOptions(response) {
|
|
868
|
+
const reply = this.serialize(response);
|
|
869
|
+
return {
|
|
870
|
+
components: reply.components,
|
|
871
|
+
flags: discord_js.MessageFlags.IsComponentsV2,
|
|
872
|
+
...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
|
|
873
|
+
...reply.files && { files: [...reply.files] }
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
createdMessage(result) {
|
|
877
|
+
return result.resource?.message ?? void 0;
|
|
1187
878
|
}
|
|
1188
879
|
};
|
|
1189
880
|
|
|
881
|
+
//#endregion
|
|
882
|
+
//#region src/bot/utilities/miscellaneous/slashRouteOf.ts
|
|
883
|
+
/**
|
|
884
|
+
* Computes the dispatch route for a chat-input or autocomplete interaction. The controller and
|
|
885
|
+
* `seedcord codegen` both assemble route keys through the same {@link buildSlashRoute}, so a dispatched
|
|
886
|
+
* interaction and a generated registry key always match.
|
|
887
|
+
*
|
|
888
|
+
* @internal
|
|
889
|
+
*/
|
|
890
|
+
function slashRouteOf(interaction) {
|
|
891
|
+
const group = interaction.options.getSubcommandGroup(false) ?? void 0;
|
|
892
|
+
const subcommand = interaction.options.getSubcommand(false) ?? void 0;
|
|
893
|
+
return (0, _seedcord_utils_internal.buildSlashRoute)(interaction.commandName, subcommand, group);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
//#endregion
|
|
897
|
+
//#region src/miscellaneous/FaultThrottle.ts
|
|
898
|
+
/**
|
|
899
|
+
* Drops duplicate fault reports inside a fixed window so a recurring fault (a database outage on a
|
|
900
|
+
* hot path) reports once per window instead of flooding the webhook. Keyed on a structural string, so
|
|
901
|
+
* the map stays small, and a stale entry is evicted on the next read of its key.
|
|
902
|
+
*
|
|
903
|
+
* @internal
|
|
904
|
+
*/
|
|
905
|
+
var FaultThrottle = class {
|
|
906
|
+
windowMs;
|
|
907
|
+
now;
|
|
908
|
+
lastReportedAt = /* @__PURE__ */ new Map();
|
|
909
|
+
constructor(windowMs = 6e4, now = () => Date.now()) {
|
|
910
|
+
this.windowMs = windowMs;
|
|
911
|
+
this.now = now;
|
|
912
|
+
}
|
|
913
|
+
/** Evicts the entry for this key if it has aged out, so the map never holds stale keys forever. */
|
|
914
|
+
shouldReport(key) {
|
|
915
|
+
const last = this.lastReportedAt.get(key);
|
|
916
|
+
if (last === void 0) return true;
|
|
917
|
+
if (this.now() - last < this.windowMs) return false;
|
|
918
|
+
this.lastReportedAt.delete(key);
|
|
919
|
+
return true;
|
|
920
|
+
}
|
|
921
|
+
markReported(key) {
|
|
922
|
+
this.lastReportedAt.set(key, this.now());
|
|
923
|
+
}
|
|
924
|
+
clear() {
|
|
925
|
+
this.lastReportedAt.clear();
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
//#endregion
|
|
930
|
+
//#region src/miscellaneous/extractErrorResponse.ts
|
|
931
|
+
const logger$2 = new _seedcord_logger.Logger("ErrorsHandling");
|
|
932
|
+
const faultThrottle = new FaultThrottle();
|
|
933
|
+
/**
|
|
934
|
+
* Processes an error into a user-facing reply and routes its report.
|
|
935
|
+
*
|
|
936
|
+
* A {@link Notice} always renders its own reply. A reporting denial (`report: true`) additionally
|
|
937
|
+
* publishes `handledException` on the interaction path. A raw, non-denial throw publishes
|
|
938
|
+
* `unknownException` and renders the configured `defaultError` (or a generic {@link Fault}). One uuid is
|
|
939
|
+
* threaded into both the reply and the bus payload.
|
|
940
|
+
*/
|
|
941
|
+
function extractErrorResponse(error, core, origin) {
|
|
942
|
+
const uuid = node_crypto.randomUUID();
|
|
943
|
+
const developerUsername = core.config.notifications?.developerUsername;
|
|
944
|
+
const ctx = developerUsername === void 0 ? { uuid } : {
|
|
945
|
+
uuid,
|
|
946
|
+
developerUsername
|
|
947
|
+
};
|
|
948
|
+
if (error instanceof _seedcord_core.Notice) {
|
|
949
|
+
if (error.report) reportFault(error, core, origin, uuid);
|
|
950
|
+
return {
|
|
951
|
+
uuid,
|
|
952
|
+
response: error.render(ctx)
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
reportRawFault(error, core, origin, uuid);
|
|
956
|
+
const override = core.config.errors?.defaultError;
|
|
957
|
+
return {
|
|
958
|
+
uuid,
|
|
959
|
+
response: override ? new override(uuid).render(ctx) : new _seedcord_core.Fault().render(ctx)
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
function withThrottle(origin, error, publish) {
|
|
963
|
+
const key = faultKey(origin, error);
|
|
964
|
+
if (!faultThrottle.shouldReport(key)) {
|
|
965
|
+
logger$2.info(`throttled duplicate fault ${key}`);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
publish();
|
|
969
|
+
faultThrottle.markReported(key);
|
|
970
|
+
}
|
|
971
|
+
function reportFault(denial, core, origin, uuid) {
|
|
972
|
+
withThrottle(origin, denial, () => {
|
|
973
|
+
logger$2.error(`${denial.name}: ${uuid}`, denial);
|
|
974
|
+
if (origin.interaction) core.bus.publish("handledException", {
|
|
975
|
+
denial,
|
|
976
|
+
uuid,
|
|
977
|
+
source: buildInteractionSource(origin.interaction)
|
|
978
|
+
});
|
|
979
|
+
else if (origin.event) core.bus.publish("handledException", {
|
|
980
|
+
denial,
|
|
981
|
+
uuid,
|
|
982
|
+
source: buildEventSource(origin.event, origin)
|
|
983
|
+
});
|
|
984
|
+
else core.bus.publish("unknownException", {
|
|
985
|
+
uuid,
|
|
986
|
+
error: denial,
|
|
987
|
+
...scalarActors(origin),
|
|
988
|
+
metadata: metadataFor(origin)
|
|
989
|
+
});
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
function causeLine(error) {
|
|
993
|
+
const { cause } = error;
|
|
994
|
+
if (!Error.isError(cause)) return "";
|
|
995
|
+
const [first] = cause.message.split("\n");
|
|
996
|
+
return `\ncaused by ${cause.name}: ${first ?? cause.message}`;
|
|
997
|
+
}
|
|
998
|
+
function reportRawFault(error, core, origin, uuid) {
|
|
999
|
+
withThrottle(origin, error, () => {
|
|
1000
|
+
if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
|
|
1001
|
+
else logger$2.error(`${uuid} | ${error.message}${causeLine(error)}`);
|
|
1002
|
+
core.bus.publish("unknownException", {
|
|
1003
|
+
uuid,
|
|
1004
|
+
error,
|
|
1005
|
+
...scalarActors(origin),
|
|
1006
|
+
metadata: metadataFor(origin)
|
|
1007
|
+
});
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
function scalarActors(origin) {
|
|
1011
|
+
return {
|
|
1012
|
+
guild: origin.guild ? {
|
|
1013
|
+
id: origin.guild.id,
|
|
1014
|
+
name: origin.guild.name
|
|
1015
|
+
} : void 0,
|
|
1016
|
+
user: origin.user ? {
|
|
1017
|
+
id: origin.user.id,
|
|
1018
|
+
username: origin.user.username
|
|
1019
|
+
} : void 0
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
function metadataFor(origin) {
|
|
1023
|
+
if (origin.event) return {
|
|
1024
|
+
eventName: origin.event.name,
|
|
1025
|
+
handler: origin.event.handler,
|
|
1026
|
+
args: origin.event.args
|
|
1027
|
+
};
|
|
1028
|
+
return origin.metadata;
|
|
1029
|
+
}
|
|
1030
|
+
function buildEventSource(event, origin) {
|
|
1031
|
+
return {
|
|
1032
|
+
kind: "event",
|
|
1033
|
+
eventName: event.name,
|
|
1034
|
+
handler: event.handler,
|
|
1035
|
+
userId: origin.user?.id ?? null,
|
|
1036
|
+
guildId: origin.guild?.id ?? null,
|
|
1037
|
+
channelId: event.channelId,
|
|
1038
|
+
raw: event.args
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
function faultKey(origin, error) {
|
|
1042
|
+
const name = error instanceof _seedcord_core.Notice ? error.name : error instanceof discord_js.DiscordAPIError ? String(error.code) : error.constructor.name;
|
|
1043
|
+
if (origin.event) return `${origin.event.name}:${origin.event.handler}:${name}`;
|
|
1044
|
+
if (origin.interaction) return `${interactionRoute(origin.interaction)}:${name}`;
|
|
1045
|
+
if (origin.route) return `autocomplete:${origin.route}:${name}`;
|
|
1046
|
+
return `autocomplete:${name}`;
|
|
1047
|
+
}
|
|
1048
|
+
/** The stable route id of a repliable interaction, its slash route path, command name, or customId prefix. */
|
|
1049
|
+
function interactionRoute(interaction) {
|
|
1050
|
+
if (interaction.isChatInputCommand()) return slashRouteOf(interaction);
|
|
1051
|
+
if (interaction.isContextMenuCommand()) return interaction.commandName;
|
|
1052
|
+
if (interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit()) return (0, _seedcord_core_internal.prefixOf)(interaction.customId) || interaction.customId;
|
|
1053
|
+
return "interaction";
|
|
1054
|
+
}
|
|
1055
|
+
function buildInteractionSource(interaction) {
|
|
1056
|
+
const command = interaction.isChatInputCommand() ? slashRouteOf(interaction) : interaction.isContextMenuCommand() ? interaction.commandName : null;
|
|
1057
|
+
const customId = interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit() ? interaction.customId : null;
|
|
1058
|
+
return {
|
|
1059
|
+
kind: "interaction",
|
|
1060
|
+
interactionKind: interactionKind(interaction),
|
|
1061
|
+
command,
|
|
1062
|
+
customId,
|
|
1063
|
+
userId: interaction.user.id,
|
|
1064
|
+
guildId: interaction.guildId,
|
|
1065
|
+
channelId: interaction.channelId,
|
|
1066
|
+
interactionId: interaction.id,
|
|
1067
|
+
raw: interaction
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
function interactionKind(interaction) {
|
|
1071
|
+
if (interaction.isChatInputCommand()) return "slash";
|
|
1072
|
+
if (interaction.isContextMenuCommand()) return "context-menu";
|
|
1073
|
+
if (interaction.isButton()) return "button";
|
|
1074
|
+
if (interaction.isAnySelectMenu()) return "select";
|
|
1075
|
+
return "modal";
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1190
1078
|
//#endregion
|
|
1191
1079
|
//#region src/bot/confirm/reserved.ts
|
|
1192
1080
|
const RESERVED_CONFIRM_PREFIX = "__seedcord_confirm";
|
|
@@ -1230,153 +1118,37 @@ async function collectChoice(message, userId, timeoutMs) {
|
|
|
1230
1118
|
} catch {
|
|
1231
1119
|
return null;
|
|
1232
1120
|
}
|
|
1233
|
-
}
|
|
1234
|
-
async function settle(sender,
|
|
1235
|
-
if (outcome === void 0) {
|
|
1236
|
-
await
|
|
1237
|
-
return;
|
|
1238
|
-
}
|
|
1239
|
-
await sender.edit(message, typeof outcome === "function" ? await outcome() : outcome);
|
|
1240
|
-
}
|
|
1241
|
-
async function getConfirmation(interaction, prompt, options) {
|
|
1242
|
-
const { ephemeral = true, timeoutMs = DEFAULT_TIMEOUT_MS } = options ?? {};
|
|
1243
|
-
const response = typeof prompt === "string" ? defaultPrompt(prompt, options) : await prompt(CONFIRM_IDS);
|
|
1244
|
-
const sender = new ReplySender(interaction);
|
|
1245
|
-
const message = await sender.send(response, ephemeral);
|
|
1246
|
-
if (!message) return false;
|
|
1247
|
-
const winner = await collectChoice(message, interaction.user.id, timeoutMs);
|
|
1248
|
-
if (!winner) {
|
|
1249
|
-
await settle(sender, interaction, message, options?.onTimeout);
|
|
1250
|
-
return false;
|
|
1251
|
-
}
|
|
1252
|
-
await winner.deferUpdate().catch(() => void 0);
|
|
1253
|
-
const confirmed = winner.customId === CONFIRM_IDS.confirm;
|
|
1254
|
-
await settle(sender, interaction, message, confirmed ? options?.onConfirm : options?.onCancel);
|
|
1255
|
-
return confirmed;
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
//#endregion
|
|
1259
|
-
//#region src/interfaces/Plugin.ts
|
|
1260
|
-
/**
|
|
1261
|
-
* Base class for Seedcord plugins
|
|
1262
|
-
*
|
|
1263
|
-
* Extend this class to create plugins that integrate with the Seedcord lifecycle.
|
|
1264
|
-
* Plugins have access to the core instance and must implement initialization logic.
|
|
1265
|
-
*/
|
|
1266
|
-
var Plugin = class extends _seedcord_event_emitter.TypedEventEmitter {
|
|
1267
|
-
pluggable;
|
|
1268
|
-
name = this.constructor.name;
|
|
1269
|
-
constructor(pluggable) {
|
|
1270
|
-
super();
|
|
1271
|
-
this.pluggable = pluggable;
|
|
1272
|
-
}
|
|
1273
|
-
/**
|
|
1274
|
-
* Reloads plugin state on an HMR update. The base implementation is a no-op, override it in your plugin.
|
|
1275
|
-
* @virtual
|
|
1276
|
-
*/
|
|
1277
|
-
onHmr(_event) {
|
|
1278
|
-
return Promise.resolve();
|
|
1279
|
-
}
|
|
1280
|
-
/**
|
|
1281
|
-
* Registers critical file patterns that should trigger a full restart when changed in Dev HMR.
|
|
1282
|
-
* @param patterns - Glob patterns relative to the project root
|
|
1283
|
-
*/
|
|
1284
|
-
registerCriticalFiles(patterns) {
|
|
1285
|
-
(0, _seedcord_core_internal.getDevChannel)()?.send("seedcord:register-critical-files", { patterns });
|
|
1286
|
-
}
|
|
1287
|
-
/** @internal */
|
|
1288
|
-
removeListener(event, listener) {
|
|
1289
|
-
return super.removeListener(event, listener);
|
|
1290
|
-
}
|
|
1291
|
-
/** @internal */
|
|
1292
|
-
removeAllListeners(event) {
|
|
1293
|
-
return super.removeAllListeners(event);
|
|
1294
|
-
}
|
|
1295
|
-
};
|
|
1296
|
-
/**
|
|
1297
|
-
* Base class for objects that can have plugins attached.
|
|
1298
|
-
*
|
|
1299
|
-
* Provides plugin attachment capabilities and lifecycle management. Plugins are attached during
|
|
1300
|
-
* configuration and initialized during startup. Not constructed directly, the host is a {@link Seedcord}.
|
|
1301
|
-
*/
|
|
1302
|
-
var Pluggable = class Pluggable extends _seedcord_event_emitter.TypedEventEmitter {
|
|
1303
|
-
isInitialized = false;
|
|
1304
|
-
shutdown;
|
|
1305
|
-
startup;
|
|
1306
|
-
plugins = [];
|
|
1307
|
-
static PLUGIN_INIT_TIMEOUT_MS = 15e3;
|
|
1308
|
-
constructor(shutdown, startup) {
|
|
1309
|
-
super();
|
|
1310
|
-
this.shutdown = shutdown;
|
|
1311
|
-
this.startup = startup;
|
|
1312
|
-
}
|
|
1313
|
-
/** @internal */
|
|
1314
|
-
async init() {
|
|
1315
|
-
if (this.isInitialized) return this;
|
|
1316
|
-
await this.startup.run();
|
|
1317
|
-
this.isInitialized = true;
|
|
1318
|
-
return this;
|
|
1319
|
-
}
|
|
1320
|
-
/**
|
|
1321
|
-
* Attaches a plugin to this instance
|
|
1322
|
-
*
|
|
1323
|
-
* Plugins provide external functionality and are initialized during the specified startup phase.
|
|
1324
|
-
* The plugin instance becomes available as a property in `core` wherever it's available.
|
|
1325
|
-
*
|
|
1326
|
-
* Make sure to augment the {@link Core} interface with the plugin type to ensure TypeScript recognizes it and provides intellisense.
|
|
1327
|
-
*
|
|
1328
|
-
* @typeParam Key - The property name for accessing the plugin
|
|
1329
|
-
* @typeParam Ctor - The plugin constructor type
|
|
1330
|
-
* @param key - Property name to access the plugin instance
|
|
1331
|
-
* @param Plugin - Plugin constructor class
|
|
1332
|
-
* @param startupPhase - When during startup to initialize this plugin ({@link StartupPhase})
|
|
1333
|
-
* @param args - Additional arguments to pass to the plugin constructor
|
|
1334
|
-
* @returns This instance with the plugin attached as a typed property
|
|
1335
|
-
* @throws A **SeedcordError** When called after initialization or if key already exists
|
|
1336
|
-
* @example
|
|
1337
|
-
* ```typescript
|
|
1338
|
-
* seedcord.attach('db', Mongo, StartupPhase.Configuration, { uri: 'mongodb://...', name: 'seedcord', dir: ... })
|
|
1339
|
-
* ```
|
|
1340
|
-
*/
|
|
1341
|
-
attach(key, Plugin, startupPhase, ...args) {
|
|
1342
|
-
if (this.isInitialized) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CorePluginAfterInit);
|
|
1343
|
-
if (key in this) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CorePluginKeyExists, [key]);
|
|
1344
|
-
const instance = new Plugin(this, ...args);
|
|
1345
|
-
this.plugins.push(instance);
|
|
1346
|
-
const entry = { [key]: instance };
|
|
1347
|
-
this.startup.addTask(startupPhase, `Plugin:${key}`, async () => {
|
|
1348
|
-
instance.logger.utils.initialization(key, "start");
|
|
1349
|
-
await instance.init();
|
|
1350
|
-
instance.logger.utils.initialization(key, "end");
|
|
1351
|
-
}, Pluggable.PLUGIN_INIT_TIMEOUT_MS);
|
|
1352
|
-
return Object.assign(this, entry);
|
|
1353
|
-
}
|
|
1354
|
-
/** @internal */
|
|
1355
|
-
removeListener(event, listener) {
|
|
1356
|
-
return super.removeListener(event, listener);
|
|
1121
|
+
}
|
|
1122
|
+
async function settle(sender, message, outcome) {
|
|
1123
|
+
if (outcome === void 0) {
|
|
1124
|
+
await sender.delete(message).catch(() => void 0);
|
|
1125
|
+
return;
|
|
1357
1126
|
}
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1127
|
+
await sender.edit(message, typeof outcome === "function" ? await outcome() : outcome);
|
|
1128
|
+
}
|
|
1129
|
+
async function getConfirmation(interaction, prompt, options) {
|
|
1130
|
+
const { ephemeral = true, timeoutMs = DEFAULT_TIMEOUT_MS } = options ?? {};
|
|
1131
|
+
const response = typeof prompt === "string" ? defaultPrompt(prompt, options) : await prompt(CONFIRM_IDS);
|
|
1132
|
+
const sender = new ReplySender(interaction, interactionRoute(interaction));
|
|
1133
|
+
const message = await sender.send(response, { ephemeral });
|
|
1134
|
+
const winner = await collectChoice(message, interaction.user.id, timeoutMs);
|
|
1135
|
+
if (!winner) {
|
|
1136
|
+
await settle(sender, message, options?.onTimeout);
|
|
1137
|
+
return false;
|
|
1361
1138
|
}
|
|
1362
|
-
|
|
1139
|
+
await winner.deferUpdate().catch(() => void 0);
|
|
1140
|
+
const confirmed = winner.customId === CONFIRM_IDS.confirm;
|
|
1141
|
+
await settle(sender, message, confirmed ? options?.onConfirm : options?.onCancel);
|
|
1142
|
+
return confirmed;
|
|
1143
|
+
}
|
|
1363
1144
|
|
|
1364
1145
|
//#endregion
|
|
1365
1146
|
//#region src/handlers/BaseHandler.ts
|
|
1366
1147
|
/**
|
|
1367
|
-
*
|
|
1148
|
+
* The gateway base, fixes the core base's `TCore` to the gateway {@link Core} and adds the `getEvent`
|
|
1149
|
+
* gate hook.
|
|
1368
1150
|
*/
|
|
1369
|
-
var BaseHandler = class {
|
|
1370
|
-
core;
|
|
1371
|
-
event;
|
|
1372
|
-
logger;
|
|
1373
|
-
dispatch;
|
|
1374
|
-
constructor(event, core, dispatch) {
|
|
1375
|
-
this.core = core;
|
|
1376
|
-
this.event = event;
|
|
1377
|
-
this.logger = new _seedcord_logger.Logger(this.constructor.name);
|
|
1378
|
-
this.dispatch = dispatch;
|
|
1379
|
-
}
|
|
1151
|
+
var BaseHandler = class extends _seedcord_core.BaseHandler {
|
|
1380
1152
|
/** @internal */
|
|
1381
1153
|
getEvent() {
|
|
1382
1154
|
return this.event;
|
|
@@ -1400,36 +1172,49 @@ var InteractionMiddleware = class extends BaseHandler {
|
|
|
1400
1172
|
};
|
|
1401
1173
|
|
|
1402
1174
|
//#endregion
|
|
1403
|
-
//#region src/handlers/
|
|
1175
|
+
//#region src/handlers/RepliableHandler.ts
|
|
1404
1176
|
/**
|
|
1405
|
-
* Shared base the
|
|
1177
|
+
* Shared base the repliable interaction handlers extend.
|
|
1406
1178
|
*
|
|
1407
|
-
* Not a public entry point.
|
|
1408
|
-
* or {@link
|
|
1409
|
-
*
|
|
1179
|
+
* Not a public entry point. Extend {@link SlashHandler}, {@link ButtonHandler}, {@link ModalHandler},
|
|
1180
|
+
* {@link SelectMenuHandler}, or {@link ContextMenuHandler} instead. This class binds the core reply base
|
|
1181
|
+
* to the gateway sender.
|
|
1410
1182
|
*
|
|
1411
|
-
* @typeParam
|
|
1183
|
+
* @typeParam Event - The repliable interaction type this handler processes
|
|
1412
1184
|
*/
|
|
1413
|
-
var
|
|
1414
|
-
|
|
1415
|
-
|
|
1185
|
+
var RepliableHandler = class extends _seedcord_core.RepliableHandler {
|
|
1186
|
+
/** @internal */
|
|
1187
|
+
getEvent() {
|
|
1188
|
+
return this.event;
|
|
1189
|
+
}
|
|
1190
|
+
buildSender(event, _core, routeId) {
|
|
1191
|
+
return new ReplySender(event, routeId);
|
|
1416
1192
|
}
|
|
1417
1193
|
};
|
|
1418
1194
|
|
|
1419
1195
|
//#endregion
|
|
1420
|
-
//#region src/
|
|
1196
|
+
//#region src/handlers/interaction/InteractionHandler.ts
|
|
1421
1197
|
/**
|
|
1422
|
-
*
|
|
1423
|
-
* `seedcord codegen` both assemble route keys through the same {@link buildSlashRoute}, so a dispatched
|
|
1424
|
-
* interaction and a generated registry key always match.
|
|
1198
|
+
* Shared base the typed interaction handlers extend.
|
|
1425
1199
|
*
|
|
1426
|
-
* @
|
|
1200
|
+
* Not a public entry point. Extend {@link SlashHandler}, {@link ButtonHandler}, {@link ModalHandler},
|
|
1201
|
+
* or {@link SelectMenuHandler} instead. This class adds `showModal` on top of the reply members those bases
|
|
1202
|
+
* share.
|
|
1203
|
+
*
|
|
1204
|
+
* @typeParam Repliable - The interaction type this handler processes
|
|
1427
1205
|
*/
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1206
|
+
var InteractionHandler = class extends RepliableHandler {
|
|
1207
|
+
constructor(event, core, dispatch) {
|
|
1208
|
+
super(event, core, dispatch);
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Open a modal. Must be the initial response to this interaction. The modal kind rejects this call at
|
|
1212
|
+
* compile time (Discord forbids a modal in response to a modal).
|
|
1213
|
+
*/
|
|
1214
|
+
showModal(modal) {
|
|
1215
|
+
return this.sender.showModal(modal);
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1433
1218
|
|
|
1434
1219
|
//#endregion
|
|
1435
1220
|
//#region src/handlers/interaction/SlashHandler.ts
|
|
@@ -1465,7 +1250,7 @@ var SlashHandler = class extends InteractionHandler {
|
|
|
1465
1250
|
}
|
|
1466
1251
|
/**
|
|
1467
1252
|
* Run the arm for whichever command fired. Use this only when the handler is registered for several
|
|
1468
|
-
* routes.
|
|
1253
|
+
* routes.
|
|
1469
1254
|
*
|
|
1470
1255
|
* Provide one arm per registered route, keyed by its route string, and each arm receives that route's
|
|
1471
1256
|
* own narrowed options. The arms must cover every route in the generic, a missing route or an unknown
|
|
@@ -1489,7 +1274,7 @@ var SlashHandler = class extends InteractionHandler {
|
|
|
1489
1274
|
*/
|
|
1490
1275
|
async match(arms) {
|
|
1491
1276
|
const route = slashRouteOf(this.event);
|
|
1492
|
-
const arm = arms[route];
|
|
1277
|
+
const arm = Object.hasOwn(arms, route) ? arms[route] : void 0;
|
|
1493
1278
|
if (!arm) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.SlashMatchArmMissing, [route]);
|
|
1494
1279
|
return await arm(this.options);
|
|
1495
1280
|
}
|
|
@@ -1560,11 +1345,11 @@ var AutocompleteHandler = class extends BaseHandler {
|
|
|
1560
1345
|
constructor(event, core, dispatch) {
|
|
1561
1346
|
super(event, core, dispatch);
|
|
1562
1347
|
}
|
|
1348
|
+
decodedFocused;
|
|
1563
1349
|
/**
|
|
1564
1350
|
* The focused option for this interaction. `value` is always the raw partial string the user is typing,
|
|
1565
1351
|
* even for an integer or number option, so coerce it yourself when you need a number.
|
|
1566
1352
|
*/
|
|
1567
|
-
decodedFocused;
|
|
1568
1353
|
get focused() {
|
|
1569
1354
|
if (this.decodedFocused) return this.decodedFocused;
|
|
1570
1355
|
const raw = this.event.options.getFocused(true);
|
|
@@ -1580,7 +1365,7 @@ var AutocompleteHandler = class extends BaseHandler {
|
|
|
1580
1365
|
* match is how you read it. There is no single-field shortcut the way slash/component handlers have one.
|
|
1581
1366
|
*
|
|
1582
1367
|
* Provide one arm per autocompletable field across the registered commands, keyed by field name. Each
|
|
1583
|
-
* arm receives the focused partial value and a `respond`
|
|
1368
|
+
* arm receives the focused partial value and a `respond` typed to that field's choice value. The
|
|
1584
1369
|
* arms must cover every autocompletable field, a missing field or an unknown key is a compile error. A
|
|
1585
1370
|
* focused field with no arm, only reachable from a stale-deployed command, throws at runtime.
|
|
1586
1371
|
*
|
|
@@ -1602,11 +1387,15 @@ var AutocompleteHandler = class extends BaseHandler {
|
|
|
1602
1387
|
*/
|
|
1603
1388
|
async match(arms) {
|
|
1604
1389
|
const { name, value } = this.focused;
|
|
1605
|
-
const respond = (choices) => this.
|
|
1606
|
-
const arm = arms[name];
|
|
1390
|
+
const respond = (choices) => this.respond(choices);
|
|
1391
|
+
const arm = Object.hasOwn(arms, name) ? arms[name] : void 0;
|
|
1607
1392
|
if (!arm) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.AutocompleteMatchArmMissing, [name]);
|
|
1608
1393
|
return await arm(value, respond);
|
|
1609
1394
|
}
|
|
1395
|
+
/** Send autocomplete suggestions, callback type 8. Prefer {@link match}, which restricts each field's choices to its declared type. */
|
|
1396
|
+
async respond(choices) {
|
|
1397
|
+
await this.event.respond(choices);
|
|
1398
|
+
}
|
|
1610
1399
|
/** The firing command route, for a field whose completion differs per registered command. */
|
|
1611
1400
|
get route() {
|
|
1612
1401
|
return slashRouteOf(this.event);
|
|
@@ -1614,7 +1403,7 @@ var AutocompleteHandler = class extends BaseHandler {
|
|
|
1614
1403
|
/**
|
|
1615
1404
|
* The already-entered options on this command, restricted to the kinds Discord resolves during autocomplete
|
|
1616
1405
|
* (string, integer, number, boolean) with every read nullable, since a sibling is partial while the user
|
|
1617
|
-
* types the focused field.
|
|
1406
|
+
* types the focused field. The focused field is on `this.focused`.
|
|
1618
1407
|
*/
|
|
1619
1408
|
get options() {
|
|
1620
1409
|
return this.event.options;
|
|
@@ -1626,25 +1415,25 @@ var AutocompleteHandler = class extends BaseHandler {
|
|
|
1626
1415
|
/**
|
|
1627
1416
|
* Shared base the customId-routed component handlers extend.
|
|
1628
1417
|
*
|
|
1629
|
-
* Not a public entry point.
|
|
1630
|
-
*
|
|
1631
|
-
* those bases share, so DO NOT use it directly.
|
|
1418
|
+
* Not a public entry point. Extend {@link ButtonHandler}, {@link SelectMenuHandler}, or {@link ModalHandler}
|
|
1419
|
+
* instead. This class defines the customId decode and the route matching those bases share.
|
|
1632
1420
|
*
|
|
1633
1421
|
* @typeParam Event - The component interaction type this handler processes
|
|
1634
1422
|
* @typeParam Defs - The customId route definitions registered on the concrete handler
|
|
1635
1423
|
*/
|
|
1636
1424
|
var ComponentHandler = class extends InteractionHandler {
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1425
|
+
/** Rewrite the source message this component interaction came from. */
|
|
1426
|
+
update(response) {
|
|
1427
|
+
return this.sender.update(response);
|
|
1428
|
+
}
|
|
1429
|
+
/** Acknowledge the component without changing the source message. */
|
|
1430
|
+
deferUpdate() {
|
|
1431
|
+
return this.sender.deferUpdate();
|
|
1641
1432
|
}
|
|
1642
1433
|
decoded;
|
|
1643
1434
|
get route() {
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
this.decoded = decoded;
|
|
1647
|
-
return decoded;
|
|
1435
|
+
this.decoded ??= (0, _seedcord_core_internal.decodeComponentRoute)(this.constructor, this.event.customId);
|
|
1436
|
+
return this.decoded;
|
|
1648
1437
|
}
|
|
1649
1438
|
/**
|
|
1650
1439
|
* The decoded params of the single route this handler is registered for.
|
|
@@ -1663,10 +1452,11 @@ var ComponentHandler = class extends InteractionHandler {
|
|
|
1663
1452
|
* handler `this.params` is `never`, so match is the only way to read the decoded params.
|
|
1664
1453
|
*
|
|
1665
1454
|
* Provide one arm per registered route, keyed by its prefix, and each arm receives that route's own
|
|
1666
|
-
* decoded params. The arms
|
|
1667
|
-
*
|
|
1455
|
+
* decoded params. The arms cover every registered route prefix, checked at compile time, and a prefix
|
|
1456
|
+
* unmatched at runtime throws `CustomIdMatchArmMissing`. Decoding runs before any arm, so a stale or
|
|
1457
|
+
* corrupt wire throws before an arm body executes.
|
|
1668
1458
|
*
|
|
1669
|
-
* @param arms - One
|
|
1459
|
+
* @param arms - One callback per registered route, keyed by prefix.
|
|
1670
1460
|
* @returns The result of the arm that ran.
|
|
1671
1461
|
*
|
|
1672
1462
|
* @example
|
|
@@ -1675,8 +1465,8 @@ var ComponentHandler = class extends InteractionHandler {
|
|
|
1675
1465
|
* class ReviewButtons extends ButtonHandler<[typeof Approve, typeof Reject]> {
|
|
1676
1466
|
* async execute() {
|
|
1677
1467
|
* await this.match({
|
|
1678
|
-
* approve: ({ userId }) => this.
|
|
1679
|
-
* reject: ({ userId }) => this.
|
|
1468
|
+
* approve: ({ userId }) => this.reply(`approved <@${userId}>`),
|
|
1469
|
+
* reject: ({ userId }) => this.reply(`rejected <@${userId}>`)
|
|
1680
1470
|
* });
|
|
1681
1471
|
* }
|
|
1682
1472
|
* }
|
|
@@ -1684,7 +1474,7 @@ var ComponentHandler = class extends InteractionHandler {
|
|
|
1684
1474
|
*/
|
|
1685
1475
|
async match(arms) {
|
|
1686
1476
|
const { prefix, params } = this.route;
|
|
1687
|
-
const arm = arms[prefix];
|
|
1477
|
+
const arm = Object.hasOwn(arms, prefix) ? arms[prefix] : void 0;
|
|
1688
1478
|
if (!arm) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CustomIdMatchArmMissing, [prefix]);
|
|
1689
1479
|
return await arm(params);
|
|
1690
1480
|
}
|
|
@@ -1696,8 +1486,9 @@ var ComponentHandler = class extends InteractionHandler {
|
|
|
1696
1486
|
* Base class for a button interaction handler.
|
|
1697
1487
|
*
|
|
1698
1488
|
* Register the customId definitions this handler decodes with `@ButtonRoute`, list the same ones in the
|
|
1699
|
-
* generic, then read `this.params` for a single route or `this.match` for several.
|
|
1700
|
-
*
|
|
1489
|
+
* generic, then read `this.params` for a single route or `this.match` for several. Reply through the handler
|
|
1490
|
+
* members, or rewrite the source message with `this.update`. Passing different definitions to the decorator
|
|
1491
|
+
* and the generic is a compile error.
|
|
1701
1492
|
*
|
|
1702
1493
|
* @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ApproveId]`.
|
|
1703
1494
|
* @typeParam Cache - The interaction cache state, `'cached'` by default.
|
|
@@ -1708,7 +1499,7 @@ var ComponentHandler = class extends InteractionHandler {
|
|
|
1708
1499
|
* class ApproveButton extends ButtonHandler<[typeof ApproveId]> {
|
|
1709
1500
|
* async execute() {
|
|
1710
1501
|
* const { userId } = this.params;
|
|
1711
|
-
* await this.
|
|
1502
|
+
* await this.reply(`approved <@${userId}>`);
|
|
1712
1503
|
* }
|
|
1713
1504
|
* }
|
|
1714
1505
|
* ```
|
|
@@ -1722,7 +1513,8 @@ var ButtonHandler = class extends ComponentHandler {};
|
|
|
1722
1513
|
*
|
|
1723
1514
|
* Register the customId definitions this handler decodes with `@ModalRoute`, list the same ones in the
|
|
1724
1515
|
* generic, then read `this.params` for a single route or `this.match` for several. Read the submitted
|
|
1725
|
-
* inputs from `this.event.fields
|
|
1516
|
+
* inputs from `this.event.fields`, and reply through the handler members. Discord forbids opening a modal in
|
|
1517
|
+
* response to a modal, so `showModal` fails compilation on this kind.
|
|
1726
1518
|
*
|
|
1727
1519
|
* @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ConfigId]`.
|
|
1728
1520
|
* @typeParam Cache - The interaction cache state, `'cached'` by default.
|
|
@@ -1734,7 +1526,7 @@ var ButtonHandler = class extends ComponentHandler {};
|
|
|
1734
1526
|
* async execute() {
|
|
1735
1527
|
* const { guildId } = this.params;
|
|
1736
1528
|
* const name = this.event.fields.getTextInputValue('name');
|
|
1737
|
-
* await this.
|
|
1529
|
+
* await this.reply(`saved ${name} for ${guildId}`);
|
|
1738
1530
|
* }
|
|
1739
1531
|
* }
|
|
1740
1532
|
* ```
|
|
@@ -1748,7 +1540,8 @@ var ModalHandler = class extends ComponentHandler {};
|
|
|
1748
1540
|
*
|
|
1749
1541
|
* Pass the select kind first and the customId definitions second, the same order as `@SelectMenuRoute`,
|
|
1750
1542
|
* so `this.event` and `this.event.values` are narrowed to that kind. Read `this.params` for a single
|
|
1751
|
-
* route or `this.match` for several
|
|
1543
|
+
* route or `this.match` for several, and reply through the handler members or rewrite the source message
|
|
1544
|
+
* with `this.update`.
|
|
1752
1545
|
*
|
|
1753
1546
|
* @typeParam Kind - The select kind from {@link SelectMenuKind}, e.g. `SelectMenuKind.User`.
|
|
1754
1547
|
* @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof AssignId]`.
|
|
@@ -1760,7 +1553,7 @@ var ModalHandler = class extends ComponentHandler {};
|
|
|
1760
1553
|
* class AssignSelect extends SelectMenuHandler<SelectMenuKind.User, [typeof AssignId]> {
|
|
1761
1554
|
* async execute() {
|
|
1762
1555
|
* const { roleId } = this.params;
|
|
1763
|
-
* await this.
|
|
1556
|
+
* await this.reply(`assigning ${this.event.values.length} member(s) to <@&${roleId}>`);
|
|
1764
1557
|
* }
|
|
1765
1558
|
* }
|
|
1766
1559
|
* ```
|
|
@@ -2026,16 +1819,16 @@ var Paginator = class {
|
|
|
2026
1819
|
const loadPage = (ctx, n) => this.page(ctx, n);
|
|
2027
1820
|
this.Handler = class Nav extends ButtonHandler {
|
|
2028
1821
|
async execute() {
|
|
2029
|
-
await this.
|
|
1822
|
+
await this.deferUpdate();
|
|
2030
1823
|
const response = await loadPage(contextOf(this.event, this.core), this.params.page);
|
|
2031
|
-
await
|
|
1824
|
+
await this.update(response);
|
|
2032
1825
|
}
|
|
2033
1826
|
};
|
|
2034
1827
|
}
|
|
2035
1828
|
/** Render page 0 and send it, picking reply or followUp from the interaction's state. */
|
|
2036
1829
|
async start(interaction, core) {
|
|
2037
1830
|
const response = await this.page(contextOf(interaction, core), 0);
|
|
2038
|
-
return new ReplySender(interaction).send(response, this.config.ephemeral ?? false);
|
|
1831
|
+
return new ReplySender(interaction, interactionRoute(interaction)).send(response, { ephemeral: this.config.ephemeral ?? false });
|
|
2039
1832
|
}
|
|
2040
1833
|
/** Render a page as a {@link ReplyResponse}. To post it elsewhere, add `flags: MessageFlags.IsComponentsV2`. */
|
|
2041
1834
|
async page(ctx, n) {
|
|
@@ -2159,19 +1952,184 @@ function Subscribe(subscriber, options) {
|
|
|
2159
1952
|
};
|
|
2160
1953
|
}
|
|
2161
1954
|
|
|
1955
|
+
//#endregion
|
|
1956
|
+
//#region src/subscribers/decorators/WebhookUrl.ts
|
|
1957
|
+
/**
|
|
1958
|
+
* Declares the environment variable a {@link WebhookLog} reporter reads its webhook url from.
|
|
1959
|
+
*
|
|
1960
|
+
* An unset variable disables the reporter with a boot warning. A set but malformed value throws
|
|
1961
|
+
* at boot.
|
|
1962
|
+
*
|
|
1963
|
+
* @param envKey - The environment variable name holding the webhook url
|
|
1964
|
+
* @decorator
|
|
1965
|
+
* @example
|
|
1966
|
+
* ```ts
|
|
1967
|
+
* \@Subscribe('memberBanned')
|
|
1968
|
+
* \@WebhookUrl('BAN_LOG_WEBHOOK_URL')
|
|
1969
|
+
* class BanLog extends WebhookLog<'memberBanned'> {
|
|
1970
|
+
* report() {
|
|
1971
|
+
* return { components: [new BanCard(this.data).component] };
|
|
1972
|
+
* }
|
|
1973
|
+
* }
|
|
1974
|
+
* ```
|
|
1975
|
+
*/
|
|
1976
|
+
function WebhookUrl(envKey) {
|
|
1977
|
+
return function(constructor) {
|
|
1978
|
+
Reflect.defineMetadata(_seedcord_core_internal.WebhookUrlMetadataKey, envKey, constructor);
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
//#endregion
|
|
1983
|
+
//#region src/miscellaneous/flagsFor.ts
|
|
1984
|
+
/** @internal */
|
|
1985
|
+
function flagsFor(ephemeral) {
|
|
1986
|
+
let flags = discord_api_types_v10.MessageFlags.IsComponentsV2;
|
|
1987
|
+
if (ephemeral) flags |= discord_api_types_v10.MessageFlags.Ephemeral;
|
|
1988
|
+
return flags;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
//#endregion
|
|
1992
|
+
//#region src/subscribers/bases/webhookHelpers.ts
|
|
1993
|
+
const DISCORD_WEBHOOK_REGEX = new RegExp(String.raw`^https?:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w$-]+$`);
|
|
1994
|
+
function isDiscordWebhookUrl(value) {
|
|
1995
|
+
return URL.canParse(value) && DISCORD_WEBHOOK_REGEX.test(value);
|
|
1996
|
+
}
|
|
1997
|
+
function jsonAttachment(name, description, data) {
|
|
1998
|
+
const content = (0, _seedcord_utils.filterCirculars)(data);
|
|
1999
|
+
return {
|
|
2000
|
+
name,
|
|
2001
|
+
description,
|
|
2002
|
+
data: Buffer.from(JSON.stringify(content, void 0, 2), "utf8")
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
function neutralizeFences(text) {
|
|
2006
|
+
return text.replaceAll("```", "```");
|
|
2007
|
+
}
|
|
2008
|
+
/** An error's stack with its direct cause appended (one level), ANSI-stripped for the webhook payload. */
|
|
2009
|
+
function errorReport(error) {
|
|
2010
|
+
let report = error.stack ?? `${error.name}: ${error.message}`;
|
|
2011
|
+
if (Error.isError(error.cause)) {
|
|
2012
|
+
const cause = error.cause.stack ?? `${error.cause.name}: ${error.cause.message}`;
|
|
2013
|
+
report += `\n\nCaused by:\n${cause}`;
|
|
2014
|
+
}
|
|
2015
|
+
const stripped = neutralizeFences((0, _seedcord_utils.stripAnsi)(report));
|
|
2016
|
+
return stripped.length > 1800 ? `${stripped.slice(0, 1799)}…` : stripped;
|
|
2017
|
+
}
|
|
2018
|
+
var WebhookSeparator = class extends _seedcord_core.BuilderComponent {
|
|
2019
|
+
constructor() {
|
|
2020
|
+
super("separator");
|
|
2021
|
+
this.instance.setSpacing(discord_api_types_v10.SeparatorSpacingSize.Small).setDivider(true);
|
|
2022
|
+
}
|
|
2023
|
+
};
|
|
2024
|
+
|
|
2025
|
+
//#endregion
|
|
2026
|
+
//#region src/subscribers/bases/WebhookSender.ts
|
|
2027
|
+
const HTTP_NOT_FOUND = 404;
|
|
2028
|
+
const HTTP_UNAUTHORIZED = 401;
|
|
2029
|
+
/**
|
|
2030
|
+
* Sends component messages to a Discord webhook through `@discordjs/rest`.
|
|
2031
|
+
*
|
|
2032
|
+
* @throws A `SeedcordError` when the url does not carry a webhook id and token.
|
|
2033
|
+
*/
|
|
2034
|
+
var WebhookSender = class {
|
|
2035
|
+
rest = new _discordjs_rest.REST();
|
|
2036
|
+
route;
|
|
2037
|
+
constructor(url) {
|
|
2038
|
+
const match = /\/webhooks\/(\d+)\/([\w$-]+)$/.exec(url);
|
|
2039
|
+
if (!match?.[1] || !match[2]) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigWebhookUrlInvalid, ["The url"]);
|
|
2040
|
+
this.route = discord_api_types_v10.Routes.webhook(match[1], match[2]);
|
|
2041
|
+
}
|
|
2042
|
+
/**
|
|
2043
|
+
* Fetches the webhook object, sending no message. Returns `missing` on a 404 or 401 from
|
|
2044
|
+
* Discord and `unreachable` on any other failure.
|
|
2045
|
+
*/
|
|
2046
|
+
async verify() {
|
|
2047
|
+
try {
|
|
2048
|
+
await this.rest.get(this.route, { auth: false });
|
|
2049
|
+
return "ok";
|
|
2050
|
+
} catch (error) {
|
|
2051
|
+
return error instanceof _discordjs_rest.DiscordAPIError && (error.status === HTTP_NOT_FOUND || error.status === HTTP_UNAUTHORIZED) ? "missing" : "unreachable";
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
async send(options) {
|
|
2055
|
+
const { flags, username, components, files } = options;
|
|
2056
|
+
await this.rest.post(this.route, {
|
|
2057
|
+
auth: false,
|
|
2058
|
+
query: new URLSearchParams({
|
|
2059
|
+
with_components: "true",
|
|
2060
|
+
wait: "true"
|
|
2061
|
+
}),
|
|
2062
|
+
body: {
|
|
2063
|
+
flags,
|
|
2064
|
+
username,
|
|
2065
|
+
components: components.map((component) => component.toJSON()),
|
|
2066
|
+
...files?.length && { attachments: files.map((file, id) => ({
|
|
2067
|
+
id,
|
|
2068
|
+
filename: file.name,
|
|
2069
|
+
description: file.description
|
|
2070
|
+
})) }
|
|
2071
|
+
},
|
|
2072
|
+
...files?.length && { files: files.map((file) => ({
|
|
2073
|
+
name: file.name,
|
|
2074
|
+
data: file.data
|
|
2075
|
+
})) }
|
|
2076
|
+
});
|
|
2077
|
+
}
|
|
2078
|
+
};
|
|
2079
|
+
|
|
2162
2080
|
//#endregion
|
|
2163
2081
|
//#region src/subscribers/bases/WebhookLog.ts
|
|
2164
2082
|
/**
|
|
2165
|
-
*
|
|
2083
|
+
* Base class for subscribers that deliver their event to a Discord webhook.
|
|
2166
2084
|
*
|
|
2167
|
-
*
|
|
2168
|
-
*
|
|
2085
|
+
* Declare the url's environment variable with `@WebhookUrl` and implement {@link report}. Url
|
|
2086
|
+
* resolution, validation, sender reuse, and sending run in the base. When the variable is unset
|
|
2087
|
+
* the reporter is skipped at registration with a boot warning.
|
|
2169
2088
|
*
|
|
2170
|
-
* @typeParam KeyOfSubscribers - The
|
|
2089
|
+
* @typeParam KeyOfSubscribers - The subscription key this reporter receives
|
|
2171
2090
|
*/
|
|
2172
|
-
var WebhookLog = class extends Subscriber {
|
|
2173
|
-
|
|
2174
|
-
|
|
2091
|
+
var WebhookLog = class WebhookLog extends Subscriber {
|
|
2092
|
+
static senders = /* @__PURE__ */ new Map();
|
|
2093
|
+
async execute() {
|
|
2094
|
+
const url = WebhookLog.urlOf(WebhookLog.envKeyOf(this.constructor));
|
|
2095
|
+
if (url === null) {
|
|
2096
|
+
this.logger.warn(`${chalk.default.bold(this.constructor.name)} has no webhook url set, this reporter is disabled`);
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
const { username, components, files } = await this.report();
|
|
2100
|
+
try {
|
|
2101
|
+
await WebhookLog.senderFor(url).send({
|
|
2102
|
+
flags: flagsFor(false),
|
|
2103
|
+
username: username ?? this.constructor.name,
|
|
2104
|
+
components,
|
|
2105
|
+
files
|
|
2106
|
+
});
|
|
2107
|
+
} catch (error) {
|
|
2108
|
+
this.logger.error("Failed to send webhook report", error);
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
/** @internal */
|
|
2112
|
+
static senderFor(url) {
|
|
2113
|
+
let sender = WebhookLog.senders.get(url);
|
|
2114
|
+
if (!sender) {
|
|
2115
|
+
sender = new WebhookSender(url);
|
|
2116
|
+
WebhookLog.senders.set(url, sender);
|
|
2117
|
+
}
|
|
2118
|
+
return sender;
|
|
2119
|
+
}
|
|
2120
|
+
/** @internal */
|
|
2121
|
+
static envKeyOf(ctor) {
|
|
2122
|
+
const envKey = Reflect.getMetadata(_seedcord_core_internal.WebhookUrlMetadataKey, ctor);
|
|
2123
|
+
if (!envKey) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.DecoratorWebhookUrlMissing, [ctor.name]);
|
|
2124
|
+
return envKey;
|
|
2125
|
+
}
|
|
2126
|
+
/** @internal Returns null when the env var is unset, throws when set but malformed. */
|
|
2127
|
+
static urlOf(envKey) {
|
|
2128
|
+
if (!envapt.Envapter.has(envKey)) return null;
|
|
2129
|
+
const raw = envapt.Envapter.getRequired(envKey, envapt.Converters.String).trim();
|
|
2130
|
+
if (raw === "") return null;
|
|
2131
|
+
if (!isDiscordWebhookUrl(raw)) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigWebhookUrlInvalid, [envKey]);
|
|
2132
|
+
return raw;
|
|
2175
2133
|
}
|
|
2176
2134
|
};
|
|
2177
2135
|
|
|
@@ -2229,7 +2187,6 @@ function slashRouteLeaves(commands) {
|
|
|
2229
2187
|
var CommandRegistry = class {
|
|
2230
2188
|
core;
|
|
2231
2189
|
onDeployed;
|
|
2232
|
-
name = "Commands";
|
|
2233
2190
|
logger = new _seedcord_logger.Logger("Commands");
|
|
2234
2191
|
isInitialised = false;
|
|
2235
2192
|
globalCommands = [];
|
|
@@ -2425,223 +2382,56 @@ function deriveEventActor(args) {
|
|
|
2425
2382
|
};
|
|
2426
2383
|
}
|
|
2427
2384
|
return {
|
|
2428
|
-
guild: null,
|
|
2429
|
-
member: null,
|
|
2430
|
-
user: null,
|
|
2431
|
-
channelId: null
|
|
2432
|
-
};
|
|
2433
|
-
}
|
|
2434
|
-
|
|
2435
|
-
//#endregion
|
|
2436
|
-
//#region src/bot/gates/runGates.ts
|
|
2437
|
-
function interactionGateContext(interaction, core) {
|
|
2438
|
-
const rawMember = interaction.member;
|
|
2439
|
-
const memberRoleIds = rawMember instanceof discord_js.GuildMember ? [...rawMember.roles.cache.keys()].filter((id) => id !== interaction.guildId) : rawMember?.roles ?? [];
|
|
2440
|
-
return {
|
|
2441
|
-
kind: "interaction",
|
|
2442
|
-
interaction,
|
|
2443
|
-
core,
|
|
2444
|
-
user: interaction.user,
|
|
2445
|
-
guild: interaction.guild,
|
|
2446
|
-
member: rawMember instanceof discord_js.GuildMember ? rawMember : null,
|
|
2447
|
-
userId: interaction.user.id,
|
|
2448
|
-
guildId: interaction.guildId,
|
|
2449
|
-
channelId: interaction.channelId,
|
|
2450
|
-
memberRoleIds,
|
|
2451
|
-
memberPermissions: interaction.memberPermissions?.bitfield ?? null,
|
|
2452
|
-
routeId: null
|
|
2453
|
-
};
|
|
2454
|
-
}
|
|
2455
|
-
function eventGateContext(eventName, args, core) {
|
|
2456
|
-
const actor = deriveEventActor(args);
|
|
2457
|
-
return {
|
|
2458
|
-
kind: "event",
|
|
2459
|
-
core,
|
|
2460
|
-
eventName,
|
|
2461
|
-
payload: args,
|
|
2462
|
-
user: actor.user,
|
|
2463
|
-
guild: actor.guild,
|
|
2464
|
-
member: actor.member,
|
|
2465
|
-
userId: actor.user?.id ?? null,
|
|
2466
|
-
guildId: actor.guild?.id ?? null,
|
|
2467
|
-
channelId: actor.channelId,
|
|
2468
|
-
memberRoleIds: actor.member ? [...actor.member.roles.cache.keys()].filter((id) => id !== actor.guild?.id) : [],
|
|
2469
|
-
memberPermissions: actor.member?.permissions.bitfield ?? null,
|
|
2470
|
-
routeId: null
|
|
2471
|
-
};
|
|
2472
|
-
}
|
|
2473
|
-
|
|
2474
|
-
//#endregion
|
|
2475
|
-
//#region src/miscellaneous/FaultThrottle.ts
|
|
2476
|
-
/**
|
|
2477
|
-
* Drops duplicate fault reports inside a fixed window so a recurring fault (a database outage on a
|
|
2478
|
-
* hot path) reports once per window instead of flooding the webhook. Keyed on a structural string, so
|
|
2479
|
-
* the map stays small, and a stale entry is evicted on the next read of its key.
|
|
2480
|
-
*
|
|
2481
|
-
* @internal
|
|
2482
|
-
*/
|
|
2483
|
-
var FaultThrottle = class {
|
|
2484
|
-
windowMs;
|
|
2485
|
-
now;
|
|
2486
|
-
lastReportedAt = /* @__PURE__ */ new Map();
|
|
2487
|
-
constructor(windowMs = 6e4, now = () => Date.now()) {
|
|
2488
|
-
this.windowMs = windowMs;
|
|
2489
|
-
this.now = now;
|
|
2490
|
-
}
|
|
2491
|
-
/** Evicts the entry for this key if it has aged out, so the map never holds stale keys forever. */
|
|
2492
|
-
shouldReport(key) {
|
|
2493
|
-
const last = this.lastReportedAt.get(key);
|
|
2494
|
-
if (last === void 0) return true;
|
|
2495
|
-
if (this.now() - last < this.windowMs) return false;
|
|
2496
|
-
this.lastReportedAt.delete(key);
|
|
2497
|
-
return true;
|
|
2498
|
-
}
|
|
2499
|
-
markReported(key) {
|
|
2500
|
-
this.lastReportedAt.set(key, this.now());
|
|
2501
|
-
}
|
|
2502
|
-
clear() {
|
|
2503
|
-
this.lastReportedAt.clear();
|
|
2504
|
-
}
|
|
2505
|
-
};
|
|
2506
|
-
|
|
2507
|
-
//#endregion
|
|
2508
|
-
//#region src/miscellaneous/extractErrorResponse.ts
|
|
2509
|
-
const logger$2 = new _seedcord_logger.Logger("ErrorsHandling");
|
|
2510
|
-
/**
|
|
2511
|
-
* The single throttle for every fault publish, so a recurring fault reports once per window across both
|
|
2512
|
-
* the interaction and event paths. Exported so tests reset it between cases.
|
|
2513
|
-
*
|
|
2514
|
-
* @internal
|
|
2515
|
-
*/
|
|
2516
|
-
const faultThrottle = new FaultThrottle();
|
|
2517
|
-
/**
|
|
2518
|
-
* Processes an error into a user-facing reply and routes its report.
|
|
2519
|
-
*
|
|
2520
|
-
* A {@link Notice} always renders its own reply. A reporting denial (`report: true`) additionally
|
|
2521
|
-
* publishes `handledException` on the interaction path. A raw, non-denial throw publishes
|
|
2522
|
-
* `unknownException` and renders the configured `defaultError` (or a generic {@link Fault}). One uuid is
|
|
2523
|
-
* threaded into both the reply and the bus payload.
|
|
2524
|
-
*
|
|
2525
|
-
* @internal
|
|
2526
|
-
*/
|
|
2527
|
-
function extractErrorResponse(error, core, origin) {
|
|
2528
|
-
const uuid = node_crypto.randomUUID();
|
|
2529
|
-
const developerUsername = core.config.notifications?.developerUsername;
|
|
2530
|
-
const ctx = developerUsername === void 0 ? { uuid } : {
|
|
2531
|
-
uuid,
|
|
2532
|
-
developerUsername
|
|
2533
|
-
};
|
|
2534
|
-
if (error instanceof _seedcord_core.Notice) {
|
|
2535
|
-
if (error.report) reportFault(error, core, origin, uuid);
|
|
2536
|
-
return {
|
|
2537
|
-
uuid,
|
|
2538
|
-
response: error.render(ctx)
|
|
2539
|
-
};
|
|
2540
|
-
}
|
|
2541
|
-
reportRawFault(error, core, origin, uuid);
|
|
2542
|
-
const override = core.config.errors?.defaultError;
|
|
2543
|
-
return {
|
|
2544
|
-
uuid,
|
|
2545
|
-
response: override ? new override(uuid).render(ctx) : new _seedcord_core.Fault().render(ctx)
|
|
2546
|
-
};
|
|
2547
|
-
}
|
|
2548
|
-
function withThrottle(origin, error, publish) {
|
|
2549
|
-
const key = faultKey(origin, error);
|
|
2550
|
-
if (!faultThrottle.shouldReport(key)) {
|
|
2551
|
-
logger$2.info(`throttled duplicate fault ${key}`);
|
|
2552
|
-
return;
|
|
2553
|
-
}
|
|
2554
|
-
publish();
|
|
2555
|
-
faultThrottle.markReported(key);
|
|
2556
|
-
}
|
|
2557
|
-
function reportFault(denial, core, origin, uuid) {
|
|
2558
|
-
withThrottle(origin, denial, () => {
|
|
2559
|
-
logger$2.error(`${denial.name}: ${uuid}`, denial);
|
|
2560
|
-
if (origin.interaction) core.bus.publish("handledException", {
|
|
2561
|
-
denial,
|
|
2562
|
-
uuid,
|
|
2563
|
-
source: buildInteractionSource(origin.interaction)
|
|
2564
|
-
});
|
|
2565
|
-
else if (origin.event) core.bus.publish("handledException", {
|
|
2566
|
-
denial,
|
|
2567
|
-
uuid,
|
|
2568
|
-
source: buildEventSource(origin.event, origin)
|
|
2569
|
-
});
|
|
2570
|
-
else core.bus.publish("unknownException", {
|
|
2571
|
-
uuid,
|
|
2572
|
-
error: denial,
|
|
2573
|
-
guild: origin.guild,
|
|
2574
|
-
user: origin.user,
|
|
2575
|
-
metadata: metadataFor(origin)
|
|
2576
|
-
});
|
|
2577
|
-
});
|
|
2578
|
-
}
|
|
2579
|
-
function reportRawFault(error, core, origin, uuid) {
|
|
2580
|
-
withThrottle(origin, error, () => {
|
|
2581
|
-
if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
|
|
2582
|
-
else logger$2.error(`${uuid} | ${error.message}`);
|
|
2583
|
-
core.bus.publish("unknownException", {
|
|
2584
|
-
uuid,
|
|
2585
|
-
error,
|
|
2586
|
-
guild: origin.guild,
|
|
2587
|
-
user: origin.user,
|
|
2588
|
-
metadata: metadataFor(origin)
|
|
2589
|
-
});
|
|
2590
|
-
});
|
|
2591
|
-
}
|
|
2592
|
-
function metadataFor(origin) {
|
|
2593
|
-
if (origin.event) return {
|
|
2594
|
-
eventName: origin.event.name,
|
|
2595
|
-
handler: origin.event.handler,
|
|
2596
|
-
args: origin.event.args
|
|
2597
|
-
};
|
|
2598
|
-
return origin.metadata;
|
|
2599
|
-
}
|
|
2600
|
-
function buildEventSource(event, origin) {
|
|
2601
|
-
return {
|
|
2602
|
-
kind: "event",
|
|
2603
|
-
eventName: event.name,
|
|
2604
|
-
handler: event.handler,
|
|
2605
|
-
userId: origin.user?.id ?? null,
|
|
2606
|
-
guildId: origin.guild?.id ?? null,
|
|
2607
|
-
channelId: event.channelId,
|
|
2608
|
-
raw: event.args
|
|
2385
|
+
guild: null,
|
|
2386
|
+
member: null,
|
|
2387
|
+
user: null,
|
|
2388
|
+
channelId: null
|
|
2609
2389
|
};
|
|
2610
2390
|
}
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
}
|
|
2618
|
-
function interactionRoute(interaction) {
|
|
2619
|
-
if (interaction.isChatInputCommand()) return slashRouteOf(interaction);
|
|
2620
|
-
if (interaction.isContextMenuCommand()) return interaction.commandName;
|
|
2621
|
-
if (interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit()) return (0, _seedcord_core_internal.prefixOf)(interaction.customId) || interaction.customId;
|
|
2622
|
-
return "interaction";
|
|
2623
|
-
}
|
|
2624
|
-
function buildInteractionSource(interaction) {
|
|
2625
|
-
const command = interaction.isChatInputCommand() ? slashRouteOf(interaction) : interaction.isContextMenuCommand() ? interaction.commandName : null;
|
|
2626
|
-
const customId = interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit() ? interaction.customId : null;
|
|
2391
|
+
|
|
2392
|
+
//#endregion
|
|
2393
|
+
//#region src/bot/gates/runGates.ts
|
|
2394
|
+
function interactionGateContext(interaction, core) {
|
|
2395
|
+
const rawMember = interaction.member;
|
|
2396
|
+
const memberRoleIds = rawMember instanceof discord_js.GuildMember ? [...rawMember.roles.cache.keys()].filter((id) => id !== interaction.guildId) : rawMember?.roles ?? [];
|
|
2627
2397
|
return {
|
|
2628
2398
|
kind: "interaction",
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2399
|
+
interaction,
|
|
2400
|
+
core,
|
|
2401
|
+
user: interaction.user,
|
|
2402
|
+
guild: interaction.guild,
|
|
2403
|
+
member: rawMember instanceof discord_js.GuildMember ? rawMember : null,
|
|
2632
2404
|
userId: interaction.user.id,
|
|
2633
2405
|
guildId: interaction.guildId,
|
|
2634
2406
|
channelId: interaction.channelId,
|
|
2635
|
-
|
|
2636
|
-
|
|
2407
|
+
memberRoleIds,
|
|
2408
|
+
memberPermissions: interaction.memberPermissions?.bitfield ?? null,
|
|
2409
|
+
appPermissions: interaction.appPermissions.bitfield,
|
|
2410
|
+
memberGuildPermissions: rawMember instanceof discord_js.GuildMember ? rawMember.permissions.bitfield : null,
|
|
2411
|
+
appGuildPermissions: interaction.guild?.members.me?.permissions.bitfield ?? null,
|
|
2412
|
+
routeId: null
|
|
2637
2413
|
};
|
|
2638
2414
|
}
|
|
2639
|
-
function
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2415
|
+
function eventGateContext(eventName, args, core) {
|
|
2416
|
+
const actor = deriveEventActor(args);
|
|
2417
|
+
return {
|
|
2418
|
+
kind: "event",
|
|
2419
|
+
core,
|
|
2420
|
+
eventName,
|
|
2421
|
+
payload: args,
|
|
2422
|
+
user: actor.user,
|
|
2423
|
+
guild: actor.guild,
|
|
2424
|
+
member: actor.member,
|
|
2425
|
+
userId: actor.user?.id ?? null,
|
|
2426
|
+
guildId: actor.guild?.id ?? null,
|
|
2427
|
+
channelId: actor.channelId,
|
|
2428
|
+
memberRoleIds: actor.member ? [...actor.member.roles.cache.keys()].filter((id) => id !== actor.guild?.id) : [],
|
|
2429
|
+
memberPermissions: actor.member?.permissions.bitfield ?? null,
|
|
2430
|
+
appPermissions: null,
|
|
2431
|
+
memberGuildPermissions: actor.member?.permissions.bitfield ?? null,
|
|
2432
|
+
appGuildPermissions: actor.guild?.members.me?.permissions.bitfield ?? null,
|
|
2433
|
+
routeId: null
|
|
2434
|
+
};
|
|
2645
2435
|
}
|
|
2646
2436
|
|
|
2647
2437
|
//#endregion
|
|
@@ -2656,7 +2446,7 @@ const logger$1 = new _seedcord_logger.Logger("EventBoundary");
|
|
|
2656
2446
|
*/
|
|
2657
2447
|
function handleEventFault(caught, eventName, handlerName, args, core) {
|
|
2658
2448
|
if (caught instanceof _seedcord_core.Silence) {
|
|
2659
|
-
if (caught.reason !== void 0) logger$1.debug(`Silence: ${caught.reason}`);
|
|
2449
|
+
if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger$1.debug(`Silence: ${caught.reason}`);
|
|
2660
2450
|
return;
|
|
2661
2451
|
}
|
|
2662
2452
|
if (!Error.isError(caught)) throw caught;
|
|
@@ -2692,11 +2482,12 @@ var EventDispatcher = class {
|
|
|
2692
2482
|
core;
|
|
2693
2483
|
logger = new _seedcord_logger.Logger("Events");
|
|
2694
2484
|
isInitialized = false;
|
|
2695
|
-
name = "Events";
|
|
2696
2485
|
eventMap = /* @__PURE__ */ new Map();
|
|
2697
2486
|
middlewares = [];
|
|
2698
2487
|
executedOnceHandlers = /* @__PURE__ */ new Set();
|
|
2699
2488
|
attachedEvents = /* @__PURE__ */ new Set();
|
|
2489
|
+
inFlight = /* @__PURE__ */ new Set();
|
|
2490
|
+
draining = false;
|
|
2700
2491
|
loading = false;
|
|
2701
2492
|
loadedHandlers = [];
|
|
2702
2493
|
loadedMiddlewares = [];
|
|
@@ -2846,7 +2637,7 @@ var EventDispatcher = class {
|
|
|
2846
2637
|
};
|
|
2847
2638
|
if (Array.isArray(raw)) for (const entry of raw) register(entry.event, entry.frequency);
|
|
2848
2639
|
else {
|
|
2849
|
-
const names = areRoutes(raw) ? raw : typeof raw === "string" ? [raw] : [];
|
|
2640
|
+
const names = (0, _seedcord_core_internal.areRoutes)(raw) ? raw : typeof raw === "string" ? [raw] : [];
|
|
2850
2641
|
for (const name of names) register(name, "on");
|
|
2851
2642
|
}
|
|
2852
2643
|
const from = (0, _seedcord_utils.formatFilePath)(relativePath);
|
|
@@ -2865,15 +2656,22 @@ var EventDispatcher = class {
|
|
|
2865
2656
|
const handlerEntries = this.eventMap.get(eventName);
|
|
2866
2657
|
this.logger.debug(`Attaching ${_seedcord_logger.paint.mint.bold(eventName)} to the client with ${_seedcord_logger.paint.mute(handlerEntries?.length ?? 0)} handler(s)`);
|
|
2867
2658
|
this.core.bot.client.on(eventName, (...args) => {
|
|
2659
|
+
if (this.draining) return;
|
|
2868
2660
|
this.core.bot.emitSafe("any:event", eventName, ...args);
|
|
2869
|
-
(
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2661
|
+
const run = this.processEvent(eventName, args).catch((err) => {
|
|
2662
|
+
this.logger.error(`[${_seedcord_logger.paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
|
|
2663
|
+
this.core.bot.emit("error:unhandled:event", err);
|
|
2664
|
+
});
|
|
2665
|
+
this.inFlight.add(run);
|
|
2666
|
+
run.finally(() => this.inFlight.delete(run));
|
|
2875
2667
|
});
|
|
2876
2668
|
}
|
|
2669
|
+
stopAccepting() {
|
|
2670
|
+
this.draining = true;
|
|
2671
|
+
}
|
|
2672
|
+
drain(timeoutMs) {
|
|
2673
|
+
return (0, _seedcord_core_node_internal.settleWithin)(Promise.allSettled(this.inFlight), timeoutMs);
|
|
2674
|
+
}
|
|
2877
2675
|
async processEvent(eventName, args) {
|
|
2878
2676
|
const handlerEntries = this.eventMap.get(eventName);
|
|
2879
2677
|
if (!handlerEntries || handlerEntries.length === 0) return;
|
|
@@ -2901,35 +2699,55 @@ var EventDispatcher = class {
|
|
|
2901
2699
|
};
|
|
2902
2700
|
|
|
2903
2701
|
//#endregion
|
|
2904
|
-
//#region src/bot/defaults/
|
|
2702
|
+
//#region src/bot/defaults/UnhandledRepliable.ts
|
|
2905
2703
|
/**
|
|
2906
|
-
* Default handler for unhandled interactions.
|
|
2907
|
-
* raw fault, so it needs no decorator of its own.
|
|
2908
|
-
*
|
|
2909
|
-
* @internal
|
|
2704
|
+
* @internal Default handler for unhandled repliable interactions.
|
|
2910
2705
|
*/
|
|
2911
|
-
var
|
|
2706
|
+
var UnhandledRepliable = class extends InteractionHandler {
|
|
2912
2707
|
async execute() {
|
|
2913
|
-
await this.
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2708
|
+
await this.reply("Feature not implemented yet.");
|
|
2709
|
+
}
|
|
2710
|
+
};
|
|
2711
|
+
|
|
2712
|
+
//#endregion
|
|
2713
|
+
//#region src/bot/defaults/UnhandledAutocomplete.ts
|
|
2714
|
+
/**
|
|
2715
|
+
* @internal Default handler for an autocomplete with no registered handler.
|
|
2716
|
+
*/
|
|
2717
|
+
var UnhandledAutocomplete = class extends AutocompleteHandler {
|
|
2718
|
+
async execute() {
|
|
2719
|
+
await this.respond([]);
|
|
2917
2720
|
}
|
|
2918
2721
|
};
|
|
2919
2722
|
|
|
2723
|
+
//#endregion
|
|
2724
|
+
//#region src/bot/harmlessApiCodes.ts
|
|
2725
|
+
/**
|
|
2726
|
+
* Discord API error codes that mean a dead end on an interaction, a dead token, a double ack, or a gone
|
|
2727
|
+
* message. Nothing to show the user and no bug to report, so the boundary's fault-card send drops them.
|
|
2728
|
+
*
|
|
2729
|
+
* @internal
|
|
2730
|
+
*/
|
|
2731
|
+
const HARMLESS_API_CODES = new Set([
|
|
2732
|
+
discord_js.RESTJSONErrorCodes.UnknownInteraction,
|
|
2733
|
+
discord_js.RESTJSONErrorCodes.InteractionHasAlreadyBeenAcknowledged,
|
|
2734
|
+
discord_js.RESTJSONErrorCodes.UnknownMessage
|
|
2735
|
+
]);
|
|
2736
|
+
|
|
2920
2737
|
//#endregion
|
|
2921
2738
|
//#region src/bot/handleInteractionFault.ts
|
|
2922
2739
|
const logger = new _seedcord_logger.Logger("InteractionBoundary");
|
|
2923
2740
|
/**
|
|
2924
2741
|
* The interaction controller boundary. Sorts a throw from any handler-lifecycle phase into the right
|
|
2925
|
-
* user reply plus the right bus event
|
|
2926
|
-
*
|
|
2742
|
+
* user reply plus the right bus event. When a handler is built its sender is passed in, so the boundary
|
|
2743
|
+
* sends from the current ack state. A middleware or pre-construction throw arrives with no sender, so the
|
|
2744
|
+
* boundary builds one from the interaction flags.
|
|
2927
2745
|
*
|
|
2928
2746
|
* @internal
|
|
2929
2747
|
*/
|
|
2930
|
-
async function handleInteractionFault(caught, interaction, core) {
|
|
2748
|
+
async function handleInteractionFault(caught, interaction, core, sender) {
|
|
2931
2749
|
if (caught instanceof _seedcord_core.Silence) {
|
|
2932
|
-
if (caught.reason !== void 0) logger.debug(`Silence: ${caught.reason}`);
|
|
2750
|
+
if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger.debug(`Silence: ${caught.reason}`);
|
|
2933
2751
|
return;
|
|
2934
2752
|
}
|
|
2935
2753
|
if (!Error.isError(caught)) throw caught;
|
|
@@ -2953,7 +2771,18 @@ async function handleInteractionFault(caught, interaction, core) {
|
|
|
2953
2771
|
user: interaction.user,
|
|
2954
2772
|
metadata: interaction
|
|
2955
2773
|
});
|
|
2956
|
-
await new ReplySender(interaction)
|
|
2774
|
+
await sendGuarded(sender ?? new ReplySender(interaction, interactionRoute(interaction)), response, caught instanceof _seedcord_core.Notice ? caught.ephemeral : true);
|
|
2775
|
+
}
|
|
2776
|
+
async function sendGuarded(sender, response, ephemeral) {
|
|
2777
|
+
try {
|
|
2778
|
+
await sender.send(response, { ephemeral });
|
|
2779
|
+
} catch (error) {
|
|
2780
|
+
if (error instanceof discord_js.DiscordAPIError && HARMLESS_API_CODES.has(error.code)) {
|
|
2781
|
+
logger.debug(`reply send hit harmless code ${error.code}`);
|
|
2782
|
+
return;
|
|
2783
|
+
}
|
|
2784
|
+
logger.error("reply send failed", error);
|
|
2785
|
+
}
|
|
2957
2786
|
}
|
|
2958
2787
|
|
|
2959
2788
|
//#endregion
|
|
@@ -2968,7 +2797,6 @@ var InteractionDispatcher = class {
|
|
|
2968
2797
|
core;
|
|
2969
2798
|
logger = new _seedcord_logger.Logger("Interactions");
|
|
2970
2799
|
isInitialized = false;
|
|
2971
|
-
name = "Interactions";
|
|
2972
2800
|
slashMap = /* @__PURE__ */ new Map();
|
|
2973
2801
|
buttonMap = /* @__PURE__ */ new Map();
|
|
2974
2802
|
modalMap = /* @__PURE__ */ new Map();
|
|
@@ -2982,6 +2810,8 @@ var InteractionDispatcher = class {
|
|
|
2982
2810
|
autocompleteMap = /* @__PURE__ */ new Map();
|
|
2983
2811
|
keysToIgnore = /* @__PURE__ */ new Set();
|
|
2984
2812
|
middlewares = [];
|
|
2813
|
+
inFlight = /* @__PURE__ */ new Set();
|
|
2814
|
+
draining = false;
|
|
2985
2815
|
loading = false;
|
|
2986
2816
|
loadedHandlers = [];
|
|
2987
2817
|
loadedMiddlewares = [];
|
|
@@ -3022,7 +2852,7 @@ var InteractionDispatcher = class {
|
|
|
3022
2852
|
}
|
|
3023
2853
|
/**
|
|
3024
2854
|
* Warns for each command route with no registered `@SlashRoute` handler, which would fall through to
|
|
3025
|
-
*
|
|
2855
|
+
* UnhandledRepliable at runtime. A bot may route commands outside the registry, so this does not throw.
|
|
3026
2856
|
*
|
|
3027
2857
|
* @internal
|
|
3028
2858
|
*/
|
|
@@ -3030,7 +2860,7 @@ var InteractionDispatcher = class {
|
|
|
3030
2860
|
for (const route of commandLeaves) if (!this.slashMap.has(route)) this.logger.warn(`Slash route ${_seedcord_logger.paint.sky.bold(route)} has no registered ${chalk.default.bold("@SlashRoute")} handler.`);
|
|
3031
2861
|
}
|
|
3032
2862
|
/**
|
|
3033
|
-
* Warns for each context-menu command with no matching handler, which would fall through to
|
|
2863
|
+
* Warns for each context-menu command with no matching handler, which would fall through to UnhandledRepliable
|
|
3034
2864
|
* at runtime. Checked per kind since a user and a message command can share a name.
|
|
3035
2865
|
*
|
|
3036
2866
|
* @internal
|
|
@@ -3043,7 +2873,7 @@ var InteractionDispatcher = class {
|
|
|
3043
2873
|
const artifacts = [];
|
|
3044
2874
|
for (const [routeType] of this.routeTypes) {
|
|
3045
2875
|
const meta = Reflect.getMetadata(_seedcord_core_internal.InteractionRouteKeys[routeType], handlerClass);
|
|
3046
|
-
if (areRoutes(meta)) artifacts.push({
|
|
2876
|
+
if ((0, _seedcord_core_internal.areRoutes)(meta)) artifacts.push({
|
|
3047
2877
|
routeType,
|
|
3048
2878
|
routes: meta
|
|
3049
2879
|
});
|
|
@@ -3138,7 +2968,7 @@ var InteractionDispatcher = class {
|
|
|
3138
2968
|
const writes = [];
|
|
3139
2969
|
for (const [routeType, map] of this.routeTypes) {
|
|
3140
2970
|
const meta = Reflect.getMetadata(_seedcord_core_internal.InteractionRouteKeys[routeType], handlerClass);
|
|
3141
|
-
if (!areRoutes(meta)) continue;
|
|
2971
|
+
if (!(0, _seedcord_core_internal.areRoutes)(meta)) continue;
|
|
3142
2972
|
for (const route of meta) {
|
|
3143
2973
|
const existing = map.get(route);
|
|
3144
2974
|
if (existing && existing !== handlerClass) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.InteractionDuplicateRoute, [
|
|
@@ -3172,7 +3002,7 @@ var InteractionDispatcher = class {
|
|
|
3172
3002
|
}
|
|
3173
3003
|
for (const [routeType, map] of this.routeTypes) {
|
|
3174
3004
|
const meta = Reflect.getMetadata(_seedcord_core_internal.InteractionRouteKeys[routeType], handlerClass);
|
|
3175
|
-
if (!areRoutes(meta)) continue;
|
|
3005
|
+
if (!(0, _seedcord_core_internal.areRoutes)(meta)) continue;
|
|
3176
3006
|
meta.forEach((route) => map.delete(route));
|
|
3177
3007
|
}
|
|
3178
3008
|
}
|
|
@@ -3182,35 +3012,53 @@ var InteractionDispatcher = class {
|
|
|
3182
3012
|
}
|
|
3183
3013
|
attachToClient() {
|
|
3184
3014
|
this.core.bot.client.on(discord_js.Events.InteractionCreate, (interaction) => {
|
|
3015
|
+
if (this.draining) return;
|
|
3185
3016
|
this.core.bot.emitSafe("any:interaction", interaction);
|
|
3186
|
-
this.handleInteraction(interaction).catch((err) => {
|
|
3017
|
+
const run = this.handleInteraction(interaction).catch((err) => {
|
|
3187
3018
|
this.logger.error(`[${_seedcord_logger.paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
|
|
3188
3019
|
this.core.bot.emit("error:unhandled:interaction", err);
|
|
3189
3020
|
});
|
|
3021
|
+
this.inFlight.add(run);
|
|
3022
|
+
run.finally(() => this.inFlight.delete(run));
|
|
3190
3023
|
});
|
|
3191
3024
|
}
|
|
3025
|
+
stopAccepting() {
|
|
3026
|
+
this.draining = true;
|
|
3027
|
+
}
|
|
3028
|
+
drain(timeoutMs) {
|
|
3029
|
+
return (0, _seedcord_core_node_internal.settleWithin)(Promise.allSettled(this.inFlight), timeoutMs);
|
|
3030
|
+
}
|
|
3192
3031
|
async handleCustomIdInteraction(interaction, getMap, interactionType) {
|
|
3193
3032
|
if ([...this.keysToIgnore].some((matcher) => matcher.owns(interaction.customId))) return;
|
|
3194
3033
|
const prefix = (0, _seedcord_core_internal.prefixOf)(interaction.customId);
|
|
3195
3034
|
if (!prefix) return this.logger.warn(`${interactionType} has invalid customId: ${interaction.customId}`);
|
|
3196
3035
|
await this.processInteraction(interaction, () => prefix, (key) => getMap().get(key));
|
|
3197
3036
|
}
|
|
3198
|
-
async processInteraction(interaction, extractKey, getHandler) {
|
|
3037
|
+
async processInteraction(interaction, extractKey, getHandler, fallback = UnhandledRepliable) {
|
|
3199
3038
|
const key = extractKey(interaction);
|
|
3039
|
+
let sender;
|
|
3200
3040
|
try {
|
|
3201
3041
|
if (!interaction.isAutocomplete()) for (const { ctor } of this.middlewares) await new ctor(interaction, this.core).execute();
|
|
3202
3042
|
let HandlerCtor = getHandler(key);
|
|
3203
3043
|
if (!HandlerCtor) {
|
|
3204
|
-
this.logger.warn(`No handler found for key ${_seedcord_logger.paint.sky.bold(key)}. Falling back to
|
|
3205
|
-
HandlerCtor =
|
|
3044
|
+
this.logger.warn(`No handler found for key ${_seedcord_logger.paint.sky.bold(key)}. Falling back to ${fallback.name}.`);
|
|
3045
|
+
HandlerCtor = fallback;
|
|
3206
3046
|
}
|
|
3207
3047
|
this.logger.debug(`Processing ${_seedcord_logger.paint.mint.bold(key)} with ${_seedcord_logger.paint.mute(HandlerCtor.name)}`);
|
|
3208
3048
|
const dispatch = new _seedcord_core.DispatchContext((0, _seedcord_core_internal.routeIdOf)(HandlerCtor));
|
|
3209
3049
|
const handler = new HandlerCtor(interaction, this.core, dispatch);
|
|
3210
|
-
if (
|
|
3050
|
+
if (handler instanceof RepliableHandler) sender = handler.getSender();
|
|
3051
|
+
if (!interaction.isAutocomplete()) {
|
|
3052
|
+
const monitor = (0, _seedcord_core_internal.slowGateMonitor)();
|
|
3053
|
+
try {
|
|
3054
|
+
await (0, _seedcord_core_internal.runHandlerGates)(HandlerCtor, interactionGateContext(interaction, this.core), dispatch.routeId ?? void 0, monitor?.observe);
|
|
3055
|
+
} finally {
|
|
3056
|
+
monitor?.report(dispatch.routeId);
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3211
3059
|
await handler.execute();
|
|
3212
3060
|
} catch (caught) {
|
|
3213
|
-
await handleInteractionFault(caught, interaction, this.core);
|
|
3061
|
+
await handleInteractionFault(caught, interaction, this.core, sender);
|
|
3214
3062
|
}
|
|
3215
3063
|
}
|
|
3216
3064
|
async handleInteraction(interaction) {
|
|
@@ -3286,12 +3134,7 @@ var InteractionDispatcher = class {
|
|
|
3286
3134
|
}
|
|
3287
3135
|
async handleAutocomplete(interaction) {
|
|
3288
3136
|
const route = slashRouteOf(interaction);
|
|
3289
|
-
|
|
3290
|
-
this.logger.warn(`No autocomplete handler for ${_seedcord_logger.paint.sky.bold(route)}.`);
|
|
3291
|
-
await interaction.respond([]);
|
|
3292
|
-
return;
|
|
3293
|
-
}
|
|
3294
|
-
await this.processInteraction(interaction, () => route, (key) => this.autocompleteMap.get(key));
|
|
3137
|
+
await this.processInteraction(interaction, () => route, (key) => this.autocompleteMap.get(key), UnhandledAutocomplete);
|
|
3295
3138
|
}
|
|
3296
3139
|
};
|
|
3297
3140
|
|
|
@@ -3312,23 +3155,25 @@ function __decorate(decorators, target, key, desc) {
|
|
|
3312
3155
|
|
|
3313
3156
|
//#endregion
|
|
3314
3157
|
//#region src/bot/Bot.ts
|
|
3158
|
+
const DISPATCH_DRAIN_TIMEOUT_MS = 5e3;
|
|
3159
|
+
const DRAIN_TASK_TIMEOUT_MS = 6e3;
|
|
3160
|
+
const UNBIND_TIMEOUT_MS = 2e3;
|
|
3161
|
+
const LOGOUT_TIMEOUT_MS = 2e3;
|
|
3315
3162
|
/**
|
|
3316
|
-
* Discord
|
|
3317
|
-
*
|
|
3318
|
-
* Don't instantiate this class directly. Use `core.bot` instead.
|
|
3163
|
+
* The Discord client and its controllers. Access it through `core.bot`.
|
|
3319
3164
|
*/
|
|
3320
|
-
var Bot = class extends
|
|
3165
|
+
var Bot = class extends _seedcord_event_emitter.TypedEventEmitter {
|
|
3321
3166
|
/** @internal */
|
|
3322
3167
|
logger = new _seedcord_logger.Logger("Bot");
|
|
3323
3168
|
isInitialized = false;
|
|
3324
3169
|
_client;
|
|
3325
3170
|
interactions;
|
|
3326
3171
|
events;
|
|
3327
|
-
commands;
|
|
3328
3172
|
emojiInjector;
|
|
3173
|
+
commands;
|
|
3329
3174
|
emojis = Emojis;
|
|
3330
3175
|
mentions = CommandMentions;
|
|
3331
|
-
/** @internal
|
|
3176
|
+
/** @internal */
|
|
3332
3177
|
async onHmr(event) {
|
|
3333
3178
|
if (this.interactions) await this.interactions.onHmr(event);
|
|
3334
3179
|
if (this.events) await this.events.onHmr(event);
|
|
@@ -3336,7 +3181,7 @@ var Bot = class extends Plugin {
|
|
|
3336
3181
|
}
|
|
3337
3182
|
/** @internal */
|
|
3338
3183
|
constructor(core) {
|
|
3339
|
-
super(
|
|
3184
|
+
super();
|
|
3340
3185
|
this._client = new discord_js.Client(core.config.bot.clientOptions);
|
|
3341
3186
|
if (core.config.bot.interactions.path) this.interactions = new InteractionDispatcher(core);
|
|
3342
3187
|
if (core.config.bot.events.path) this.events = new EventDispatcher(core);
|
|
@@ -3347,12 +3192,24 @@ var Bot = class extends Plugin {
|
|
|
3347
3192
|
});
|
|
3348
3193
|
}
|
|
3349
3194
|
this.emojiInjector = new EmojiInjector(core);
|
|
3350
|
-
|
|
3195
|
+
this.registerShutdownTasks(core);
|
|
3351
3196
|
}
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3197
|
+
registerShutdownTasks(core) {
|
|
3198
|
+
core.shutdown.addTask(_seedcord_core_node_internal.ShutdownPhase.Unbind, "stop-dispatch", () => {
|
|
3199
|
+
this.stopAccepting();
|
|
3200
|
+
return Promise.resolve();
|
|
3201
|
+
}, UNBIND_TIMEOUT_MS);
|
|
3202
|
+
core.shutdown.addTask(_seedcord_core_node_internal.ShutdownPhase.Drain, "drain-dispatch", () => this.drain(), DRAIN_TASK_TIMEOUT_MS);
|
|
3203
|
+
core.shutdown.addTask(_seedcord_core_node_internal.ShutdownPhase.Logout, "stop-bot", async () => await this.stop(), LOGOUT_TIMEOUT_MS);
|
|
3204
|
+
}
|
|
3205
|
+
stopAccepting() {
|
|
3206
|
+
this.interactions?.stopAccepting();
|
|
3207
|
+
this.events?.stopAccepting();
|
|
3208
|
+
}
|
|
3209
|
+
async drain() {
|
|
3210
|
+
await Promise.allSettled([this.interactions?.drain(DISPATCH_DRAIN_TIMEOUT_MS), this.events?.drain(DISPATCH_DRAIN_TIMEOUT_MS)]);
|
|
3211
|
+
}
|
|
3212
|
+
/** @internal */
|
|
3356
3213
|
async init() {
|
|
3357
3214
|
if (this.isInitialized) return;
|
|
3358
3215
|
this.isInitialized = true;
|
|
@@ -3368,25 +3225,18 @@ var Bot = class extends Plugin {
|
|
|
3368
3225
|
this.interactions?.warnUnhandledContextMenuRoutes(this.commands.contextMenuLeaves());
|
|
3369
3226
|
}
|
|
3370
3227
|
}
|
|
3371
|
-
/**
|
|
3372
|
-
* Stops the bot and cleans up connections
|
|
3373
|
-
* @internal
|
|
3374
|
-
*/
|
|
3228
|
+
/** @internal */
|
|
3375
3229
|
async stop() {
|
|
3376
3230
|
this._client.removeAllListeners();
|
|
3377
3231
|
await this.logout();
|
|
3378
3232
|
}
|
|
3379
|
-
/**
|
|
3380
|
-
* Logs the bot into Discord using the configured token
|
|
3381
|
-
*/
|
|
3382
3233
|
async login(token) {
|
|
3383
|
-
|
|
3234
|
+
this._client.once(discord_js.Events.ClientReady, () => this.emit("ready"));
|
|
3235
|
+
this._client.login(token);
|
|
3236
|
+
await this.waitFor("ready");
|
|
3384
3237
|
this.logger.info(`Logged in as ${chalk.default.bold.magenta(this._client.user?.username)}!`);
|
|
3385
3238
|
return this;
|
|
3386
3239
|
}
|
|
3387
|
-
/**
|
|
3388
|
-
* Logs out and destroys the Discord client connection
|
|
3389
|
-
*/
|
|
3390
3240
|
async logout() {
|
|
3391
3241
|
await this._client.destroy();
|
|
3392
3242
|
this.logger.info(chalk.default.bold.red("Logged out of Discord!"));
|
|
@@ -3394,6 +3244,12 @@ var Bot = class extends Plugin {
|
|
|
3394
3244
|
get client() {
|
|
3395
3245
|
return this._client;
|
|
3396
3246
|
}
|
|
3247
|
+
/**
|
|
3248
|
+
* The overloads enforce the key/args correlation at compile time only. At runtime this forwards
|
|
3249
|
+
* straight to the underlying TypedEventEmitter.
|
|
3250
|
+
*
|
|
3251
|
+
* @internal
|
|
3252
|
+
*/
|
|
3397
3253
|
emit(event, ...args) {
|
|
3398
3254
|
return super.emit(event, ...args);
|
|
3399
3255
|
}
|
|
@@ -3406,157 +3262,70 @@ var Bot = class extends Plugin {
|
|
|
3406
3262
|
};
|
|
3407
3263
|
__decorate([(0, envapt_legacy.Envapt)("DISCORD_BOT_TOKEN", { converter: (raw) => (0, _seedcord_errors_internal.validateDiscordToken)(raw) }), __decorateMetadata("design:type", String)], Bot.prototype, "botToken", void 0);
|
|
3408
3264
|
|
|
3409
|
-
//#endregion
|
|
3410
|
-
//#region src/subscribers/bases/webhookHelpers.ts
|
|
3411
|
-
const DISCORD_WEBHOOK_REGEX = new RegExp(String.raw`^https?:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w$-]+$`);
|
|
3412
|
-
/**
|
|
3413
|
-
* Whether a string is a well-formed Discord webhook URL.
|
|
3414
|
-
*
|
|
3415
|
-
* @internal
|
|
3416
|
-
*/
|
|
3417
|
-
function isDiscordWebhookUrl(value) {
|
|
3418
|
-
return URL.canParse(value) && DISCORD_WEBHOOK_REGEX.test(value);
|
|
3419
|
-
}
|
|
3420
|
-
/**
|
|
3421
|
-
* Serializes a value to a JSON attachment, made circular-safe first.
|
|
3422
|
-
*
|
|
3423
|
-
* @internal
|
|
3424
|
-
*/
|
|
3425
|
-
function jsonAttachment(name, description, data) {
|
|
3426
|
-
const content = (0, _seedcord_utils.filterCirculars)(data);
|
|
3427
|
-
return new discord_js.AttachmentBuilder(Buffer.from(JSON.stringify(content, void 0, 2), "utf8"), {
|
|
3428
|
-
name,
|
|
3429
|
-
description
|
|
3430
|
-
});
|
|
3431
|
-
}
|
|
3432
|
-
/**
|
|
3433
|
-
* The small divider the default webhook reporters place between sections.
|
|
3434
|
-
*
|
|
3435
|
-
* @internal
|
|
3436
|
-
*/
|
|
3437
|
-
var WebhookSeparator = class extends _seedcord_core.BuilderComponent {
|
|
3438
|
-
constructor() {
|
|
3439
|
-
super("separator");
|
|
3440
|
-
this.instance.setSpacing(discord_js.SeparatorSpacingSize.Small).setDivider(true);
|
|
3441
|
-
}
|
|
3442
|
-
};
|
|
3443
|
-
|
|
3444
3265
|
//#endregion
|
|
3445
3266
|
//#region src/subscribers/default/HandledException.ts
|
|
3446
|
-
var _HandledException;
|
|
3447
|
-
function webhookUrlValidator$1(raw) {
|
|
3448
|
-
if (raw !== null && typeof raw !== "string") throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigHandledExceptionWebhookInvalid);
|
|
3449
|
-
const value = raw?.trim() ?? "";
|
|
3450
|
-
if (value === "") throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigHandledExceptionWebhookMissing);
|
|
3451
|
-
if (!isDiscordWebhookUrl(value)) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigHandledExceptionWebhookInvalid);
|
|
3452
|
-
return value;
|
|
3453
|
-
}
|
|
3454
3267
|
let HandledException = class HandledException extends WebhookLog {
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
try {
|
|
3462
|
-
await this.webhook.send({
|
|
3463
|
-
flags: flagsFor(false),
|
|
3464
|
-
withComponents: true,
|
|
3465
|
-
username: "Handled Exception",
|
|
3466
|
-
components: [new HandledExceptionContainer(this.data).component],
|
|
3467
|
-
files: [jsonAttachment("source.json", "The raw source the fault came from", source.raw)]
|
|
3468
|
-
});
|
|
3469
|
-
} catch (error) {
|
|
3470
|
-
this.logger.error("Failed to send handled exception webhook", error);
|
|
3471
|
-
}
|
|
3268
|
+
report() {
|
|
3269
|
+
return {
|
|
3270
|
+
username: "Handled Exception",
|
|
3271
|
+
components: [new HandledExceptionContainer(this.data).component],
|
|
3272
|
+
files: [jsonAttachment("source.json", "The raw source the fault came from", this.data.source.raw)]
|
|
3273
|
+
};
|
|
3472
3274
|
}
|
|
3473
3275
|
};
|
|
3474
|
-
__decorate([(
|
|
3475
|
-
HandledException = _HandledException = __decorate([Subscribe("handledException")], HandledException);
|
|
3276
|
+
HandledException = __decorate([Subscribe("handledException"), WebhookUrl("HANDLED_EXCEPTION_WEBHOOK_URL")], HandledException);
|
|
3476
3277
|
var HandledExceptionContainer = class extends _seedcord_core.BuilderComponent {
|
|
3477
3278
|
constructor(data) {
|
|
3478
3279
|
super("container");
|
|
3479
3280
|
const { denial, uuid, source } = data;
|
|
3480
|
-
this.instance.addTextDisplayComponents((text) => text.setContent(faultSummary(denial, source))).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### UUID \`${uuid}\`\n\`\`\`${causeStack(denial)}\`\`\``)).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent("### Source")).addFileComponents((file) => file.setURL("attachment://source.json"));
|
|
3281
|
+
this.instance.addTextDisplayComponents((text) => text.setContent(faultSummary(denial, source))).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### UUID \`${uuid}\`\n\`\`\`${neutralizeFences(causeStack(denial))}\`\`\``)).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent("### Source")).addFileComponents((file) => file.setURL("attachment://source.json"));
|
|
3481
3282
|
}
|
|
3482
3283
|
};
|
|
3483
|
-
/**
|
|
3484
|
-
* The summary block of a fault report, narrowed per source kind so an interaction fault shows its
|
|
3485
|
-
* command and customId while an event fault shows its event name and handler.
|
|
3486
|
-
*
|
|
3487
|
-
* @internal
|
|
3488
|
-
*/
|
|
3489
3284
|
function faultSummary(denial, source) {
|
|
3490
|
-
const head = `### A handled fault was reported: \`${denial.name}\`\n**Message:** ${denial.message}\n
|
|
3285
|
+
const head = (0, _seedcord_utils.stripAnsi)(`### A handled fault was reported: \`${denial.name}\`\n**Message:** ${denial.message}\n`);
|
|
3491
3286
|
if (source.kind === "event") return `${head}**Event:** ${source.eventName}\n**Handler:** ${source.handler}\n**User ID:** \`${source.userId ?? "n/a"}\`\n**Guild ID:** \`${source.guildId ?? "n/a"}\`\n**Channel ID:** \`${source.channelId ?? "n/a"}\``;
|
|
3492
3287
|
return `${head}**Kind:** ${source.interactionKind}\n**Command:** ${source.command ?? "n/a"}\n**CustomId:** ${source.customId ?? "n/a"}\n**User ID:** \`${source.userId}\`\n**Guild ID:** \`${source.guildId ?? "n/a"}\`\n**Channel ID:** \`${source.channelId ?? "n/a"}\`\n**Interaction ID:** \`${source.interactionId}\``;
|
|
3493
3288
|
}
|
|
3494
|
-
/**
|
|
3495
|
-
* The text shown for a denial's cause in the report. Tolerates a non-serializable cause (a bigint or a
|
|
3496
|
-
* circular object) so building the report never throws.
|
|
3497
|
-
*
|
|
3498
|
-
* @internal
|
|
3499
|
-
*/
|
|
3500
3289
|
function causeStack(denial) {
|
|
3501
3290
|
const { cause } = denial;
|
|
3502
|
-
if (Error.isError(cause)) return cause.stack ?? cause.message;
|
|
3503
|
-
if (typeof cause === "string") return cause;
|
|
3291
|
+
if (Error.isError(cause)) return (0, _seedcord_utils.stripAnsi)(cause.stack ?? cause.message);
|
|
3292
|
+
if (typeof cause === "string") return (0, _seedcord_utils.stripAnsi)(cause);
|
|
3504
3293
|
if (typeof cause === "bigint" || typeof cause === "symbol" || typeof cause === "function") return cause.toString();
|
|
3505
3294
|
if (cause !== void 0) try {
|
|
3506
3295
|
return JSON.stringify(cause);
|
|
3507
3296
|
} catch {
|
|
3508
3297
|
return "[unserializable cause]";
|
|
3509
3298
|
}
|
|
3510
|
-
return denial.stack ?? "No cause recorded.";
|
|
3299
|
+
return (0, _seedcord_utils.stripAnsi)(denial.stack ?? "No cause recorded.");
|
|
3511
3300
|
}
|
|
3512
3301
|
|
|
3513
3302
|
//#endregion
|
|
3514
3303
|
//#region src/subscribers/default/UnknownException.ts
|
|
3515
|
-
var _UnknownException;
|
|
3516
|
-
function webhookUrlValidator(raw) {
|
|
3517
|
-
if (raw !== null && typeof raw !== "string") throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigUnknownExceptionWebhookInvalid);
|
|
3518
|
-
const value = raw?.trim() ?? "";
|
|
3519
|
-
if (value === "") throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigUnknownExceptionWebhookMissing);
|
|
3520
|
-
if (!isDiscordWebhookUrl(value)) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigUnknownExceptionWebhookInvalid);
|
|
3521
|
-
return value;
|
|
3522
|
-
}
|
|
3523
3304
|
let UnknownException = class UnknownException extends WebhookLog {
|
|
3524
|
-
|
|
3525
|
-
_UnknownException = this;
|
|
3526
|
-
}
|
|
3527
|
-
webhook = new discord_js.WebhookClient({ url: _UnknownException.unknownExceptionWebhookUrl });
|
|
3528
|
-
async execute() {
|
|
3305
|
+
report() {
|
|
3529
3306
|
const { metadata } = this.data;
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
username: "Unknown Exception",
|
|
3536
|
-
components: [new UnhandledErrorContainer(this.data).component],
|
|
3537
|
-
files: metadataFile ? [metadataFile] : []
|
|
3538
|
-
});
|
|
3539
|
-
} catch (error) {
|
|
3540
|
-
this.logger.error("Failed to send unknown exception webhook", error);
|
|
3541
|
-
}
|
|
3307
|
+
return {
|
|
3308
|
+
username: "Unknown Exception",
|
|
3309
|
+
components: [new UnhandledErrorContainer(this.data).component],
|
|
3310
|
+
files: metadata ? [jsonAttachment("metadata.json", "Metadata associated with the error", metadata)] : void 0
|
|
3311
|
+
};
|
|
3542
3312
|
}
|
|
3543
3313
|
};
|
|
3544
|
-
__decorate([(
|
|
3545
|
-
UnknownException = _UnknownException = __decorate([Subscribe("unknownException")], UnknownException);
|
|
3314
|
+
UnknownException = __decorate([Subscribe("unknownException"), WebhookUrl("UNKNOWN_EXCEPTION_WEBHOOK_URL")], UnknownException);
|
|
3546
3315
|
var UnhandledErrorContainer = class extends _seedcord_core.BuilderComponent {
|
|
3547
3316
|
constructor(data) {
|
|
3548
3317
|
super("container");
|
|
3549
3318
|
const { uuid, error, guild, user, metadata } = data;
|
|
3550
|
-
this.instance.addTextDisplayComponents((text) => text.setContent(`### An unknown exception was thrown\n**Guild ID:** \`${guild?.id ?? "Not used in a guild"}\`\n**Guild Name:** ${guild?.name ?? "Not used in a guild"}\n**User ID:** \`${user?.id ?? "Missing user info"}\`\n**Username:** ${user?.username ?? "Missing user info"}\n`)).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### UUID \`${uuid}\`\n\`\`\`${error
|
|
3319
|
+
this.instance.addTextDisplayComponents((text) => text.setContent(`### An unknown exception was thrown\n**Guild ID:** \`${guild?.id ?? "Not used in a guild"}\`\n**Guild Name:** ${guild?.name ?? "Not used in a guild"}\n**User ID:** \`${user?.id ?? "Missing user info"}\`\n**Username:** ${user?.username ?? "Missing user info"}\n`)).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### UUID \`${uuid}\`\n\`\`\`${errorReport(error)}\`\`\``));
|
|
3551
3320
|
this.addTimestampsIfAvailable(error);
|
|
3552
3321
|
this.addMetadataIfAvailable(metadata);
|
|
3553
3322
|
}
|
|
3554
3323
|
addTimestampsIfAvailable(error) {
|
|
3555
|
-
if (!(error instanceof
|
|
3324
|
+
if (!(error instanceof _discordjs_rest.DiscordAPIError)) return;
|
|
3556
3325
|
const now = Date.now();
|
|
3557
3326
|
const snowflake = error.url.match(/\/interactions\/(\d+)\//)?.[1];
|
|
3558
3327
|
if (!snowflake) return void 0;
|
|
3559
|
-
const interactionTs =
|
|
3328
|
+
const interactionTs = (0, _seedcord_utils.timestampFromSnowflake)(snowflake);
|
|
3560
3329
|
const diff = now - interactionTs;
|
|
3561
3330
|
const seconds = Math.floor(diff / 1e3);
|
|
3562
3331
|
const millis = diff % 1e3;
|
|
@@ -3571,24 +3340,19 @@ var UnhandledErrorContainer = class extends _seedcord_core.BuilderComponent {
|
|
|
3571
3340
|
//#endregion
|
|
3572
3341
|
//#region src/subscribers/Bus.ts
|
|
3573
3342
|
/**
|
|
3574
|
-
*
|
|
3575
|
-
*
|
|
3576
|
-
* Provides a centralized system for registering and executing custom subscribers
|
|
3577
|
-
* throughout the application lifecycle. Bus subscribers are loaded from configured directories
|
|
3578
|
-
* and can be triggered programmatically or by framework events. Accessed via `core.bus`. Do not
|
|
3579
|
-
* construct it directly.
|
|
3343
|
+
* Registers and dispatches subscribers. Subscribers load from the configured directories and run
|
|
3344
|
+
* on a programmatic or framework-event publish. Accessed via `core.bus`. Do not construct it directly.
|
|
3580
3345
|
*/
|
|
3581
|
-
var Bus = class extends
|
|
3346
|
+
var Bus = class extends _seedcord_event_emitter.TypedEventEmitter {
|
|
3582
3347
|
core;
|
|
3583
3348
|
logger = new _seedcord_logger.Logger("Subscribers");
|
|
3584
|
-
/** @internal */
|
|
3585
|
-
name = "Subscribers";
|
|
3586
3349
|
isInitialized = false;
|
|
3587
3350
|
subscribersMap = /* @__PURE__ */ new Map();
|
|
3588
3351
|
executedOnceHandlers = /* @__PURE__ */ new Set();
|
|
3352
|
+
webhookProbes = /* @__PURE__ */ new Map();
|
|
3589
3353
|
hmrHandler;
|
|
3590
3354
|
constructor(core) {
|
|
3591
|
-
super(
|
|
3355
|
+
super();
|
|
3592
3356
|
this.core = core;
|
|
3593
3357
|
if (this.core.config.subscribers.path) {
|
|
3594
3358
|
if (!envapt.Envapter.isDevelopment && !envapt.Envapter.isTest) return;
|
|
@@ -3611,7 +3375,6 @@ var Bus = class extends Plugin {
|
|
|
3611
3375
|
this.isInitialized = true;
|
|
3612
3376
|
this.registerSubscriber(UnknownException);
|
|
3613
3377
|
this.registerSubscriber(HandledException);
|
|
3614
|
-
envapt.Envapter.require("UNKNOWN_EXCEPTION_WEBHOOK_URL", "HANDLED_EXCEPTION_WEBHOOK_URL");
|
|
3615
3378
|
const subscribersDir = this.core.config.subscribers.path;
|
|
3616
3379
|
if (subscribersDir) {
|
|
3617
3380
|
this.logger.info(chalk.default.bold(subscribersDir));
|
|
@@ -3619,8 +3382,19 @@ var Bus = class extends Plugin {
|
|
|
3619
3382
|
const totalSubscribers = [...this.subscribersMap.values()].reduce((acc, handlers) => acc + handlers.length, 0);
|
|
3620
3383
|
this.logger.utils.list([`${chalk.default.bold.magenta(totalSubscribers)} subscribers`], chalk.default.bold.green("Loaded"));
|
|
3621
3384
|
}
|
|
3385
|
+
if (!envapt.Envapter.isTest) await this.verifyWebhooks();
|
|
3386
|
+
}
|
|
3387
|
+
/** @internal Exposed for tests. */
|
|
3388
|
+
async verifyWebhooks() {
|
|
3389
|
+
const missing = [];
|
|
3390
|
+
await Promise.all([...this.webhookProbes].map(async ([url, envKeys]) => {
|
|
3391
|
+
const result = await WebhookLog.senderFor(url).verify();
|
|
3392
|
+
if (result === "missing") missing.push(...envKeys);
|
|
3393
|
+
else if (result === "unreachable") this.logger.warn(`could not verify webhook at ${_seedcord_logger.paint.sky.bold(envKeys.join(", "))}`);
|
|
3394
|
+
}));
|
|
3395
|
+
if (missing.length > 0) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigWebhookNotFound, [missing.join(", ")]);
|
|
3622
3396
|
}
|
|
3623
|
-
/** @internal
|
|
3397
|
+
/** @internal */
|
|
3624
3398
|
async onHmr(event) {
|
|
3625
3399
|
if (this.hmrHandler) await this.hmrHandler.handle(event);
|
|
3626
3400
|
}
|
|
@@ -3637,6 +3411,17 @@ var Bus = class extends Plugin {
|
|
|
3637
3411
|
}, this.logger);
|
|
3638
3412
|
}
|
|
3639
3413
|
registerSubscriber(handler) {
|
|
3414
|
+
if (handler.prototype instanceof WebhookLog) {
|
|
3415
|
+
const envKey = WebhookLog.envKeyOf(handler);
|
|
3416
|
+
const url = WebhookLog.urlOf(envKey);
|
|
3417
|
+
if (url === null) {
|
|
3418
|
+
this.logger.warn(`${chalk.default.bold(handler.name)} disabled, ${_seedcord_logger.paint.sky.bold(envKey)} is not set`);
|
|
3419
|
+
return;
|
|
3420
|
+
}
|
|
3421
|
+
const envKeys = this.webhookProbes.get(url) ?? [];
|
|
3422
|
+
if (!envKeys.includes(envKey)) envKeys.push(envKey);
|
|
3423
|
+
this.webhookProbes.set(url, envKeys);
|
|
3424
|
+
}
|
|
3640
3425
|
const options = Reflect.getMetadata(_seedcord_core_internal.SubscribeMetadataKey, handler);
|
|
3641
3426
|
let handlers = this.subscribersMap.get(options.subscriber);
|
|
3642
3427
|
if (!handlers) {
|
|
@@ -3696,17 +3481,15 @@ var Bus = class extends Plugin {
|
|
|
3696
3481
|
//#endregion
|
|
3697
3482
|
//#region src/version.ts
|
|
3698
3483
|
/** Package version */
|
|
3699
|
-
const version = "0.1.0-next.
|
|
3484
|
+
const version = "0.1.0-next.4";
|
|
3700
3485
|
|
|
3701
3486
|
//#endregion
|
|
3702
3487
|
//#region src/Seedcord.ts
|
|
3703
3488
|
/**
|
|
3704
|
-
*
|
|
3705
|
-
*
|
|
3706
|
-
* Primary entry point for creating Discord bots with Seedcord.
|
|
3707
|
-
* Manages component lifecycle and provides plugin support.
|
|
3489
|
+
* The gateway bot host. Opens a discord.js gateway session, discovers handlers, and runs
|
|
3490
|
+
* coordinated startup and shutdown. Attach plugins with `attach()`.
|
|
3708
3491
|
*/
|
|
3709
|
-
var Seedcord = class Seedcord extends Pluggable {
|
|
3492
|
+
var Seedcord = class Seedcord extends _seedcord_core_node_internal.Pluggable {
|
|
3710
3493
|
config;
|
|
3711
3494
|
/** @internal */
|
|
3712
3495
|
[_seedcord_types_internal.SeedcordBrand] = true;
|
|
@@ -3714,7 +3497,6 @@ var Seedcord = class Seedcord extends Pluggable {
|
|
|
3714
3497
|
augmentTarget = "@seedcord/gateway";
|
|
3715
3498
|
/** @internal */
|
|
3716
3499
|
version = version;
|
|
3717
|
-
static isInstantiated = false;
|
|
3718
3500
|
healthCheck;
|
|
3719
3501
|
hmrManager;
|
|
3720
3502
|
/** @see {@link CoordinatedShutdown} */
|
|
@@ -3728,16 +3510,12 @@ var Seedcord = class Seedcord extends Pluggable {
|
|
|
3728
3510
|
/** @see {@link IRateLimiter} */
|
|
3729
3511
|
rateLimiter;
|
|
3730
3512
|
/**
|
|
3731
|
-
* Creates a new Seedcord instance
|
|
3732
|
-
*
|
|
3733
3513
|
* @param config - Bot configuration including paths and Discord client options
|
|
3734
3514
|
* @throws A **SeedcordError** When attempting to create multiple instances (singleton)
|
|
3735
3515
|
*/
|
|
3736
3516
|
constructor(config) {
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
const shutdown = new _seedcord_services.CoordinatedShutdown(config.shutdownEnabled);
|
|
3740
|
-
const startup = new _seedcord_services.CoordinatedStartup();
|
|
3517
|
+
const shutdown = new _seedcord_core_node_internal.CoordinatedShutdown();
|
|
3518
|
+
const startup = new _seedcord_core_node_internal.CoordinatedStartup();
|
|
3741
3519
|
super(shutdown, startup);
|
|
3742
3520
|
this.config = config;
|
|
3743
3521
|
(0, _seedcord_logger_node.installNodeDefaults)(config.logger);
|
|
@@ -3749,46 +3527,60 @@ var Seedcord = class Seedcord extends Pluggable {
|
|
|
3749
3527
|
this.bus = new Bus(this);
|
|
3750
3528
|
this.bot = new Bot(this);
|
|
3751
3529
|
this.rateLimiter = config.store ?? new _seedcord_rate_limiter.MemoryRateLimiter();
|
|
3752
|
-
this.healthCheck =
|
|
3530
|
+
this.healthCheck = _seedcord_core_node_internal.HealthCheck.fromOption(this.shutdown, config.healthCheck);
|
|
3753
3531
|
this.registerStartupTasks();
|
|
3754
3532
|
}
|
|
3755
3533
|
/** The bot's discord username, populated after login. */
|
|
3756
3534
|
get username() {
|
|
3757
3535
|
return this.bot.client.user?.username;
|
|
3758
3536
|
}
|
|
3537
|
+
pluginCapabilities() {
|
|
3538
|
+
return {
|
|
3539
|
+
client: this.bot.client,
|
|
3540
|
+
token: this.bot.botToken,
|
|
3541
|
+
rest: this.bot.client.rest
|
|
3542
|
+
};
|
|
3543
|
+
}
|
|
3759
3544
|
static reset() {
|
|
3760
|
-
|
|
3545
|
+
super.reset();
|
|
3761
3546
|
_seedcord_logger.LoggerChannelRegistry.instance.reset();
|
|
3762
3547
|
}
|
|
3763
3548
|
registerStartupTasks() {
|
|
3764
3549
|
if (envapt.Envapter.isDevelopment || envapt.Envapter.isTest) this.registerHmrAwareModules();
|
|
3765
|
-
this.startup.addTask(
|
|
3550
|
+
this.startup.addTask(_seedcord_core_node_internal.StartupPhase.Configuration, "Bus Initialization", async () => {
|
|
3766
3551
|
this.bus.logger.utils.initialization("Subscribers", "start");
|
|
3767
3552
|
await this.bus.init();
|
|
3768
3553
|
this.bus.logger.utils.initialization("Subscribers", "end");
|
|
3769
3554
|
});
|
|
3770
|
-
this.startup.addTask(
|
|
3555
|
+
this.startup.addTask(_seedcord_core_node_internal.StartupPhase.Login, "Bot Initialization", async () => {
|
|
3771
3556
|
this.bot.logger.utils.initialization("Bot", "start");
|
|
3772
3557
|
await this.bot.init();
|
|
3773
3558
|
this.bot.logger.utils.initialization("Bot", "end");
|
|
3774
3559
|
});
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3560
|
+
const { healthCheck } = this;
|
|
3561
|
+
if (healthCheck) this.startup.addTask(_seedcord_core_node_internal.StartupPhase.Ready, "Health Check", async () => {
|
|
3562
|
+
healthCheck.logger.utils.initialization("HealthCheck", "start");
|
|
3563
|
+
await healthCheck.init();
|
|
3564
|
+
healthCheck.logger.utils.initialization("HealthCheck", "end");
|
|
3779
3565
|
});
|
|
3780
3566
|
}
|
|
3781
3567
|
/**
|
|
3782
3568
|
* Starts the bot and runs all initialization tasks
|
|
3783
3569
|
*
|
|
3784
|
-
* @returns This Seedcord instance when
|
|
3570
|
+
* @returns This Seedcord instance when initialized
|
|
3785
3571
|
*/
|
|
3786
3572
|
async start() {
|
|
3787
|
-
|
|
3573
|
+
try {
|
|
3574
|
+
await super.init();
|
|
3575
|
+
} catch (caught) {
|
|
3576
|
+
await this.shutdown.run(1, false);
|
|
3577
|
+
Seedcord.reset();
|
|
3578
|
+
throw caught;
|
|
3579
|
+
}
|
|
3788
3580
|
return this;
|
|
3789
3581
|
}
|
|
3790
3582
|
registerHmrAwareModules() {
|
|
3791
|
-
this.startup.addTask(
|
|
3583
|
+
this.startup.addTask(_seedcord_core_node_internal.StartupPhase.Configuration, "HMR Registration", async () => {
|
|
3792
3584
|
this.hmrManager.register(this.bot);
|
|
3793
3585
|
this.hmrManager.register(this.bus);
|
|
3794
3586
|
for (const plugin of this.plugins) this.hmrManager.register(plugin);
|
|
@@ -3800,12 +3592,9 @@ var Seedcord = class Seedcord extends Pluggable {
|
|
|
3800
3592
|
//#endregion
|
|
3801
3593
|
exports.ArraySource = ArraySource;
|
|
3802
3594
|
exports.AutocompleteHandler = AutocompleteHandler;
|
|
3803
|
-
exports.AutocompleteRoute = AutocompleteRoute;
|
|
3804
3595
|
exports.ButtonHandler = ButtonHandler;
|
|
3805
|
-
exports.ButtonRoute = ButtonRoute;
|
|
3806
3596
|
exports.CommandMentions = CommandMentions;
|
|
3807
3597
|
exports.ContextMenuHandler = ContextMenuHandler;
|
|
3808
|
-
exports.ContextMenuRoute = ContextMenuRoute;
|
|
3809
3598
|
exports.CursorSource = CursorSource;
|
|
3810
3599
|
exports.Emojis = Emojis;
|
|
3811
3600
|
exports.EventHandler = EventHandler;
|
|
@@ -3817,31 +3606,17 @@ exports.InteractionMiddleware = InteractionMiddleware;
|
|
|
3817
3606
|
exports.Middleware = Middleware;
|
|
3818
3607
|
exports.MiddlewareType = MiddlewareType;
|
|
3819
3608
|
exports.ModalHandler = ModalHandler;
|
|
3820
|
-
exports.ModalRoute = ModalRoute;
|
|
3821
3609
|
exports.Nsfw = Nsfw;
|
|
3822
3610
|
exports.Paginator = Paginator;
|
|
3823
|
-
exports.Pluggable = Pluggable;
|
|
3824
|
-
exports.Plugin = Plugin;
|
|
3825
|
-
Object.defineProperty(exports, 'RegisterCommand', {
|
|
3826
|
-
enumerable: true,
|
|
3827
|
-
get: function () {
|
|
3828
|
-
return _seedcord_core.RegisterCommand;
|
|
3829
|
-
}
|
|
3830
|
-
});
|
|
3831
3611
|
exports.RegisterEvent = RegisterEvent;
|
|
3832
|
-
exports.
|
|
3833
|
-
exports.RequireBotPermissions = RequireBotPermissions;
|
|
3834
|
-
exports.RequirePermissions = RequirePermissions;
|
|
3835
|
-
exports.RequireRole = RequireRole;
|
|
3612
|
+
exports.RepliableHandler = RepliableHandler;
|
|
3836
3613
|
exports.Seedcord = Seedcord;
|
|
3837
3614
|
exports.SelectMenuHandler = SelectMenuHandler;
|
|
3838
|
-
exports.SelectMenuKind = SelectMenuKind;
|
|
3839
|
-
exports.SelectMenuRoute = SelectMenuRoute;
|
|
3840
3615
|
exports.SlashHandler = SlashHandler;
|
|
3841
|
-
exports.SlashRoute = SlashRoute;
|
|
3842
3616
|
exports.Subscribe = Subscribe;
|
|
3843
3617
|
exports.Subscriber = Subscriber;
|
|
3844
3618
|
exports.WebhookLog = WebhookLog;
|
|
3619
|
+
exports.WebhookUrl = WebhookUrl;
|
|
3845
3620
|
exports.checkBotPermissions = checkBotPermissions;
|
|
3846
3621
|
exports.checkPermissions = checkPermissions;
|
|
3847
3622
|
exports.fetchGuildMember = fetchGuildMember;
|
|
@@ -3890,13 +3665,6 @@ Object.keys(_seedcord_rate_limiter).forEach(function (k) {
|
|
|
3890
3665
|
});
|
|
3891
3666
|
});
|
|
3892
3667
|
|
|
3893
|
-
Object.keys(_seedcord_services).forEach(function (k) {
|
|
3894
|
-
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
|
3895
|
-
enumerable: true,
|
|
3896
|
-
get: function () { return _seedcord_services[k]; }
|
|
3897
|
-
});
|
|
3898
|
-
});
|
|
3899
|
-
|
|
3900
3668
|
Object.keys(_seedcord_utils).forEach(function (k) {
|
|
3901
3669
|
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
|
3902
3670
|
enumerable: true,
|