@seedcord/gateway 0.1.0-next.4 → 0.1.0-next.6
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 +535 -1666
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +159 -749
- package/dist/index.mjs +538 -1660
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -8
package/dist/index.mjs
CHANGED
|
@@ -1,27 +1,24 @@
|
|
|
1
1
|
import "reflect-metadata";
|
|
2
2
|
import { installNodeDefaults } from "@seedcord/logger/node";
|
|
3
|
-
import { BaseReplySender,
|
|
3
|
+
import { BaseReplySender, CommandInjector, EventMetadataKey, FaultThrottle, GateNotice, GatedMetadataKey, HmrManager, InteractionMetadataKey, InteractionRouteKeys, InteractionRoutes, MiddlewareMetadataKey, MissingPermissions, NoticeCard, PermissionNames, PublishDefault, accessorStore, areRoutes, asError, clearStore, decodeComponentRoute, deferFlags, dispatchedPayload, guardedAccessor, isEmojiTuple, outcomeFor, pickNotice, prefixOf, queuedMsFor, reportedWrite, routeIdOf, runHandlerGates, sendFlags, setBotColor, slowGateMonitor } from "@seedcord/core/internal";
|
|
4
4
|
import { SeedcordErrorCode } from "@seedcord/errors";
|
|
5
|
-
import { SeedcordError,
|
|
6
|
-
import { BaseHandler, BuilderComponent, CustomId, DispatchContext, Fault, Notice, RepliableHandler as RepliableHandler$1, RowComponent, Silence,
|
|
5
|
+
import { SeedcordError, SeedcordTypeError, validateDiscordToken } from "@seedcord/errors/internal";
|
|
6
|
+
import { ArraySource as ArraySource$1, BaseHandler, BuilderComponent, Bus, CursorSource as CursorSource$1, CustomId, DispatchContext, Fault, Notice, PaginatorBase, RepliableHandler as RepliableHandler$1, RowComponent, Silence, Subscriber as Subscriber$1, WebhookLog as WebhookLog$1, assertPermissions, defineGate } from "@seedcord/core";
|
|
7
7
|
import { Logger, LoggerChannelRegistry, paint } from "@seedcord/logger";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import { Converters, Envapter } from "envapt";
|
|
14
|
-
import { MessageFlags as MessageFlags$1, Routes, SeparatorSpacingSize } from "discord-api-types/v10";
|
|
15
|
-
import { filterCirculars, formatFilePath, hasKeys, stripAnsi, timestampFromSnowflake } from "@seedcord/utils";
|
|
16
|
-
import { DiscordAPIError as DiscordAPIError$1, REST } from "@discordjs/rest";
|
|
17
|
-
import { CoordinatedShutdown, CoordinatedStartup, HealthCheck, Pluggable, ShutdownPhase, StartupPhase, settleWithin } from "@seedcord/core/node/internal";
|
|
8
|
+
import { ButtonStyle, Client, ComponentType, DiscordAPIError, Events, Guild, GuildMember, Message, MessageFlags, PermissionFlagsBits, RESTJSONErrorCodes, Role, User } from "discord.js";
|
|
9
|
+
import { ButtonBuilder, TextDisplayBuilder } from "@discordjs/builders";
|
|
10
|
+
import { buildSlashRoute } from "@seedcord/utils/internal";
|
|
11
|
+
import { Plugin as Plugin$1 } from "@seedcord/core/plugin";
|
|
12
|
+
import { CommandRegistry, CoordinatedShutdown, CoordinatedStartup, HealthCheck, Pluggable, ShutdownPhase, StartupPhase, SubscriberLoader, settleWithin } from "@seedcord/core/node/internal";
|
|
18
13
|
import { MemoryRateLimiter } from "@seedcord/rate-limiter";
|
|
19
14
|
import { SeedcordBrand } from "@seedcord/types/internal";
|
|
20
|
-
import {
|
|
15
|
+
import { Envapter } from "envapt";
|
|
16
|
+
import chalk from "chalk";
|
|
21
17
|
import { Envapt } from "envapt/legacy";
|
|
22
|
-
import { resolve } from "node:path";
|
|
23
18
|
import { HmrModuleHandler } from "@seedcord/core/hmr";
|
|
19
|
+
import { formatFilePath, hasKeys } from "@seedcord/utils";
|
|
24
20
|
import { traverseDirectory } from "@seedcord/utils/node";
|
|
21
|
+
import * as crypto from "node:crypto";
|
|
25
22
|
|
|
26
23
|
export * from "@seedcord/errors"
|
|
27
24
|
|
|
@@ -29,10 +26,10 @@ export * from "@seedcord/core"
|
|
|
29
26
|
|
|
30
27
|
export * from "@seedcord/logger"
|
|
31
28
|
|
|
32
|
-
export * from "@seedcord/utils"
|
|
33
|
-
|
|
34
29
|
export * from "@seedcord/rate-limiter"
|
|
35
30
|
|
|
31
|
+
export * from "@seedcord/utils"
|
|
32
|
+
|
|
36
33
|
export * from "@seedcord/event-emitter"
|
|
37
34
|
|
|
38
35
|
//#region src/logger-bootstrap.ts
|
|
@@ -147,7 +144,7 @@ let MiddlewareType = /* @__PURE__ */ function(MiddlewareType) {
|
|
|
147
144
|
* Interaction middleware cannot specify event filters.
|
|
148
145
|
*
|
|
149
146
|
* @param type - Middleware kind from {@link MiddlewareType}
|
|
150
|
-
* @param priority - Ordering value where lower runs earlier. {@default `0`}
|
|
147
|
+
* @param priority - Ordering value where lower runs earlier. {@default `0` }
|
|
151
148
|
* @param options - Additional registration options
|
|
152
149
|
*
|
|
153
150
|
* @decorator
|
|
@@ -226,59 +223,6 @@ var CannotAssignBotRole = class extends Notice {
|
|
|
226
223
|
return { components: [new NoticeCard("I cannot assign a managed role.").component] };
|
|
227
224
|
}
|
|
228
225
|
};
|
|
229
|
-
var HasDangerousPermissions = class extends Notice {
|
|
230
|
-
subject;
|
|
231
|
-
dangerousPerms;
|
|
232
|
-
customLead;
|
|
233
|
-
constructor(message, subject, dangerousPerms) {
|
|
234
|
-
super(message ?? "A dangerous permission is enabled.");
|
|
235
|
-
this.subject = subject;
|
|
236
|
-
this.dangerousPerms = dangerousPerms;
|
|
237
|
-
this.customLead = message;
|
|
238
|
-
}
|
|
239
|
-
render() {
|
|
240
|
-
const bullets = this.dangerousPerms.map((perm) => `• ${perm}`).join("\n");
|
|
241
|
-
return { components: [new NoticeCard(`${this.customLead ?? `${this.subject} has the following permission entries that must not be enabled:`}\n\n${bullets}`).component] };
|
|
242
|
-
}
|
|
243
|
-
};
|
|
244
|
-
var UserNotFound = class extends Notice {
|
|
245
|
-
userArg;
|
|
246
|
-
constructor(userArg) {
|
|
247
|
-
super(`User not found: ${userArg}`);
|
|
248
|
-
this.userArg = userArg;
|
|
249
|
-
}
|
|
250
|
-
render() {
|
|
251
|
-
return { components: [new NoticeCard(`User probably doesn't exist or was deleted.\n**User Argument:** \`${this.userArg}\`\nPlease check the user ID and try again. Only pass valid user IDs as the argument.`, "User Not Found").component] };
|
|
252
|
-
}
|
|
253
|
-
};
|
|
254
|
-
var UserNotInGuild = class extends Notice {
|
|
255
|
-
constructor(message = "User is not in the guild.") {
|
|
256
|
-
super(message);
|
|
257
|
-
}
|
|
258
|
-
render() {
|
|
259
|
-
return { components: [new NoticeCard(this.message).component] };
|
|
260
|
-
}
|
|
261
|
-
};
|
|
262
|
-
var RoleDoesNotExist = class extends Notice {
|
|
263
|
-
roleId;
|
|
264
|
-
constructor(message, roleId) {
|
|
265
|
-
super(message);
|
|
266
|
-
this.roleId = roleId;
|
|
267
|
-
}
|
|
268
|
-
render() {
|
|
269
|
-
return { components: [new NoticeCard(`The role with ID \`${this.roleId}\` does not exist.`).component] };
|
|
270
|
-
}
|
|
271
|
-
};
|
|
272
|
-
var CouldNotFindChannel = class extends Notice {
|
|
273
|
-
channelId;
|
|
274
|
-
constructor(message, channelId) {
|
|
275
|
-
super(message);
|
|
276
|
-
this.channelId = channelId;
|
|
277
|
-
}
|
|
278
|
-
render() {
|
|
279
|
-
return { components: [new NoticeCard(`Could not find channel with ID \`${this.channelId}\`. It could also be that the channel is not a text channel.`).component] };
|
|
280
|
-
}
|
|
281
|
-
};
|
|
282
226
|
|
|
283
227
|
//#endregion
|
|
284
228
|
//#region src/bot/gates/catalog/Nsfw.ts
|
|
@@ -316,26 +260,21 @@ function Nsfw(options) {
|
|
|
316
260
|
|
|
317
261
|
//#endregion
|
|
318
262
|
//#region src/bot/injectors/EmojiInjector.ts
|
|
319
|
-
const emojiStorage =
|
|
320
|
-
function isEmojiTuple(v) {
|
|
321
|
-
return Array.isArray(v) && v.length === 2 && typeof v[0] === "string" && typeof v[1] === "string";
|
|
322
|
-
}
|
|
263
|
+
const emojiStorage = accessorStore();
|
|
323
264
|
/**
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
* @see {@link EmojiMap}
|
|
265
|
+
* The bot's resolved emojis, keyed by {@link EmojiMap}. Filled by {@link EmojiInjector} during startup.
|
|
266
|
+
* A read before that throws.
|
|
327
267
|
*/
|
|
328
|
-
const Emojis = emojiStorage;
|
|
268
|
+
const Emojis = guardedAccessor("Emojis", emojiStorage);
|
|
329
269
|
/**
|
|
330
|
-
* Loads and injects emojis based on bot configuration.
|
|
331
|
-
*
|
|
332
|
-
* For emojis injected from specific guilds, ensure that the Guilds intent is provided in client options.
|
|
270
|
+
* Loads and injects emojis based on bot configuration. A guild emoji requires the Guilds intent in the client
|
|
271
|
+
* options.
|
|
333
272
|
*
|
|
334
273
|
* @internal
|
|
335
274
|
*/
|
|
336
275
|
var EmojiInjector = class {
|
|
337
276
|
core;
|
|
338
|
-
logger = new Logger("Emojis");
|
|
277
|
+
logger = new Logger("Emojis", { channel: "bot" });
|
|
339
278
|
constructor(core) {
|
|
340
279
|
this.core = core;
|
|
341
280
|
}
|
|
@@ -382,126 +321,10 @@ var EmojiInjector = class {
|
|
|
382
321
|
emojiStorage[key] = appEmoji;
|
|
383
322
|
}
|
|
384
323
|
clearEmojis() {
|
|
385
|
-
|
|
324
|
+
clearStore(emojiStorage);
|
|
386
325
|
}
|
|
387
326
|
};
|
|
388
327
|
|
|
389
|
-
//#endregion
|
|
390
|
-
//#region src/bot/injectors/CommandMentionInjector.ts
|
|
391
|
-
const mentionStorage = {};
|
|
392
|
-
/**
|
|
393
|
-
* Ready-to-send command mention strings, keyed by {@link SlashOptionRegistry}.
|
|
394
|
-
*
|
|
395
|
-
* A value is a clickable `</name:id>` when the command's id resolved from the deploy, otherwise the plain
|
|
396
|
-
* `/name` text. Populated by {@link CommandMentionInjector} after each deploy.
|
|
397
|
-
*/
|
|
398
|
-
const CommandMentions = mentionStorage;
|
|
399
|
-
const MULTI_GUILD = Symbol("seedcord:commandmentions:multi-guild");
|
|
400
|
-
/**
|
|
401
|
-
* Turns each deployed slash route into a clickable command mention exposed through {@link CommandMentions}.
|
|
402
|
-
*
|
|
403
|
-
* Consumes the ids {@link DeployResult} carries, so it runs after a deploy rather than resolving from a name
|
|
404
|
-
* cache (discord.js offers none for command ids). A global command is clickable everywhere by its app-wide id.
|
|
405
|
-
* A guild command deployed to one guild is clickable by that guild's id. A command deployed to two or more
|
|
406
|
-
* guilds mints a different id per guild, so one accessor key cannot resolve it, and it falls back to plain
|
|
407
|
-
* `/name` text with one warn. On a non-owning shard a guild command also falls back to plain text.
|
|
408
|
-
*
|
|
409
|
-
* @internal
|
|
410
|
-
*/
|
|
411
|
-
var CommandMentionInjector = class {
|
|
412
|
-
core;
|
|
413
|
-
logger = new Logger("CommandMentions");
|
|
414
|
-
constructor(core) {
|
|
415
|
-
this.core = core;
|
|
416
|
-
}
|
|
417
|
-
inject(deploy) {
|
|
418
|
-
this.clear();
|
|
419
|
-
const globalIds = this.indexByName(deploy.global);
|
|
420
|
-
const guildIds = this.indexGuilds(deploy.guilds);
|
|
421
|
-
const warned = /* @__PURE__ */ new Set();
|
|
422
|
-
let clickable = 0;
|
|
423
|
-
for (const command of this.allSlashBuilders()) {
|
|
424
|
-
const id = this.resolveId(command.name, globalIds, guildIds, warned);
|
|
425
|
-
for (const leaf of routeLeavesOf(command.toJSON())) {
|
|
426
|
-
mentionStorage[leaf.route] = id ? this.toMention(leaf.route, id) : this.toPlain(leaf.route);
|
|
427
|
-
if (id) clickable++;
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
this.logger.utils.summary("Linked mentions", {
|
|
431
|
-
clickable,
|
|
432
|
-
plain: Object.keys(mentionStorage).length - clickable
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
allSlashBuilders() {
|
|
436
|
-
const registry = this.core.bot.commands;
|
|
437
|
-
if (!registry) return [];
|
|
438
|
-
const slash = [...registry.globalCommands, ...registry.guildCommands.values()].flat().filter((command) => command instanceof SlashCommandBuilder);
|
|
439
|
-
return [...new Set(slash)];
|
|
440
|
-
}
|
|
441
|
-
indexByName(collection) {
|
|
442
|
-
const map = /* @__PURE__ */ new Map();
|
|
443
|
-
for (const command of collection.values()) map.set(command.name, command.id);
|
|
444
|
-
return map;
|
|
445
|
-
}
|
|
446
|
-
indexGuilds(guilds) {
|
|
447
|
-
const map = /* @__PURE__ */ new Map();
|
|
448
|
-
for (const collection of guilds.values()) for (const command of collection.values()) {
|
|
449
|
-
const existing = map.get(command.name);
|
|
450
|
-
if (existing === void 0) map.set(command.name, command.id);
|
|
451
|
-
else if (existing !== command.id) map.set(command.name, MULTI_GUILD);
|
|
452
|
-
}
|
|
453
|
-
return map;
|
|
454
|
-
}
|
|
455
|
-
resolveId(name, globalIds, guildIds, warned) {
|
|
456
|
-
const global = globalIds.get(name);
|
|
457
|
-
if (global) return global;
|
|
458
|
-
const guild = guildIds.get(name);
|
|
459
|
-
if (guild === void 0) return void 0;
|
|
460
|
-
if (guild === MULTI_GUILD) {
|
|
461
|
-
if (!warned.has(name)) {
|
|
462
|
-
this.logger.warn(`${name} is deployed to multiple guilds, falling back to plain text (not clickable).`);
|
|
463
|
-
warned.add(name);
|
|
464
|
-
}
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
return guild;
|
|
468
|
-
}
|
|
469
|
-
toMention(route, id) {
|
|
470
|
-
const [name, middle, sub] = route.split("/");
|
|
471
|
-
if (name && middle && sub) return chatInputApplicationCommandMention(name, middle, sub, id);
|
|
472
|
-
if (name && middle) return chatInputApplicationCommandMention(name, middle, id);
|
|
473
|
-
return chatInputApplicationCommandMention(name ?? route, id);
|
|
474
|
-
}
|
|
475
|
-
toPlain(route) {
|
|
476
|
-
return `/${route.replaceAll("/", " ")}`;
|
|
477
|
-
}
|
|
478
|
-
clear() {
|
|
479
|
-
for (const key of Object.keys(mentionStorage)) Reflect.deleteProperty(mentionStorage, key);
|
|
480
|
-
}
|
|
481
|
-
};
|
|
482
|
-
|
|
483
|
-
//#endregion
|
|
484
|
-
//#region src/bot/utilities/channels/fetchText.ts
|
|
485
|
-
/**
|
|
486
|
-
* Fetches and validates a text channel. Refuses when the channel doesn't exist or isn't a text channel.
|
|
487
|
-
*
|
|
488
|
-
* @param client - The Discord client instance
|
|
489
|
-
* @param channelId - Channel ID or TextChannel instance
|
|
490
|
-
* @returns Promise resolving to the text channel
|
|
491
|
-
*/
|
|
492
|
-
async function fetchText(client, channelId) {
|
|
493
|
-
if (channelId instanceof TextChannel) return channelId;
|
|
494
|
-
let channel = client.channels.cache.get(channelId);
|
|
495
|
-
if (!channel) try {
|
|
496
|
-
channel = await client.channels.fetch(channelId);
|
|
497
|
-
} catch (err) {
|
|
498
|
-
if (err instanceof DiscordAPIError && err.code === RESTJSONErrorCodes.UnknownChannel) throw new CouldNotFindChannel("Channel not found or not a text channel", channelId);
|
|
499
|
-
throw err;
|
|
500
|
-
}
|
|
501
|
-
if (channel?.isTextBased()) return channel;
|
|
502
|
-
throw new CouldNotFindChannel("Channel not found or not a text channel", channelId);
|
|
503
|
-
}
|
|
504
|
-
|
|
505
328
|
//#endregion
|
|
506
329
|
//#region src/bot/notices/utils.ts
|
|
507
330
|
function mentionFor(subject) {
|
|
@@ -512,48 +335,37 @@ function mentionFor(subject) {
|
|
|
512
335
|
//#endregion
|
|
513
336
|
//#region src/bot/utilities/permissions/checkPermissions.ts
|
|
514
337
|
function checkPermissions(a, b, c, d, e) {
|
|
515
|
-
const { for: pFor, in: pIn, scope, inverse = false, missingNotice
|
|
338
|
+
const { for: pFor, in: pIn, scope, inverse = false, missingNotice, dangerousNotice } = a instanceof Role || a instanceof GuildMember ? {
|
|
516
339
|
for: a,
|
|
517
340
|
in: b,
|
|
518
341
|
scope: c,
|
|
519
342
|
inverse: d ?? false,
|
|
520
343
|
...e
|
|
521
344
|
} : a;
|
|
522
|
-
const Missing = missingCtor ?? MissingPermissions;
|
|
523
|
-
const Dangerous = dangerousCtor ?? HasDangerousPermissions;
|
|
524
|
-
const subject = mentionFor(pFor);
|
|
525
345
|
const perms = pIn instanceof Guild ? pFor.permissions : pIn.permissionsFor(pFor, true);
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
346
|
+
assertPermissions({
|
|
347
|
+
subject: mentionFor(pFor),
|
|
348
|
+
permissions: perms.bitfield,
|
|
349
|
+
scope,
|
|
350
|
+
inverse,
|
|
351
|
+
...missingNotice && { missingNotice },
|
|
352
|
+
...dangerousNotice && { dangerousNotice }
|
|
353
|
+
});
|
|
534
354
|
}
|
|
535
355
|
|
|
536
356
|
//#endregion
|
|
537
357
|
//#region src/bot/utilities/permissions/checkBotPermissions.ts
|
|
538
358
|
/**
|
|
539
|
-
* Checks
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
* @param target - Guild or text channel to check in
|
|
543
|
-
* @param scope - Permission bits to validate
|
|
544
|
-
* @param inverse - Whether to check for absence of the given permissions
|
|
545
|
-
* @param errors - Optional custom error constructors
|
|
359
|
+
* Checks the bot's own permissions in a {@link Guild} or {@link TextChannel}. Refuses when a permission in
|
|
360
|
+
* `scope` is missing, or with `inverse` when one is present. When the bot member is uncached, the refusal
|
|
361
|
+
* reports every permission in `scope` as missing.
|
|
546
362
|
*
|
|
547
363
|
* @example
|
|
548
364
|
* ```ts
|
|
549
365
|
* import { PermissionFlagsBits } from 'discord.js';
|
|
550
366
|
* import { checkBotPermissions } from '@seedcord/gateway';
|
|
551
367
|
*
|
|
552
|
-
*
|
|
553
|
-
* checkBotPermissions(textChannel, [
|
|
554
|
-
* PermissionFlagsBits.SendMessages,
|
|
555
|
-
* PermissionFlagsBits.ViewChannel
|
|
556
|
-
* ]);
|
|
368
|
+
* checkBotPermissions(textChannel, [PermissionFlagsBits.SendMessages, PermissionFlagsBits.ViewChannel]);
|
|
557
369
|
* ```
|
|
558
370
|
*/
|
|
559
371
|
function checkBotPermissions(target, scope, inverse = false, errors) {
|
|
@@ -568,7 +380,7 @@ function checkBotPermissions(target, scope, inverse = false, errors) {
|
|
|
568
380
|
}
|
|
569
381
|
const me = target.guild.members.me;
|
|
570
382
|
if (!me) {
|
|
571
|
-
new Logger("checkBotPermissions").warn(`Bot member is unavailable in guild ${target.guild.id} while checking permissions in channel ${target.id}`);
|
|
383
|
+
new Logger("checkBotPermissions", { channel: "errors" }).warn(`Bot member is unavailable in guild ${target.guild.id} while checking permissions in channel ${target.id}`);
|
|
572
384
|
const names = scope.map((bit) => PermissionNames.get(bit) ?? String(bit));
|
|
573
385
|
throw new ((errors?.missingNotice) ?? MissingPermissions)(void 0, "The bot", names);
|
|
574
386
|
}
|
|
@@ -576,26 +388,17 @@ function checkBotPermissions(target, scope, inverse = false, errors) {
|
|
|
576
388
|
}
|
|
577
389
|
|
|
578
390
|
//#endregion
|
|
579
|
-
//#region src/bot/utilities/
|
|
580
|
-
|
|
581
|
-
* Gets the bot's managed role in a guild.
|
|
582
|
-
*
|
|
583
|
-
* @param guild - The guild to get the bot role from
|
|
584
|
-
* @returns The bot's managed role in the guild
|
|
585
|
-
* @throws A **SeedcordError** if the client user is unavailable or if the bot role is missing
|
|
586
|
-
*/
|
|
587
|
-
function getBotRole(guild) {
|
|
391
|
+
//#region src/bot/utilities/permissions/hasPermsToAssign.ts
|
|
392
|
+
function botRoleOf(guild) {
|
|
588
393
|
const botRole = guild.roles.botRoleFor(guild.client.user);
|
|
589
394
|
if (!botRole) throw new SeedcordError(SeedcordErrorCode.CoreBotRoleMissing, [guild.id]);
|
|
590
395
|
return botRole;
|
|
591
396
|
}
|
|
592
|
-
|
|
593
|
-
//#endregion
|
|
594
|
-
//#region src/bot/utilities/permissions/hasPermsToAssign.ts
|
|
595
397
|
/**
|
|
596
|
-
* Validates
|
|
398
|
+
* Validates that the bot can assign a target role. Refuses when the target role is above the bot's, is managed, or the bot lacks Manage Roles.
|
|
597
399
|
*
|
|
598
400
|
* @param roleOrOptions - Target role or complete options for the check
|
|
401
|
+
* @throws A `Notice` on refusal, or a `SeedcordError` when the bot has no role in the guild's role cache.
|
|
599
402
|
*
|
|
600
403
|
* @example
|
|
601
404
|
* ```ts
|
|
@@ -623,135 +426,108 @@ function hasPermsToAssign(roleOrOptions) {
|
|
|
623
426
|
const HigherNotice = errors?.higherNotice ?? RoleHigherThanMe;
|
|
624
427
|
const ManagedNotice = errors?.managedNotice ?? CannotAssignBotRole;
|
|
625
428
|
const MissingNotice = errors?.missingNotice ?? MissingPermissions;
|
|
626
|
-
const botRole =
|
|
429
|
+
const botRole = botRoleOf(targetRole.guild);
|
|
627
430
|
if (targetRole.comparePositionTo(botRole) >= 0) throw new HigherNotice("Role is higher than me", targetRole, botRole);
|
|
628
431
|
if (targetRole.managed) throw new ManagedNotice(`Cannot assign managed role ${targetRole.name}`);
|
|
629
432
|
checkBotPermissions(targetRole.guild, [PermissionFlagsBits.ManageRoles], false, { missingNotice: MissingNotice });
|
|
630
433
|
}
|
|
631
434
|
|
|
632
435
|
//#endregion
|
|
633
|
-
//#region src/bot/
|
|
634
|
-
|
|
635
|
-
return err instanceof DiscordAPIError && err.code === RESTJSONErrorCodes.UnknownRole;
|
|
636
|
-
}
|
|
637
|
-
async function fetchRoleInGuild(guild, roleId) {
|
|
638
|
-
const cached = guild.roles.cache.get(roleId);
|
|
639
|
-
if (cached) return cached;
|
|
640
|
-
try {
|
|
641
|
-
return await guild.roles.fetch(roleId);
|
|
642
|
-
} catch (err) {
|
|
643
|
-
if (isUnknownRole(err)) throw new RoleDoesNotExist("Role not found in specified guild", roleId);
|
|
644
|
-
throw err;
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
async function scanGuildsForRole(client, roleId) {
|
|
648
|
-
const cached = client.guilds.cache.map((guild) => guild.roles.cache.get(roleId)).find(Boolean);
|
|
649
|
-
if (cached) return cached;
|
|
650
|
-
for (const guild of client.guilds.cache.values()) try {
|
|
651
|
-
const role = await guild.roles.fetch(roleId);
|
|
652
|
-
if (role) return role;
|
|
653
|
-
} catch (err) {
|
|
654
|
-
if (isUnknownRole(err)) continue;
|
|
655
|
-
throw err;
|
|
656
|
-
}
|
|
657
|
-
return null;
|
|
658
|
-
}
|
|
436
|
+
//#region src/bot/confirm/reserved.ts
|
|
437
|
+
const RESERVED_CONFIRM_PREFIX = "__seedcord_confirm";
|
|
659
438
|
/**
|
|
660
|
-
*
|
|
439
|
+
* The reserved confirm/cancel codec. It has no route, so the dispatcher ignores its clicks and only the
|
|
440
|
+
* in-process collector reads them. Do not mint a real component from it.
|
|
661
441
|
*
|
|
662
|
-
* @
|
|
663
|
-
* @param roleId - The role ID to fetch
|
|
664
|
-
* @returns Promise resolving to the role
|
|
442
|
+
* @internal
|
|
665
443
|
*/
|
|
666
|
-
|
|
667
|
-
if (!roleId) throw new RoleDoesNotExist("Role ID is null or undefined", roleId);
|
|
668
|
-
const role = clientOrGuild instanceof Guild ? await fetchRoleInGuild(clientOrGuild, roleId) : await scanGuildsForRole(clientOrGuild, roleId);
|
|
669
|
-
if (!role) throw new RoleDoesNotExist("Role not found", roleId);
|
|
670
|
-
return role;
|
|
671
|
-
}
|
|
444
|
+
const CONFIRM_DEF = new CustomId(RESERVED_CONFIRM_PREFIX).oneOf("choice", ["confirm", "cancel"]);
|
|
672
445
|
|
|
673
446
|
//#endregion
|
|
674
|
-
//#region src/bot/
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
447
|
+
//#region src/bot/confirm/getConfirmation.ts
|
|
448
|
+
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
449
|
+
const CONFIRM_IDS = {
|
|
450
|
+
confirm: CONFIRM_DEF.encode({ choice: "confirm" }),
|
|
451
|
+
cancel: CONFIRM_DEF.encode({ choice: "cancel" })
|
|
452
|
+
};
|
|
453
|
+
var ConfirmButtons = class extends RowComponent {
|
|
454
|
+
constructor(options) {
|
|
455
|
+
super("button");
|
|
456
|
+
this.instance.addComponents(new ButtonBuilder().setCustomId(CONFIRM_IDS.confirm).setLabel(options?.confirmLabel ?? "Confirm").setStyle(options?.confirmStyle ?? ButtonStyle.Danger), new ButtonBuilder().setCustomId(CONFIRM_IDS.cancel).setLabel(options?.cancelLabel ?? "Cancel").setStyle(ButtonStyle.Secondary));
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
var ConfirmPromptCard = class extends BuilderComponent {
|
|
460
|
+
constructor(text, options) {
|
|
461
|
+
super("container");
|
|
462
|
+
this.instance.addTextDisplayComponents(new TextDisplayBuilder().setContent(text)).addActionRowComponents(new ConfirmButtons(options).component);
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
function defaultPrompt(text, options) {
|
|
466
|
+
return { components: [new ConfirmPromptCard(text, options).component] };
|
|
689
467
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
async function fetchManyGuildMembers(guild, userIds) {
|
|
701
|
-
return (await Promise.allSettled(userIds.map((userId) => fetchGuildMember(guild, userId)))).filter((result) => result.status === "fulfilled").map((result) => result.value);
|
|
468
|
+
async function collectChoice(message, userId, timeoutMs) {
|
|
469
|
+
try {
|
|
470
|
+
return await message.awaitMessageComponent({
|
|
471
|
+
componentType: ComponentType.Button,
|
|
472
|
+
time: timeoutMs,
|
|
473
|
+
filter: (click) => click.user.id === userId && CONFIRM_DEF.owns(click.customId)
|
|
474
|
+
});
|
|
475
|
+
} catch {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
702
478
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
}
|
|
721
|
-
|
|
479
|
+
async function settle(sender, message, outcome) {
|
|
480
|
+
if (outcome === void 0) {
|
|
481
|
+
await sender.delete(message).catch(() => void 0);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
await sender.edit(message, typeof outcome === "function" ? await outcome() : outcome);
|
|
485
|
+
}
|
|
486
|
+
async function getConfirmation(handler, prompt, options) {
|
|
487
|
+
const { ephemeral = true, timeoutMs = DEFAULT_TIMEOUT_MS } = options ?? {};
|
|
488
|
+
const response = typeof prompt === "string" ? defaultPrompt(prompt, options) : await prompt(CONFIRM_IDS);
|
|
489
|
+
const interaction = handler.getEvent();
|
|
490
|
+
const sender = handler.getSender();
|
|
491
|
+
const message = await sender.send(response, { ephemeral });
|
|
492
|
+
const winner = await collectChoice(message, interaction.user.id, timeoutMs);
|
|
493
|
+
if (!winner) {
|
|
494
|
+
await settle(sender, message, options?.onTimeout);
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
await winner.deferUpdate().catch(() => void 0);
|
|
498
|
+
const confirmed = winner.customId === CONFIRM_IDS.confirm;
|
|
499
|
+
await settle(sender, message, confirmed ? options?.onConfirm : options?.onCancel);
|
|
500
|
+
return confirmed;
|
|
722
501
|
}
|
|
723
502
|
|
|
724
503
|
//#endregion
|
|
725
|
-
//#region src/
|
|
504
|
+
//#region src/handlers/BaseHandler.ts
|
|
726
505
|
/**
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
* @param client - The Discord client instance
|
|
730
|
-
* @param userIds - Array of Discord user IDs
|
|
731
|
-
* @param options - Optional overrides threaded to each {@link fetchUser} call
|
|
732
|
-
* @returns Promise resolving to array of successfully fetched users
|
|
506
|
+
* The gateway base, fixes the core base's `TCore` to the gateway {@link Core} and adds the `getEvent`
|
|
507
|
+
* gate hook.
|
|
733
508
|
*/
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
509
|
+
var BaseHandler$1 = class extends BaseHandler {
|
|
510
|
+
/** @internal */
|
|
511
|
+
getEvent() {
|
|
512
|
+
return this.event;
|
|
513
|
+
}
|
|
514
|
+
};
|
|
737
515
|
|
|
738
516
|
//#endregion
|
|
739
|
-
//#region src/
|
|
517
|
+
//#region src/handlers/interaction/InteractionMiddleware.ts
|
|
740
518
|
/**
|
|
741
|
-
*
|
|
519
|
+
* Base class for interaction middleware
|
|
742
520
|
*
|
|
743
|
-
*
|
|
744
|
-
*
|
|
745
|
-
*
|
|
746
|
-
* @
|
|
521
|
+
* Middleware runs before interaction handlers and can modify behavior or block execution.
|
|
522
|
+
* Unlike handlers, middleware should not send responses directly.
|
|
523
|
+
*
|
|
524
|
+
* @typeParam Repliable - The interaction type this middleware processes
|
|
747
525
|
*/
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
await member.roles.set([...updated]);
|
|
754
|
-
}
|
|
526
|
+
var InteractionMiddleware = class extends BaseHandler$1 {
|
|
527
|
+
constructor(event, core) {
|
|
528
|
+
super(event, core, void 0, "interactions");
|
|
529
|
+
}
|
|
530
|
+
};
|
|
755
531
|
|
|
756
532
|
//#endregion
|
|
757
533
|
//#region src/bot/ReplySender.ts
|
|
@@ -767,8 +543,11 @@ function seedState(interaction) {
|
|
|
767
543
|
*/
|
|
768
544
|
var ReplySender = class extends BaseReplySender {
|
|
769
545
|
interaction;
|
|
770
|
-
constructor(interaction, routeId) {
|
|
771
|
-
super(routeId, seedState(interaction)
|
|
546
|
+
constructor(interaction, routeId, bus) {
|
|
547
|
+
super(routeId, seedState(interaction), bus && {
|
|
548
|
+
bus,
|
|
549
|
+
interactionId: interaction.id
|
|
550
|
+
});
|
|
772
551
|
this.interaction = interaction;
|
|
773
552
|
}
|
|
774
553
|
async writeReply(response, opts) {
|
|
@@ -837,319 +616,26 @@ var ReplySender = class extends BaseReplySender {
|
|
|
837
616
|
...reply.files && { files: [...reply.files] }
|
|
838
617
|
};
|
|
839
618
|
}
|
|
840
|
-
editOptions(response) {
|
|
841
|
-
const reply = this.serialize(response);
|
|
842
|
-
return {
|
|
843
|
-
components: reply.components,
|
|
844
|
-
flags: MessageFlags.IsComponentsV2,
|
|
845
|
-
...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
|
|
846
|
-
...reply.files && { files: [...reply.files] }
|
|
847
|
-
};
|
|
848
|
-
}
|
|
849
|
-
updateOptions(response) {
|
|
850
|
-
const reply = this.serialize(response);
|
|
851
|
-
return {
|
|
852
|
-
components: reply.components,
|
|
853
|
-
flags: MessageFlags.IsComponentsV2,
|
|
854
|
-
...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
|
|
855
|
-
...reply.files && { files: [...reply.files] }
|
|
856
|
-
};
|
|
857
|
-
}
|
|
858
|
-
createdMessage(result) {
|
|
859
|
-
return result.resource?.message ?? void 0;
|
|
860
|
-
}
|
|
861
|
-
};
|
|
862
|
-
|
|
863
|
-
//#endregion
|
|
864
|
-
//#region src/bot/utilities/miscellaneous/slashRouteOf.ts
|
|
865
|
-
/**
|
|
866
|
-
* Computes the dispatch route for a chat-input or autocomplete interaction. The controller and
|
|
867
|
-
* `seedcord codegen` both assemble route keys through the same {@link buildSlashRoute}, so a dispatched
|
|
868
|
-
* interaction and a generated registry key always match.
|
|
869
|
-
*
|
|
870
|
-
* @internal
|
|
871
|
-
*/
|
|
872
|
-
function slashRouteOf(interaction) {
|
|
873
|
-
const group = interaction.options.getSubcommandGroup(false) ?? void 0;
|
|
874
|
-
const subcommand = interaction.options.getSubcommand(false) ?? void 0;
|
|
875
|
-
return buildSlashRoute(interaction.commandName, subcommand, group);
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
//#endregion
|
|
879
|
-
//#region src/miscellaneous/FaultThrottle.ts
|
|
880
|
-
/**
|
|
881
|
-
* Drops duplicate fault reports inside a fixed window so a recurring fault (a database outage on a
|
|
882
|
-
* hot path) reports once per window instead of flooding the webhook. Keyed on a structural string, so
|
|
883
|
-
* the map stays small, and a stale entry is evicted on the next read of its key.
|
|
884
|
-
*
|
|
885
|
-
* @internal
|
|
886
|
-
*/
|
|
887
|
-
var FaultThrottle = class {
|
|
888
|
-
windowMs;
|
|
889
|
-
now;
|
|
890
|
-
lastReportedAt = /* @__PURE__ */ new Map();
|
|
891
|
-
constructor(windowMs = 6e4, now = () => Date.now()) {
|
|
892
|
-
this.windowMs = windowMs;
|
|
893
|
-
this.now = now;
|
|
894
|
-
}
|
|
895
|
-
/** Evicts the entry for this key if it has aged out, so the map never holds stale keys forever. */
|
|
896
|
-
shouldReport(key) {
|
|
897
|
-
const last = this.lastReportedAt.get(key);
|
|
898
|
-
if (last === void 0) return true;
|
|
899
|
-
if (this.now() - last < this.windowMs) return false;
|
|
900
|
-
this.lastReportedAt.delete(key);
|
|
901
|
-
return true;
|
|
902
|
-
}
|
|
903
|
-
markReported(key) {
|
|
904
|
-
this.lastReportedAt.set(key, this.now());
|
|
905
|
-
}
|
|
906
|
-
clear() {
|
|
907
|
-
this.lastReportedAt.clear();
|
|
908
|
-
}
|
|
909
|
-
};
|
|
910
|
-
|
|
911
|
-
//#endregion
|
|
912
|
-
//#region src/miscellaneous/extractErrorResponse.ts
|
|
913
|
-
const logger$2 = new Logger("ErrorsHandling");
|
|
914
|
-
const faultThrottle = new FaultThrottle();
|
|
915
|
-
/**
|
|
916
|
-
* Processes an error into a user-facing reply and routes its report.
|
|
917
|
-
*
|
|
918
|
-
* A {@link Notice} always renders its own reply. A reporting denial (`report: true`) additionally
|
|
919
|
-
* publishes `handledException` on the interaction path. A raw, non-denial throw publishes
|
|
920
|
-
* `unknownException` and renders the configured `defaultError` (or a generic {@link Fault}). One uuid is
|
|
921
|
-
* threaded into both the reply and the bus payload.
|
|
922
|
-
*/
|
|
923
|
-
function extractErrorResponse(error, core, origin) {
|
|
924
|
-
const uuid = crypto.randomUUID();
|
|
925
|
-
const developerUsername = core.config.notifications?.developerUsername;
|
|
926
|
-
const ctx = developerUsername === void 0 ? { uuid } : {
|
|
927
|
-
uuid,
|
|
928
|
-
developerUsername
|
|
929
|
-
};
|
|
930
|
-
if (error instanceof Notice) {
|
|
931
|
-
if (error.report) reportFault(error, core, origin, uuid);
|
|
932
|
-
return {
|
|
933
|
-
uuid,
|
|
934
|
-
response: error.render(ctx)
|
|
935
|
-
};
|
|
936
|
-
}
|
|
937
|
-
reportRawFault(error, core, origin, uuid);
|
|
938
|
-
const override = core.config.errors?.defaultError;
|
|
939
|
-
return {
|
|
940
|
-
uuid,
|
|
941
|
-
response: override ? new override(uuid).render(ctx) : new Fault().render(ctx)
|
|
942
|
-
};
|
|
943
|
-
}
|
|
944
|
-
function withThrottle(origin, error, publish) {
|
|
945
|
-
const key = faultKey(origin, error);
|
|
946
|
-
if (!faultThrottle.shouldReport(key)) {
|
|
947
|
-
logger$2.info(`throttled duplicate fault ${key}`);
|
|
948
|
-
return;
|
|
949
|
-
}
|
|
950
|
-
publish();
|
|
951
|
-
faultThrottle.markReported(key);
|
|
952
|
-
}
|
|
953
|
-
function reportFault(denial, core, origin, uuid) {
|
|
954
|
-
withThrottle(origin, denial, () => {
|
|
955
|
-
logger$2.error(`${denial.name}: ${uuid}`, denial);
|
|
956
|
-
if (origin.interaction) core.bus.publish("handledException", {
|
|
957
|
-
denial,
|
|
958
|
-
uuid,
|
|
959
|
-
source: buildInteractionSource(origin.interaction)
|
|
960
|
-
});
|
|
961
|
-
else if (origin.event) core.bus.publish("handledException", {
|
|
962
|
-
denial,
|
|
963
|
-
uuid,
|
|
964
|
-
source: buildEventSource(origin.event, origin)
|
|
965
|
-
});
|
|
966
|
-
else core.bus.publish("unknownException", {
|
|
967
|
-
uuid,
|
|
968
|
-
error: denial,
|
|
969
|
-
...scalarActors(origin),
|
|
970
|
-
metadata: metadataFor(origin)
|
|
971
|
-
});
|
|
972
|
-
});
|
|
973
|
-
}
|
|
974
|
-
function causeLine(error) {
|
|
975
|
-
const { cause } = error;
|
|
976
|
-
if (!Error.isError(cause)) return "";
|
|
977
|
-
const [first] = cause.message.split("\n");
|
|
978
|
-
return `\ncaused by ${cause.name}: ${first ?? cause.message}`;
|
|
979
|
-
}
|
|
980
|
-
function reportRawFault(error, core, origin, uuid) {
|
|
981
|
-
withThrottle(origin, error, () => {
|
|
982
|
-
if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
|
|
983
|
-
else logger$2.error(`${uuid} | ${error.message}${causeLine(error)}`);
|
|
984
|
-
core.bus.publish("unknownException", {
|
|
985
|
-
uuid,
|
|
986
|
-
error,
|
|
987
|
-
...scalarActors(origin),
|
|
988
|
-
metadata: metadataFor(origin)
|
|
989
|
-
});
|
|
990
|
-
});
|
|
991
|
-
}
|
|
992
|
-
function scalarActors(origin) {
|
|
993
|
-
return {
|
|
994
|
-
guild: origin.guild ? {
|
|
995
|
-
id: origin.guild.id,
|
|
996
|
-
name: origin.guild.name
|
|
997
|
-
} : void 0,
|
|
998
|
-
user: origin.user ? {
|
|
999
|
-
id: origin.user.id,
|
|
1000
|
-
username: origin.user.username
|
|
1001
|
-
} : void 0
|
|
1002
|
-
};
|
|
1003
|
-
}
|
|
1004
|
-
function metadataFor(origin) {
|
|
1005
|
-
if (origin.event) return {
|
|
1006
|
-
eventName: origin.event.name,
|
|
1007
|
-
handler: origin.event.handler,
|
|
1008
|
-
args: origin.event.args
|
|
1009
|
-
};
|
|
1010
|
-
return origin.metadata;
|
|
1011
|
-
}
|
|
1012
|
-
function buildEventSource(event, origin) {
|
|
1013
|
-
return {
|
|
1014
|
-
kind: "event",
|
|
1015
|
-
eventName: event.name,
|
|
1016
|
-
handler: event.handler,
|
|
1017
|
-
userId: origin.user?.id ?? null,
|
|
1018
|
-
guildId: origin.guild?.id ?? null,
|
|
1019
|
-
channelId: event.channelId,
|
|
1020
|
-
raw: event.args
|
|
1021
|
-
};
|
|
1022
|
-
}
|
|
1023
|
-
function faultKey(origin, error) {
|
|
1024
|
-
const name = error instanceof Notice ? error.name : error instanceof DiscordAPIError ? String(error.code) : error.constructor.name;
|
|
1025
|
-
if (origin.event) return `${origin.event.name}:${origin.event.handler}:${name}`;
|
|
1026
|
-
if (origin.interaction) return `${interactionRoute(origin.interaction)}:${name}`;
|
|
1027
|
-
if (origin.route) return `autocomplete:${origin.route}:${name}`;
|
|
1028
|
-
return `autocomplete:${name}`;
|
|
1029
|
-
}
|
|
1030
|
-
/** The stable route id of a repliable interaction, its slash route path, command name, or customId prefix. */
|
|
1031
|
-
function interactionRoute(interaction) {
|
|
1032
|
-
if (interaction.isChatInputCommand()) return slashRouteOf(interaction);
|
|
1033
|
-
if (interaction.isContextMenuCommand()) return interaction.commandName;
|
|
1034
|
-
if (interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit()) return prefixOf(interaction.customId) || interaction.customId;
|
|
1035
|
-
return "interaction";
|
|
1036
|
-
}
|
|
1037
|
-
function buildInteractionSource(interaction) {
|
|
1038
|
-
const command = interaction.isChatInputCommand() ? slashRouteOf(interaction) : interaction.isContextMenuCommand() ? interaction.commandName : null;
|
|
1039
|
-
const customId = interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit() ? interaction.customId : null;
|
|
1040
|
-
return {
|
|
1041
|
-
kind: "interaction",
|
|
1042
|
-
interactionKind: interactionKind(interaction),
|
|
1043
|
-
command,
|
|
1044
|
-
customId,
|
|
1045
|
-
userId: interaction.user.id,
|
|
1046
|
-
guildId: interaction.guildId,
|
|
1047
|
-
channelId: interaction.channelId,
|
|
1048
|
-
interactionId: interaction.id,
|
|
1049
|
-
raw: interaction
|
|
1050
|
-
};
|
|
1051
|
-
}
|
|
1052
|
-
function interactionKind(interaction) {
|
|
1053
|
-
if (interaction.isChatInputCommand()) return "slash";
|
|
1054
|
-
if (interaction.isContextMenuCommand()) return "context-menu";
|
|
1055
|
-
if (interaction.isButton()) return "button";
|
|
1056
|
-
if (interaction.isAnySelectMenu()) return "select";
|
|
1057
|
-
return "modal";
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
//#endregion
|
|
1061
|
-
//#region src/bot/confirm/reserved.ts
|
|
1062
|
-
const RESERVED_CONFIRM_PREFIX = "__seedcord_confirm";
|
|
1063
|
-
/**
|
|
1064
|
-
* The reserved confirm/cancel codec. It has no route, so the dispatcher ignores its clicks and only the
|
|
1065
|
-
* in-process collector reads them. Do not mint a real component from it.
|
|
1066
|
-
*
|
|
1067
|
-
* @internal
|
|
1068
|
-
*/
|
|
1069
|
-
const CONFIRM_DEF = new CustomId(RESERVED_CONFIRM_PREFIX).oneOf("choice", ["confirm", "cancel"]);
|
|
1070
|
-
|
|
1071
|
-
//#endregion
|
|
1072
|
-
//#region src/bot/confirm/getConfirmation.ts
|
|
1073
|
-
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
1074
|
-
const CONFIRM_IDS = {
|
|
1075
|
-
confirm: CONFIRM_DEF.encode({ choice: "confirm" }),
|
|
1076
|
-
cancel: CONFIRM_DEF.encode({ choice: "cancel" })
|
|
1077
|
-
};
|
|
1078
|
-
var ConfirmButtons = class extends RowComponent {
|
|
1079
|
-
constructor(options) {
|
|
1080
|
-
super("button");
|
|
1081
|
-
this.instance.addComponents(new ButtonBuilder().setCustomId(CONFIRM_IDS.confirm).setLabel(options?.confirmLabel ?? "Confirm").setStyle(options?.confirmStyle ?? ButtonStyle.Danger), new ButtonBuilder().setCustomId(CONFIRM_IDS.cancel).setLabel(options?.cancelLabel ?? "Cancel").setStyle(ButtonStyle.Secondary));
|
|
1082
|
-
}
|
|
1083
|
-
};
|
|
1084
|
-
var ConfirmPromptCard = class extends BuilderComponent {
|
|
1085
|
-
constructor(text, options) {
|
|
1086
|
-
super("container");
|
|
1087
|
-
this.instance.addTextDisplayComponents(new TextDisplayBuilder().setContent(text)).addActionRowComponents(new ConfirmButtons(options).component);
|
|
1088
|
-
}
|
|
1089
|
-
};
|
|
1090
|
-
function defaultPrompt(text, options) {
|
|
1091
|
-
return { components: [new ConfirmPromptCard(text, options).component] };
|
|
1092
|
-
}
|
|
1093
|
-
async function collectChoice(message, userId, timeoutMs) {
|
|
1094
|
-
try {
|
|
1095
|
-
return await message.awaitMessageComponent({
|
|
1096
|
-
componentType: ComponentType.Button,
|
|
1097
|
-
time: timeoutMs,
|
|
1098
|
-
filter: (click) => click.user.id === userId && CONFIRM_DEF.owns(click.customId)
|
|
1099
|
-
});
|
|
1100
|
-
} catch {
|
|
1101
|
-
return null;
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
async function settle(sender, message, outcome) {
|
|
1105
|
-
if (outcome === void 0) {
|
|
1106
|
-
await sender.delete(message).catch(() => void 0);
|
|
1107
|
-
return;
|
|
1108
|
-
}
|
|
1109
|
-
await sender.edit(message, typeof outcome === "function" ? await outcome() : outcome);
|
|
1110
|
-
}
|
|
1111
|
-
async function getConfirmation(interaction, prompt, options) {
|
|
1112
|
-
const { ephemeral = true, timeoutMs = DEFAULT_TIMEOUT_MS } = options ?? {};
|
|
1113
|
-
const response = typeof prompt === "string" ? defaultPrompt(prompt, options) : await prompt(CONFIRM_IDS);
|
|
1114
|
-
const sender = new ReplySender(interaction, interactionRoute(interaction));
|
|
1115
|
-
const message = await sender.send(response, { ephemeral });
|
|
1116
|
-
const winner = await collectChoice(message, interaction.user.id, timeoutMs);
|
|
1117
|
-
if (!winner) {
|
|
1118
|
-
await settle(sender, message, options?.onTimeout);
|
|
1119
|
-
return false;
|
|
1120
|
-
}
|
|
1121
|
-
await winner.deferUpdate().catch(() => void 0);
|
|
1122
|
-
const confirmed = winner.customId === CONFIRM_IDS.confirm;
|
|
1123
|
-
await settle(sender, message, confirmed ? options?.onConfirm : options?.onCancel);
|
|
1124
|
-
return confirmed;
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
//#endregion
|
|
1128
|
-
//#region src/handlers/BaseHandler.ts
|
|
1129
|
-
/**
|
|
1130
|
-
* The gateway base, fixes the core base's `TCore` to the gateway {@link Core} and adds the `getEvent`
|
|
1131
|
-
* gate hook.
|
|
1132
|
-
*/
|
|
1133
|
-
var BaseHandler$1 = class extends BaseHandler {
|
|
1134
|
-
/** @internal */
|
|
1135
|
-
getEvent() {
|
|
1136
|
-
return this.event;
|
|
1137
|
-
}
|
|
1138
|
-
};
|
|
1139
|
-
|
|
1140
|
-
//#endregion
|
|
1141
|
-
//#region src/handlers/interaction/InteractionMiddleware.ts
|
|
1142
|
-
/**
|
|
1143
|
-
* Base class for interaction middleware
|
|
1144
|
-
*
|
|
1145
|
-
* Middleware runs before interaction handlers and can modify behavior or block execution.
|
|
1146
|
-
* Unlike handlers, middleware should not send responses directly.
|
|
1147
|
-
*
|
|
1148
|
-
* @typeParam Repliable - The interaction type this middleware processes
|
|
1149
|
-
*/
|
|
1150
|
-
var InteractionMiddleware = class extends BaseHandler$1 {
|
|
1151
|
-
constructor(event, core) {
|
|
1152
|
-
super(event, core);
|
|
619
|
+
editOptions(response) {
|
|
620
|
+
const reply = this.serialize(response);
|
|
621
|
+
return {
|
|
622
|
+
components: reply.components,
|
|
623
|
+
flags: MessageFlags.IsComponentsV2,
|
|
624
|
+
...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
|
|
625
|
+
...reply.files && { files: [...reply.files] }
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
updateOptions(response) {
|
|
629
|
+
const reply = this.serialize(response);
|
|
630
|
+
return {
|
|
631
|
+
components: reply.components,
|
|
632
|
+
flags: MessageFlags.IsComponentsV2,
|
|
633
|
+
...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
|
|
634
|
+
...reply.files && { files: [...reply.files] }
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
createdMessage(result) {
|
|
638
|
+
return result.resource?.message ?? void 0;
|
|
1153
639
|
}
|
|
1154
640
|
};
|
|
1155
641
|
|
|
@@ -1169,8 +655,8 @@ var RepliableHandler = class extends RepliableHandler$1 {
|
|
|
1169
655
|
getEvent() {
|
|
1170
656
|
return this.event;
|
|
1171
657
|
}
|
|
1172
|
-
buildSender(event,
|
|
1173
|
-
return new ReplySender(event, routeId);
|
|
658
|
+
buildSender(event, core, routeId) {
|
|
659
|
+
return new ReplySender(event, routeId, core.bus);
|
|
1174
660
|
}
|
|
1175
661
|
};
|
|
1176
662
|
|
|
@@ -1198,6 +684,21 @@ var InteractionHandler = class extends RepliableHandler {
|
|
|
1198
684
|
}
|
|
1199
685
|
};
|
|
1200
686
|
|
|
687
|
+
//#endregion
|
|
688
|
+
//#region src/bot/utilities/miscellaneous/slashRouteOf.ts
|
|
689
|
+
/**
|
|
690
|
+
* Computes the dispatch route for a chat-input or autocomplete interaction. The controller and
|
|
691
|
+
* `seedcord codegen` both assemble route keys through the same {@link buildSlashRoute}, so a dispatched
|
|
692
|
+
* interaction and a generated registry key always match.
|
|
693
|
+
*
|
|
694
|
+
* @internal
|
|
695
|
+
*/
|
|
696
|
+
function slashRouteOf(interaction) {
|
|
697
|
+
const group = interaction.options.getSubcommandGroup(false) ?? void 0;
|
|
698
|
+
const subcommand = interaction.options.getSubcommand(false) ?? void 0;
|
|
699
|
+
return buildSlashRoute(interaction.commandName, subcommand, group);
|
|
700
|
+
}
|
|
701
|
+
|
|
1201
702
|
//#endregion
|
|
1202
703
|
//#region src/handlers/interaction/SlashHandler.ts
|
|
1203
704
|
/**
|
|
@@ -1325,7 +826,7 @@ var ContextMenuHandler = class extends InteractionHandler {
|
|
|
1325
826
|
*/
|
|
1326
827
|
var AutocompleteHandler = class extends BaseHandler$1 {
|
|
1327
828
|
constructor(event, core, dispatch) {
|
|
1328
|
-
super(event, core, dispatch);
|
|
829
|
+
super(event, core, dispatch, "interactions");
|
|
1329
830
|
}
|
|
1330
831
|
decodedFocused;
|
|
1331
832
|
/**
|
|
@@ -1376,7 +877,11 @@ var AutocompleteHandler = class extends BaseHandler$1 {
|
|
|
1376
877
|
}
|
|
1377
878
|
/** Send autocomplete suggestions, callback type 8. Prefer {@link match}, which restricts each field's choices to its declared type. */
|
|
1378
879
|
async respond(choices) {
|
|
1379
|
-
|
|
880
|
+
const routeId = this.dispatch?.routeId ?? "autocomplete";
|
|
881
|
+
await reportedWrite({
|
|
882
|
+
bus: this.core.bus,
|
|
883
|
+
interactionId: this.event.id
|
|
884
|
+
}, routeId, "respond", () => this.event.respond(choices));
|
|
1380
885
|
}
|
|
1381
886
|
/** The firing command route, for a field whose completion differs per registered command. */
|
|
1382
887
|
get route() {
|
|
@@ -1513,7 +1018,22 @@ var ButtonHandler = class extends ComponentHandler {};
|
|
|
1513
1018
|
* }
|
|
1514
1019
|
* ```
|
|
1515
1020
|
*/
|
|
1516
|
-
var ModalHandler = class extends ComponentHandler {
|
|
1021
|
+
var ModalHandler = class extends ComponentHandler {
|
|
1022
|
+
/** Rewrite the message this modal was opened from. Throws when a command opened the modal. */
|
|
1023
|
+
update(response) {
|
|
1024
|
+
this.ensureSourceMessage("update");
|
|
1025
|
+
return super.update(response);
|
|
1026
|
+
}
|
|
1027
|
+
/** Acknowledge the submit without changing the source message. Throws when a command opened the modal. */
|
|
1028
|
+
deferUpdate() {
|
|
1029
|
+
this.ensureSourceMessage("deferUpdate");
|
|
1030
|
+
return super.deferUpdate();
|
|
1031
|
+
}
|
|
1032
|
+
ensureSourceMessage(method) {
|
|
1033
|
+
if (this.event.isFromMessage()) return;
|
|
1034
|
+
throw new SeedcordError(SeedcordErrorCode.ReplyUpdateWithoutSource, [method, this.routeId]);
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1517
1037
|
|
|
1518
1038
|
//#endregion
|
|
1519
1039
|
//#region src/handlers/interaction/components/SelectMenuHandler.ts
|
|
@@ -1569,7 +1089,7 @@ var SelectMenuHandler = class extends ComponentHandler {};
|
|
|
1569
1089
|
var EventHandler = class extends BaseHandler$1 {
|
|
1570
1090
|
firedEvent;
|
|
1571
1091
|
constructor(event, core, eventName) {
|
|
1572
|
-
super(event, core);
|
|
1092
|
+
super(event, core, void 0, "events");
|
|
1573
1093
|
this.firedEvent = eventName;
|
|
1574
1094
|
}
|
|
1575
1095
|
/**
|
|
@@ -1622,7 +1142,7 @@ var EventHandler = class extends BaseHandler$1 {
|
|
|
1622
1142
|
var EventMiddleware = class extends BaseHandler$1 {
|
|
1623
1143
|
firedEvent;
|
|
1624
1144
|
constructor(event, core, eventName) {
|
|
1625
|
-
super(event, core);
|
|
1145
|
+
super(event, core, void 0, "events");
|
|
1626
1146
|
this.firedEvent = eventName;
|
|
1627
1147
|
}
|
|
1628
1148
|
/**
|
|
@@ -1636,130 +1156,6 @@ var EventMiddleware = class extends BaseHandler$1 {
|
|
|
1636
1156
|
}
|
|
1637
1157
|
};
|
|
1638
1158
|
|
|
1639
|
-
//#endregion
|
|
1640
|
-
//#region src/pagination/controls.ts
|
|
1641
|
-
const DEFAULT_LABEL = {
|
|
1642
|
-
first: "First",
|
|
1643
|
-
prev: "Prev",
|
|
1644
|
-
indicator: "",
|
|
1645
|
-
next: "Next",
|
|
1646
|
-
last: "Last"
|
|
1647
|
-
};
|
|
1648
|
-
const CONTROL_SLOT = {
|
|
1649
|
-
first: 0,
|
|
1650
|
-
prev: 1,
|
|
1651
|
-
indicator: 2,
|
|
1652
|
-
next: 3,
|
|
1653
|
-
last: 4
|
|
1654
|
-
};
|
|
1655
|
-
const MAX_BUTTONS_PER_ROW = 5;
|
|
1656
|
-
var Controls = class {
|
|
1657
|
-
cursor;
|
|
1658
|
-
view;
|
|
1659
|
-
constructor(cursor, view) {
|
|
1660
|
-
this.cursor = cursor;
|
|
1661
|
-
this.view = view;
|
|
1662
|
-
}
|
|
1663
|
-
button(key, cosmetics) {
|
|
1664
|
-
const { target, disabled } = this.navState(key);
|
|
1665
|
-
const defaultLabel = key === "indicator" ? this.indicatorText() : DEFAULT_LABEL[key];
|
|
1666
|
-
const label = cosmetics?.label ?? (key === "indicator" || !cosmetics?.emoji ? defaultLabel : void 0);
|
|
1667
|
-
const button = new ButtonBuilder().setCustomId(this.cursor.encode({
|
|
1668
|
-
page: Math.min(Math.max(0, target), PAGE_MAX),
|
|
1669
|
-
slot: CONTROL_SLOT[key]
|
|
1670
|
-
})).setStyle(cosmetics?.style ?? ButtonStyle.Secondary).setDisabled(disabled);
|
|
1671
|
-
if (label !== void 0) button.setLabel(label);
|
|
1672
|
-
if (cosmetics?.emoji) button.setEmoji(cosmetics.emoji);
|
|
1673
|
-
return button;
|
|
1674
|
-
}
|
|
1675
|
-
navState(key) {
|
|
1676
|
-
const { page, totalPages, hasPrev, hasNext } = this.view;
|
|
1677
|
-
const knownTotal = totalPages !== void 0;
|
|
1678
|
-
return {
|
|
1679
|
-
target: {
|
|
1680
|
-
first: 0,
|
|
1681
|
-
prev: page - 1,
|
|
1682
|
-
indicator: page,
|
|
1683
|
-
next: page + 1,
|
|
1684
|
-
last: knownTotal ? totalPages - 1 : page
|
|
1685
|
-
}[key],
|
|
1686
|
-
disabled: {
|
|
1687
|
-
first: !hasPrev,
|
|
1688
|
-
prev: !hasPrev,
|
|
1689
|
-
indicator: true,
|
|
1690
|
-
next: !hasNext || page + 1 > PAGE_MAX,
|
|
1691
|
-
last: !hasNext || !knownTotal || totalPages - 1 > PAGE_MAX
|
|
1692
|
-
}[key]
|
|
1693
|
-
};
|
|
1694
|
-
}
|
|
1695
|
-
row(...keys) {
|
|
1696
|
-
if (keys.length === 0) throw new SeedcordRangeError(SeedcordErrorCode.PaginationEmptyControls);
|
|
1697
|
-
if (keys.length > MAX_BUTTONS_PER_ROW) throw new SeedcordRangeError(SeedcordErrorCode.PaginationTooManyControls, [keys.length]);
|
|
1698
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1699
|
-
for (const key of keys) {
|
|
1700
|
-
if (seen.has(key)) throw new SeedcordTypeError(SeedcordErrorCode.PaginationDuplicateControls, [key]);
|
|
1701
|
-
seen.add(key);
|
|
1702
|
-
}
|
|
1703
|
-
return new ActionRowBuilder().addComponents(keys.map((key) => this.button(key)));
|
|
1704
|
-
}
|
|
1705
|
-
indicatorText() {
|
|
1706
|
-
const { page, totalPages } = this.view;
|
|
1707
|
-
return totalPages === void 0 ? `Page ${page + 1}` : `Page ${page + 1} of ${totalPages}`;
|
|
1708
|
-
}
|
|
1709
|
-
};
|
|
1710
|
-
|
|
1711
|
-
//#endregion
|
|
1712
|
-
//#region src/pagination/render.ts
|
|
1713
|
-
var PageContainer = class extends BuilderComponent {
|
|
1714
|
-
constructor(view, renderItem, controlRow) {
|
|
1715
|
-
super("container");
|
|
1716
|
-
const base = view.page * view.perPage;
|
|
1717
|
-
let lines = [];
|
|
1718
|
-
const flush = () => {
|
|
1719
|
-
if (lines.length === 0) return;
|
|
1720
|
-
this.instance.addTextDisplayComponents(new TextDisplayBuilder().setContent(lines.join("\n")));
|
|
1721
|
-
lines = [];
|
|
1722
|
-
};
|
|
1723
|
-
view.items.forEach((item, offset) => {
|
|
1724
|
-
const rendered = renderItem(item, base + offset);
|
|
1725
|
-
if (typeof rendered === "string") {
|
|
1726
|
-
lines.push(rendered);
|
|
1727
|
-
return;
|
|
1728
|
-
}
|
|
1729
|
-
flush();
|
|
1730
|
-
this.instance.addSectionComponents(rendered.component);
|
|
1731
|
-
});
|
|
1732
|
-
flush();
|
|
1733
|
-
this.instance.addActionRowComponents(controlRow);
|
|
1734
|
-
}
|
|
1735
|
-
};
|
|
1736
|
-
function toReplyResponse(renderable) {
|
|
1737
|
-
if (typeof renderable === "string") return { components: [new TextDisplayBuilder().setContent(renderable)] };
|
|
1738
|
-
if (Array.isArray(renderable)) return { components: renderable };
|
|
1739
|
-
return renderable;
|
|
1740
|
-
}
|
|
1741
|
-
const ARRAY_KEYS = [
|
|
1742
|
-
"first",
|
|
1743
|
-
"prev",
|
|
1744
|
-
"indicator",
|
|
1745
|
-
"next",
|
|
1746
|
-
"last"
|
|
1747
|
-
];
|
|
1748
|
-
const CURSOR_KEYS = [
|
|
1749
|
-
"prev",
|
|
1750
|
-
"indicator",
|
|
1751
|
-
"next"
|
|
1752
|
-
];
|
|
1753
|
-
/**
|
|
1754
|
-
* Build a page's V2 reply. A `render` override builds the whole tree, otherwise the default container lists
|
|
1755
|
-
* the items and appends the controls (all five for a known total, prev/indicator/next for a cursor source).
|
|
1756
|
-
*/
|
|
1757
|
-
function renderPage(view, cursor, config) {
|
|
1758
|
-
const controls = new Controls(cursor, view);
|
|
1759
|
-
if (config.render) return toReplyResponse(config.render(view, controls));
|
|
1760
|
-
return { components: [new PageContainer(view, config.renderItem ?? String, controls.row(...view.totalPages === void 0 ? CURSOR_KEYS : ARRAY_KEYS)).component] };
|
|
1761
|
-
}
|
|
1762
|
-
|
|
1763
1159
|
//#endregion
|
|
1764
1160
|
//#region src/pagination/Paginator.ts
|
|
1765
1161
|
function contextOf(interaction, core) {
|
|
@@ -1767,7 +1163,7 @@ function contextOf(interaction, core) {
|
|
|
1767
1163
|
interaction,
|
|
1768
1164
|
user: interaction.user,
|
|
1769
1165
|
guild: interaction.guild,
|
|
1770
|
-
|
|
1166
|
+
core
|
|
1771
1167
|
};
|
|
1772
1168
|
}
|
|
1773
1169
|
/**
|
|
@@ -1789,15 +1185,11 @@ function contextOf(interaction, core) {
|
|
|
1789
1185
|
* export class BansNav extends Bans.Handler {}
|
|
1790
1186
|
* ```
|
|
1791
1187
|
*/
|
|
1792
|
-
var Paginator = class {
|
|
1793
|
-
/** The page cursor, pass it to your `@ButtonRoute`. */
|
|
1794
|
-
cursor;
|
|
1188
|
+
var Paginator = class extends PaginatorBase {
|
|
1795
1189
|
/** The nav handler base. Extend it with an empty body and decorate it, `@ButtonRoute(p.cursor)`. */
|
|
1796
1190
|
Handler;
|
|
1797
|
-
config;
|
|
1798
1191
|
constructor(config) {
|
|
1799
|
-
|
|
1800
|
-
this.config = config;
|
|
1192
|
+
super(config);
|
|
1801
1193
|
const loadPage = (ctx, n) => this.page(ctx, n);
|
|
1802
1194
|
this.Handler = class Nav extends ButtonHandler {
|
|
1803
1195
|
async execute() {
|
|
@@ -1807,39 +1199,29 @@ var Paginator = class {
|
|
|
1807
1199
|
}
|
|
1808
1200
|
};
|
|
1809
1201
|
}
|
|
1810
|
-
/**
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
async
|
|
1817
|
-
|
|
1202
|
+
/**
|
|
1203
|
+
* Render page 0 and send it through the handler's sender, which picks reply, edit, or followUp from its
|
|
1204
|
+
* ack state.
|
|
1205
|
+
*
|
|
1206
|
+
* @param handler - The handler starting the paginator, normally `this`.
|
|
1207
|
+
*/
|
|
1208
|
+
async start(handler) {
|
|
1209
|
+
const interaction = handler.getEvent();
|
|
1210
|
+
const response = await this.page(contextOf(interaction, handler.core), 0);
|
|
1211
|
+
return handler.getSender().send(response, { ephemeral: this.config.ephemeral ?? false });
|
|
1818
1212
|
}
|
|
1819
1213
|
};
|
|
1820
1214
|
|
|
1821
1215
|
//#endregion
|
|
1822
1216
|
//#region src/pagination/sources.ts
|
|
1823
|
-
const DEFAULT_PER_PAGE = 10;
|
|
1824
1217
|
/**
|
|
1825
1218
|
* A source for a bounded list you can load whole. It loads the full list on every click, then slices the
|
|
1826
|
-
* page
|
|
1827
|
-
*
|
|
1219
|
+
* page, so the real total is known (a real last button, "Page X of Y"). Cache inside your loader if the
|
|
1220
|
+
* load is expensive.
|
|
1828
1221
|
*
|
|
1829
1222
|
* @typeParam Item - The item type, inferred from the loader's return.
|
|
1830
1223
|
*/
|
|
1831
|
-
var ArraySource = class {
|
|
1832
|
-
load;
|
|
1833
|
-
perPage;
|
|
1834
|
-
constructor(load, opts) {
|
|
1835
|
-
this.load = load;
|
|
1836
|
-
this.perPage = opts?.perPage ?? DEFAULT_PER_PAGE;
|
|
1837
|
-
if (!Number.isSafeInteger(this.perPage) || this.perPage <= 0) throw new SeedcordRangeError(SeedcordErrorCode.PaginationInvalidPerPage, [this.perPage]);
|
|
1838
|
-
}
|
|
1839
|
-
async page(ctx, n) {
|
|
1840
|
-
return paginate(await this.load(ctx), n, this.perPage);
|
|
1841
|
-
}
|
|
1842
|
-
};
|
|
1224
|
+
var ArraySource = class extends ArraySource$1 {};
|
|
1843
1225
|
/**
|
|
1844
1226
|
* A source for a large or unknown-length set you fetch one page at a time (SQL LIMIT/OFFSET, a paged API).
|
|
1845
1227
|
* The fetcher receives the page index and the page size and reports whether a next page exists. A cursor
|
|
@@ -1848,496 +1230,35 @@ var ArraySource = class {
|
|
|
1848
1230
|
*
|
|
1849
1231
|
* @typeParam Item - The item type, inferred from the fetcher's slice.
|
|
1850
1232
|
*/
|
|
1851
|
-
var CursorSource = class {
|
|
1852
|
-
fetch;
|
|
1853
|
-
perPage;
|
|
1854
|
-
constructor(fetch, opts) {
|
|
1855
|
-
this.fetch = fetch;
|
|
1856
|
-
this.perPage = opts?.perPage ?? DEFAULT_PER_PAGE;
|
|
1857
|
-
if (!Number.isSafeInteger(this.perPage) || this.perPage <= 0) throw new SeedcordRangeError(SeedcordErrorCode.PaginationInvalidPerPage, [this.perPage]);
|
|
1858
|
-
}
|
|
1859
|
-
async page(ctx, n) {
|
|
1860
|
-
const page = Math.max(0, Math.trunc(n));
|
|
1861
|
-
const { items, hasNext } = await this.fetch(ctx, page, this.perPage);
|
|
1862
|
-
return {
|
|
1863
|
-
items: [...items],
|
|
1864
|
-
page,
|
|
1865
|
-
perPage: this.perPage,
|
|
1866
|
-
hasPrev: page > 0,
|
|
1867
|
-
hasNext
|
|
1868
|
-
};
|
|
1869
|
-
}
|
|
1870
|
-
};
|
|
1871
|
-
|
|
1872
|
-
//#endregion
|
|
1873
|
-
//#region src/subscribers/Subscriber.ts
|
|
1874
|
-
/**
|
|
1875
|
-
* Abstract base class for handling application subscribers.
|
|
1876
|
-
*
|
|
1877
|
-
* Provides type-safe access to subscribers data and the core framework instance.
|
|
1878
|
-
* Extend this class to create custom subscriber handlers.
|
|
1879
|
-
*
|
|
1880
|
-
* @typeParam KeyOfSubscribers - The specific subscriber type this handler processes
|
|
1881
|
-
*/
|
|
1882
|
-
var Subscriber = class {
|
|
1883
|
-
data;
|
|
1884
|
-
core;
|
|
1885
|
-
logger;
|
|
1886
|
-
/**
|
|
1887
|
-
* Instantiates the subscriber with event data and core.
|
|
1888
|
-
*
|
|
1889
|
-
* @param data - The subscriber event data
|
|
1890
|
-
* @param core - The core framework instance
|
|
1891
|
-
*/
|
|
1892
|
-
constructor(data, core) {
|
|
1893
|
-
this.data = data;
|
|
1894
|
-
this.core = core;
|
|
1895
|
-
this.logger = new Logger(this.constructor.name);
|
|
1896
|
-
}
|
|
1897
|
-
};
|
|
1898
|
-
|
|
1899
|
-
//#endregion
|
|
1900
|
-
//#region src/subscribers/decorators/Subscribe.ts
|
|
1901
|
-
/**
|
|
1902
|
-
* Registers a subscriber handler class with a specific subscriber event.
|
|
1903
|
-
*
|
|
1904
|
-
* Associates the decorated class with a subscriber event type for automatic
|
|
1905
|
-
* registration and execution when the subscriber is emitted.
|
|
1906
|
-
*
|
|
1907
|
-
* @typeParam TSubscriber - The subscriber event name to register for
|
|
1908
|
-
* @param subscriber - The subscriber event name to register for
|
|
1909
|
-
* @param options - Options to configure the subscriber handler registration.
|
|
1910
|
-
* @decorator
|
|
1911
|
-
* @example
|
|
1912
|
-
* ```typescript
|
|
1913
|
-
* \@Subscribe('userJoin')
|
|
1914
|
-
* class WelcomeHandler extends Subscriber<'userJoin'> {
|
|
1915
|
-
* async execute() {
|
|
1916
|
-
* // Handle user join event
|
|
1917
|
-
* }
|
|
1918
|
-
* }
|
|
1919
|
-
* ```
|
|
1920
|
-
* @example
|
|
1921
|
-
* ```ts
|
|
1922
|
-
* \@Subscribe('userJoin', { frequency: 'once' })
|
|
1923
|
-
* // or
|
|
1924
|
-
* \@Subscribe('userJoin')
|
|
1925
|
-
* ```
|
|
1926
|
-
*/
|
|
1927
|
-
function Subscribe(subscriber, options) {
|
|
1928
|
-
return function(constructor) {
|
|
1929
|
-
const meta = {
|
|
1930
|
-
subscriber,
|
|
1931
|
-
frequency: options?.frequency
|
|
1932
|
-
};
|
|
1933
|
-
Reflect.defineMetadata(SubscribeMetadataKey, meta, constructor);
|
|
1934
|
-
};
|
|
1935
|
-
}
|
|
1936
|
-
|
|
1937
|
-
//#endregion
|
|
1938
|
-
//#region src/subscribers/decorators/WebhookUrl.ts
|
|
1939
|
-
/**
|
|
1940
|
-
* Declares the environment variable a {@link WebhookLog} reporter reads its webhook url from.
|
|
1941
|
-
*
|
|
1942
|
-
* An unset variable disables the reporter with a boot warning. A set but malformed value throws
|
|
1943
|
-
* at boot.
|
|
1944
|
-
*
|
|
1945
|
-
* @param envKey - The environment variable name holding the webhook url
|
|
1946
|
-
* @decorator
|
|
1947
|
-
* @example
|
|
1948
|
-
* ```ts
|
|
1949
|
-
* \@Subscribe('memberBanned')
|
|
1950
|
-
* \@WebhookUrl('BAN_LOG_WEBHOOK_URL')
|
|
1951
|
-
* class BanLog extends WebhookLog<'memberBanned'> {
|
|
1952
|
-
* report() {
|
|
1953
|
-
* return { components: [new BanCard(this.data).component] };
|
|
1954
|
-
* }
|
|
1955
|
-
* }
|
|
1956
|
-
* ```
|
|
1957
|
-
*/
|
|
1958
|
-
function WebhookUrl(envKey) {
|
|
1959
|
-
return function(constructor) {
|
|
1960
|
-
Reflect.defineMetadata(WebhookUrlMetadataKey, envKey, constructor);
|
|
1961
|
-
};
|
|
1962
|
-
}
|
|
1963
|
-
|
|
1964
|
-
//#endregion
|
|
1965
|
-
//#region src/miscellaneous/flagsFor.ts
|
|
1966
|
-
/** @internal */
|
|
1967
|
-
function flagsFor(ephemeral) {
|
|
1968
|
-
let flags = MessageFlags$1.IsComponentsV2;
|
|
1969
|
-
if (ephemeral) flags |= MessageFlags$1.Ephemeral;
|
|
1970
|
-
return flags;
|
|
1971
|
-
}
|
|
1233
|
+
var CursorSource = class extends CursorSource$1 {};
|
|
1972
1234
|
|
|
1973
1235
|
//#endregion
|
|
1974
|
-
//#region src/subscribers/
|
|
1975
|
-
const DISCORD_WEBHOOK_REGEX = new RegExp(String.raw`^https?:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w$-]+$`);
|
|
1976
|
-
function isDiscordWebhookUrl(value) {
|
|
1977
|
-
return URL.canParse(value) && DISCORD_WEBHOOK_REGEX.test(value);
|
|
1978
|
-
}
|
|
1979
|
-
function jsonAttachment(name, description, data) {
|
|
1980
|
-
const content = filterCirculars(data);
|
|
1981
|
-
return {
|
|
1982
|
-
name,
|
|
1983
|
-
description,
|
|
1984
|
-
data: Buffer.from(JSON.stringify(content, void 0, 2), "utf8")
|
|
1985
|
-
};
|
|
1986
|
-
}
|
|
1987
|
-
function neutralizeFences(text) {
|
|
1988
|
-
return text.replaceAll("```", "```");
|
|
1989
|
-
}
|
|
1990
|
-
/** An error's stack with its direct cause appended (one level), ANSI-stripped for the webhook payload. */
|
|
1991
|
-
function errorReport(error) {
|
|
1992
|
-
let report = error.stack ?? `${error.name}: ${error.message}`;
|
|
1993
|
-
if (Error.isError(error.cause)) {
|
|
1994
|
-
const cause = error.cause.stack ?? `${error.cause.name}: ${error.cause.message}`;
|
|
1995
|
-
report += `\n\nCaused by:\n${cause}`;
|
|
1996
|
-
}
|
|
1997
|
-
const stripped = neutralizeFences(stripAnsi(report));
|
|
1998
|
-
return stripped.length > 1800 ? `${stripped.slice(0, 1799)}…` : stripped;
|
|
1999
|
-
}
|
|
2000
|
-
var WebhookSeparator = class extends BuilderComponent {
|
|
2001
|
-
constructor() {
|
|
2002
|
-
super("separator");
|
|
2003
|
-
this.instance.setSpacing(SeparatorSpacingSize.Small).setDivider(true);
|
|
2004
|
-
}
|
|
2005
|
-
};
|
|
2006
|
-
|
|
2007
|
-
//#endregion
|
|
2008
|
-
//#region src/subscribers/bases/WebhookSender.ts
|
|
2009
|
-
const HTTP_NOT_FOUND = 404;
|
|
2010
|
-
const HTTP_UNAUTHORIZED = 401;
|
|
1236
|
+
//#region src/subscribers/index.ts
|
|
2011
1237
|
/**
|
|
2012
|
-
*
|
|
1238
|
+
* Base class for a gateway subscriber, binding `this.core` to the gateway {@link Core}.
|
|
2013
1239
|
*
|
|
2014
|
-
* @
|
|
1240
|
+
* @typeParam KeyOfSubscribers - The subscription key this handler receives
|
|
2015
1241
|
*/
|
|
2016
|
-
var
|
|
2017
|
-
rest = new REST();
|
|
2018
|
-
route;
|
|
2019
|
-
constructor(url) {
|
|
2020
|
-
const match = /\/webhooks\/(\d+)\/([\w$-]+)$/.exec(url);
|
|
2021
|
-
if (!match?.[1] || !match[2]) throw new SeedcordError(SeedcordErrorCode.ConfigWebhookUrlInvalid, ["The url"]);
|
|
2022
|
-
this.route = Routes.webhook(match[1], match[2]);
|
|
2023
|
-
}
|
|
2024
|
-
/**
|
|
2025
|
-
* Fetches the webhook object, sending no message. Returns `missing` on a 404 or 401 from
|
|
2026
|
-
* Discord and `unreachable` on any other failure.
|
|
2027
|
-
*/
|
|
2028
|
-
async verify() {
|
|
2029
|
-
try {
|
|
2030
|
-
await this.rest.get(this.route, { auth: false });
|
|
2031
|
-
return "ok";
|
|
2032
|
-
} catch (error) {
|
|
2033
|
-
return error instanceof DiscordAPIError$1 && (error.status === HTTP_NOT_FOUND || error.status === HTTP_UNAUTHORIZED) ? "missing" : "unreachable";
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2036
|
-
async send(options) {
|
|
2037
|
-
const { flags, username, components, files } = options;
|
|
2038
|
-
await this.rest.post(this.route, {
|
|
2039
|
-
auth: false,
|
|
2040
|
-
query: new URLSearchParams({
|
|
2041
|
-
with_components: "true",
|
|
2042
|
-
wait: "true"
|
|
2043
|
-
}),
|
|
2044
|
-
body: {
|
|
2045
|
-
flags,
|
|
2046
|
-
username,
|
|
2047
|
-
components: components.map((component) => component.toJSON()),
|
|
2048
|
-
...files?.length && { attachments: files.map((file, id) => ({
|
|
2049
|
-
id,
|
|
2050
|
-
filename: file.name,
|
|
2051
|
-
description: file.description
|
|
2052
|
-
})) }
|
|
2053
|
-
},
|
|
2054
|
-
...files?.length && { files: files.map((file) => ({
|
|
2055
|
-
name: file.name,
|
|
2056
|
-
data: file.data
|
|
2057
|
-
})) }
|
|
2058
|
-
});
|
|
2059
|
-
}
|
|
2060
|
-
};
|
|
2061
|
-
|
|
2062
|
-
//#endregion
|
|
2063
|
-
//#region src/subscribers/bases/WebhookLog.ts
|
|
1242
|
+
var Subscriber = class extends Subscriber$1 {};
|
|
2064
1243
|
/**
|
|
2065
|
-
* Base class for
|
|
2066
|
-
*
|
|
2067
|
-
* Declare the url's environment variable with `@WebhookUrl` and implement {@link report}. Url
|
|
2068
|
-
* resolution, validation, sender reuse, and sending run in the base. When the variable is unset
|
|
2069
|
-
* the reporter is skipped at registration with a boot warning.
|
|
1244
|
+
* Base class for a gateway subscriber that delivers its event to a Discord webhook, binding
|
|
1245
|
+
* `this.core` to the gateway {@link Core}.
|
|
2070
1246
|
*
|
|
2071
1247
|
* @typeParam KeyOfSubscribers - The subscription key this reporter receives
|
|
2072
1248
|
*/
|
|
2073
|
-
var WebhookLog = class
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
components,
|
|
2087
|
-
files
|
|
2088
|
-
});
|
|
2089
|
-
} catch (error) {
|
|
2090
|
-
this.logger.error("Failed to send webhook report", error);
|
|
2091
|
-
}
|
|
2092
|
-
}
|
|
2093
|
-
/** @internal */
|
|
2094
|
-
static senderFor(url) {
|
|
2095
|
-
let sender = WebhookLog.senders.get(url);
|
|
2096
|
-
if (!sender) {
|
|
2097
|
-
sender = new WebhookSender(url);
|
|
2098
|
-
WebhookLog.senders.set(url, sender);
|
|
2099
|
-
}
|
|
2100
|
-
return sender;
|
|
2101
|
-
}
|
|
2102
|
-
/** @internal */
|
|
2103
|
-
static envKeyOf(ctor) {
|
|
2104
|
-
const envKey = Reflect.getMetadata(WebhookUrlMetadataKey, ctor);
|
|
2105
|
-
if (!envKey) throw new SeedcordError(SeedcordErrorCode.DecoratorWebhookUrlMissing, [ctor.name]);
|
|
2106
|
-
return envKey;
|
|
2107
|
-
}
|
|
2108
|
-
/** @internal Returns null when the env var is unset, throws when set but malformed. */
|
|
2109
|
-
static urlOf(envKey) {
|
|
2110
|
-
if (!Envapter.has(envKey)) return null;
|
|
2111
|
-
const raw = Envapter.getRequired(envKey, Converters.String).trim();
|
|
2112
|
-
if (raw === "") return null;
|
|
2113
|
-
if (!isDiscordWebhookUrl(raw)) throw new SeedcordError(SeedcordErrorCode.ConfigWebhookUrlInvalid, [envKey]);
|
|
2114
|
-
return raw;
|
|
2115
|
-
}
|
|
2116
|
-
};
|
|
2117
|
-
|
|
2118
|
-
//#endregion
|
|
2119
|
-
//#region src/bot/utilities/miscellaneous/contextMenuLeaves.ts
|
|
2120
|
-
/**
|
|
2121
|
-
* Collects the names of every registered context-menu command across a set of command builders, split into a
|
|
2122
|
-
* user set and a message set. The same builder may appear more than once (a guild command is pushed once per
|
|
2123
|
-
* guild), and slash commands carry no context-menu name, so both collapse here. The split mirrors the
|
|
2124
|
-
* controller's two name-keyed maps, so the boot guard checks each name against its own kind.
|
|
2125
|
-
*
|
|
2126
|
-
* @internal
|
|
2127
|
-
*/
|
|
2128
|
-
function contextMenuLeaves(commands) {
|
|
2129
|
-
const user = /* @__PURE__ */ new Set();
|
|
2130
|
-
const message = /* @__PURE__ */ new Set();
|
|
2131
|
-
for (const command of commands) {
|
|
2132
|
-
if (!(command instanceof ContextMenuCommandBuilder)) continue;
|
|
2133
|
-
if (command.type === ApplicationCommandType.User) user.add(command.name);
|
|
2134
|
-
else message.add(command.name);
|
|
2135
|
-
}
|
|
2136
|
-
return {
|
|
2137
|
-
user,
|
|
2138
|
-
message
|
|
2139
|
-
};
|
|
2140
|
-
}
|
|
2141
|
-
|
|
2142
|
-
//#endregion
|
|
2143
|
-
//#region src/bot/utilities/miscellaneous/slashRouteLeaves.ts
|
|
2144
|
-
/**
|
|
2145
|
-
* Collects the route keys of every executable slash leaf across a set of command builders, deduplicated. The
|
|
2146
|
-
* same builder may appear more than once (a guild command is pushed once per guild), and context-menu
|
|
2147
|
-
* commands carry no slash route, so both collapse here. Route keys come from the same {@link routeLeavesOf}
|
|
2148
|
-
* that `seedcord codegen` reads, so the boot-time check compares against the generated registry's keys.
|
|
2149
|
-
*
|
|
2150
|
-
* @internal
|
|
2151
|
-
*/
|
|
2152
|
-
function slashRouteLeaves(commands) {
|
|
2153
|
-
const leaves = /* @__PURE__ */ new Set();
|
|
2154
|
-
for (const command of commands) {
|
|
2155
|
-
if (!(command instanceof SlashCommandBuilder)) continue;
|
|
2156
|
-
for (const leaf of routeLeavesOf(command.toJSON())) leaves.add(leaf.route);
|
|
2157
|
-
}
|
|
2158
|
-
return leaves;
|
|
2159
|
-
}
|
|
2160
|
-
|
|
2161
|
-
//#endregion
|
|
2162
|
-
//#region src/bot/controllers/CommandRegistry.ts
|
|
2163
|
-
/**
|
|
2164
|
-
* Manages Discord application command registration and deployment.
|
|
2165
|
-
*
|
|
2166
|
-
* Scans command directories, builds command structures, and registers both global and guild-scoped commands
|
|
2167
|
-
* to Discord's API. Accessed via `core.bot.commands`. Do not construct it directly.
|
|
2168
|
-
*/
|
|
2169
|
-
var CommandRegistry = class {
|
|
2170
|
-
core;
|
|
2171
|
-
onDeployed;
|
|
2172
|
-
logger = new Logger("Commands");
|
|
2173
|
-
isInitialised = false;
|
|
2174
|
-
globalCommands = [];
|
|
2175
|
-
guildCommands = new Collection();
|
|
2176
|
-
ctorToCommand = /* @__PURE__ */ new Map();
|
|
2177
|
-
loading = false;
|
|
2178
|
-
loadedCommands = [];
|
|
2179
|
-
hmrHandler;
|
|
2180
|
-
pendingEvents = /* @__PURE__ */ new Map();
|
|
2181
|
-
constructor(core, onDeployed) {
|
|
2182
|
-
this.core = core;
|
|
2183
|
-
this.onDeployed = onDeployed;
|
|
2184
|
-
const commandsDir = this.core.config.bot.commands.path;
|
|
2185
|
-
if (!commandsDir) throw new SeedcordError(SeedcordErrorCode.CoreControllerPathMissing, ["CommandRegistry", "commands"]);
|
|
2186
|
-
if (!Envapter.isDevelopment && !Envapter.isTest) return;
|
|
2187
|
-
this.hmrHandler = new HmrModuleHandler({
|
|
2188
|
-
handlersDir: commandsDir,
|
|
2189
|
-
isHandler: this.isCommandClass.bind(this),
|
|
2190
|
-
registerHandler: this.registerCommand.bind(this),
|
|
2191
|
-
unregisterHandler: this.unregisterCommand.bind(this),
|
|
2192
|
-
getArtifacts: this.getArtifacts.bind(this),
|
|
2193
|
-
logger: this.logger
|
|
2194
|
-
});
|
|
2195
|
-
}
|
|
2196
|
-
getArtifacts(ctor) {
|
|
2197
|
-
return this.ctorToCommand.get(ctor);
|
|
2198
|
-
}
|
|
2199
|
-
/** @internal */
|
|
2200
|
-
async init() {
|
|
2201
|
-
if (this.isInitialised) return;
|
|
2202
|
-
this.isInitialised = true;
|
|
2203
|
-
const commandsDir = this.core.config.bot.commands.path;
|
|
2204
|
-
if (!commandsDir) return;
|
|
2205
|
-
this.loading = true;
|
|
2206
|
-
this.loadedCommands.length = 0;
|
|
2207
|
-
try {
|
|
2208
|
-
await this.loadCommands(commandsDir);
|
|
2209
|
-
} finally {
|
|
2210
|
-
this.loading = false;
|
|
2211
|
-
}
|
|
2212
|
-
this.reportLoad();
|
|
2213
|
-
getDevChannel()?.on("seedcord:refresh-commands", (data) => {
|
|
2214
|
-
this.refresh(data.shouldRefresh);
|
|
2215
|
-
});
|
|
2216
|
-
}
|
|
2217
|
-
reportLoad() {
|
|
2218
|
-
const { utils } = this.logger;
|
|
2219
|
-
utils.summary("Loaded commands", {
|
|
2220
|
-
global: this.globalCommands.length,
|
|
2221
|
-
"guild groups": this.guildCommands.size
|
|
2222
|
-
});
|
|
2223
|
-
let slash = 0;
|
|
2224
|
-
let menu = 0;
|
|
2225
|
-
for (const command of this.loadedCommands) if (command.kind === "slash command") slash++;
|
|
2226
|
-
else menu++;
|
|
2227
|
-
utils.block("Loaded commands", [...utils.entries(this.loadedCommands), ...utils.counts({
|
|
2228
|
-
"slash commands": slash,
|
|
2229
|
-
"context menus": menu
|
|
2230
|
-
})]);
|
|
2231
|
-
}
|
|
2232
|
-
/** @internal */
|
|
2233
|
-
async refresh(shouldRefresh = true) {
|
|
2234
|
-
if (!shouldRefresh) {
|
|
2235
|
-
this.logger.info(chalk.italic("Command refresh cancelled."));
|
|
2236
|
-
this.pendingEvents.clear();
|
|
2237
|
-
return;
|
|
2238
|
-
}
|
|
2239
|
-
this.logger.info(chalk.italic("Refreshing commands..."));
|
|
2240
|
-
for (const event of this.pendingEvents.values()) await this.hmrHandler?.handle(event);
|
|
2241
|
-
this.pendingEvents.clear();
|
|
2242
|
-
await this.setCommands();
|
|
2243
|
-
}
|
|
2244
|
-
/** @internal */
|
|
2245
|
-
async onHmr(event) {
|
|
2246
|
-
const commandsDir = this.core.config.bot.commands.path;
|
|
2247
|
-
if (commandsDir && event.file.startsWith(resolve(process.cwd(), commandsDir))) {
|
|
2248
|
-
this.pendingEvents.delete(event.file);
|
|
2249
|
-
this.pendingEvents.set(event.file, event);
|
|
2250
|
-
getDevChannel()?.send("seedcord:commands-update-prompt", { files: [...this.pendingEvents.keys()].map((f) => formatFilePath(f)) });
|
|
2251
|
-
} else await this.hmrHandler?.handle(event);
|
|
2252
|
-
}
|
|
2253
|
-
async loadCommands(dir) {
|
|
2254
|
-
await traverseDirectory(dir, (fullPath, rel, mod) => {
|
|
2255
|
-
for (const exported of Object.values(mod)) if (this.isCommandClass(exported)) {
|
|
2256
|
-
this.registerCommand(exported, rel);
|
|
2257
|
-
this.hmrHandler?.trackHandler(fullPath, exported);
|
|
2258
|
-
}
|
|
2259
|
-
}, this.logger);
|
|
2260
|
-
}
|
|
2261
|
-
isCommandClass(obj) {
|
|
2262
|
-
if (typeof obj !== "function") return false;
|
|
2263
|
-
return obj.prototype instanceof BuilderComponent && Reflect.hasMetadata(CommandMetadataKey, obj);
|
|
2264
|
-
}
|
|
2265
|
-
registerCommand(ctor, rel) {
|
|
2266
|
-
const meta = Reflect.getMetadata(CommandMetadataKey, ctor);
|
|
2267
|
-
if (!meta) return;
|
|
2268
|
-
const comp = new ctor().component;
|
|
2269
|
-
const kind = comp instanceof SlashCommandBuilder ? "slash command" : "context menu";
|
|
2270
|
-
if (meta.scope === "global") this.globalCommands.push(comp);
|
|
2271
|
-
else for (const g of meta.guilds) {
|
|
2272
|
-
const arr = this.guildCommands.get(g) ?? [];
|
|
2273
|
-
arr.push(comp);
|
|
2274
|
-
this.guildCommands.set(g, arr);
|
|
2275
|
-
}
|
|
2276
|
-
this.ctorToCommand.set(ctor, {
|
|
2277
|
-
name: comp.name,
|
|
2278
|
-
scope: meta.scope,
|
|
2279
|
-
...meta.scope === "guild" && { guilds: meta.guilds }
|
|
2280
|
-
});
|
|
2281
|
-
if (this.loading) {
|
|
2282
|
-
this.loadedCommands.push({
|
|
2283
|
-
name: comp.name,
|
|
2284
|
-
from: formatFilePath(rel),
|
|
2285
|
-
kind
|
|
2286
|
-
});
|
|
2287
|
-
return;
|
|
2288
|
-
}
|
|
2289
|
-
this.logger.utils.registration(comp.name, rel, `${meta.scope} ${kind}`);
|
|
2290
|
-
}
|
|
2291
|
-
unregisterCommand(ctor, artifacts) {
|
|
2292
|
-
const info = artifacts ?? this.ctorToCommand.get(ctor);
|
|
2293
|
-
if (!info) return;
|
|
2294
|
-
if (info.scope === "global") {
|
|
2295
|
-
const idx = this.globalCommands.findIndex((c) => c.name === info.name);
|
|
2296
|
-
if (idx !== -1) this.globalCommands.splice(idx, 1);
|
|
2297
|
-
} else for (const g of info.guilds ?? []) {
|
|
2298
|
-
const arr = this.guildCommands.get(g);
|
|
2299
|
-
if (arr) {
|
|
2300
|
-
const idx = arr.findIndex((c) => c.name === info.name);
|
|
2301
|
-
if (idx !== -1) arr.splice(idx, 1);
|
|
2302
|
-
}
|
|
2303
|
-
}
|
|
2304
|
-
this.ctorToCommand.delete(ctor);
|
|
2305
|
-
}
|
|
2306
|
-
/** @internal */
|
|
2307
|
-
async setCommands() {
|
|
2308
|
-
const result = {
|
|
2309
|
-
global: new Collection(),
|
|
2310
|
-
guilds: new Collection()
|
|
2311
|
-
};
|
|
2312
|
-
if (this.globalCommands.length > 0) {
|
|
2313
|
-
const deployed = await this.core.bot.client.application?.commands.set(this.globalCommands);
|
|
2314
|
-
if (deployed) result.global = deployed;
|
|
2315
|
-
const tag = this.globalCommands.length === 1 ? "command" : "commands";
|
|
2316
|
-
this.logger.utils.block(`Deployed ${this.globalCommands.length} global ${tag}`, this.logger.utils.wrap(this.globalCommands.map((command) => command.name)), "info");
|
|
2317
|
-
}
|
|
2318
|
-
for (const [guildId, commands] of this.guildCommands.entries()) {
|
|
2319
|
-
const guild = this.core.bot.client.guilds.cache.get(guildId);
|
|
2320
|
-
if (!guild) {
|
|
2321
|
-
this.logger.warn(`Guild with ID ${guildId} not found, skipping command registration.`);
|
|
2322
|
-
continue;
|
|
2323
|
-
}
|
|
2324
|
-
const deployed = await guild.commands.set(commands);
|
|
2325
|
-
result.guilds.set(guildId, deployed);
|
|
2326
|
-
const tag = commands.length === 1 ? "command" : "commands";
|
|
2327
|
-
this.logger.utils.block(`Deployed ${commands.length} ${tag} to ${paint.amber.bold(guild.name)}`, this.logger.utils.wrap(commands.map((command) => command.name)), "info");
|
|
2328
|
-
}
|
|
2329
|
-
this.onDeployed?.(result);
|
|
2330
|
-
return result;
|
|
2331
|
-
}
|
|
2332
|
-
/** The deduplicated slash route keys across every global and guild command. @internal */
|
|
2333
|
-
routeLeaves() {
|
|
2334
|
-
return slashRouteLeaves([...this.globalCommands, ...this.guildCommands.values()].flat());
|
|
2335
|
-
}
|
|
2336
|
-
/** The registered context-menu command names, split by kind, across every global and guild command. @internal */
|
|
2337
|
-
contextMenuLeaves() {
|
|
2338
|
-
return contextMenuLeaves([...this.globalCommands, ...this.guildCommands.values()].flat());
|
|
2339
|
-
}
|
|
2340
|
-
};
|
|
1249
|
+
var WebhookLog = class extends WebhookLog$1 {};
|
|
1250
|
+
|
|
1251
|
+
//#endregion
|
|
1252
|
+
//#region src/plugin.ts
|
|
1253
|
+
/**
|
|
1254
|
+
* Base class for a gateway plugin, binding `this.core` to the gateway {@link Core}.
|
|
1255
|
+
*
|
|
1256
|
+
* A plugin that runs on either transport extends the base from `@seedcord/core/plugin`, whose
|
|
1257
|
+
* `this.core` is typed to the shared members only.
|
|
1258
|
+
*
|
|
1259
|
+
* @typeParam Opts - The transport and runtime the plugin declares.
|
|
1260
|
+
*/
|
|
1261
|
+
var Plugin = class extends Plugin$1 {};
|
|
2341
1262
|
|
|
2342
1263
|
//#endregion
|
|
2343
1264
|
//#region src/miscellaneous/deriveEventActor.ts
|
|
@@ -2416,9 +1337,157 @@ function eventGateContext(eventName, args, core) {
|
|
|
2416
1337
|
};
|
|
2417
1338
|
}
|
|
2418
1339
|
|
|
1340
|
+
//#endregion
|
|
1341
|
+
//#region src/miscellaneous/extractErrorResponse.ts
|
|
1342
|
+
const logger$2 = new Logger("ErrorsHandling", { channel: "errors" });
|
|
1343
|
+
/**
|
|
1344
|
+
* Processes an error into a user-facing reply and routes its report.
|
|
1345
|
+
*
|
|
1346
|
+
* A {@link Notice} always renders its own reply. A reporting denial (`report: true`) additionally
|
|
1347
|
+
* publishes `handledException` on the interaction path. A raw, non-denial throw publishes
|
|
1348
|
+
* `unknownException` and renders the configured `defaultError` (or a generic {@link Fault}). One uuid is
|
|
1349
|
+
* threaded into both the reply and the bus payload.
|
|
1350
|
+
*/
|
|
1351
|
+
function extractErrorResponse(error, core, origin) {
|
|
1352
|
+
const uuid = crypto.randomUUID();
|
|
1353
|
+
const developerUsername = core.config.notifications?.developerUsername;
|
|
1354
|
+
const ctx = developerUsername === void 0 ? { uuid } : {
|
|
1355
|
+
uuid,
|
|
1356
|
+
developerUsername
|
|
1357
|
+
};
|
|
1358
|
+
if (error instanceof Notice) {
|
|
1359
|
+
if (error.report) reportFault(error, core, origin, uuid);
|
|
1360
|
+
return {
|
|
1361
|
+
uuid,
|
|
1362
|
+
response: error.render(ctx)
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
reportRawFault(error, core, origin, uuid);
|
|
1366
|
+
const override = core.config.errors?.defaultError;
|
|
1367
|
+
return {
|
|
1368
|
+
uuid,
|
|
1369
|
+
response: override ? new override(uuid).render(ctx) : new Fault().render(ctx)
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
function withThrottle(core, origin, error, publish) {
|
|
1373
|
+
const throttle = FaultThrottle.for(core);
|
|
1374
|
+
const key = faultKey(origin, error);
|
|
1375
|
+
if (!throttle.shouldReport(key)) {
|
|
1376
|
+
logger$2.debug(`throttled duplicate fault ${key}`);
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
publish();
|
|
1380
|
+
throttle.markReported(key);
|
|
1381
|
+
}
|
|
1382
|
+
function reportFault(denial, core, origin, uuid) {
|
|
1383
|
+
logger$2.error(`${denial.name}: ${uuid}`, denial);
|
|
1384
|
+
withThrottle(core, origin, denial, () => {
|
|
1385
|
+
if (origin.interaction) core.bus[PublishDefault]("handledException", {
|
|
1386
|
+
denial,
|
|
1387
|
+
uuid,
|
|
1388
|
+
source: buildInteractionSource(origin.interaction)
|
|
1389
|
+
});
|
|
1390
|
+
else if (origin.event) core.bus[PublishDefault]("handledException", {
|
|
1391
|
+
denial,
|
|
1392
|
+
uuid,
|
|
1393
|
+
source: buildEventSource(origin.event, origin)
|
|
1394
|
+
});
|
|
1395
|
+
else core.bus[PublishDefault]("unknownException", {
|
|
1396
|
+
uuid,
|
|
1397
|
+
error: denial,
|
|
1398
|
+
...scalarActors(origin),
|
|
1399
|
+
metadata: metadataFor(origin)
|
|
1400
|
+
});
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
function causeLine(error) {
|
|
1404
|
+
const { cause } = error;
|
|
1405
|
+
if (!Error.isError(cause)) return "";
|
|
1406
|
+
const [first] = cause.message.split("\n");
|
|
1407
|
+
return `\ncaused by ${cause.name}: ${first ?? cause.message}`;
|
|
1408
|
+
}
|
|
1409
|
+
function reportRawFault(error, core, origin, uuid) {
|
|
1410
|
+
if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
|
|
1411
|
+
else logger$2.error(`${uuid} | ${error.message}${causeLine(error)}`);
|
|
1412
|
+
withThrottle(core, origin, error, () => {
|
|
1413
|
+
core.bus[PublishDefault]("unknownException", {
|
|
1414
|
+
uuid,
|
|
1415
|
+
error,
|
|
1416
|
+
...scalarActors(origin),
|
|
1417
|
+
metadata: metadataFor(origin)
|
|
1418
|
+
});
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
function scalarActors(origin) {
|
|
1422
|
+
return {
|
|
1423
|
+
guild: origin.guild ? {
|
|
1424
|
+
id: origin.guild.id,
|
|
1425
|
+
name: origin.guild.name
|
|
1426
|
+
} : void 0,
|
|
1427
|
+
user: origin.user ? {
|
|
1428
|
+
id: origin.user.id,
|
|
1429
|
+
username: origin.user.username
|
|
1430
|
+
} : void 0
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
function metadataFor(origin) {
|
|
1434
|
+
if (origin.event) return {
|
|
1435
|
+
eventName: origin.event.name,
|
|
1436
|
+
handler: origin.event.handler,
|
|
1437
|
+
args: origin.event.args
|
|
1438
|
+
};
|
|
1439
|
+
return origin.metadata;
|
|
1440
|
+
}
|
|
1441
|
+
function buildEventSource(event, origin) {
|
|
1442
|
+
return {
|
|
1443
|
+
kind: "event",
|
|
1444
|
+
eventName: event.name,
|
|
1445
|
+
handler: event.handler,
|
|
1446
|
+
userId: origin.user?.id ?? null,
|
|
1447
|
+
guildId: origin.guild?.id ?? null,
|
|
1448
|
+
channelId: event.channelId,
|
|
1449
|
+
raw: event.args
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
function faultKey(origin, error) {
|
|
1453
|
+
const name = error instanceof Notice ? error.name : error instanceof DiscordAPIError ? String(error.code) : error.constructor.name;
|
|
1454
|
+
if (origin.event) return `${origin.event.name}:${origin.event.handler}:${name}`;
|
|
1455
|
+
if (origin.interaction) return `${interactionRoute(origin.interaction)}:${name}`;
|
|
1456
|
+
if (origin.route) return `autocomplete:${origin.route}:${name}`;
|
|
1457
|
+
return `autocomplete:${name}`;
|
|
1458
|
+
}
|
|
1459
|
+
function interactionRoute(interaction) {
|
|
1460
|
+
if (interaction.isChatInputCommand()) return slashRouteOf(interaction);
|
|
1461
|
+
if (interaction.isContextMenuCommand()) return interaction.commandName;
|
|
1462
|
+
if (interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit()) return prefixOf(interaction.customId) || interaction.customId;
|
|
1463
|
+
return "interaction";
|
|
1464
|
+
}
|
|
1465
|
+
function buildInteractionSource(interaction) {
|
|
1466
|
+
const command = interaction.isChatInputCommand() ? slashRouteOf(interaction) : interaction.isContextMenuCommand() ? interaction.commandName : null;
|
|
1467
|
+
const customId = interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit() ? interaction.customId : null;
|
|
1468
|
+
return {
|
|
1469
|
+
kind: "interaction",
|
|
1470
|
+
interactionKind: interactionKind(interaction),
|
|
1471
|
+
command,
|
|
1472
|
+
customId,
|
|
1473
|
+
userId: interaction.user.id,
|
|
1474
|
+
guildId: interaction.guildId,
|
|
1475
|
+
channelId: interaction.channelId,
|
|
1476
|
+
interactionId: interaction.id,
|
|
1477
|
+
raw: interaction
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
function interactionKind(interaction) {
|
|
1481
|
+
if (interaction.isChatInputCommand()) return "slash";
|
|
1482
|
+
if (interaction.isContextMenuCommand()) return "context-menu";
|
|
1483
|
+
if (interaction.isButton()) return "button";
|
|
1484
|
+
if (interaction.isAnySelectMenu()) return "select";
|
|
1485
|
+
return "modal";
|
|
1486
|
+
}
|
|
1487
|
+
|
|
2419
1488
|
//#endregion
|
|
2420
1489
|
//#region src/bot/handleEventFault.ts
|
|
2421
|
-
const logger$1 = new Logger("EventBoundary");
|
|
1490
|
+
const logger$1 = new Logger("EventBoundary", { channel: "errors" });
|
|
2422
1491
|
/**
|
|
2423
1492
|
* The event controller boundary. Reports a throw from an event handler and never replies, since a
|
|
2424
1493
|
* generic event has no reply target. A {@link Silence} or a non-reporting {@link Notice} stops quietly, a
|
|
@@ -2431,15 +1500,15 @@ function handleEventFault(caught, eventName, handlerName, args, core) {
|
|
|
2431
1500
|
if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger$1.debug(`Silence: ${caught.reason}`);
|
|
2432
1501
|
return;
|
|
2433
1502
|
}
|
|
2434
|
-
|
|
1503
|
+
const error = asError(caught);
|
|
2435
1504
|
const ignore = new Set(core.config.errors?.ignoreEventApiCodes ?? []);
|
|
2436
|
-
if (
|
|
2437
|
-
logger$1.debug(`swallowed api code ${
|
|
1505
|
+
if (error instanceof DiscordAPIError && ignore.has(error.code)) {
|
|
1506
|
+
logger$1.debug(`swallowed api code ${error.code}`);
|
|
2438
1507
|
return;
|
|
2439
1508
|
}
|
|
2440
|
-
if (
|
|
1509
|
+
if (error instanceof Notice && !error.report) return;
|
|
2441
1510
|
const actor = deriveEventActor(args);
|
|
2442
|
-
extractErrorResponse(
|
|
1511
|
+
extractErrorResponse(error, core, {
|
|
2443
1512
|
event: {
|
|
2444
1513
|
name: eventName,
|
|
2445
1514
|
handler: handlerName,
|
|
@@ -2454,15 +1523,15 @@ function handleEventFault(caught, eventName, handlerName, args, core) {
|
|
|
2454
1523
|
//#endregion
|
|
2455
1524
|
//#region src/bot/controllers/EventDispatcher.ts
|
|
2456
1525
|
/**
|
|
2457
|
-
* Scans event handler directories,
|
|
2458
|
-
*
|
|
2459
|
-
*
|
|
1526
|
+
* Scans event handler directories, attaches one client listener per registered event, and runs the
|
|
1527
|
+
* handlers for each fire. Interactions route through InteractionDispatcher. Multiple handlers can
|
|
1528
|
+
* point to one event.
|
|
2460
1529
|
*
|
|
2461
1530
|
* @internal
|
|
2462
1531
|
*/
|
|
2463
1532
|
var EventDispatcher = class {
|
|
2464
1533
|
core;
|
|
2465
|
-
logger = new Logger("Events");
|
|
1534
|
+
logger = new Logger("Events", { channel: "events" });
|
|
2466
1535
|
isInitialized = false;
|
|
2467
1536
|
eventMap = /* @__PURE__ */ new Map();
|
|
2468
1537
|
middlewares = [];
|
|
@@ -2533,7 +1602,7 @@ var EventDispatcher = class {
|
|
|
2533
1602
|
this.registerHandler(val, relativePath);
|
|
2534
1603
|
this.hmrHandler?.trackHandler(fullPath, val);
|
|
2535
1604
|
}
|
|
2536
|
-
}
|
|
1605
|
+
});
|
|
2537
1606
|
}
|
|
2538
1607
|
async loadMiddlewares(dir) {
|
|
2539
1608
|
await traverseDirectory(dir, (fullPath, relativePath, imported) => {
|
|
@@ -2542,7 +1611,7 @@ var EventDispatcher = class {
|
|
|
2542
1611
|
this.registerMiddleware(val, relativePath);
|
|
2543
1612
|
this.hmrHandler?.trackMiddleware(fullPath, val);
|
|
2544
1613
|
}
|
|
2545
|
-
}
|
|
1614
|
+
});
|
|
2546
1615
|
}
|
|
2547
1616
|
/** @internal For use in dev mode */
|
|
2548
1617
|
async onHmr(event) {
|
|
@@ -2639,10 +1708,14 @@ var EventDispatcher = class {
|
|
|
2639
1708
|
this.logger.debug(`Attaching ${paint.mint.bold(eventName)} to the client with ${paint.mute(handlerEntries?.length ?? 0)} handler(s)`);
|
|
2640
1709
|
this.core.bot.client.on(eventName, (...args) => {
|
|
2641
1710
|
if (this.draining) return;
|
|
2642
|
-
this.core.
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
1711
|
+
this.core.bus[PublishDefault]("anyEvent", {
|
|
1712
|
+
name: eventName,
|
|
1713
|
+
args
|
|
1714
|
+
});
|
|
1715
|
+
const run = this.processEvent(eventName, args).catch((caught) => {
|
|
1716
|
+
const error = asError(caught);
|
|
1717
|
+
this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${error.name}`, error.stack);
|
|
1718
|
+
this.core.bus[PublishDefault]("unhandledEventError", { error });
|
|
2646
1719
|
});
|
|
2647
1720
|
this.inFlight.add(run);
|
|
2648
1721
|
run.finally(() => this.inFlight.delete(run));
|
|
@@ -2718,7 +1791,7 @@ const HARMLESS_API_CODES = new Set([
|
|
|
2718
1791
|
|
|
2719
1792
|
//#endregion
|
|
2720
1793
|
//#region src/bot/handleInteractionFault.ts
|
|
2721
|
-
const logger = new Logger("InteractionBoundary");
|
|
1794
|
+
const logger = new Logger("InteractionBoundary", { channel: "errors" });
|
|
2722
1795
|
/**
|
|
2723
1796
|
* The interaction controller boundary. Sorts a throw from any handler-lifecycle phase into the right
|
|
2724
1797
|
* user reply plus the right bus event. When a handler is built its sender is passed in, so the boundary
|
|
@@ -2727,33 +1800,45 @@ const logger = new Logger("InteractionBoundary");
|
|
|
2727
1800
|
*
|
|
2728
1801
|
* @internal
|
|
2729
1802
|
*/
|
|
2730
|
-
async function handleInteractionFault(caught, interaction, core, sender) {
|
|
1803
|
+
async function handleInteractionFault(caught, interaction, core, routeId, sender) {
|
|
2731
1804
|
if (caught instanceof Silence) {
|
|
2732
1805
|
if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger.debug(`Silence: ${caught.reason}`);
|
|
2733
1806
|
return;
|
|
2734
1807
|
}
|
|
2735
|
-
|
|
1808
|
+
const error = asError(caught);
|
|
2736
1809
|
const ignore = new Set(core.config.errors?.ignoreApiCodes ?? []);
|
|
2737
|
-
if (
|
|
2738
|
-
logger.debug(`swallowed api code ${
|
|
1810
|
+
if (error instanceof DiscordAPIError && ignore.has(error.code)) {
|
|
1811
|
+
logger.debug(`swallowed api code ${error.code}`);
|
|
2739
1812
|
return;
|
|
2740
1813
|
}
|
|
2741
1814
|
if (interaction.isAutocomplete()) {
|
|
2742
|
-
extractErrorResponse(
|
|
1815
|
+
extractErrorResponse(error, core, {
|
|
2743
1816
|
guild: interaction.guild,
|
|
2744
1817
|
user: interaction.user,
|
|
2745
1818
|
metadata: interaction,
|
|
2746
1819
|
route: slashRouteOf(interaction)
|
|
2747
1820
|
});
|
|
1821
|
+
await sendEmptyChoices(interaction, core, routeId);
|
|
2748
1822
|
return;
|
|
2749
1823
|
}
|
|
2750
|
-
const { response } = extractErrorResponse(
|
|
1824
|
+
const { response } = extractErrorResponse(error, core, {
|
|
2751
1825
|
interaction,
|
|
2752
1826
|
guild: interaction.guild,
|
|
2753
1827
|
user: interaction.user,
|
|
2754
1828
|
metadata: interaction
|
|
2755
1829
|
});
|
|
2756
|
-
await sendGuarded(sender ?? new ReplySender(interaction,
|
|
1830
|
+
await sendGuarded(sender ?? new ReplySender(interaction, routeId, core.bus), response, error instanceof Notice ? error.ephemeral : true);
|
|
1831
|
+
}
|
|
1832
|
+
async function sendEmptyChoices(interaction, core, routeId) {
|
|
1833
|
+
const telemetry = {
|
|
1834
|
+
bus: core.bus,
|
|
1835
|
+
interactionId: interaction.id
|
|
1836
|
+
};
|
|
1837
|
+
try {
|
|
1838
|
+
await reportedWrite(telemetry, routeId, "respond", () => interaction.respond([]));
|
|
1839
|
+
} catch (error) {
|
|
1840
|
+
logger.debug(`autocomplete empty-choices send failed: ${String(error)}`);
|
|
1841
|
+
}
|
|
2757
1842
|
}
|
|
2758
1843
|
async function sendGuarded(sender, response, ephemeral) {
|
|
2759
1844
|
try {
|
|
@@ -2777,7 +1862,7 @@ async function sendGuarded(sender, response, ephemeral) {
|
|
|
2777
1862
|
*/
|
|
2778
1863
|
var InteractionDispatcher = class {
|
|
2779
1864
|
core;
|
|
2780
|
-
logger = new Logger("Interactions");
|
|
1865
|
+
logger = new Logger("Interactions", { channel: "interactions" });
|
|
2781
1866
|
isInitialized = false;
|
|
2782
1867
|
slashMap = /* @__PURE__ */ new Map();
|
|
2783
1868
|
buttonMap = /* @__PURE__ */ new Map();
|
|
@@ -2790,6 +1875,7 @@ var InteractionDispatcher = class {
|
|
|
2790
1875
|
messageContextMenuMap = /* @__PURE__ */ new Map();
|
|
2791
1876
|
userContextMenuMap = /* @__PURE__ */ new Map();
|
|
2792
1877
|
autocompleteMap = /* @__PURE__ */ new Map();
|
|
1878
|
+
handlerFiles = /* @__PURE__ */ new Map();
|
|
2793
1879
|
keysToIgnore = /* @__PURE__ */ new Set();
|
|
2794
1880
|
middlewares = [];
|
|
2795
1881
|
inFlight = /* @__PURE__ */ new Set();
|
|
@@ -2832,21 +1918,9 @@ var InteractionDispatcher = class {
|
|
|
2832
1918
|
logger: this.logger
|
|
2833
1919
|
});
|
|
2834
1920
|
}
|
|
2835
|
-
/**
|
|
2836
|
-
* Warns for each command route with no registered `@SlashRoute` handler, which would fall through to
|
|
2837
|
-
* UnhandledRepliable at runtime. A bot may route commands outside the registry, so this does not throw.
|
|
2838
|
-
*
|
|
2839
|
-
* @internal
|
|
2840
|
-
*/
|
|
2841
1921
|
warnUnhandledRoutes(commandLeaves) {
|
|
2842
1922
|
for (const route of commandLeaves) if (!this.slashMap.has(route)) this.logger.warn(`Slash route ${paint.sky.bold(route)} has no registered ${chalk.bold("@SlashRoute")} handler.`);
|
|
2843
1923
|
}
|
|
2844
|
-
/**
|
|
2845
|
-
* Warns for each context-menu command with no matching handler, which would fall through to UnhandledRepliable
|
|
2846
|
-
* at runtime. Checked per kind since a user and a message command can share a name.
|
|
2847
|
-
*
|
|
2848
|
-
* @internal
|
|
2849
|
-
*/
|
|
2850
1924
|
warnUnhandledContextMenuRoutes(leaves) {
|
|
2851
1925
|
for (const name of leaves.user) if (!this.userContextMenuMap.has(name)) this.logger.warn(`User context menu ${paint.sky.bold(name)} has no registered ${chalk.bold("@ContextMenuRoute")} handler.`);
|
|
2852
1926
|
for (const name of leaves.message) if (!this.messageContextMenuMap.has(name)) this.logger.warn(`Message context menu ${paint.sky.bold(name)} has no registered ${chalk.bold("@ContextMenuRoute")} handler.`);
|
|
@@ -2908,7 +1982,7 @@ var InteractionDispatcher = class {
|
|
|
2908
1982
|
this.registerHandler(val, relativePath);
|
|
2909
1983
|
this.hmrHandler?.trackHandler(fullPath, val);
|
|
2910
1984
|
}
|
|
2911
|
-
}
|
|
1985
|
+
});
|
|
2912
1986
|
}
|
|
2913
1987
|
async loadMiddlewares(dir) {
|
|
2914
1988
|
await traverseDirectory(dir, (fullPath, relativePath, imported) => {
|
|
@@ -2917,7 +1991,7 @@ var InteractionDispatcher = class {
|
|
|
2917
1991
|
this.registerMiddleware(val, relativePath);
|
|
2918
1992
|
this.hmrHandler?.trackMiddleware(fullPath, val);
|
|
2919
1993
|
}
|
|
2920
|
-
}
|
|
1994
|
+
});
|
|
2921
1995
|
}
|
|
2922
1996
|
registerMiddleware(middlewareCtor, relativePath) {
|
|
2923
1997
|
const metadata = Reflect.getMetadata(MiddlewareMetadataKey, middlewareCtor);
|
|
@@ -2947,6 +2021,7 @@ var InteractionDispatcher = class {
|
|
|
2947
2021
|
return obj.prototype instanceof InteractionMiddleware && Reflect.hasMetadata(MiddlewareMetadataKey, obj);
|
|
2948
2022
|
}
|
|
2949
2023
|
registerHandler(handlerClass, relativePath) {
|
|
2024
|
+
const from = formatFilePath(relativePath);
|
|
2950
2025
|
const writes = [];
|
|
2951
2026
|
for (const [routeType, map] of this.routeTypes) {
|
|
2952
2027
|
const meta = Reflect.getMetadata(InteractionRouteKeys[routeType], handlerClass);
|
|
@@ -2954,16 +2029,16 @@ var InteractionDispatcher = class {
|
|
|
2954
2029
|
for (const route of meta) {
|
|
2955
2030
|
const existing = map.get(route);
|
|
2956
2031
|
if (existing && existing !== handlerClass) throw new SeedcordError(SeedcordErrorCode.InteractionDuplicateRoute, [
|
|
2957
|
-
route
|
|
2958
|
-
existing.name
|
|
2959
|
-
handlerClass.name
|
|
2032
|
+
`${routeType}:${route}`,
|
|
2033
|
+
`${existing.name} (${this.handlerFiles.get(existing) ?? "unknown file"})`,
|
|
2034
|
+
`${handlerClass.name} (${from})`
|
|
2960
2035
|
]);
|
|
2961
2036
|
writes.push([map, route]);
|
|
2962
2037
|
}
|
|
2963
2038
|
}
|
|
2964
2039
|
if (writes.length === 0) return;
|
|
2965
2040
|
for (const [map, route] of writes) map.set(route, handlerClass);
|
|
2966
|
-
|
|
2041
|
+
this.handlerFiles.set(handlerClass, from);
|
|
2967
2042
|
if (this.loading) this.loadedHandlers.push({
|
|
2968
2043
|
name: handlerClass.name,
|
|
2969
2044
|
from
|
|
@@ -2975,6 +2050,7 @@ var InteractionDispatcher = class {
|
|
|
2975
2050
|
await this.hmrHandler?.handle(event);
|
|
2976
2051
|
}
|
|
2977
2052
|
unregisterHandler(handlerClass, artifacts) {
|
|
2053
|
+
this.handlerFiles.delete(handlerClass);
|
|
2978
2054
|
if (artifacts) {
|
|
2979
2055
|
for (const { routeType, routes } of artifacts) {
|
|
2980
2056
|
const map = this.routeTypes.find(([type]) => type === routeType)?.[1];
|
|
@@ -2995,10 +2071,11 @@ var InteractionDispatcher = class {
|
|
|
2995
2071
|
attachToClient() {
|
|
2996
2072
|
this.core.bot.client.on(Events.InteractionCreate, (interaction) => {
|
|
2997
2073
|
if (this.draining) return;
|
|
2998
|
-
this.core.
|
|
2999
|
-
const run = this.handleInteraction(interaction).catch((
|
|
3000
|
-
|
|
3001
|
-
this.
|
|
2074
|
+
this.core.bus[PublishDefault]("anyInteraction", { interaction });
|
|
2075
|
+
const run = this.handleInteraction(interaction).catch((caught) => {
|
|
2076
|
+
const error = asError(caught);
|
|
2077
|
+
this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${error.name}`, error.stack);
|
|
2078
|
+
this.core.bus[PublishDefault]("unhandledInteractionError", { error });
|
|
3002
2079
|
});
|
|
3003
2080
|
this.inFlight.add(run);
|
|
3004
2081
|
run.finally(() => this.inFlight.delete(run));
|
|
@@ -3010,37 +2087,75 @@ var InteractionDispatcher = class {
|
|
|
3010
2087
|
drain(timeoutMs) {
|
|
3011
2088
|
return settleWithin(Promise.allSettled(this.inFlight), timeoutMs);
|
|
3012
2089
|
}
|
|
3013
|
-
async handleCustomIdInteraction(interaction,
|
|
2090
|
+
async handleCustomIdInteraction(interaction, kind, getMap) {
|
|
3014
2091
|
if ([...this.keysToIgnore].some((matcher) => matcher.owns(interaction.customId))) return;
|
|
3015
2092
|
const prefix = prefixOf(interaction.customId);
|
|
3016
|
-
if (!prefix)
|
|
3017
|
-
await this.processInteraction(interaction, () => prefix, (key) => getMap().get(key));
|
|
2093
|
+
if (!prefix) this.logger.warn(`${paint.sky.bold(kind)} has invalid customId: ${paint.mute(interaction.customId)}`);
|
|
2094
|
+
await this.processInteraction(interaction, kind, () => prefix, (key) => getMap().get(key));
|
|
3018
2095
|
}
|
|
3019
|
-
async processInteraction(interaction, extractKey, getHandler, fallback = UnhandledRepliable) {
|
|
2096
|
+
async processInteraction(interaction, kind, extractKey, getHandler, fallback = UnhandledRepliable) {
|
|
3020
2097
|
const key = extractKey(interaction);
|
|
2098
|
+
const startedAt = performance.now();
|
|
2099
|
+
const queuedMs = queuedMsFor(interaction.id);
|
|
2100
|
+
const matched = getHandler(key);
|
|
2101
|
+
let routeId = `${kind}:${key || "unrouted"}`;
|
|
2102
|
+
let reported = false;
|
|
2103
|
+
const report = (outcome) => {
|
|
2104
|
+
if (reported) return;
|
|
2105
|
+
reported = true;
|
|
2106
|
+
this.core.bus[PublishDefault]("interactionDispatched", dispatchedPayload({
|
|
2107
|
+
routeId,
|
|
2108
|
+
interactionId: interaction.id,
|
|
2109
|
+
kind,
|
|
2110
|
+
outcome,
|
|
2111
|
+
startedAt,
|
|
2112
|
+
fallback: !matched,
|
|
2113
|
+
queuedMs
|
|
2114
|
+
}));
|
|
2115
|
+
};
|
|
3021
2116
|
let sender;
|
|
3022
2117
|
try {
|
|
3023
|
-
if (!interaction.isAutocomplete())
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
}
|
|
3029
|
-
this.logger.debug(`Processing ${paint.mint.bold(key)} with ${paint.mute(HandlerCtor.name)}`);
|
|
3030
|
-
const dispatch = new DispatchContext(routeIdOf(HandlerCtor));
|
|
3031
|
-
const handler = new HandlerCtor(interaction, this.core, dispatch);
|
|
2118
|
+
if (!interaction.isAutocomplete()) await this.runMiddlewares(interaction);
|
|
2119
|
+
const HandlerCtor = matched ?? fallback;
|
|
2120
|
+
const dispatch = new DispatchContext(routeIdOf(HandlerCtor) ?? routeId);
|
|
2121
|
+
routeId = dispatch.routeId ?? routeId;
|
|
2122
|
+
const handler = this.buildHandler(HandlerCtor, interaction, dispatch, key, !matched);
|
|
3032
2123
|
if (handler instanceof RepliableHandler) sender = handler.getSender();
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
} finally {
|
|
3038
|
-
monitor?.report(dispatch.routeId);
|
|
3039
|
-
}
|
|
2124
|
+
const refusal = interaction.isAutocomplete() ? null : await this.gateRefusal(HandlerCtor, interaction, dispatch.routeId);
|
|
2125
|
+
if (refusal) {
|
|
2126
|
+
await this.answer(refusal.caught, interaction, routeId, sender, report);
|
|
2127
|
+
return;
|
|
3040
2128
|
}
|
|
3041
2129
|
await handler.execute();
|
|
2130
|
+
report("handled");
|
|
2131
|
+
} catch (caught) {
|
|
2132
|
+
await this.answer(caught, interaction, routeId, sender, report);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
async answer(caught, interaction, routeId, sender, report) {
|
|
2136
|
+
try {
|
|
2137
|
+
await handleInteractionFault(caught, interaction, this.core, routeId, sender);
|
|
2138
|
+
} finally {
|
|
2139
|
+
report(outcomeFor(caught));
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
buildHandler(HandlerCtor, interaction, dispatch, key, isFallback) {
|
|
2143
|
+
if (isFallback) this.logger.warn(`No handler found for key ${paint.sky.bold(key)}. Falling back to ${HandlerCtor.name}.`);
|
|
2144
|
+
this.logger.debug(`Processing ${paint.mint.bold(key)} with ${paint.mute(HandlerCtor.name)}`);
|
|
2145
|
+
return new HandlerCtor(interaction, this.core, dispatch);
|
|
2146
|
+
}
|
|
2147
|
+
async runMiddlewares(interaction) {
|
|
2148
|
+
for (const { ctor } of this.middlewares) await new ctor(interaction, this.core).execute();
|
|
2149
|
+
}
|
|
2150
|
+
async gateRefusal(HandlerCtor, interaction, routeId) {
|
|
2151
|
+
const monitor = slowGateMonitor();
|
|
2152
|
+
try {
|
|
2153
|
+
await runHandlerGates(HandlerCtor, interactionGateContext(interaction, this.core), routeId ?? void 0, monitor?.observe);
|
|
2154
|
+
return null;
|
|
3042
2155
|
} catch (caught) {
|
|
3043
|
-
|
|
2156
|
+
return { caught };
|
|
2157
|
+
} finally {
|
|
2158
|
+
monitor?.report(routeId);
|
|
3044
2159
|
}
|
|
3045
2160
|
}
|
|
3046
2161
|
async handleInteraction(interaction) {
|
|
@@ -3085,47 +2200,41 @@ var InteractionDispatcher = class {
|
|
|
3085
2200
|
}
|
|
3086
2201
|
async handleSlashCommand(interaction) {
|
|
3087
2202
|
const route = slashRouteOf(interaction);
|
|
3088
|
-
await this.processInteraction(interaction, () => route, (key) => this.slashMap.get(key));
|
|
2203
|
+
await this.processInteraction(interaction, InteractionRoutes.Slash, () => route, (key) => this.slashMap.get(key));
|
|
3089
2204
|
}
|
|
3090
2205
|
async handleButton(interaction) {
|
|
3091
|
-
await this.handleCustomIdInteraction(interaction, () => this.buttonMap
|
|
2206
|
+
await this.handleCustomIdInteraction(interaction, InteractionRoutes.Button, () => this.buttonMap);
|
|
3092
2207
|
}
|
|
3093
2208
|
async handleModal(interaction) {
|
|
3094
|
-
await this.handleCustomIdInteraction(interaction, () => this.modalMap
|
|
2209
|
+
await this.handleCustomIdInteraction(interaction, InteractionRoutes.Modal, () => this.modalMap);
|
|
3095
2210
|
}
|
|
3096
2211
|
async handleStringSelectMenu(interaction) {
|
|
3097
|
-
await this.handleCustomIdInteraction(interaction, () => this.stringSelectMap
|
|
2212
|
+
await this.handleCustomIdInteraction(interaction, InteractionRoutes.StringMenu, () => this.stringSelectMap);
|
|
3098
2213
|
}
|
|
3099
2214
|
async handleUserSelectMenu(interaction) {
|
|
3100
|
-
await this.handleCustomIdInteraction(interaction, () => this.userSelectMap
|
|
2215
|
+
await this.handleCustomIdInteraction(interaction, InteractionRoutes.UserMenu, () => this.userSelectMap);
|
|
3101
2216
|
}
|
|
3102
2217
|
async handleRoleSelectMenu(interaction) {
|
|
3103
|
-
await this.handleCustomIdInteraction(interaction, () => this.roleSelectMap
|
|
2218
|
+
await this.handleCustomIdInteraction(interaction, InteractionRoutes.RoleMenu, () => this.roleSelectMap);
|
|
3104
2219
|
}
|
|
3105
2220
|
async handleChannelSelectMenu(interaction) {
|
|
3106
|
-
await this.handleCustomIdInteraction(interaction, () => this.channelSelectMap
|
|
2221
|
+
await this.handleCustomIdInteraction(interaction, InteractionRoutes.ChannelMenu, () => this.channelSelectMap);
|
|
3107
2222
|
}
|
|
3108
2223
|
async handleMentionableSelectMenu(interaction) {
|
|
3109
|
-
await this.handleCustomIdInteraction(interaction, () => this.mentionableSelectMap
|
|
2224
|
+
await this.handleCustomIdInteraction(interaction, InteractionRoutes.MentionableMenu, () => this.mentionableSelectMap);
|
|
3110
2225
|
}
|
|
3111
2226
|
async handleMessageContextMenu(interaction) {
|
|
3112
|
-
await this.processInteraction(interaction, () => interaction.commandName, (key) => this.messageContextMenuMap.get(key));
|
|
2227
|
+
await this.processInteraction(interaction, InteractionRoutes.MessageContextMenu, () => interaction.commandName, (key) => this.messageContextMenuMap.get(key));
|
|
3113
2228
|
}
|
|
3114
2229
|
async handleUserContextMenu(interaction) {
|
|
3115
|
-
await this.processInteraction(interaction, () => interaction.commandName, (key) => this.userContextMenuMap.get(key));
|
|
2230
|
+
await this.processInteraction(interaction, InteractionRoutes.UserContextMenu, () => interaction.commandName, (key) => this.userContextMenuMap.get(key));
|
|
3116
2231
|
}
|
|
3117
2232
|
async handleAutocomplete(interaction) {
|
|
3118
2233
|
const route = slashRouteOf(interaction);
|
|
3119
|
-
await this.processInteraction(interaction, () => route, (key) => this.autocompleteMap.get(key), UnhandledAutocomplete);
|
|
2234
|
+
await this.processInteraction(interaction, InteractionRoutes.Autocomplete, () => route, (key) => this.autocompleteMap.get(key), UnhandledAutocomplete);
|
|
3120
2235
|
}
|
|
3121
2236
|
};
|
|
3122
2237
|
|
|
3123
|
-
//#endregion
|
|
3124
|
-
//#region \0@oxc-project+runtime@0.135.0/helpers/esm/decorateMetadata.js
|
|
3125
|
-
function __decorateMetadata(k, v) {
|
|
3126
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
3127
|
-
}
|
|
3128
|
-
|
|
3129
2238
|
//#endregion
|
|
3130
2239
|
//#region \0@oxc-project+runtime@0.135.0/helpers/esm/decorate.js
|
|
3131
2240
|
function __decorate(decorators, target, key, desc) {
|
|
@@ -3144,38 +2253,47 @@ const LOGOUT_TIMEOUT_MS = 2e3;
|
|
|
3144
2253
|
/**
|
|
3145
2254
|
* The Discord client and its controllers. Access it through `core.bot`.
|
|
3146
2255
|
*/
|
|
3147
|
-
var Bot = class
|
|
2256
|
+
var Bot = class {
|
|
3148
2257
|
/** @internal */
|
|
3149
|
-
logger = new Logger("Bot");
|
|
2258
|
+
logger = new Logger("Bot", { channel: "bot" });
|
|
3150
2259
|
isInitialized = false;
|
|
3151
2260
|
_client;
|
|
3152
2261
|
interactions;
|
|
3153
2262
|
events;
|
|
3154
2263
|
emojiInjector;
|
|
3155
|
-
|
|
3156
|
-
emojis = Emojis;
|
|
3157
|
-
mentions = CommandMentions;
|
|
2264
|
+
commandRegistry;
|
|
3158
2265
|
/** @internal */
|
|
3159
2266
|
async onHmr(event) {
|
|
3160
2267
|
if (this.interactions) await this.interactions.onHmr(event);
|
|
3161
2268
|
if (this.events) await this.events.onHmr(event);
|
|
3162
|
-
if (this.
|
|
2269
|
+
if (this.commandRegistry) await this.commandRegistry.onHmr(event);
|
|
3163
2270
|
}
|
|
3164
2271
|
/** @internal */
|
|
3165
2272
|
constructor(core) {
|
|
3166
|
-
super();
|
|
3167
2273
|
this._client = new Client(core.config.bot.clientOptions);
|
|
3168
2274
|
if (core.config.bot.interactions.path) this.interactions = new InteractionDispatcher(core);
|
|
3169
2275
|
if (core.config.bot.events.path) this.events = new EventDispatcher(core);
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
2276
|
+
const commandsDir = core.config.bot.commands.path;
|
|
2277
|
+
if (commandsDir) {
|
|
2278
|
+
const injector = new CommandInjector();
|
|
2279
|
+
const registry = new CommandRegistry({
|
|
2280
|
+
dir: commandsDir,
|
|
2281
|
+
rest: this._client.rest,
|
|
2282
|
+
applicationId: () => this.applicationId(),
|
|
2283
|
+
onDeployed: (result) => {
|
|
2284
|
+
injector.inject(result, registry.allCommands());
|
|
2285
|
+
}
|
|
3174
2286
|
});
|
|
2287
|
+
this.commandRegistry = registry;
|
|
3175
2288
|
}
|
|
3176
2289
|
this.emojiInjector = new EmojiInjector(core);
|
|
3177
2290
|
this.registerShutdownTasks(core);
|
|
3178
2291
|
}
|
|
2292
|
+
applicationId() {
|
|
2293
|
+
const { application } = this._client;
|
|
2294
|
+
if (!application) throw new SeedcordError(SeedcordErrorCode.CoreApplicationUnavailable);
|
|
2295
|
+
return application.id;
|
|
2296
|
+
}
|
|
3179
2297
|
registerShutdownTasks(core) {
|
|
3180
2298
|
core.shutdown.addTask(ShutdownPhase.Unbind, "stop-dispatch", () => {
|
|
3181
2299
|
this.stopAccepting();
|
|
@@ -3200,11 +2318,11 @@ var Bot = class extends TypedEventEmitter {
|
|
|
3200
2318
|
if (this.events) await this.events.init();
|
|
3201
2319
|
await this.login(token);
|
|
3202
2320
|
await this.emojiInjector.init();
|
|
3203
|
-
if (this.
|
|
3204
|
-
await this.
|
|
3205
|
-
await this.
|
|
3206
|
-
this.interactions?.warnUnhandledRoutes(this.
|
|
3207
|
-
this.interactions?.warnUnhandledContextMenuRoutes(this.
|
|
2321
|
+
if (this.commandRegistry) {
|
|
2322
|
+
await this.commandRegistry.init();
|
|
2323
|
+
await this.commandRegistry.setCommands();
|
|
2324
|
+
this.interactions?.warnUnhandledRoutes(this.commandRegistry.routeLeaves());
|
|
2325
|
+
this.interactions?.warnUnhandledContextMenuRoutes(this.commandRegistry.contextMenuLeaves());
|
|
3208
2326
|
}
|
|
3209
2327
|
}
|
|
3210
2328
|
/** @internal */
|
|
@@ -3213,9 +2331,10 @@ var Bot = class extends TypedEventEmitter {
|
|
|
3213
2331
|
await this.logout();
|
|
3214
2332
|
}
|
|
3215
2333
|
async login(token) {
|
|
3216
|
-
|
|
2334
|
+
const ready = Promise.withResolvers();
|
|
2335
|
+
this._client.once(Events.ClientReady, () => ready.resolve());
|
|
3217
2336
|
this._client.login(token);
|
|
3218
|
-
await
|
|
2337
|
+
await ready.promise;
|
|
3219
2338
|
this.logger.info(`Logged in as ${chalk.bold.magenta(this._client.user?.username)}!`);
|
|
3220
2339
|
return this;
|
|
3221
2340
|
}
|
|
@@ -3226,244 +2345,13 @@ var Bot = class extends TypedEventEmitter {
|
|
|
3226
2345
|
get client() {
|
|
3227
2346
|
return this._client;
|
|
3228
2347
|
}
|
|
3229
|
-
/**
|
|
3230
|
-
* The overloads enforce the key/args correlation at compile time only. At runtime this forwards
|
|
3231
|
-
* straight to the underlying TypedEventEmitter.
|
|
3232
|
-
*
|
|
3233
|
-
* @internal
|
|
3234
|
-
*/
|
|
3235
|
-
emit(event, ...args) {
|
|
3236
|
-
return super.emit(event, ...args);
|
|
3237
|
-
}
|
|
3238
|
-
emitSafe(event, ...args) {
|
|
3239
|
-
return super.emitSafe(event, ...args);
|
|
3240
|
-
}
|
|
3241
|
-
onListenerError(error, event) {
|
|
3242
|
-
this.logger.error(`listener for ${String(event)} threw`, error);
|
|
3243
|
-
}
|
|
3244
|
-
};
|
|
3245
|
-
__decorate([Envapt("DISCORD_BOT_TOKEN", { converter: (raw) => validateDiscordToken(raw) }), __decorateMetadata("design:type", String)], Bot.prototype, "botToken", void 0);
|
|
3246
|
-
|
|
3247
|
-
//#endregion
|
|
3248
|
-
//#region src/subscribers/default/HandledException.ts
|
|
3249
|
-
let HandledException = class HandledException extends WebhookLog {
|
|
3250
|
-
report() {
|
|
3251
|
-
return {
|
|
3252
|
-
username: "Handled Exception",
|
|
3253
|
-
components: [new HandledExceptionContainer(this.data).component],
|
|
3254
|
-
files: [jsonAttachment("source.json", "The raw source the fault came from", this.data.source.raw)]
|
|
3255
|
-
};
|
|
3256
|
-
}
|
|
3257
|
-
};
|
|
3258
|
-
HandledException = __decorate([Subscribe("handledException"), WebhookUrl("HANDLED_EXCEPTION_WEBHOOK_URL")], HandledException);
|
|
3259
|
-
var HandledExceptionContainer = class extends BuilderComponent {
|
|
3260
|
-
constructor(data) {
|
|
3261
|
-
super("container");
|
|
3262
|
-
const { denial, uuid, source } = data;
|
|
3263
|
-
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"));
|
|
3264
|
-
}
|
|
3265
|
-
};
|
|
3266
|
-
function faultSummary(denial, source) {
|
|
3267
|
-
const head = stripAnsi(`### A handled fault was reported: \`${denial.name}\`\n**Message:** ${denial.message}\n`);
|
|
3268
|
-
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"}\``;
|
|
3269
|
-
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}\``;
|
|
3270
|
-
}
|
|
3271
|
-
function causeStack(denial) {
|
|
3272
|
-
const { cause } = denial;
|
|
3273
|
-
if (Error.isError(cause)) return stripAnsi(cause.stack ?? cause.message);
|
|
3274
|
-
if (typeof cause === "string") return stripAnsi(cause);
|
|
3275
|
-
if (typeof cause === "bigint" || typeof cause === "symbol" || typeof cause === "function") return cause.toString();
|
|
3276
|
-
if (cause !== void 0) try {
|
|
3277
|
-
return JSON.stringify(cause);
|
|
3278
|
-
} catch {
|
|
3279
|
-
return "[unserializable cause]";
|
|
3280
|
-
}
|
|
3281
|
-
return stripAnsi(denial.stack ?? "No cause recorded.");
|
|
3282
|
-
}
|
|
3283
|
-
|
|
3284
|
-
//#endregion
|
|
3285
|
-
//#region src/subscribers/default/UnknownException.ts
|
|
3286
|
-
let UnknownException = class UnknownException extends WebhookLog {
|
|
3287
|
-
report() {
|
|
3288
|
-
const { metadata } = this.data;
|
|
3289
|
-
return {
|
|
3290
|
-
username: "Unknown Exception",
|
|
3291
|
-
components: [new UnhandledErrorContainer(this.data).component],
|
|
3292
|
-
files: metadata ? [jsonAttachment("metadata.json", "Metadata associated with the error", metadata)] : void 0
|
|
3293
|
-
};
|
|
3294
|
-
}
|
|
3295
|
-
};
|
|
3296
|
-
UnknownException = __decorate([Subscribe("unknownException"), WebhookUrl("UNKNOWN_EXCEPTION_WEBHOOK_URL")], UnknownException);
|
|
3297
|
-
var UnhandledErrorContainer = class extends BuilderComponent {
|
|
3298
|
-
constructor(data) {
|
|
3299
|
-
super("container");
|
|
3300
|
-
const { uuid, error, guild, user, metadata } = data;
|
|
3301
|
-
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)}\`\`\``));
|
|
3302
|
-
this.addTimestampsIfAvailable(error);
|
|
3303
|
-
this.addMetadataIfAvailable(metadata);
|
|
3304
|
-
}
|
|
3305
|
-
addTimestampsIfAvailable(error) {
|
|
3306
|
-
if (!(error instanceof DiscordAPIError$1)) return;
|
|
3307
|
-
const now = Date.now();
|
|
3308
|
-
const snowflake = error.url.match(/\/interactions\/(\d+)\//)?.[1];
|
|
3309
|
-
if (!snowflake) return void 0;
|
|
3310
|
-
const interactionTs = timestampFromSnowflake(snowflake);
|
|
3311
|
-
const diff = now - interactionTs;
|
|
3312
|
-
const seconds = Math.floor(diff / 1e3);
|
|
3313
|
-
const millis = diff % 1e3;
|
|
3314
|
-
this.instance.addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### Timestamps\n- **\`Interaction sent\` :** ${new Date(interactionTs).toISOString()} (${interactionTs})\n- **\`Error logged \` :** ${new Date(now).toISOString()} (${now})\n- **\`Offset \` :** ${seconds}s ${millis}ms`));
|
|
3315
|
-
}
|
|
3316
|
-
addMetadataIfAvailable(metadata) {
|
|
3317
|
-
if (!metadata) return;
|
|
3318
|
-
this.instance.addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent("### Metadata")).addFileComponents((file) => file.setURL("attachment://metadata.json"));
|
|
3319
|
-
}
|
|
3320
|
-
};
|
|
3321
|
-
|
|
3322
|
-
//#endregion
|
|
3323
|
-
//#region src/subscribers/Bus.ts
|
|
3324
|
-
/**
|
|
3325
|
-
* Registers and dispatches subscribers. Subscribers load from the configured directories and run
|
|
3326
|
-
* on a programmatic or framework-event publish. Accessed via `core.bus`. Do not construct it directly.
|
|
3327
|
-
*/
|
|
3328
|
-
var Bus = class extends TypedEventEmitter {
|
|
3329
|
-
core;
|
|
3330
|
-
logger = new Logger("Subscribers");
|
|
3331
|
-
isInitialized = false;
|
|
3332
|
-
subscribersMap = /* @__PURE__ */ new Map();
|
|
3333
|
-
executedOnceHandlers = /* @__PURE__ */ new Set();
|
|
3334
|
-
webhookProbes = /* @__PURE__ */ new Map();
|
|
3335
|
-
hmrHandler;
|
|
3336
|
-
constructor(core) {
|
|
3337
|
-
super();
|
|
3338
|
-
this.core = core;
|
|
3339
|
-
if (this.core.config.subscribers.path) {
|
|
3340
|
-
if (!Envapter.isDevelopment && !Envapter.isTest) return;
|
|
3341
|
-
this.hmrHandler = new HmrModuleHandler({
|
|
3342
|
-
handlersDir: this.core.config.subscribers.path,
|
|
3343
|
-
isHandler: this.isSubscriber.bind(this),
|
|
3344
|
-
registerHandler: this.registerSubscriber.bind(this),
|
|
3345
|
-
unregisterHandler: this.unregisterSubscriber.bind(this),
|
|
3346
|
-
getArtifacts: this.getArtifacts.bind(this),
|
|
3347
|
-
logger: this.logger
|
|
3348
|
-
});
|
|
3349
|
-
}
|
|
3350
|
-
}
|
|
3351
|
-
getArtifacts(ctor) {
|
|
3352
|
-
return [Reflect.getMetadata(SubscribeMetadataKey, ctor).subscriber];
|
|
3353
|
-
}
|
|
3354
|
-
/** @internal */
|
|
3355
|
-
async init() {
|
|
3356
|
-
if (this.isInitialized) return;
|
|
3357
|
-
this.isInitialized = true;
|
|
3358
|
-
this.registerSubscriber(UnknownException);
|
|
3359
|
-
this.registerSubscriber(HandledException);
|
|
3360
|
-
const subscribersDir = this.core.config.subscribers.path;
|
|
3361
|
-
if (subscribersDir) {
|
|
3362
|
-
this.logger.info(chalk.bold(subscribersDir));
|
|
3363
|
-
await this.loadSubscribers(subscribersDir);
|
|
3364
|
-
const totalSubscribers = [...this.subscribersMap.values()].reduce((acc, handlers) => acc + handlers.length, 0);
|
|
3365
|
-
this.logger.utils.list([`${chalk.bold.magenta(totalSubscribers)} subscribers`], chalk.bold.green("Loaded"));
|
|
3366
|
-
}
|
|
3367
|
-
if (!Envapter.isTest) await this.verifyWebhooks();
|
|
3368
|
-
}
|
|
3369
|
-
/** @internal Exposed for tests. */
|
|
3370
|
-
async verifyWebhooks() {
|
|
3371
|
-
const missing = [];
|
|
3372
|
-
await Promise.all([...this.webhookProbes].map(async ([url, envKeys]) => {
|
|
3373
|
-
const result = await WebhookLog.senderFor(url).verify();
|
|
3374
|
-
if (result === "missing") missing.push(...envKeys);
|
|
3375
|
-
else if (result === "unreachable") this.logger.warn(`could not verify webhook at ${paint.sky.bold(envKeys.join(", "))}`);
|
|
3376
|
-
}));
|
|
3377
|
-
if (missing.length > 0) throw new SeedcordError(SeedcordErrorCode.ConfigWebhookNotFound, [missing.join(", ")]);
|
|
3378
|
-
}
|
|
3379
|
-
/** @internal */
|
|
3380
|
-
async onHmr(event) {
|
|
3381
|
-
if (this.hmrHandler) await this.hmrHandler.handle(event);
|
|
3382
|
-
}
|
|
3383
|
-
async loadSubscribers(dir) {
|
|
3384
|
-
await traverseDirectory(dir, (fullPath, relativePath, imported) => {
|
|
3385
|
-
for (const exportName of Object.keys(imported)) {
|
|
3386
|
-
const val = imported[exportName];
|
|
3387
|
-
if (this.isSubscriber(val)) {
|
|
3388
|
-
this.registerSubscriber(val);
|
|
3389
|
-
this.hmrHandler?.trackHandler(fullPath, val);
|
|
3390
|
-
this.logger.utils.registration(val.name, relativePath);
|
|
3391
|
-
}
|
|
3392
|
-
}
|
|
3393
|
-
}, this.logger);
|
|
3394
|
-
}
|
|
3395
|
-
registerSubscriber(handler) {
|
|
3396
|
-
if (handler.prototype instanceof WebhookLog) {
|
|
3397
|
-
const envKey = WebhookLog.envKeyOf(handler);
|
|
3398
|
-
const url = WebhookLog.urlOf(envKey);
|
|
3399
|
-
if (url === null) {
|
|
3400
|
-
this.logger.warn(`${chalk.bold(handler.name)} disabled, ${paint.sky.bold(envKey)} is not set`);
|
|
3401
|
-
return;
|
|
3402
|
-
}
|
|
3403
|
-
const envKeys = this.webhookProbes.get(url) ?? [];
|
|
3404
|
-
if (!envKeys.includes(envKey)) envKeys.push(envKey);
|
|
3405
|
-
this.webhookProbes.set(url, envKeys);
|
|
3406
|
-
}
|
|
3407
|
-
const options = Reflect.getMetadata(SubscribeMetadataKey, handler);
|
|
3408
|
-
let handlers = this.subscribersMap.get(options.subscriber);
|
|
3409
|
-
if (!handlers) {
|
|
3410
|
-
handlers = [];
|
|
3411
|
-
this.subscribersMap.set(options.subscriber, handlers);
|
|
3412
|
-
}
|
|
3413
|
-
handlers.push({
|
|
3414
|
-
ctor: handler,
|
|
3415
|
-
frequency: options.frequency ?? "on"
|
|
3416
|
-
});
|
|
3417
|
-
}
|
|
3418
|
-
unregisterSubscriber(handler, artifacts) {
|
|
3419
|
-
const subscribers = artifacts ?? [...this.subscribersMap.keys()];
|
|
3420
|
-
for (const subscriber of subscribers) {
|
|
3421
|
-
const handlers = this.subscribersMap.get(subscriber);
|
|
3422
|
-
if (!handlers) continue;
|
|
3423
|
-
const index = handlers.findIndex((entry) => entry.ctor === handler);
|
|
3424
|
-
if (index !== -1) handlers.splice(index, 1);
|
|
3425
|
-
}
|
|
3426
|
-
}
|
|
3427
|
-
isSubscriber(obj) {
|
|
3428
|
-
if (typeof obj !== "function") return false;
|
|
3429
|
-
return obj.prototype instanceof Subscriber && Reflect.hasMetadata(SubscribeMetadataKey, obj);
|
|
3430
|
-
}
|
|
3431
|
-
/**
|
|
3432
|
-
* Publishes an event to its subscribers and native listeners.
|
|
3433
|
-
*
|
|
3434
|
-
* Fire-and-forget, so subscriber handlers run asynchronously and this returns before they
|
|
3435
|
-
* complete, and callers must not assume side effects have landed. Errors thrown by a subscriber
|
|
3436
|
-
* are caught and logged, never surfaced here. A `'once'` subscriber is marked as executed when
|
|
3437
|
-
* it starts (even if it throws), so it never runs twice.
|
|
3438
|
-
*
|
|
3439
|
-
* @param event - The subscription key to publish
|
|
3440
|
-
* @param data - Payload passed to each subscriber
|
|
3441
|
-
* @returns Whether the underlying emitter had native listeners for the event
|
|
3442
|
-
*/
|
|
3443
|
-
publish(event, data) {
|
|
3444
|
-
this.processSubscriber(event, data);
|
|
3445
|
-
return super.emit(event, ...[data]);
|
|
3446
|
-
}
|
|
3447
|
-
async processSubscriber(subscriberName, data) {
|
|
3448
|
-
const handlers = this.subscribersMap.get(subscriberName);
|
|
3449
|
-
if (!handlers) return;
|
|
3450
|
-
for (const entry of handlers) {
|
|
3451
|
-
if (entry.frequency === "once" && this.executedOnceHandlers.has(entry.ctor)) continue;
|
|
3452
|
-
try {
|
|
3453
|
-
if (entry.frequency === "once") this.executedOnceHandlers.add(entry.ctor);
|
|
3454
|
-
const Ctor = entry.ctor;
|
|
3455
|
-
await new Ctor(data, this.core).execute();
|
|
3456
|
-
} catch (err) {
|
|
3457
|
-
this.logger.error(`Error in subscriber ${String(subscriberName)} handler ${entry.ctor.name}:`, err);
|
|
3458
|
-
}
|
|
3459
|
-
}
|
|
3460
|
-
}
|
|
3461
2348
|
};
|
|
2349
|
+
__decorate([Envapt("DISCORD_BOT_TOKEN", { converter: (raw) => validateDiscordToken(raw) })], Bot.prototype, "botToken", void 0);
|
|
3462
2350
|
|
|
3463
2351
|
//#endregion
|
|
3464
2352
|
//#region src/version.ts
|
|
3465
2353
|
/** Package version */
|
|
3466
|
-
const version = "0.1.0-next.
|
|
2354
|
+
const version = "0.1.0-next.6";
|
|
3467
2355
|
|
|
3468
2356
|
//#endregion
|
|
3469
2357
|
//#region src/Seedcord.ts
|
|
@@ -3481,14 +2369,13 @@ var Seedcord = class Seedcord extends Pluggable {
|
|
|
3481
2369
|
version = version;
|
|
3482
2370
|
healthCheck;
|
|
3483
2371
|
hmrManager;
|
|
3484
|
-
/** @see {@link CoordinatedShutdown} */
|
|
3485
|
-
shutdown;
|
|
3486
|
-
/** @see {@link CoordinatedStartup} */
|
|
3487
|
-
startup;
|
|
3488
2372
|
/** @see {@link Bus} */
|
|
3489
2373
|
bus;
|
|
2374
|
+
subscribers;
|
|
3490
2375
|
/** @see {@link Bot} */
|
|
3491
2376
|
bot;
|
|
2377
|
+
/** The discord.js client's REST client. Its token is set during the Login phase. */
|
|
2378
|
+
rest;
|
|
3492
2379
|
/** @see {@link IRateLimiter} */
|
|
3493
2380
|
rateLimiter;
|
|
3494
2381
|
/**
|
|
@@ -3496,18 +2383,16 @@ var Seedcord = class Seedcord extends Pluggable {
|
|
|
3496
2383
|
* @throws A **SeedcordError** When attempting to create multiple instances (singleton)
|
|
3497
2384
|
*/
|
|
3498
2385
|
constructor(config) {
|
|
3499
|
-
|
|
3500
|
-
const startup = new CoordinatedStartup();
|
|
3501
|
-
super(shutdown, startup);
|
|
2386
|
+
super(new CoordinatedShutdown(), new CoordinatedStartup());
|
|
3502
2387
|
this.config = config;
|
|
3503
2388
|
installNodeDefaults(config.logger);
|
|
3504
2389
|
setBotColor(config.botColor);
|
|
3505
|
-
this.shutdown = shutdown;
|
|
3506
|
-
this.startup = startup;
|
|
3507
2390
|
this.hmrManager = new HmrManager();
|
|
3508
2391
|
this.hmrManager.init();
|
|
3509
2392
|
this.bus = new Bus(this);
|
|
2393
|
+
this.subscribers = new SubscriberLoader(this.bus, config.subscribers.path);
|
|
3510
2394
|
this.bot = new Bot(this);
|
|
2395
|
+
this.rest = this.bot.client.rest;
|
|
3511
2396
|
this.rateLimiter = config.store ?? new MemoryRateLimiter();
|
|
3512
2397
|
this.healthCheck = HealthCheck.fromOption(this.shutdown, config.healthCheck);
|
|
3513
2398
|
this.registerStartupTasks();
|
|
@@ -3516,31 +2401,24 @@ var Seedcord = class Seedcord extends Pluggable {
|
|
|
3516
2401
|
get username() {
|
|
3517
2402
|
return this.bot.client.user?.username;
|
|
3518
2403
|
}
|
|
3519
|
-
pluginCapabilities() {
|
|
3520
|
-
return {
|
|
3521
|
-
client: this.bot.client,
|
|
3522
|
-
token: this.bot.botToken,
|
|
3523
|
-
rest: this.bot.client.rest
|
|
3524
|
-
};
|
|
3525
|
-
}
|
|
3526
2404
|
static reset() {
|
|
3527
2405
|
super.reset();
|
|
3528
2406
|
LoggerChannelRegistry.instance.reset();
|
|
3529
2407
|
}
|
|
3530
2408
|
registerStartupTasks() {
|
|
3531
2409
|
if (Envapter.isDevelopment || Envapter.isTest) this.registerHmrAwareModules();
|
|
3532
|
-
this.startup.addTask(StartupPhase.Configuration, "
|
|
2410
|
+
this.startup.addTask(StartupPhase.Configuration, "bus-initialization", async () => {
|
|
3533
2411
|
this.bus.logger.utils.initialization("Subscribers", "start");
|
|
3534
|
-
await this.
|
|
2412
|
+
await this.subscribers.init();
|
|
3535
2413
|
this.bus.logger.utils.initialization("Subscribers", "end");
|
|
3536
2414
|
});
|
|
3537
|
-
this.startup.addTask(StartupPhase.Login, "
|
|
2415
|
+
this.startup.addTask(StartupPhase.Login, "bot-initialization", async () => {
|
|
3538
2416
|
this.bot.logger.utils.initialization("Bot", "start");
|
|
3539
2417
|
await this.bot.init();
|
|
3540
2418
|
this.bot.logger.utils.initialization("Bot", "end");
|
|
3541
2419
|
});
|
|
3542
2420
|
const { healthCheck } = this;
|
|
3543
|
-
if (healthCheck) this.startup.addTask(StartupPhase.Ready, "
|
|
2421
|
+
if (healthCheck) this.startup.addTask(StartupPhase.Ready, "health-check", async () => {
|
|
3544
2422
|
healthCheck.logger.utils.initialization("HealthCheck", "start");
|
|
3545
2423
|
await healthCheck.init();
|
|
3546
2424
|
healthCheck.logger.utils.initialization("HealthCheck", "end");
|
|
@@ -3562,9 +2440,9 @@ var Seedcord = class Seedcord extends Pluggable {
|
|
|
3562
2440
|
return this;
|
|
3563
2441
|
}
|
|
3564
2442
|
registerHmrAwareModules() {
|
|
3565
|
-
this.startup.addTask(StartupPhase.Configuration, "
|
|
2443
|
+
this.startup.addTask(StartupPhase.Configuration, "hmr-registration", async () => {
|
|
3566
2444
|
this.hmrManager.register(this.bot);
|
|
3567
|
-
this.hmrManager.register(this.
|
|
2445
|
+
this.hmrManager.register(this.subscribers);
|
|
3568
2446
|
for (const plugin of this.plugins) this.hmrManager.register(plugin);
|
|
3569
2447
|
await Promise.resolve();
|
|
3570
2448
|
});
|
|
@@ -3572,5 +2450,5 @@ var Seedcord = class Seedcord extends Pluggable {
|
|
|
3572
2450
|
};
|
|
3573
2451
|
|
|
3574
2452
|
//#endregion
|
|
3575
|
-
export { ArraySource, AutocompleteHandler, ButtonHandler,
|
|
2453
|
+
export { ArraySource, AutocompleteHandler, ButtonHandler, ContextMenuHandler, CursorSource, Emojis, EventHandler, EventMiddleware, Gated, IgnoreBots, InteractionHandler, InteractionMiddleware, Middleware, MiddlewareType, ModalHandler, Nsfw, Paginator, Plugin, RegisterEvent, RepliableHandler, Seedcord, SelectMenuHandler, SlashHandler, Subscriber, WebhookLog, checkBotPermissions, checkPermissions, getConfirmation, hasPermsToAssign, version };
|
|
3576
2454
|
//# sourceMappingURL=index.mjs.map
|