@seedcord/gateway 0.1.0-next.2 → 0.1.0-next.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +875 -1107
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +397 -768
- package/dist/index.mjs +871 -1081
- package/dist/index.mjs.map +1 -1
- package/package.json +15 -13
package/dist/index.mjs
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
1
|
import "reflect-metadata";
|
|
2
2
|
import { installNodeDefaults } from "@seedcord/logger/node";
|
|
3
|
-
import {
|
|
4
|
-
import { CommandMetadataKey, ComponentDefsKey, EventMetadataKey, GateNotice, GatedMetadataKey, HmrManager, InteractionMetadataKey, InteractionRouteKeys, InteractionRoutes, MiddlewareMetadataKey, NotInGuild, NoticeCard, PAGE_MAX, SubscribeMetadataKey, decodeFor, getDevChannel, pageCursor, pickNotice, prefixOf, routeIdOf, runHandlerGates, setBotColor } from "@seedcord/core/internal";
|
|
5
|
-
import { ApplicationCommandType, AttachmentBuilder, ButtonStyle, Client, Collection, ComponentType, DiscordAPIError, Events, Guild, GuildMember, Message, MessageFlags, PermissionFlagsBits, RESTJSONErrorCodes, Role, SeparatorSpacingSize, SnowflakeUtil, TextChannel, User, WebhookClient, chatInputApplicationCommandMention } from "discord.js";
|
|
3
|
+
import { BaseReplySender, CommandMetadataKey, EventMetadataKey, GateNotice, GatedMetadataKey, HmrManager, InteractionMetadataKey, InteractionRouteKeys, InteractionRoutes, MiddlewareMetadataKey, MissingPermissions, NoticeCard, PAGE_MAX, PermissionNames, SubscribeMetadataKey, WebhookUrlMetadataKey, areRoutes, decodeComponentRoute, deferFlags, getDevChannel, pageCursor, pickNotice, prefixOf, routeIdOf, runHandlerGates, sendFlags, setBotColor, slowGateMonitor } from "@seedcord/core/internal";
|
|
6
4
|
import { SeedcordErrorCode } from "@seedcord/errors";
|
|
7
5
|
import { SeedcordError, SeedcordRangeError, SeedcordTypeError, validateDiscordToken } from "@seedcord/errors/internal";
|
|
8
|
-
import {
|
|
6
|
+
import { BaseHandler, BuilderComponent, CustomId, DispatchContext, Fault, Notice, RepliableHandler as RepliableHandler$1, RowComponent, Silence, defineGate, paginate } from "@seedcord/core";
|
|
9
7
|
import { Logger, LoggerChannelRegistry, paint } from "@seedcord/logger";
|
|
10
8
|
import { ActionRowBuilder, ButtonBuilder, ContextMenuCommandBuilder, SlashCommandBuilder, TextDisplayBuilder } from "@discordjs/builders";
|
|
11
9
|
import { buildSlashRoute, routeLeavesOf } from "@seedcord/utils/internal";
|
|
12
|
-
import {
|
|
10
|
+
import { ApplicationCommandType, ButtonStyle, Client, Collection, ComponentType, DiscordAPIError, Events, Guild, GuildMember, Message, MessageFlags, PermissionFlagsBits, RESTJSONErrorCodes, Role, TextChannel, User, chatInputApplicationCommandMention } from "discord.js";
|
|
11
|
+
import * as crypto from "node:crypto";
|
|
12
|
+
import chalk from "chalk";
|
|
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";
|
|
13
18
|
import { MemoryRateLimiter } from "@seedcord/rate-limiter";
|
|
14
|
-
import { CoordinatedShutdown, CoordinatedStartup, HealthCheck, ShutdownPhase, StartupPhase } from "@seedcord/services";
|
|
15
19
|
import { SeedcordBrand } from "@seedcord/types/internal";
|
|
16
|
-
import {
|
|
17
|
-
import chalk from "chalk";
|
|
20
|
+
import { TypedEventEmitter } from "@seedcord/event-emitter";
|
|
18
21
|
import { Envapt } from "envapt/legacy";
|
|
19
22
|
import { resolve } from "node:path";
|
|
20
23
|
import { HmrModuleHandler } from "@seedcord/core/hmr";
|
|
21
24
|
import { traverseDirectory } from "@seedcord/utils/node";
|
|
22
|
-
import * as crypto from "node:crypto";
|
|
23
|
-
|
|
24
|
-
export * from "@seedcord/core"
|
|
25
25
|
|
|
26
26
|
export * from "@seedcord/errors"
|
|
27
27
|
|
|
28
|
-
export * from "@seedcord/
|
|
28
|
+
export * from "@seedcord/core"
|
|
29
29
|
|
|
30
30
|
export * from "@seedcord/logger"
|
|
31
31
|
|
|
32
|
-
export * from "@seedcord/
|
|
32
|
+
export * from "@seedcord/utils"
|
|
33
33
|
|
|
34
34
|
export * from "@seedcord/rate-limiter"
|
|
35
35
|
|
|
36
|
-
export * from "@seedcord/
|
|
36
|
+
export * from "@seedcord/event-emitter"
|
|
37
37
|
|
|
38
38
|
//#region src/logger-bootstrap.ts
|
|
39
39
|
installNodeDefaults();
|
|
@@ -131,190 +131,6 @@ function Gated(...gates) {
|
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
//#endregion
|
|
135
|
-
//#region src/miscellaneous/areRoutes.ts
|
|
136
|
-
/**
|
|
137
|
-
* Checks if the provided value is an array of strings.
|
|
138
|
-
*
|
|
139
|
-
* @internal
|
|
140
|
-
*/
|
|
141
|
-
function areRoutes(routes) {
|
|
142
|
-
return Array.isArray(routes) && routes.every((r) => typeof r === "string");
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
//#endregion
|
|
146
|
-
//#region src/bot/decorators/Interactions.ts
|
|
147
|
-
/**
|
|
148
|
-
* Types of select menus supported for routing
|
|
149
|
-
*/
|
|
150
|
-
let SelectMenuKind = /* @__PURE__ */ function(SelectMenuKind) {
|
|
151
|
-
SelectMenuKind["String"] = "string";
|
|
152
|
-
SelectMenuKind["User"] = "user";
|
|
153
|
-
SelectMenuKind["Role"] = "role";
|
|
154
|
-
SelectMenuKind["Channel"] = "channel";
|
|
155
|
-
SelectMenuKind["Mentionable"] = "mentionable";
|
|
156
|
-
return SelectMenuKind;
|
|
157
|
-
}({});
|
|
158
|
-
/**
|
|
159
|
-
* Routes one or more slash commands to a {@link SlashHandler}.
|
|
160
|
-
*
|
|
161
|
-
* Pass the same route string(s) as the handler's generic. A single command reads `this.options`, several
|
|
162
|
-
* commands branch with `this.match`. The decorator routes and the generic must match exactly, so listing
|
|
163
|
-
* fewer or more than the handler declares is a compile error. Subcommands use a slash path.
|
|
164
|
-
*
|
|
165
|
-
* @param routes - The route string(s) this handler serves, e.g. `'ban'`, `'ban', 'kick'`, or `'demo/setup'`.
|
|
166
|
-
* @decorator
|
|
167
|
-
* @example
|
|
168
|
-
* ```ts
|
|
169
|
-
* \@SlashRoute('ban')
|
|
170
|
-
* class BanHandler extends SlashHandler<'ban'> {
|
|
171
|
-
* async execute() {
|
|
172
|
-
* const target = this.options.getUser('target');
|
|
173
|
-
* }
|
|
174
|
-
* }
|
|
175
|
-
* ```
|
|
176
|
-
*
|
|
177
|
-
* @example
|
|
178
|
-
* ```ts
|
|
179
|
-
* \@SlashRoute('ban', 'kick')
|
|
180
|
-
* class ModerationHandler extends SlashHandler<'ban' | 'kick'> {
|
|
181
|
-
* async execute() {
|
|
182
|
-
* await this.match({ ban: (o) => o.getUser('target'), kick: (o) => o.getUser('member') });
|
|
183
|
-
* }
|
|
184
|
-
* }
|
|
185
|
-
* ```
|
|
186
|
-
*/
|
|
187
|
-
function SlashRoute(...routes) {
|
|
188
|
-
return function(constructor) {
|
|
189
|
-
storeMetadata(InteractionRoutes.Slash, routes, constructor);
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Routes button interactions to handler classes.
|
|
194
|
-
*
|
|
195
|
-
* Pass the {@link CustomId} definition(s) this handler decodes and list the same ones in the handler's
|
|
196
|
-
* generic. Passing different definitions to the decorator and the generic is a compile error. Routing
|
|
197
|
-
* matches the stable prefix, so a wire minted from an older shape still reaches the handler, where
|
|
198
|
-
* reading this.params throws StaleCustomId and the controller boundary turns it into a reply.
|
|
199
|
-
*
|
|
200
|
-
* @param defs - The customId definition(s) this handler decodes, one per route.
|
|
201
|
-
* @decorator
|
|
202
|
-
*/
|
|
203
|
-
function ButtonRoute(...defs) {
|
|
204
|
-
return function(constructor) {
|
|
205
|
-
storeComponentRoute(InteractionRoutes.Button, defs, constructor);
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Routes modal submissions to handler classes.
|
|
210
|
-
*
|
|
211
|
-
* Pass the {@link CustomId} definition(s) this handler decodes and list the same ones in the handler's
|
|
212
|
-
* generic. Passing different definitions to the decorator and the generic is a compile error. Routing
|
|
213
|
-
* matches the stable prefix, so an older-shape wire still reaches the handler and throws StaleCustomId
|
|
214
|
-
* on read, the same as a button route.
|
|
215
|
-
*
|
|
216
|
-
* @param defs - The customId definition(s) this handler decodes, one per route.
|
|
217
|
-
* @decorator
|
|
218
|
-
*/
|
|
219
|
-
function ModalRoute(...defs) {
|
|
220
|
-
return function(constructor) {
|
|
221
|
-
storeComponentRoute(InteractionRoutes.Modal, defs, constructor);
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* Routes one or more context-menu commands to a {@link ContextMenuHandler}.
|
|
226
|
-
*
|
|
227
|
-
* Pass the kind (`ApplicationCommandType.User` or `ApplicationCommandType.Message`) and the command name(s),
|
|
228
|
-
* each checked against that kind's registry, so a typo is a compile error. The same kind must sit on the
|
|
229
|
-
* handler's generic, cross-checked both directions. Context menus carry no options, so a multi-name handler
|
|
230
|
-
* reads `this.target` uniformly with no branch.
|
|
231
|
-
*
|
|
232
|
-
* @param kind - The context-menu kind, `ApplicationCommandType.User` or `ApplicationCommandType.Message`.
|
|
233
|
-
* @param names - The command name(s) this handler serves, keyed off the matching registry.
|
|
234
|
-
* @decorator
|
|
235
|
-
* @example
|
|
236
|
-
* ```ts
|
|
237
|
-
* \@ContextMenuRoute(ApplicationCommandType.User, 'View Profile')
|
|
238
|
-
* class ViewProfile extends ContextMenuHandler<ApplicationCommandType.User> {
|
|
239
|
-
* async execute() {
|
|
240
|
-
* const user = this.target;
|
|
241
|
-
* }
|
|
242
|
-
* }
|
|
243
|
-
* ```
|
|
244
|
-
*/
|
|
245
|
-
function ContextMenuRoute(kind, ...names) {
|
|
246
|
-
return function(constructor) {
|
|
247
|
-
storeMetadata(kind === ApplicationCommandType.User ? InteractionRoutes.UserContextMenu : InteractionRoutes.MessageContextMenu, names, constructor);
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Routes one or more commands' autocomplete to an {@link AutocompleteHandler}.
|
|
252
|
-
*
|
|
253
|
-
* Pass the same command route string(s) as the handler's generic. One command branches with `this.match`
|
|
254
|
-
* over its autocompletable fields, several commands share one handler whose arms span every command's
|
|
255
|
-
* fields. The decorator routes and the generic must match exactly, so listing fewer or more than the
|
|
256
|
-
* handler declares is a compile error.
|
|
257
|
-
*
|
|
258
|
-
* @param routes - The command route string(s) this handler serves, e.g. `'search'` or `'search', 'find'`.
|
|
259
|
-
* @decorator
|
|
260
|
-
* @example
|
|
261
|
-
* ```ts
|
|
262
|
-
* \@AutocompleteRoute('search')
|
|
263
|
-
* class SearchAutocomplete extends AutocompleteHandler<'search'> {
|
|
264
|
-
* async execute() {
|
|
265
|
-
* await this.match({ query: (value, respond) => respond([{ name: value, value }]) });
|
|
266
|
-
* }
|
|
267
|
-
* }
|
|
268
|
-
* ```
|
|
269
|
-
*/
|
|
270
|
-
function AutocompleteRoute(...routes) {
|
|
271
|
-
return function(constructor) {
|
|
272
|
-
storeMetadata(InteractionRoutes.Autocomplete, routes, constructor);
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* Routes select menu interactions to handler classes.
|
|
277
|
-
*
|
|
278
|
-
* Pass the select kind and the {@link CustomId} definition(s) this handler decodes. The handler's
|
|
279
|
-
* generic must list the same definitions, and its second generic argument must be the matching select
|
|
280
|
-
* interaction type, or it is a compile error.
|
|
281
|
-
*
|
|
282
|
-
* @param type - Select menu kind from {@link SelectMenuKind}.
|
|
283
|
-
* @param defs - The customId definition(s) this handler decodes, one per route.
|
|
284
|
-
* @decorator
|
|
285
|
-
*
|
|
286
|
-
* @example
|
|
287
|
-
* ```typescript
|
|
288
|
-
* \@SelectMenuRoute(SelectMenuKind.User, AssignId)
|
|
289
|
-
* class AssignSelect extends SelectMenuHandler<SelectMenuKind.User, [typeof AssignId]> {
|
|
290
|
-
* // handles user select menus minted from AssignId
|
|
291
|
-
* }
|
|
292
|
-
* ```
|
|
293
|
-
*/
|
|
294
|
-
function SelectMenuRoute(type, ...defs) {
|
|
295
|
-
return function(constructor) {
|
|
296
|
-
storeComponentRoute({
|
|
297
|
-
["string"]: InteractionRoutes.StringMenu,
|
|
298
|
-
["user"]: InteractionRoutes.UserMenu,
|
|
299
|
-
["role"]: InteractionRoutes.RoleMenu,
|
|
300
|
-
["channel"]: InteractionRoutes.ChannelMenu,
|
|
301
|
-
["mentionable"]: InteractionRoutes.MentionableMenu
|
|
302
|
-
}[type], defs, constructor);
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
function storeComponentRoute(route, defs, constructor) {
|
|
306
|
-
storeMetadata(route, defs.map((def) => def.prefix), constructor);
|
|
307
|
-
Reflect.defineMetadata(ComponentDefsKey, defs, constructor);
|
|
308
|
-
}
|
|
309
|
-
function storeMetadata(route, routes, constructor) {
|
|
310
|
-
const key = InteractionRouteKeys[route];
|
|
311
|
-
const savedRoutes = Reflect.getMetadata(key, constructor);
|
|
312
|
-
const existing = areRoutes(savedRoutes) ? savedRoutes : [];
|
|
313
|
-
const toStore = Array.isArray(routes) ? routes : [routes];
|
|
314
|
-
Reflect.defineMetadata(key, [...existing, ...toStore], constructor);
|
|
315
|
-
Reflect.defineMetadata(InteractionMetadataKey, true, constructor);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
134
|
//#endregion
|
|
319
135
|
//#region src/bot/decorators/Middlewares.ts
|
|
320
136
|
/**
|
|
@@ -383,21 +199,6 @@ const IgnoreBots = defineGate("IgnoreBots", (ctx) => {
|
|
|
383
199
|
if (ctx.user?.bot) throw new Silence("actor is a bot");
|
|
384
200
|
});
|
|
385
201
|
|
|
386
|
-
//#endregion
|
|
387
|
-
//#region src/bot/notices/utils.ts
|
|
388
|
-
function mentionFor(subject) {
|
|
389
|
-
if (subject instanceof Role) return `<@&${subject.id}>`;
|
|
390
|
-
if (subject instanceof TextChannel) return `<#${subject.id}>`;
|
|
391
|
-
if (subject instanceof GuildMember) return `<@${subject.id}>`;
|
|
392
|
-
return `\`${subject.name}\``;
|
|
393
|
-
}
|
|
394
|
-
function labelFor(subject) {
|
|
395
|
-
if (subject instanceof Role) return "role";
|
|
396
|
-
if (subject instanceof TextChannel) return "channel";
|
|
397
|
-
if (subject instanceof GuildMember) return "member";
|
|
398
|
-
return "guild";
|
|
399
|
-
}
|
|
400
|
-
|
|
401
202
|
//#endregion
|
|
402
203
|
//#region src/bot/notices/index.ts
|
|
403
204
|
var NotNsfw = class extends GateNotice {
|
|
@@ -405,13 +206,6 @@ var NotNsfw = class extends GateNotice {
|
|
|
405
206
|
super(message);
|
|
406
207
|
}
|
|
407
208
|
};
|
|
408
|
-
var MissingRole = class extends GateNotice {
|
|
409
|
-
role;
|
|
410
|
-
constructor(message, role) {
|
|
411
|
-
super(message ?? (role ? `You need the ${role.name} role to use this.` : "You do not have the required role."));
|
|
412
|
-
this.role = role;
|
|
413
|
-
}
|
|
414
|
-
};
|
|
415
209
|
var RoleHigherThanMe = class extends Notice {
|
|
416
210
|
role;
|
|
417
211
|
botRole;
|
|
@@ -432,30 +226,19 @@ var CannotAssignBotRole = class extends Notice {
|
|
|
432
226
|
return { components: [new NoticeCard("I cannot assign a managed role.").component] };
|
|
433
227
|
}
|
|
434
228
|
};
|
|
435
|
-
var MissingPermissions = class extends Notice {
|
|
436
|
-
where;
|
|
437
|
-
missingPerms;
|
|
438
|
-
constructor(message, where, missingPerms) {
|
|
439
|
-
super(message);
|
|
440
|
-
this.where = where;
|
|
441
|
-
this.missingPerms = missingPerms;
|
|
442
|
-
}
|
|
443
|
-
render() {
|
|
444
|
-
const bullets = this.missingPerms.map((perm) => `• ${perm}`).join("\n");
|
|
445
|
-
return { components: [new NoticeCard(`The ${labelFor(this.where)} ${mentionFor(this.where)} is missing the following permission entries:\n\n${bullets}`).component] };
|
|
446
|
-
}
|
|
447
|
-
};
|
|
448
229
|
var HasDangerousPermissions = class extends Notice {
|
|
449
|
-
|
|
230
|
+
subject;
|
|
450
231
|
dangerousPerms;
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
232
|
+
customLead;
|
|
233
|
+
constructor(message, subject, dangerousPerms) {
|
|
234
|
+
super(message ?? "A dangerous permission is enabled.");
|
|
235
|
+
this.subject = subject;
|
|
454
236
|
this.dangerousPerms = dangerousPerms;
|
|
237
|
+
this.customLead = message;
|
|
455
238
|
}
|
|
456
239
|
render() {
|
|
457
240
|
const bullets = this.dangerousPerms.map((perm) => `• ${perm}`).join("\n");
|
|
458
|
-
return { components: [new NoticeCard(
|
|
241
|
+
return { components: [new NoticeCard(`${this.customLead ?? `${this.subject} has the following permission entries that must not be enabled:`}\n\n${bullets}`).component] };
|
|
459
242
|
}
|
|
460
243
|
};
|
|
461
244
|
var UserNotFound = class extends Notice {
|
|
@@ -531,129 +314,6 @@ function Nsfw(options) {
|
|
|
531
314
|
});
|
|
532
315
|
}
|
|
533
316
|
|
|
534
|
-
//#endregion
|
|
535
|
-
//#region src/bot/utilities/permissions/checkPermissions.ts
|
|
536
|
-
/**
|
|
537
|
-
* Map of permission bits to their prettified human readable names.
|
|
538
|
-
*/
|
|
539
|
-
const PermissionNames = new Map(Object.entries(PermissionFlagsBits).map(([key, bit]) => [bit, prettify(key)]));
|
|
540
|
-
function checkPermissions(a, b, c, d, e) {
|
|
541
|
-
const { for: pFor, in: pIn, scope, inverse = false, missing: missingCtor, dangerous: dangerousCtor } = a instanceof Role || a instanceof GuildMember ? {
|
|
542
|
-
for: a,
|
|
543
|
-
in: b,
|
|
544
|
-
scope: c,
|
|
545
|
-
inverse: d ?? false,
|
|
546
|
-
...e
|
|
547
|
-
} : a;
|
|
548
|
-
const Missing = missingCtor ?? MissingPermissions;
|
|
549
|
-
const Dangerous = dangerousCtor ?? HasDangerousPermissions;
|
|
550
|
-
const perms = pIn instanceof Guild ? pFor.permissions : pIn.permissionsFor(pFor, true);
|
|
551
|
-
const names = (bits) => bits.map((bit) => PermissionNames.get(bit) ?? String(bit));
|
|
552
|
-
const present = scope.filter((bit) => perms.has(bit, true));
|
|
553
|
-
const presentNames = names(present);
|
|
554
|
-
if (inverse) {
|
|
555
|
-
if (present.length > 0) throw new Dangerous(`${pFor instanceof Role ? "Role" : "Member"} has dangerous permissions in this ${pIn instanceof Guild ? "guild" : "channel"}`, pFor, presentNames);
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
|
-
const missingBits = scope.filter((bit) => !perms.has(bit, true));
|
|
559
|
-
if (missingBits.length > 0) throw new Missing("Missing Any/All/No Permissions", pFor, names(missingBits));
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
//#endregion
|
|
563
|
-
//#region src/bot/gates/catalog/permissions.ts
|
|
564
|
-
const UNCACHED_MEMBER = "Your server member data could not be resolved. Try again.";
|
|
565
|
-
/**
|
|
566
|
-
* Requires the caller to hold every permission in `scope`, via `checkPermissions`. Refuses outside a guild.
|
|
567
|
-
*
|
|
568
|
-
* Interaction-only and excludes ModalSubmit, since a modal lacks a reliable cached caller member.
|
|
569
|
-
*
|
|
570
|
-
* @param scope - The permission flag bits the caller must all hold.
|
|
571
|
-
* @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-permissions one with `missing`.
|
|
572
|
-
*
|
|
573
|
-
* @see {@link Gated}
|
|
574
|
-
*
|
|
575
|
-
* @example
|
|
576
|
-
* ```ts
|
|
577
|
-
* import { PermissionFlagsBits } from 'discord.js';
|
|
578
|
-
*
|
|
579
|
-
* \@Gated(RequirePermissions([PermissionFlagsBits.BanMembers]))
|
|
580
|
-
* \@SlashRoute('ban')
|
|
581
|
-
* class BanHandler extends SlashHandler<'ban'> {
|
|
582
|
-
* async execute() {
|
|
583
|
-
* const target = this.options.getUser('target');
|
|
584
|
-
* }
|
|
585
|
-
* }
|
|
586
|
-
* ```
|
|
587
|
-
*/
|
|
588
|
-
function RequirePermissions(scope, options) {
|
|
589
|
-
return defineGate("RequirePermissions", (ctx) => {
|
|
590
|
-
if (!ctx.guild) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message));
|
|
591
|
-
if (!ctx.member) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message ?? UNCACHED_MEMBER));
|
|
592
|
-
checkPermissions(ctx.member, ctx.guild, scope, false, options?.missing ? { missing: options.missing } : void 0);
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
/**
|
|
596
|
-
* Requires the bot to hold every permission in `scope`, via `checkPermissions`. Refuses outside a guild.
|
|
597
|
-
*
|
|
598
|
-
* It reads the bot's own member, so it attaches to modal handlers as well.
|
|
599
|
-
*
|
|
600
|
-
* @param scope - The permission flag bits the bot must all hold.
|
|
601
|
-
* @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-permissions one with `missing`.
|
|
602
|
-
*
|
|
603
|
-
* @see {@link Gated}
|
|
604
|
-
*
|
|
605
|
-
* @example
|
|
606
|
-
* ```ts
|
|
607
|
-
* import { PermissionFlagsBits } from 'discord.js';
|
|
608
|
-
*
|
|
609
|
-
* \@Gated(RequireBotPermissions([PermissionFlagsBits.BanMembers]))
|
|
610
|
-
* \@ModalRoute(MyModalId)
|
|
611
|
-
* class MyModalHandler extends ModalHandler<[typeof MyModalId]> {
|
|
612
|
-
* async execute() {
|
|
613
|
-
* // ...
|
|
614
|
-
* }
|
|
615
|
-
* }
|
|
616
|
-
* ```
|
|
617
|
-
*/
|
|
618
|
-
function RequireBotPermissions(scope, options) {
|
|
619
|
-
return defineGate("RequireBotPermissions", (ctx) => {
|
|
620
|
-
const botMember = ctx.guild?.members.me;
|
|
621
|
-
if (!ctx.guild || !botMember) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message));
|
|
622
|
-
checkPermissions(botMember, ctx.guild, scope, false, options?.missing ? { missing: options.missing } : void 0);
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
/**
|
|
626
|
-
* Requires the caller to have `roleId`, read from the member's role cache. Refuses outside a guild first, then
|
|
627
|
-
* when the role is missing.
|
|
628
|
-
*
|
|
629
|
-
* Interaction-only and excludes ModalSubmit.
|
|
630
|
-
*
|
|
631
|
-
* @param roleId - The role snowflake the caller must hold, read from the member's role cache.
|
|
632
|
-
* @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-role one with `missingRole`.
|
|
633
|
-
*
|
|
634
|
-
* @see {@link Gated}
|
|
635
|
-
*
|
|
636
|
-
* @example
|
|
637
|
-
* ```ts
|
|
638
|
-
* \@Gated(RequireRole('123456789012345678'))
|
|
639
|
-
* \@SlashRoute('vip')
|
|
640
|
-
* class VipHandler extends SlashHandler<'vip'> {
|
|
641
|
-
* async execute() {
|
|
642
|
-
* // ...
|
|
643
|
-
* }
|
|
644
|
-
* }
|
|
645
|
-
* ```
|
|
646
|
-
*/
|
|
647
|
-
function RequireRole(roleId, options) {
|
|
648
|
-
return defineGate("RequireRole", (ctx) => {
|
|
649
|
-
if (!ctx.guild) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message));
|
|
650
|
-
if (!ctx.member) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message ?? UNCACHED_MEMBER));
|
|
651
|
-
if (ctx.member.roles.cache.has(roleId)) return;
|
|
652
|
-
const role = ctx.guild.roles.cache.get(roleId) ?? null;
|
|
653
|
-
throw pickNotice(options?.missingRole, (message) => new MissingRole(message, role));
|
|
654
|
-
});
|
|
655
|
-
}
|
|
656
|
-
|
|
657
317
|
//#endregion
|
|
658
318
|
//#region src/bot/injectors/EmojiInjector.ts
|
|
659
319
|
const emojiStorage = {};
|
|
@@ -842,6 +502,37 @@ async function fetchText(client, channelId) {
|
|
|
842
502
|
throw new CouldNotFindChannel("Channel not found or not a text channel", channelId);
|
|
843
503
|
}
|
|
844
504
|
|
|
505
|
+
//#endregion
|
|
506
|
+
//#region src/bot/notices/utils.ts
|
|
507
|
+
function mentionFor(subject) {
|
|
508
|
+
if (subject instanceof Role) return `<@&${subject.id}>`;
|
|
509
|
+
return `<@${subject.id}>`;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
//#endregion
|
|
513
|
+
//#region src/bot/utilities/permissions/checkPermissions.ts
|
|
514
|
+
function checkPermissions(a, b, c, d, e) {
|
|
515
|
+
const { for: pFor, in: pIn, scope, inverse = false, missingNotice: missingCtor, dangerousNotice: dangerousCtor } = a instanceof Role || a instanceof GuildMember ? {
|
|
516
|
+
for: a,
|
|
517
|
+
in: b,
|
|
518
|
+
scope: c,
|
|
519
|
+
inverse: d ?? false,
|
|
520
|
+
...e
|
|
521
|
+
} : a;
|
|
522
|
+
const Missing = missingCtor ?? MissingPermissions;
|
|
523
|
+
const Dangerous = dangerousCtor ?? HasDangerousPermissions;
|
|
524
|
+
const subject = mentionFor(pFor);
|
|
525
|
+
const perms = pIn instanceof Guild ? pFor.permissions : pIn.permissionsFor(pFor, true);
|
|
526
|
+
const names = (bits) => bits.map((bit) => PermissionNames.get(bit) ?? String(bit));
|
|
527
|
+
if (inverse) {
|
|
528
|
+
const present = scope.filter((bit) => perms.has(bit, true));
|
|
529
|
+
if (present.length > 0) throw new Dangerous(void 0, subject, names(present));
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const missingBits = scope.filter((bit) => !perms.has(bit, true));
|
|
533
|
+
if (missingBits.length > 0) throw new Missing(void 0, subject, names(missingBits));
|
|
534
|
+
}
|
|
535
|
+
|
|
845
536
|
//#endregion
|
|
846
537
|
//#region src/bot/utilities/permissions/checkBotPermissions.ts
|
|
847
538
|
/**
|
|
@@ -870,7 +561,7 @@ function checkBotPermissions(target, scope, inverse = false, errors) {
|
|
|
870
561
|
const me = target.members.me;
|
|
871
562
|
if (!me) {
|
|
872
563
|
const names = scope.map((bit) => PermissionNames.get(bit) ?? String(bit));
|
|
873
|
-
throw new ((errors?.
|
|
564
|
+
throw new ((errors?.missingNotice) ?? MissingPermissions)(void 0, "The bot", names);
|
|
874
565
|
}
|
|
875
566
|
checkPermissions(me, target, scope, inverse, errors);
|
|
876
567
|
return;
|
|
@@ -879,7 +570,7 @@ function checkBotPermissions(target, scope, inverse = false, errors) {
|
|
|
879
570
|
if (!me) {
|
|
880
571
|
new Logger("checkBotPermissions").warn(`Bot member is unavailable in guild ${target.guild.id} while checking permissions in channel ${target.id}`);
|
|
881
572
|
const names = scope.map((bit) => PermissionNames.get(bit) ?? String(bit));
|
|
882
|
-
throw new ((errors?.
|
|
573
|
+
throw new ((errors?.missingNotice) ?? MissingPermissions)(void 0, "The bot", names);
|
|
883
574
|
}
|
|
884
575
|
checkPermissions(me, target, scope, inverse, errors);
|
|
885
576
|
}
|
|
@@ -916,26 +607,26 @@ function getBotRole(guild) {
|
|
|
916
607
|
* // Check with custom error handling
|
|
917
608
|
* hasPermsToAssign({
|
|
918
609
|
* targetRole: role,
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
*
|
|
922
|
-
*
|
|
610
|
+
* noticeOverrides: {
|
|
611
|
+
* higherNotice: CustomHigherNotice,
|
|
612
|
+
* managedNotice: CustomManagedNotice,
|
|
613
|
+
* missingNotice: CustomMissingPermissionsNotice
|
|
923
614
|
* }
|
|
924
615
|
* });
|
|
925
616
|
* ```
|
|
926
617
|
*/
|
|
927
618
|
function hasPermsToAssign(roleOrOptions) {
|
|
928
|
-
const { targetRole, errors } = roleOrOptions instanceof Role ? {
|
|
619
|
+
const { targetRole, noticeOverrides: errors } = roleOrOptions instanceof Role ? {
|
|
929
620
|
targetRole: roleOrOptions,
|
|
930
|
-
|
|
621
|
+
noticeOverrides: void 0
|
|
931
622
|
} : roleOrOptions;
|
|
932
|
-
const
|
|
933
|
-
const
|
|
934
|
-
const
|
|
623
|
+
const HigherNotice = errors?.higherNotice ?? RoleHigherThanMe;
|
|
624
|
+
const ManagedNotice = errors?.managedNotice ?? CannotAssignBotRole;
|
|
625
|
+
const MissingNotice = errors?.missingNotice ?? MissingPermissions;
|
|
935
626
|
const botRole = getBotRole(targetRole.guild);
|
|
936
|
-
if (targetRole.comparePositionTo(botRole) >= 0) throw new
|
|
937
|
-
if (targetRole.managed) throw new
|
|
938
|
-
checkBotPermissions(targetRole.guild, [PermissionFlagsBits.ManageRoles], false, {
|
|
627
|
+
if (targetRole.comparePositionTo(botRole) >= 0) throw new HigherNotice("Role is higher than me", targetRole, botRole);
|
|
628
|
+
if (targetRole.managed) throw new ManagedNotice(`Cannot assign managed role ${targetRole.name}`);
|
|
629
|
+
checkBotPermissions(targetRole.guild, [PermissionFlagsBits.ManageRoles], false, { missingNotice: MissingNotice });
|
|
939
630
|
}
|
|
940
631
|
|
|
941
632
|
//#endregion
|
|
@@ -1062,115 +753,310 @@ async function updateMemberRoles(rolesToAdd, rolesToRemove, member) {
|
|
|
1062
753
|
await member.roles.set([...updated]);
|
|
1063
754
|
}
|
|
1064
755
|
|
|
1065
|
-
//#endregion
|
|
1066
|
-
//#region src/bot/harmlessApiCodes.ts
|
|
1067
|
-
/**
|
|
1068
|
-
* Discord API error codes that mean a dead end on an interaction, a dead token, a double ack, or a gone
|
|
1069
|
-
* message. There is nothing to show the user and no bug to report. The controller boundary drops them on
|
|
1070
|
-
* the original throw, and {@link ReplySender} drops them on its own send.
|
|
1071
|
-
*
|
|
1072
|
-
* @internal
|
|
1073
|
-
*/
|
|
1074
|
-
const HARMLESS_API_CODES = new Set([
|
|
1075
|
-
RESTJSONErrorCodes.UnknownInteraction,
|
|
1076
|
-
RESTJSONErrorCodes.InteractionHasAlreadyBeenAcknowledged,
|
|
1077
|
-
RESTJSONErrorCodes.UnknownMessage
|
|
1078
|
-
]);
|
|
1079
|
-
|
|
1080
|
-
//#endregion
|
|
1081
|
-
//#region src/miscellaneous/flagsFor.ts
|
|
1082
|
-
/**
|
|
1083
|
-
* Builds the `flags` bitfield for a framework reply. Every framework reply is ComponentsV2, so the
|
|
1084
|
-
* IsComponentsV2 flag is always set.
|
|
1085
|
-
*
|
|
1086
|
-
* @param ephemeral - Whether the reply is ephemeral.
|
|
1087
|
-
* @returns The OR'd `MessageFlags` value as a plain number.
|
|
1088
|
-
*
|
|
1089
|
-
* @internal
|
|
1090
|
-
*/
|
|
1091
|
-
function flagsFor(ephemeral) {
|
|
1092
|
-
let flags = MessageFlags.IsComponentsV2;
|
|
1093
|
-
if (ephemeral) flags |= MessageFlags.Ephemeral;
|
|
1094
|
-
return flags;
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
756
|
//#endregion
|
|
1098
757
|
//#region src/bot/ReplySender.ts
|
|
758
|
+
function seedState(interaction) {
|
|
759
|
+
if (interaction.deferred && interaction.ephemeral === null) return "deferred-update";
|
|
760
|
+
if (interaction.replied) return "replied";
|
|
761
|
+
if (interaction.deferred) return "deferred-reply";
|
|
762
|
+
return "unacked";
|
|
763
|
+
}
|
|
1099
764
|
/**
|
|
1100
|
-
*
|
|
1101
|
-
*
|
|
1102
|
-
* controller.
|
|
765
|
+
* Writes interaction responses through the discord.js interaction. Construction is internal to the repliable
|
|
766
|
+
* handler bases and the dispatcher.
|
|
1103
767
|
*/
|
|
1104
|
-
var ReplySender = class {
|
|
768
|
+
var ReplySender = class extends BaseReplySender {
|
|
1105
769
|
interaction;
|
|
1106
|
-
|
|
1107
|
-
|
|
770
|
+
constructor(interaction, routeId) {
|
|
771
|
+
super(routeId, seedState(interaction));
|
|
1108
772
|
this.interaction = interaction;
|
|
1109
773
|
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
* @returns The sent {@link Message}, or `undefined` when the send was swallowed.
|
|
1117
|
-
*/
|
|
1118
|
-
async send(response, ephemeral = true) {
|
|
1119
|
-
try {
|
|
1120
|
-
return await this.dispatch(response, ephemeral);
|
|
1121
|
-
} catch (error) {
|
|
1122
|
-
this.logSwallowed("send", error);
|
|
1123
|
-
return;
|
|
1124
|
-
}
|
|
774
|
+
async writeReply(response, opts) {
|
|
775
|
+
const result = await this.interaction.reply({
|
|
776
|
+
...this.replyOptions(response, sendFlags(opts)),
|
|
777
|
+
withResponse: true
|
|
778
|
+
});
|
|
779
|
+
return this.createdMessage(result);
|
|
1125
780
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
* its outcome. Never throws, a failed edit is logged and dropped.
|
|
1129
|
-
*
|
|
1130
|
-
* @param message - The message to edit (the handle a prior {@link send} returned).
|
|
1131
|
-
* @param response - The reply to replace it with.
|
|
1132
|
-
*/
|
|
1133
|
-
async edit(message, response) {
|
|
1134
|
-
try {
|
|
1135
|
-
await this.interaction.webhook.editMessage(message, this.editBody(response));
|
|
1136
|
-
} catch (error) {
|
|
1137
|
-
this.logSwallowed("edit", error);
|
|
1138
|
-
}
|
|
781
|
+
async writeDefer(opts) {
|
|
782
|
+
await this.interaction.deferReply({ flags: deferFlags(opts) });
|
|
1139
783
|
}
|
|
1140
|
-
async
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
784
|
+
async writeDeferUpdate() {
|
|
785
|
+
await this.sourceInteraction("deferUpdate").deferUpdate();
|
|
786
|
+
}
|
|
787
|
+
async writeUpdate(response) {
|
|
788
|
+
const result = await this.sourceInteraction("update").update({
|
|
789
|
+
...this.updateOptions(response),
|
|
790
|
+
withResponse: true
|
|
791
|
+
});
|
|
792
|
+
return this.createdMessage(result);
|
|
793
|
+
}
|
|
794
|
+
async writeFollowUp(response, opts) {
|
|
795
|
+
return await this.interaction.followUp(this.replyOptions(response, sendFlags(opts)));
|
|
796
|
+
}
|
|
797
|
+
async writeEditOriginal(response) {
|
|
798
|
+
return await this.interaction.editReply(this.editOptions(response));
|
|
1148
799
|
}
|
|
1149
|
-
|
|
800
|
+
async writeEditTarget(targetId, response) {
|
|
801
|
+
return await this.interaction.webhook.editMessage(targetId, this.editOptions(response));
|
|
802
|
+
}
|
|
803
|
+
async writeDeleteOriginal() {
|
|
804
|
+
await this.interaction.deleteReply();
|
|
805
|
+
}
|
|
806
|
+
async writeDeleteTarget(targetId) {
|
|
807
|
+
await this.interaction.deleteReply(targetId);
|
|
808
|
+
}
|
|
809
|
+
async writeModal(data) {
|
|
810
|
+
const { interaction } = this;
|
|
811
|
+
if (interaction.isModalSubmit()) this.rejectModalSubmit();
|
|
812
|
+
await interaction.showModal(data);
|
|
813
|
+
}
|
|
814
|
+
guardModalSource() {
|
|
815
|
+
if (this.interaction.isModalSubmit()) this.rejectModalSubmit();
|
|
816
|
+
}
|
|
817
|
+
rejectModalSubmit() {
|
|
818
|
+
throw new SeedcordError(SeedcordErrorCode.ReplyIllegalAckState, [
|
|
819
|
+
"showModal",
|
|
820
|
+
"this interaction is a modal submit",
|
|
821
|
+
"A modal cannot open another modal.",
|
|
822
|
+
this.routeId
|
|
823
|
+
]);
|
|
824
|
+
}
|
|
825
|
+
sourceInteraction(method) {
|
|
826
|
+
const { interaction } = this;
|
|
827
|
+
if (interaction.isMessageComponent()) return interaction;
|
|
828
|
+
if (interaction.isModalSubmit() && interaction.isFromMessage()) return interaction;
|
|
829
|
+
throw new SeedcordError(SeedcordErrorCode.ReplyUpdateWithoutSource, [method, this.routeId]);
|
|
830
|
+
}
|
|
831
|
+
replyOptions(response, flags) {
|
|
832
|
+
const reply = this.serialize(response);
|
|
1150
833
|
return {
|
|
1151
|
-
components:
|
|
1152
|
-
flags
|
|
1153
|
-
...
|
|
1154
|
-
...
|
|
834
|
+
components: reply.components,
|
|
835
|
+
flags,
|
|
836
|
+
...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
|
|
837
|
+
...reply.files && { files: [...reply.files] }
|
|
1155
838
|
};
|
|
1156
839
|
}
|
|
1157
|
-
|
|
840
|
+
editOptions(response) {
|
|
841
|
+
const reply = this.serialize(response);
|
|
1158
842
|
return {
|
|
1159
|
-
components:
|
|
843
|
+
components: reply.components,
|
|
1160
844
|
flags: MessageFlags.IsComponentsV2,
|
|
1161
|
-
...
|
|
1162
|
-
...
|
|
845
|
+
...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
|
|
846
|
+
...reply.files && { files: [...reply.files] }
|
|
1163
847
|
};
|
|
1164
848
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
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;
|
|
1171
860
|
}
|
|
1172
861
|
};
|
|
1173
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
|
+
|
|
1174
1060
|
//#endregion
|
|
1175
1061
|
//#region src/bot/confirm/reserved.ts
|
|
1176
1062
|
const RESERVED_CONFIRM_PREFIX = "__seedcord_confirm";
|
|
@@ -1215,152 +1101,36 @@ async function collectChoice(message, userId, timeoutMs) {
|
|
|
1215
1101
|
return null;
|
|
1216
1102
|
}
|
|
1217
1103
|
}
|
|
1218
|
-
async function settle(sender,
|
|
1104
|
+
async function settle(sender, message, outcome) {
|
|
1219
1105
|
if (outcome === void 0) {
|
|
1220
|
-
await
|
|
1106
|
+
await sender.delete(message).catch(() => void 0);
|
|
1221
1107
|
return;
|
|
1222
1108
|
}
|
|
1223
1109
|
await sender.edit(message, typeof outcome === "function" ? await outcome() : outcome);
|
|
1224
1110
|
}
|
|
1225
|
-
async function getConfirmation(interaction, prompt, options) {
|
|
1226
|
-
const { ephemeral = true, timeoutMs = DEFAULT_TIMEOUT_MS } = options ?? {};
|
|
1227
|
-
const response = typeof prompt === "string" ? defaultPrompt(prompt, options) : await prompt(CONFIRM_IDS);
|
|
1228
|
-
const sender = new ReplySender(interaction);
|
|
1229
|
-
const message = await sender.send(response, ephemeral);
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
return false;
|
|
1235
|
-
}
|
|
1236
|
-
await winner.deferUpdate().catch(() => void 0);
|
|
1237
|
-
const confirmed = winner.customId === CONFIRM_IDS.confirm;
|
|
1238
|
-
await settle(sender, interaction, message, confirmed ? options?.onConfirm : options?.onCancel);
|
|
1239
|
-
return confirmed;
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
//#endregion
|
|
1243
|
-
//#region src/interfaces/Plugin.ts
|
|
1244
|
-
/**
|
|
1245
|
-
* Base class for Seedcord plugins
|
|
1246
|
-
*
|
|
1247
|
-
* Extend this class to create plugins that integrate with the Seedcord lifecycle.
|
|
1248
|
-
* Plugins have access to the core instance and must implement initialization logic.
|
|
1249
|
-
*/
|
|
1250
|
-
var Plugin = class extends TypedEventEmitter {
|
|
1251
|
-
pluggable;
|
|
1252
|
-
name = this.constructor.name;
|
|
1253
|
-
constructor(pluggable) {
|
|
1254
|
-
super();
|
|
1255
|
-
this.pluggable = pluggable;
|
|
1256
|
-
}
|
|
1257
|
-
/**
|
|
1258
|
-
* Reloads plugin state on an HMR update. The base implementation is a no-op, override it in your plugin.
|
|
1259
|
-
* @virtual
|
|
1260
|
-
*/
|
|
1261
|
-
onHmr(_event) {
|
|
1262
|
-
return Promise.resolve();
|
|
1263
|
-
}
|
|
1264
|
-
/**
|
|
1265
|
-
* Registers critical file patterns that should trigger a full restart when changed in Dev HMR.
|
|
1266
|
-
* @param patterns - Glob patterns relative to the project root
|
|
1267
|
-
*/
|
|
1268
|
-
registerCriticalFiles(patterns) {
|
|
1269
|
-
getDevChannel()?.send("seedcord:register-critical-files", { patterns });
|
|
1270
|
-
}
|
|
1271
|
-
/** @internal */
|
|
1272
|
-
removeListener(event, listener) {
|
|
1273
|
-
return super.removeListener(event, listener);
|
|
1274
|
-
}
|
|
1275
|
-
/** @internal */
|
|
1276
|
-
removeAllListeners(event) {
|
|
1277
|
-
return super.removeAllListeners(event);
|
|
1278
|
-
}
|
|
1279
|
-
};
|
|
1280
|
-
/**
|
|
1281
|
-
* Base class for objects that can have plugins attached.
|
|
1282
|
-
*
|
|
1283
|
-
* Provides plugin attachment capabilities and lifecycle management. Plugins are attached during
|
|
1284
|
-
* configuration and initialized during startup. Not constructed directly, the host is a {@link Seedcord}.
|
|
1285
|
-
*/
|
|
1286
|
-
var Pluggable = class Pluggable extends TypedEventEmitter {
|
|
1287
|
-
isInitialized = false;
|
|
1288
|
-
shutdown;
|
|
1289
|
-
startup;
|
|
1290
|
-
plugins = [];
|
|
1291
|
-
static PLUGIN_INIT_TIMEOUT_MS = 15e3;
|
|
1292
|
-
constructor(shutdown, startup) {
|
|
1293
|
-
super();
|
|
1294
|
-
this.shutdown = shutdown;
|
|
1295
|
-
this.startup = startup;
|
|
1296
|
-
}
|
|
1297
|
-
/** @internal */
|
|
1298
|
-
async init() {
|
|
1299
|
-
if (this.isInitialized) return this;
|
|
1300
|
-
await this.startup.run();
|
|
1301
|
-
this.isInitialized = true;
|
|
1302
|
-
return this;
|
|
1303
|
-
}
|
|
1304
|
-
/**
|
|
1305
|
-
* Attaches a plugin to this instance
|
|
1306
|
-
*
|
|
1307
|
-
* Plugins provide external functionality and are initialized during the specified startup phase.
|
|
1308
|
-
* The plugin instance becomes available as a property in `core` wherever it's available.
|
|
1309
|
-
*
|
|
1310
|
-
* Make sure to augment the {@link Core} interface with the plugin type to ensure TypeScript recognizes it and provides intellisense.
|
|
1311
|
-
*
|
|
1312
|
-
* @typeParam Key - The property name for accessing the plugin
|
|
1313
|
-
* @typeParam Ctor - The plugin constructor type
|
|
1314
|
-
* @param key - Property name to access the plugin instance
|
|
1315
|
-
* @param Plugin - Plugin constructor class
|
|
1316
|
-
* @param startupPhase - When during startup to initialize this plugin ({@link StartupPhase})
|
|
1317
|
-
* @param args - Additional arguments to pass to the plugin constructor
|
|
1318
|
-
* @returns This instance with the plugin attached as a typed property
|
|
1319
|
-
* @throws A **SeedcordError** When called after initialization or if key already exists
|
|
1320
|
-
* @example
|
|
1321
|
-
* ```typescript
|
|
1322
|
-
* seedcord.attach('db', Mongo, StartupPhase.Configuration, { uri: 'mongodb://...', name: 'seedcord', dir: ... })
|
|
1323
|
-
* ```
|
|
1324
|
-
*/
|
|
1325
|
-
attach(key, Plugin, startupPhase, ...args) {
|
|
1326
|
-
if (this.isInitialized) throw new SeedcordError(SeedcordErrorCode.CorePluginAfterInit);
|
|
1327
|
-
if (key in this) throw new SeedcordError(SeedcordErrorCode.CorePluginKeyExists, [key]);
|
|
1328
|
-
const instance = new Plugin(this, ...args);
|
|
1329
|
-
this.plugins.push(instance);
|
|
1330
|
-
const entry = { [key]: instance };
|
|
1331
|
-
this.startup.addTask(startupPhase, `Plugin:${key}`, async () => {
|
|
1332
|
-
instance.logger.utils.initialization(key, "start");
|
|
1333
|
-
await instance.init();
|
|
1334
|
-
instance.logger.utils.initialization(key, "end");
|
|
1335
|
-
}, Pluggable.PLUGIN_INIT_TIMEOUT_MS);
|
|
1336
|
-
return Object.assign(this, entry);
|
|
1337
|
-
}
|
|
1338
|
-
/** @internal */
|
|
1339
|
-
removeListener(event, listener) {
|
|
1340
|
-
return super.removeListener(event, listener);
|
|
1341
|
-
}
|
|
1342
|
-
/** @internal */
|
|
1343
|
-
removeAllListeners(event) {
|
|
1344
|
-
return super.removeAllListeners(event);
|
|
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;
|
|
1345
1120
|
}
|
|
1346
|
-
|
|
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
|
+
}
|
|
1347
1126
|
|
|
1348
1127
|
//#endregion
|
|
1349
1128
|
//#region src/handlers/BaseHandler.ts
|
|
1350
1129
|
/**
|
|
1351
|
-
*
|
|
1130
|
+
* The gateway base, fixes the core base's `TCore` to the gateway {@link Core} and adds the `getEvent`
|
|
1131
|
+
* gate hook.
|
|
1352
1132
|
*/
|
|
1353
|
-
var BaseHandler = class {
|
|
1354
|
-
core;
|
|
1355
|
-
event;
|
|
1356
|
-
logger;
|
|
1357
|
-
dispatch;
|
|
1358
|
-
constructor(event, core, dispatch) {
|
|
1359
|
-
this.core = core;
|
|
1360
|
-
this.event = event;
|
|
1361
|
-
this.logger = new Logger(this.constructor.name);
|
|
1362
|
-
this.dispatch = dispatch;
|
|
1363
|
-
}
|
|
1133
|
+
var BaseHandler$1 = class extends BaseHandler {
|
|
1364
1134
|
/** @internal */
|
|
1365
1135
|
getEvent() {
|
|
1366
1136
|
return this.event;
|
|
@@ -1377,43 +1147,56 @@ var BaseHandler = class {
|
|
|
1377
1147
|
*
|
|
1378
1148
|
* @typeParam Repliable - The interaction type this middleware processes
|
|
1379
1149
|
*/
|
|
1380
|
-
var InteractionMiddleware = class extends BaseHandler {
|
|
1150
|
+
var InteractionMiddleware = class extends BaseHandler$1 {
|
|
1381
1151
|
constructor(event, core) {
|
|
1382
1152
|
super(event, core);
|
|
1383
1153
|
}
|
|
1384
1154
|
};
|
|
1385
1155
|
|
|
1386
1156
|
//#endregion
|
|
1387
|
-
//#region src/handlers/
|
|
1157
|
+
//#region src/handlers/RepliableHandler.ts
|
|
1388
1158
|
/**
|
|
1389
|
-
* Shared base the
|
|
1159
|
+
* Shared base the repliable interaction handlers extend.
|
|
1390
1160
|
*
|
|
1391
|
-
* Not a public entry point.
|
|
1392
|
-
* or {@link
|
|
1393
|
-
*
|
|
1161
|
+
* Not a public entry point. Extend {@link SlashHandler}, {@link ButtonHandler}, {@link ModalHandler},
|
|
1162
|
+
* {@link SelectMenuHandler}, or {@link ContextMenuHandler} instead. This class binds the core reply base
|
|
1163
|
+
* to the gateway sender.
|
|
1394
1164
|
*
|
|
1395
|
-
* @typeParam
|
|
1165
|
+
* @typeParam Event - The repliable interaction type this handler processes
|
|
1396
1166
|
*/
|
|
1397
|
-
var
|
|
1398
|
-
|
|
1399
|
-
|
|
1167
|
+
var RepliableHandler = class extends RepliableHandler$1 {
|
|
1168
|
+
/** @internal */
|
|
1169
|
+
getEvent() {
|
|
1170
|
+
return this.event;
|
|
1171
|
+
}
|
|
1172
|
+
buildSender(event, _core, routeId) {
|
|
1173
|
+
return new ReplySender(event, routeId);
|
|
1400
1174
|
}
|
|
1401
1175
|
};
|
|
1402
1176
|
|
|
1403
1177
|
//#endregion
|
|
1404
|
-
//#region src/
|
|
1178
|
+
//#region src/handlers/interaction/InteractionHandler.ts
|
|
1405
1179
|
/**
|
|
1406
|
-
*
|
|
1407
|
-
* `seedcord codegen` both assemble route keys through the same {@link buildSlashRoute}, so a dispatched
|
|
1408
|
-
* interaction and a generated registry key always match.
|
|
1180
|
+
* Shared base the typed interaction handlers extend.
|
|
1409
1181
|
*
|
|
1410
|
-
* @
|
|
1182
|
+
* Not a public entry point. Extend {@link SlashHandler}, {@link ButtonHandler}, {@link ModalHandler},
|
|
1183
|
+
* or {@link SelectMenuHandler} instead. This class adds `showModal` on top of the reply members those bases
|
|
1184
|
+
* share.
|
|
1185
|
+
*
|
|
1186
|
+
* @typeParam Repliable - The interaction type this handler processes
|
|
1411
1187
|
*/
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1188
|
+
var InteractionHandler = class extends RepliableHandler {
|
|
1189
|
+
constructor(event, core, dispatch) {
|
|
1190
|
+
super(event, core, dispatch);
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Open a modal. Must be the initial response to this interaction. The modal kind rejects this call at
|
|
1194
|
+
* compile time (Discord forbids a modal in response to a modal).
|
|
1195
|
+
*/
|
|
1196
|
+
showModal(modal) {
|
|
1197
|
+
return this.sender.showModal(modal);
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1417
1200
|
|
|
1418
1201
|
//#endregion
|
|
1419
1202
|
//#region src/handlers/interaction/SlashHandler.ts
|
|
@@ -1449,7 +1232,7 @@ var SlashHandler = class extends InteractionHandler {
|
|
|
1449
1232
|
}
|
|
1450
1233
|
/**
|
|
1451
1234
|
* Run the arm for whichever command fired. Use this only when the handler is registered for several
|
|
1452
|
-
* routes.
|
|
1235
|
+
* routes.
|
|
1453
1236
|
*
|
|
1454
1237
|
* Provide one arm per registered route, keyed by its route string, and each arm receives that route's
|
|
1455
1238
|
* own narrowed options. The arms must cover every route in the generic, a missing route or an unknown
|
|
@@ -1473,7 +1256,7 @@ var SlashHandler = class extends InteractionHandler {
|
|
|
1473
1256
|
*/
|
|
1474
1257
|
async match(arms) {
|
|
1475
1258
|
const route = slashRouteOf(this.event);
|
|
1476
|
-
const arm = arms[route];
|
|
1259
|
+
const arm = Object.hasOwn(arms, route) ? arms[route] : void 0;
|
|
1477
1260
|
if (!arm) throw new SeedcordError(SeedcordErrorCode.SlashMatchArmMissing, [route]);
|
|
1478
1261
|
return await arm(this.options);
|
|
1479
1262
|
}
|
|
@@ -1540,15 +1323,15 @@ var ContextMenuHandler = class extends InteractionHandler {
|
|
|
1540
1323
|
* }
|
|
1541
1324
|
* ```
|
|
1542
1325
|
*/
|
|
1543
|
-
var AutocompleteHandler = class extends BaseHandler {
|
|
1326
|
+
var AutocompleteHandler = class extends BaseHandler$1 {
|
|
1544
1327
|
constructor(event, core, dispatch) {
|
|
1545
1328
|
super(event, core, dispatch);
|
|
1546
1329
|
}
|
|
1330
|
+
decodedFocused;
|
|
1547
1331
|
/**
|
|
1548
1332
|
* The focused option for this interaction. `value` is always the raw partial string the user is typing,
|
|
1549
1333
|
* even for an integer or number option, so coerce it yourself when you need a number.
|
|
1550
1334
|
*/
|
|
1551
|
-
decodedFocused;
|
|
1552
1335
|
get focused() {
|
|
1553
1336
|
if (this.decodedFocused) return this.decodedFocused;
|
|
1554
1337
|
const raw = this.event.options.getFocused(true);
|
|
@@ -1564,7 +1347,7 @@ var AutocompleteHandler = class extends BaseHandler {
|
|
|
1564
1347
|
* match is how you read it. There is no single-field shortcut the way slash/component handlers have one.
|
|
1565
1348
|
*
|
|
1566
1349
|
* Provide one arm per autocompletable field across the registered commands, keyed by field name. Each
|
|
1567
|
-
* arm receives the focused partial value and a `respond`
|
|
1350
|
+
* arm receives the focused partial value and a `respond` typed to that field's choice value. The
|
|
1568
1351
|
* arms must cover every autocompletable field, a missing field or an unknown key is a compile error. A
|
|
1569
1352
|
* focused field with no arm, only reachable from a stale-deployed command, throws at runtime.
|
|
1570
1353
|
*
|
|
@@ -1586,11 +1369,15 @@ var AutocompleteHandler = class extends BaseHandler {
|
|
|
1586
1369
|
*/
|
|
1587
1370
|
async match(arms) {
|
|
1588
1371
|
const { name, value } = this.focused;
|
|
1589
|
-
const respond = (choices) => this.
|
|
1590
|
-
const arm = arms[name];
|
|
1372
|
+
const respond = (choices) => this.respond(choices);
|
|
1373
|
+
const arm = Object.hasOwn(arms, name) ? arms[name] : void 0;
|
|
1591
1374
|
if (!arm) throw new SeedcordError(SeedcordErrorCode.AutocompleteMatchArmMissing, [name]);
|
|
1592
1375
|
return await arm(value, respond);
|
|
1593
1376
|
}
|
|
1377
|
+
/** Send autocomplete suggestions, callback type 8. Prefer {@link match}, which restricts each field's choices to its declared type. */
|
|
1378
|
+
async respond(choices) {
|
|
1379
|
+
await this.event.respond(choices);
|
|
1380
|
+
}
|
|
1594
1381
|
/** The firing command route, for a field whose completion differs per registered command. */
|
|
1595
1382
|
get route() {
|
|
1596
1383
|
return slashRouteOf(this.event);
|
|
@@ -1598,7 +1385,7 @@ var AutocompleteHandler = class extends BaseHandler {
|
|
|
1598
1385
|
/**
|
|
1599
1386
|
* The already-entered options on this command, restricted to the kinds Discord resolves during autocomplete
|
|
1600
1387
|
* (string, integer, number, boolean) with every read nullable, since a sibling is partial while the user
|
|
1601
|
-
* types the focused field.
|
|
1388
|
+
* types the focused field. The focused field is on `this.focused`.
|
|
1602
1389
|
*/
|
|
1603
1390
|
get options() {
|
|
1604
1391
|
return this.event.options;
|
|
@@ -1610,25 +1397,25 @@ var AutocompleteHandler = class extends BaseHandler {
|
|
|
1610
1397
|
/**
|
|
1611
1398
|
* Shared base the customId-routed component handlers extend.
|
|
1612
1399
|
*
|
|
1613
|
-
* Not a public entry point.
|
|
1614
|
-
*
|
|
1615
|
-
* those bases share, so DO NOT use it directly.
|
|
1400
|
+
* Not a public entry point. Extend {@link ButtonHandler}, {@link SelectMenuHandler}, or {@link ModalHandler}
|
|
1401
|
+
* instead. This class defines the customId decode and the route matching those bases share.
|
|
1616
1402
|
*
|
|
1617
1403
|
* @typeParam Event - The component interaction type this handler processes
|
|
1618
1404
|
* @typeParam Defs - The customId route definitions registered on the concrete handler
|
|
1619
1405
|
*/
|
|
1620
1406
|
var ComponentHandler = class extends InteractionHandler {
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1407
|
+
/** Rewrite the source message this component interaction came from. */
|
|
1408
|
+
update(response) {
|
|
1409
|
+
return this.sender.update(response);
|
|
1410
|
+
}
|
|
1411
|
+
/** Acknowledge the component without changing the source message. */
|
|
1412
|
+
deferUpdate() {
|
|
1413
|
+
return this.sender.deferUpdate();
|
|
1625
1414
|
}
|
|
1626
1415
|
decoded;
|
|
1627
1416
|
get route() {
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
this.decoded = decoded;
|
|
1631
|
-
return decoded;
|
|
1417
|
+
this.decoded ??= decodeComponentRoute(this.constructor, this.event.customId);
|
|
1418
|
+
return this.decoded;
|
|
1632
1419
|
}
|
|
1633
1420
|
/**
|
|
1634
1421
|
* The decoded params of the single route this handler is registered for.
|
|
@@ -1647,10 +1434,11 @@ var ComponentHandler = class extends InteractionHandler {
|
|
|
1647
1434
|
* handler `this.params` is `never`, so match is the only way to read the decoded params.
|
|
1648
1435
|
*
|
|
1649
1436
|
* Provide one arm per registered route, keyed by its prefix, and each arm receives that route's own
|
|
1650
|
-
* decoded params. The arms
|
|
1651
|
-
*
|
|
1437
|
+
* decoded params. The arms cover every registered route prefix, checked at compile time, and a prefix
|
|
1438
|
+
* unmatched at runtime throws `CustomIdMatchArmMissing`. Decoding runs before any arm, so a stale or
|
|
1439
|
+
* corrupt wire throws before an arm body executes.
|
|
1652
1440
|
*
|
|
1653
|
-
* @param arms - One
|
|
1441
|
+
* @param arms - One callback per registered route, keyed by prefix.
|
|
1654
1442
|
* @returns The result of the arm that ran.
|
|
1655
1443
|
*
|
|
1656
1444
|
* @example
|
|
@@ -1659,8 +1447,8 @@ var ComponentHandler = class extends InteractionHandler {
|
|
|
1659
1447
|
* class ReviewButtons extends ButtonHandler<[typeof Approve, typeof Reject]> {
|
|
1660
1448
|
* async execute() {
|
|
1661
1449
|
* await this.match({
|
|
1662
|
-
* approve: ({ userId }) => this.
|
|
1663
|
-
* reject: ({ userId }) => this.
|
|
1450
|
+
* approve: ({ userId }) => this.reply(`approved <@${userId}>`),
|
|
1451
|
+
* reject: ({ userId }) => this.reply(`rejected <@${userId}>`)
|
|
1664
1452
|
* });
|
|
1665
1453
|
* }
|
|
1666
1454
|
* }
|
|
@@ -1668,7 +1456,7 @@ var ComponentHandler = class extends InteractionHandler {
|
|
|
1668
1456
|
*/
|
|
1669
1457
|
async match(arms) {
|
|
1670
1458
|
const { prefix, params } = this.route;
|
|
1671
|
-
const arm = arms[prefix];
|
|
1459
|
+
const arm = Object.hasOwn(arms, prefix) ? arms[prefix] : void 0;
|
|
1672
1460
|
if (!arm) throw new SeedcordError(SeedcordErrorCode.CustomIdMatchArmMissing, [prefix]);
|
|
1673
1461
|
return await arm(params);
|
|
1674
1462
|
}
|
|
@@ -1680,8 +1468,9 @@ var ComponentHandler = class extends InteractionHandler {
|
|
|
1680
1468
|
* Base class for a button interaction handler.
|
|
1681
1469
|
*
|
|
1682
1470
|
* Register the customId definitions this handler decodes with `@ButtonRoute`, list the same ones in the
|
|
1683
|
-
* generic, then read `this.params` for a single route or `this.match` for several.
|
|
1684
|
-
*
|
|
1471
|
+
* generic, then read `this.params` for a single route or `this.match` for several. Reply through the handler
|
|
1472
|
+
* members, or rewrite the source message with `this.update`. Passing different definitions to the decorator
|
|
1473
|
+
* and the generic is a compile error.
|
|
1685
1474
|
*
|
|
1686
1475
|
* @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ApproveId]`.
|
|
1687
1476
|
* @typeParam Cache - The interaction cache state, `'cached'` by default.
|
|
@@ -1692,7 +1481,7 @@ var ComponentHandler = class extends InteractionHandler {
|
|
|
1692
1481
|
* class ApproveButton extends ButtonHandler<[typeof ApproveId]> {
|
|
1693
1482
|
* async execute() {
|
|
1694
1483
|
* const { userId } = this.params;
|
|
1695
|
-
* await this.
|
|
1484
|
+
* await this.reply(`approved <@${userId}>`);
|
|
1696
1485
|
* }
|
|
1697
1486
|
* }
|
|
1698
1487
|
* ```
|
|
@@ -1706,7 +1495,8 @@ var ButtonHandler = class extends ComponentHandler {};
|
|
|
1706
1495
|
*
|
|
1707
1496
|
* Register the customId definitions this handler decodes with `@ModalRoute`, list the same ones in the
|
|
1708
1497
|
* generic, then read `this.params` for a single route or `this.match` for several. Read the submitted
|
|
1709
|
-
* inputs from `this.event.fields
|
|
1498
|
+
* inputs from `this.event.fields`, and reply through the handler members. Discord forbids opening a modal in
|
|
1499
|
+
* response to a modal, so `showModal` fails compilation on this kind.
|
|
1710
1500
|
*
|
|
1711
1501
|
* @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ConfigId]`.
|
|
1712
1502
|
* @typeParam Cache - The interaction cache state, `'cached'` by default.
|
|
@@ -1718,7 +1508,7 @@ var ButtonHandler = class extends ComponentHandler {};
|
|
|
1718
1508
|
* async execute() {
|
|
1719
1509
|
* const { guildId } = this.params;
|
|
1720
1510
|
* const name = this.event.fields.getTextInputValue('name');
|
|
1721
|
-
* await this.
|
|
1511
|
+
* await this.reply(`saved ${name} for ${guildId}`);
|
|
1722
1512
|
* }
|
|
1723
1513
|
* }
|
|
1724
1514
|
* ```
|
|
@@ -1732,7 +1522,8 @@ var ModalHandler = class extends ComponentHandler {};
|
|
|
1732
1522
|
*
|
|
1733
1523
|
* Pass the select kind first and the customId definitions second, the same order as `@SelectMenuRoute`,
|
|
1734
1524
|
* so `this.event` and `this.event.values` are narrowed to that kind. Read `this.params` for a single
|
|
1735
|
-
* route or `this.match` for several
|
|
1525
|
+
* route or `this.match` for several, and reply through the handler members or rewrite the source message
|
|
1526
|
+
* with `this.update`.
|
|
1736
1527
|
*
|
|
1737
1528
|
* @typeParam Kind - The select kind from {@link SelectMenuKind}, e.g. `SelectMenuKind.User`.
|
|
1738
1529
|
* @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof AssignId]`.
|
|
@@ -1744,7 +1535,7 @@ var ModalHandler = class extends ComponentHandler {};
|
|
|
1744
1535
|
* class AssignSelect extends SelectMenuHandler<SelectMenuKind.User, [typeof AssignId]> {
|
|
1745
1536
|
* async execute() {
|
|
1746
1537
|
* const { roleId } = this.params;
|
|
1747
|
-
* await this.
|
|
1538
|
+
* await this.reply(`assigning ${this.event.values.length} member(s) to <@&${roleId}>`);
|
|
1748
1539
|
* }
|
|
1749
1540
|
* }
|
|
1750
1541
|
* ```
|
|
@@ -1775,7 +1566,7 @@ var SelectMenuHandler = class extends ComponentHandler {};
|
|
|
1775
1566
|
* }
|
|
1776
1567
|
* ```
|
|
1777
1568
|
*/
|
|
1778
|
-
var EventHandler = class extends BaseHandler {
|
|
1569
|
+
var EventHandler = class extends BaseHandler$1 {
|
|
1779
1570
|
firedEvent;
|
|
1780
1571
|
constructor(event, core, eventName) {
|
|
1781
1572
|
super(event, core);
|
|
@@ -1828,7 +1619,7 @@ var EventHandler = class extends BaseHandler {
|
|
|
1828
1619
|
*
|
|
1829
1620
|
* @typeParam EventName - One or more `ClientEvents` keys. Defaults to every event (catchall).
|
|
1830
1621
|
*/
|
|
1831
|
-
var EventMiddleware = class extends BaseHandler {
|
|
1622
|
+
var EventMiddleware = class extends BaseHandler$1 {
|
|
1832
1623
|
firedEvent;
|
|
1833
1624
|
constructor(event, core, eventName) {
|
|
1834
1625
|
super(event, core);
|
|
@@ -2010,16 +1801,16 @@ var Paginator = class {
|
|
|
2010
1801
|
const loadPage = (ctx, n) => this.page(ctx, n);
|
|
2011
1802
|
this.Handler = class Nav extends ButtonHandler {
|
|
2012
1803
|
async execute() {
|
|
2013
|
-
await this.
|
|
1804
|
+
await this.deferUpdate();
|
|
2014
1805
|
const response = await loadPage(contextOf(this.event, this.core), this.params.page);
|
|
2015
|
-
await
|
|
1806
|
+
await this.update(response);
|
|
2016
1807
|
}
|
|
2017
1808
|
};
|
|
2018
1809
|
}
|
|
2019
1810
|
/** Render page 0 and send it, picking reply or followUp from the interaction's state. */
|
|
2020
1811
|
async start(interaction, core) {
|
|
2021
1812
|
const response = await this.page(contextOf(interaction, core), 0);
|
|
2022
|
-
return new ReplySender(interaction).send(response, this.config.ephemeral ?? false);
|
|
1813
|
+
return new ReplySender(interaction, interactionRoute(interaction)).send(response, { ephemeral: this.config.ephemeral ?? false });
|
|
2023
1814
|
}
|
|
2024
1815
|
/** Render a page as a {@link ReplyResponse}. To post it elsewhere, add `flags: MessageFlags.IsComponentsV2`. */
|
|
2025
1816
|
async page(ctx, n) {
|
|
@@ -2143,19 +1934,184 @@ function Subscribe(subscriber, options) {
|
|
|
2143
1934
|
};
|
|
2144
1935
|
}
|
|
2145
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
|
+
}
|
|
1972
|
+
|
|
1973
|
+
//#endregion
|
|
1974
|
+
//#region src/subscribers/bases/webhookHelpers.ts
|
|
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;
|
|
2011
|
+
/**
|
|
2012
|
+
* Sends component messages to a Discord webhook through `@discordjs/rest`.
|
|
2013
|
+
*
|
|
2014
|
+
* @throws A `SeedcordError` when the url does not carry a webhook id and token.
|
|
2015
|
+
*/
|
|
2016
|
+
var WebhookSender = class {
|
|
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
|
+
|
|
2146
2062
|
//#endregion
|
|
2147
2063
|
//#region src/subscribers/bases/WebhookLog.ts
|
|
2148
2064
|
/**
|
|
2149
|
-
*
|
|
2065
|
+
* Base class for subscribers that deliver their event to a Discord webhook.
|
|
2150
2066
|
*
|
|
2151
|
-
*
|
|
2152
|
-
*
|
|
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.
|
|
2153
2070
|
*
|
|
2154
|
-
* @typeParam KeyOfSubscribers - The
|
|
2071
|
+
* @typeParam KeyOfSubscribers - The subscription key this reporter receives
|
|
2155
2072
|
*/
|
|
2156
|
-
var WebhookLog = class extends Subscriber {
|
|
2157
|
-
|
|
2158
|
-
|
|
2073
|
+
var WebhookLog = class WebhookLog extends Subscriber {
|
|
2074
|
+
static senders = /* @__PURE__ */ new Map();
|
|
2075
|
+
async execute() {
|
|
2076
|
+
const url = WebhookLog.urlOf(WebhookLog.envKeyOf(this.constructor));
|
|
2077
|
+
if (url === null) {
|
|
2078
|
+
this.logger.warn(`${chalk.bold(this.constructor.name)} has no webhook url set, this reporter is disabled`);
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
const { username, components, files } = await this.report();
|
|
2082
|
+
try {
|
|
2083
|
+
await WebhookLog.senderFor(url).send({
|
|
2084
|
+
flags: flagsFor(false),
|
|
2085
|
+
username: username ?? this.constructor.name,
|
|
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;
|
|
2159
2115
|
}
|
|
2160
2116
|
};
|
|
2161
2117
|
|
|
@@ -2213,7 +2169,6 @@ function slashRouteLeaves(commands) {
|
|
|
2213
2169
|
var CommandRegistry = class {
|
|
2214
2170
|
core;
|
|
2215
2171
|
onDeployed;
|
|
2216
|
-
name = "Commands";
|
|
2217
2172
|
logger = new Logger("Commands");
|
|
2218
2173
|
isInitialised = false;
|
|
2219
2174
|
globalCommands = [];
|
|
@@ -2401,231 +2356,64 @@ function deriveEventActor(args) {
|
|
|
2401
2356
|
user: arg.user,
|
|
2402
2357
|
channelId: null
|
|
2403
2358
|
};
|
|
2404
|
-
if (arg instanceof User) return {
|
|
2405
|
-
guild: null,
|
|
2406
|
-
member: null,
|
|
2407
|
-
user: arg,
|
|
2408
|
-
channelId: null
|
|
2409
|
-
};
|
|
2410
|
-
}
|
|
2411
|
-
return {
|
|
2412
|
-
guild: null,
|
|
2413
|
-
member: null,
|
|
2414
|
-
user: null,
|
|
2415
|
-
channelId: null
|
|
2416
|
-
};
|
|
2417
|
-
}
|
|
2418
|
-
|
|
2419
|
-
//#endregion
|
|
2420
|
-
//#region src/bot/gates/runGates.ts
|
|
2421
|
-
function interactionGateContext(interaction, core) {
|
|
2422
|
-
const rawMember = interaction.member;
|
|
2423
|
-
const memberRoleIds = rawMember instanceof GuildMember ? [...rawMember.roles.cache.keys()].filter((id) => id !== interaction.guildId) : rawMember?.roles ?? [];
|
|
2424
|
-
return {
|
|
2425
|
-
kind: "interaction",
|
|
2426
|
-
interaction,
|
|
2427
|
-
core,
|
|
2428
|
-
user: interaction.user,
|
|
2429
|
-
guild: interaction.guild,
|
|
2430
|
-
member: rawMember instanceof GuildMember ? rawMember : null,
|
|
2431
|
-
userId: interaction.user.id,
|
|
2432
|
-
guildId: interaction.guildId,
|
|
2433
|
-
channelId: interaction.channelId,
|
|
2434
|
-
memberRoleIds,
|
|
2435
|
-
memberPermissions: interaction.memberPermissions?.bitfield ?? null,
|
|
2436
|
-
routeId: null
|
|
2437
|
-
};
|
|
2438
|
-
}
|
|
2439
|
-
function eventGateContext(eventName, args, core) {
|
|
2440
|
-
const actor = deriveEventActor(args);
|
|
2441
|
-
return {
|
|
2442
|
-
kind: "event",
|
|
2443
|
-
core,
|
|
2444
|
-
eventName,
|
|
2445
|
-
payload: args,
|
|
2446
|
-
user: actor.user,
|
|
2447
|
-
guild: actor.guild,
|
|
2448
|
-
member: actor.member,
|
|
2449
|
-
userId: actor.user?.id ?? null,
|
|
2450
|
-
guildId: actor.guild?.id ?? null,
|
|
2451
|
-
channelId: actor.channelId,
|
|
2452
|
-
memberRoleIds: actor.member ? [...actor.member.roles.cache.keys()].filter((id) => id !== actor.guild?.id) : [],
|
|
2453
|
-
memberPermissions: actor.member?.permissions.bitfield ?? null,
|
|
2454
|
-
routeId: null
|
|
2455
|
-
};
|
|
2456
|
-
}
|
|
2457
|
-
|
|
2458
|
-
//#endregion
|
|
2459
|
-
//#region src/miscellaneous/FaultThrottle.ts
|
|
2460
|
-
/**
|
|
2461
|
-
* Drops duplicate fault reports inside a fixed window so a recurring fault (a database outage on a
|
|
2462
|
-
* hot path) reports once per window instead of flooding the webhook. Keyed on a structural string, so
|
|
2463
|
-
* the map stays small, and a stale entry is evicted on the next read of its key.
|
|
2464
|
-
*
|
|
2465
|
-
* @internal
|
|
2466
|
-
*/
|
|
2467
|
-
var FaultThrottle = class {
|
|
2468
|
-
windowMs;
|
|
2469
|
-
now;
|
|
2470
|
-
lastReportedAt = /* @__PURE__ */ new Map();
|
|
2471
|
-
constructor(windowMs = 6e4, now = () => Date.now()) {
|
|
2472
|
-
this.windowMs = windowMs;
|
|
2473
|
-
this.now = now;
|
|
2474
|
-
}
|
|
2475
|
-
/** Evicts the entry for this key if it has aged out, so the map never holds stale keys forever. */
|
|
2476
|
-
shouldReport(key) {
|
|
2477
|
-
const last = this.lastReportedAt.get(key);
|
|
2478
|
-
if (last === void 0) return true;
|
|
2479
|
-
if (this.now() - last < this.windowMs) return false;
|
|
2480
|
-
this.lastReportedAt.delete(key);
|
|
2481
|
-
return true;
|
|
2482
|
-
}
|
|
2483
|
-
markReported(key) {
|
|
2484
|
-
this.lastReportedAt.set(key, this.now());
|
|
2485
|
-
}
|
|
2486
|
-
clear() {
|
|
2487
|
-
this.lastReportedAt.clear();
|
|
2488
|
-
}
|
|
2489
|
-
};
|
|
2490
|
-
|
|
2491
|
-
//#endregion
|
|
2492
|
-
//#region src/miscellaneous/extractErrorResponse.ts
|
|
2493
|
-
const logger$2 = new Logger("ErrorsHandling");
|
|
2494
|
-
/**
|
|
2495
|
-
* The single throttle for every fault publish, so a recurring fault reports once per window across both
|
|
2496
|
-
* the interaction and event paths. Exported so tests reset it between cases.
|
|
2497
|
-
*
|
|
2498
|
-
* @internal
|
|
2499
|
-
*/
|
|
2500
|
-
const faultThrottle = new FaultThrottle();
|
|
2501
|
-
/**
|
|
2502
|
-
* Processes an error into a user-facing reply and routes its report.
|
|
2503
|
-
*
|
|
2504
|
-
* A {@link Notice} always renders its own reply. A reporting denial (`report: true`) additionally
|
|
2505
|
-
* publishes `handledException` on the interaction path. A raw, non-denial throw publishes
|
|
2506
|
-
* `unknownException` and renders the configured `defaultError` (or a generic {@link Fault}). One uuid is
|
|
2507
|
-
* threaded into both the reply and the bus payload.
|
|
2508
|
-
*
|
|
2509
|
-
* @internal
|
|
2510
|
-
*/
|
|
2511
|
-
function extractErrorResponse(error, core, origin) {
|
|
2512
|
-
const uuid = crypto.randomUUID();
|
|
2513
|
-
const developerUsername = core.config.notifications?.developerUsername;
|
|
2514
|
-
const ctx = developerUsername === void 0 ? { uuid } : {
|
|
2515
|
-
uuid,
|
|
2516
|
-
developerUsername
|
|
2517
|
-
};
|
|
2518
|
-
if (error instanceof Notice) {
|
|
2519
|
-
if (error.report) reportFault(error, core, origin, uuid);
|
|
2520
|
-
return {
|
|
2521
|
-
uuid,
|
|
2522
|
-
response: error.render(ctx)
|
|
2523
|
-
};
|
|
2524
|
-
}
|
|
2525
|
-
reportRawFault(error, core, origin, uuid);
|
|
2526
|
-
const override = core.config.errors?.defaultError;
|
|
2527
|
-
return {
|
|
2528
|
-
uuid,
|
|
2529
|
-
response: override ? new override(uuid).render(ctx) : new Fault().render(ctx)
|
|
2530
|
-
};
|
|
2531
|
-
}
|
|
2532
|
-
function withThrottle(origin, error, publish) {
|
|
2533
|
-
const key = faultKey(origin, error);
|
|
2534
|
-
if (!faultThrottle.shouldReport(key)) {
|
|
2535
|
-
logger$2.info(`throttled duplicate fault ${key}`);
|
|
2536
|
-
return;
|
|
2359
|
+
if (arg instanceof User) return {
|
|
2360
|
+
guild: null,
|
|
2361
|
+
member: null,
|
|
2362
|
+
user: arg,
|
|
2363
|
+
channelId: null
|
|
2364
|
+
};
|
|
2537
2365
|
}
|
|
2538
|
-
publish();
|
|
2539
|
-
faultThrottle.markReported(key);
|
|
2540
|
-
}
|
|
2541
|
-
function reportFault(denial, core, origin, uuid) {
|
|
2542
|
-
withThrottle(origin, denial, () => {
|
|
2543
|
-
logger$2.error(`${denial.name}: ${uuid}`, denial);
|
|
2544
|
-
if (origin.interaction) core.bus.publish("handledException", {
|
|
2545
|
-
denial,
|
|
2546
|
-
uuid,
|
|
2547
|
-
source: buildInteractionSource(origin.interaction)
|
|
2548
|
-
});
|
|
2549
|
-
else if (origin.event) core.bus.publish("handledException", {
|
|
2550
|
-
denial,
|
|
2551
|
-
uuid,
|
|
2552
|
-
source: buildEventSource(origin.event, origin)
|
|
2553
|
-
});
|
|
2554
|
-
else core.bus.publish("unknownException", {
|
|
2555
|
-
uuid,
|
|
2556
|
-
error: denial,
|
|
2557
|
-
guild: origin.guild,
|
|
2558
|
-
user: origin.user,
|
|
2559
|
-
metadata: metadataFor(origin)
|
|
2560
|
-
});
|
|
2561
|
-
});
|
|
2562
|
-
}
|
|
2563
|
-
function reportRawFault(error, core, origin, uuid) {
|
|
2564
|
-
withThrottle(origin, error, () => {
|
|
2565
|
-
if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
|
|
2566
|
-
else logger$2.error(`${uuid} | ${error.message}`);
|
|
2567
|
-
core.bus.publish("unknownException", {
|
|
2568
|
-
uuid,
|
|
2569
|
-
error,
|
|
2570
|
-
guild: origin.guild,
|
|
2571
|
-
user: origin.user,
|
|
2572
|
-
metadata: metadataFor(origin)
|
|
2573
|
-
});
|
|
2574
|
-
});
|
|
2575
|
-
}
|
|
2576
|
-
function metadataFor(origin) {
|
|
2577
|
-
if (origin.event) return {
|
|
2578
|
-
eventName: origin.event.name,
|
|
2579
|
-
handler: origin.event.handler,
|
|
2580
|
-
args: origin.event.args
|
|
2581
|
-
};
|
|
2582
|
-
return origin.metadata;
|
|
2583
|
-
}
|
|
2584
|
-
function buildEventSource(event, origin) {
|
|
2585
2366
|
return {
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
guildId: origin.guild?.id ?? null,
|
|
2591
|
-
channelId: event.channelId,
|
|
2592
|
-
raw: event.args
|
|
2367
|
+
guild: null,
|
|
2368
|
+
member: null,
|
|
2369
|
+
user: null,
|
|
2370
|
+
channelId: null
|
|
2593
2371
|
};
|
|
2594
2372
|
}
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
}
|
|
2602
|
-
function interactionRoute(interaction) {
|
|
2603
|
-
if (interaction.isChatInputCommand()) return slashRouteOf(interaction);
|
|
2604
|
-
if (interaction.isContextMenuCommand()) return interaction.commandName;
|
|
2605
|
-
if (interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit()) return prefixOf(interaction.customId) || interaction.customId;
|
|
2606
|
-
return "interaction";
|
|
2607
|
-
}
|
|
2608
|
-
function buildInteractionSource(interaction) {
|
|
2609
|
-
const command = interaction.isChatInputCommand() ? slashRouteOf(interaction) : interaction.isContextMenuCommand() ? interaction.commandName : null;
|
|
2610
|
-
const customId = interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit() ? interaction.customId : null;
|
|
2373
|
+
|
|
2374
|
+
//#endregion
|
|
2375
|
+
//#region src/bot/gates/runGates.ts
|
|
2376
|
+
function interactionGateContext(interaction, core) {
|
|
2377
|
+
const rawMember = interaction.member;
|
|
2378
|
+
const memberRoleIds = rawMember instanceof GuildMember ? [...rawMember.roles.cache.keys()].filter((id) => id !== interaction.guildId) : rawMember?.roles ?? [];
|
|
2611
2379
|
return {
|
|
2612
2380
|
kind: "interaction",
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2381
|
+
interaction,
|
|
2382
|
+
core,
|
|
2383
|
+
user: interaction.user,
|
|
2384
|
+
guild: interaction.guild,
|
|
2385
|
+
member: rawMember instanceof GuildMember ? rawMember : null,
|
|
2616
2386
|
userId: interaction.user.id,
|
|
2617
2387
|
guildId: interaction.guildId,
|
|
2618
2388
|
channelId: interaction.channelId,
|
|
2619
|
-
|
|
2620
|
-
|
|
2389
|
+
memberRoleIds,
|
|
2390
|
+
memberPermissions: interaction.memberPermissions?.bitfield ?? null,
|
|
2391
|
+
appPermissions: interaction.appPermissions.bitfield,
|
|
2392
|
+
memberGuildPermissions: rawMember instanceof GuildMember ? rawMember.permissions.bitfield : null,
|
|
2393
|
+
appGuildPermissions: interaction.guild?.members.me?.permissions.bitfield ?? null,
|
|
2394
|
+
routeId: null
|
|
2621
2395
|
};
|
|
2622
2396
|
}
|
|
2623
|
-
function
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2397
|
+
function eventGateContext(eventName, args, core) {
|
|
2398
|
+
const actor = deriveEventActor(args);
|
|
2399
|
+
return {
|
|
2400
|
+
kind: "event",
|
|
2401
|
+
core,
|
|
2402
|
+
eventName,
|
|
2403
|
+
payload: args,
|
|
2404
|
+
user: actor.user,
|
|
2405
|
+
guild: actor.guild,
|
|
2406
|
+
member: actor.member,
|
|
2407
|
+
userId: actor.user?.id ?? null,
|
|
2408
|
+
guildId: actor.guild?.id ?? null,
|
|
2409
|
+
channelId: actor.channelId,
|
|
2410
|
+
memberRoleIds: actor.member ? [...actor.member.roles.cache.keys()].filter((id) => id !== actor.guild?.id) : [],
|
|
2411
|
+
memberPermissions: actor.member?.permissions.bitfield ?? null,
|
|
2412
|
+
appPermissions: null,
|
|
2413
|
+
memberGuildPermissions: actor.member?.permissions.bitfield ?? null,
|
|
2414
|
+
appGuildPermissions: actor.guild?.members.me?.permissions.bitfield ?? null,
|
|
2415
|
+
routeId: null
|
|
2416
|
+
};
|
|
2629
2417
|
}
|
|
2630
2418
|
|
|
2631
2419
|
//#endregion
|
|
@@ -2640,7 +2428,7 @@ const logger$1 = new Logger("EventBoundary");
|
|
|
2640
2428
|
*/
|
|
2641
2429
|
function handleEventFault(caught, eventName, handlerName, args, core) {
|
|
2642
2430
|
if (caught instanceof Silence) {
|
|
2643
|
-
if (caught.reason !== void 0) logger$1.debug(`Silence: ${caught.reason}`);
|
|
2431
|
+
if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger$1.debug(`Silence: ${caught.reason}`);
|
|
2644
2432
|
return;
|
|
2645
2433
|
}
|
|
2646
2434
|
if (!Error.isError(caught)) throw caught;
|
|
@@ -2676,11 +2464,12 @@ var EventDispatcher = class {
|
|
|
2676
2464
|
core;
|
|
2677
2465
|
logger = new Logger("Events");
|
|
2678
2466
|
isInitialized = false;
|
|
2679
|
-
name = "Events";
|
|
2680
2467
|
eventMap = /* @__PURE__ */ new Map();
|
|
2681
2468
|
middlewares = [];
|
|
2682
2469
|
executedOnceHandlers = /* @__PURE__ */ new Set();
|
|
2683
2470
|
attachedEvents = /* @__PURE__ */ new Set();
|
|
2471
|
+
inFlight = /* @__PURE__ */ new Set();
|
|
2472
|
+
draining = false;
|
|
2684
2473
|
loading = false;
|
|
2685
2474
|
loadedHandlers = [];
|
|
2686
2475
|
loadedMiddlewares = [];
|
|
@@ -2849,15 +2638,22 @@ var EventDispatcher = class {
|
|
|
2849
2638
|
const handlerEntries = this.eventMap.get(eventName);
|
|
2850
2639
|
this.logger.debug(`Attaching ${paint.mint.bold(eventName)} to the client with ${paint.mute(handlerEntries?.length ?? 0)} handler(s)`);
|
|
2851
2640
|
this.core.bot.client.on(eventName, (...args) => {
|
|
2641
|
+
if (this.draining) return;
|
|
2852
2642
|
this.core.bot.emitSafe("any:event", eventName, ...args);
|
|
2853
|
-
(
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2643
|
+
const run = this.processEvent(eventName, args).catch((err) => {
|
|
2644
|
+
this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
|
|
2645
|
+
this.core.bot.emit("error:unhandled:event", err);
|
|
2646
|
+
});
|
|
2647
|
+
this.inFlight.add(run);
|
|
2648
|
+
run.finally(() => this.inFlight.delete(run));
|
|
2859
2649
|
});
|
|
2860
2650
|
}
|
|
2651
|
+
stopAccepting() {
|
|
2652
|
+
this.draining = true;
|
|
2653
|
+
}
|
|
2654
|
+
drain(timeoutMs) {
|
|
2655
|
+
return settleWithin(Promise.allSettled(this.inFlight), timeoutMs);
|
|
2656
|
+
}
|
|
2861
2657
|
async processEvent(eventName, args) {
|
|
2862
2658
|
const handlerEntries = this.eventMap.get(eventName);
|
|
2863
2659
|
if (!handlerEntries || handlerEntries.length === 0) return;
|
|
@@ -2885,35 +2681,55 @@ var EventDispatcher = class {
|
|
|
2885
2681
|
};
|
|
2886
2682
|
|
|
2887
2683
|
//#endregion
|
|
2888
|
-
//#region src/bot/defaults/
|
|
2684
|
+
//#region src/bot/defaults/UnhandledRepliable.ts
|
|
2889
2685
|
/**
|
|
2890
|
-
* Default handler for unhandled interactions.
|
|
2891
|
-
* raw fault, so it needs no decorator of its own.
|
|
2892
|
-
*
|
|
2893
|
-
* @internal
|
|
2686
|
+
* @internal Default handler for unhandled repliable interactions.
|
|
2894
2687
|
*/
|
|
2895
|
-
var
|
|
2688
|
+
var UnhandledRepliable = class extends InteractionHandler {
|
|
2896
2689
|
async execute() {
|
|
2897
|
-
await this.
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2690
|
+
await this.reply("Feature not implemented yet.");
|
|
2691
|
+
}
|
|
2692
|
+
};
|
|
2693
|
+
|
|
2694
|
+
//#endregion
|
|
2695
|
+
//#region src/bot/defaults/UnhandledAutocomplete.ts
|
|
2696
|
+
/**
|
|
2697
|
+
* @internal Default handler for an autocomplete with no registered handler.
|
|
2698
|
+
*/
|
|
2699
|
+
var UnhandledAutocomplete = class extends AutocompleteHandler {
|
|
2700
|
+
async execute() {
|
|
2701
|
+
await this.respond([]);
|
|
2901
2702
|
}
|
|
2902
2703
|
};
|
|
2903
2704
|
|
|
2705
|
+
//#endregion
|
|
2706
|
+
//#region src/bot/harmlessApiCodes.ts
|
|
2707
|
+
/**
|
|
2708
|
+
* Discord API error codes that mean a dead end on an interaction, a dead token, a double ack, or a gone
|
|
2709
|
+
* message. Nothing to show the user and no bug to report, so the boundary's fault-card send drops them.
|
|
2710
|
+
*
|
|
2711
|
+
* @internal
|
|
2712
|
+
*/
|
|
2713
|
+
const HARMLESS_API_CODES = new Set([
|
|
2714
|
+
RESTJSONErrorCodes.UnknownInteraction,
|
|
2715
|
+
RESTJSONErrorCodes.InteractionHasAlreadyBeenAcknowledged,
|
|
2716
|
+
RESTJSONErrorCodes.UnknownMessage
|
|
2717
|
+
]);
|
|
2718
|
+
|
|
2904
2719
|
//#endregion
|
|
2905
2720
|
//#region src/bot/handleInteractionFault.ts
|
|
2906
2721
|
const logger = new Logger("InteractionBoundary");
|
|
2907
2722
|
/**
|
|
2908
2723
|
* The interaction controller boundary. Sorts a throw from any handler-lifecycle phase into the right
|
|
2909
|
-
* user reply plus the right bus event
|
|
2910
|
-
*
|
|
2724
|
+
* user reply plus the right bus event. When a handler is built its sender is passed in, so the boundary
|
|
2725
|
+
* sends from the current ack state. A middleware or pre-construction throw arrives with no sender, so the
|
|
2726
|
+
* boundary builds one from the interaction flags.
|
|
2911
2727
|
*
|
|
2912
2728
|
* @internal
|
|
2913
2729
|
*/
|
|
2914
|
-
async function handleInteractionFault(caught, interaction, core) {
|
|
2730
|
+
async function handleInteractionFault(caught, interaction, core, sender) {
|
|
2915
2731
|
if (caught instanceof Silence) {
|
|
2916
|
-
if (caught.reason !== void 0) logger.debug(`Silence: ${caught.reason}`);
|
|
2732
|
+
if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger.debug(`Silence: ${caught.reason}`);
|
|
2917
2733
|
return;
|
|
2918
2734
|
}
|
|
2919
2735
|
if (!Error.isError(caught)) throw caught;
|
|
@@ -2937,7 +2753,18 @@ async function handleInteractionFault(caught, interaction, core) {
|
|
|
2937
2753
|
user: interaction.user,
|
|
2938
2754
|
metadata: interaction
|
|
2939
2755
|
});
|
|
2940
|
-
await new ReplySender(interaction)
|
|
2756
|
+
await sendGuarded(sender ?? new ReplySender(interaction, interactionRoute(interaction)), response, caught instanceof Notice ? caught.ephemeral : true);
|
|
2757
|
+
}
|
|
2758
|
+
async function sendGuarded(sender, response, ephemeral) {
|
|
2759
|
+
try {
|
|
2760
|
+
await sender.send(response, { ephemeral });
|
|
2761
|
+
} catch (error) {
|
|
2762
|
+
if (error instanceof DiscordAPIError && HARMLESS_API_CODES.has(error.code)) {
|
|
2763
|
+
logger.debug(`reply send hit harmless code ${error.code}`);
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2766
|
+
logger.error("reply send failed", error);
|
|
2767
|
+
}
|
|
2941
2768
|
}
|
|
2942
2769
|
|
|
2943
2770
|
//#endregion
|
|
@@ -2952,7 +2779,6 @@ var InteractionDispatcher = class {
|
|
|
2952
2779
|
core;
|
|
2953
2780
|
logger = new Logger("Interactions");
|
|
2954
2781
|
isInitialized = false;
|
|
2955
|
-
name = "Interactions";
|
|
2956
2782
|
slashMap = /* @__PURE__ */ new Map();
|
|
2957
2783
|
buttonMap = /* @__PURE__ */ new Map();
|
|
2958
2784
|
modalMap = /* @__PURE__ */ new Map();
|
|
@@ -2966,6 +2792,8 @@ var InteractionDispatcher = class {
|
|
|
2966
2792
|
autocompleteMap = /* @__PURE__ */ new Map();
|
|
2967
2793
|
keysToIgnore = /* @__PURE__ */ new Set();
|
|
2968
2794
|
middlewares = [];
|
|
2795
|
+
inFlight = /* @__PURE__ */ new Set();
|
|
2796
|
+
draining = false;
|
|
2969
2797
|
loading = false;
|
|
2970
2798
|
loadedHandlers = [];
|
|
2971
2799
|
loadedMiddlewares = [];
|
|
@@ -3006,7 +2834,7 @@ var InteractionDispatcher = class {
|
|
|
3006
2834
|
}
|
|
3007
2835
|
/**
|
|
3008
2836
|
* Warns for each command route with no registered `@SlashRoute` handler, which would fall through to
|
|
3009
|
-
*
|
|
2837
|
+
* UnhandledRepliable at runtime. A bot may route commands outside the registry, so this does not throw.
|
|
3010
2838
|
*
|
|
3011
2839
|
* @internal
|
|
3012
2840
|
*/
|
|
@@ -3014,7 +2842,7 @@ var InteractionDispatcher = class {
|
|
|
3014
2842
|
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.`);
|
|
3015
2843
|
}
|
|
3016
2844
|
/**
|
|
3017
|
-
* Warns for each context-menu command with no matching handler, which would fall through to
|
|
2845
|
+
* Warns for each context-menu command with no matching handler, which would fall through to UnhandledRepliable
|
|
3018
2846
|
* at runtime. Checked per kind since a user and a message command can share a name.
|
|
3019
2847
|
*
|
|
3020
2848
|
* @internal
|
|
@@ -3166,35 +2994,53 @@ var InteractionDispatcher = class {
|
|
|
3166
2994
|
}
|
|
3167
2995
|
attachToClient() {
|
|
3168
2996
|
this.core.bot.client.on(Events.InteractionCreate, (interaction) => {
|
|
2997
|
+
if (this.draining) return;
|
|
3169
2998
|
this.core.bot.emitSafe("any:interaction", interaction);
|
|
3170
|
-
this.handleInteraction(interaction).catch((err) => {
|
|
2999
|
+
const run = this.handleInteraction(interaction).catch((err) => {
|
|
3171
3000
|
this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
|
|
3172
3001
|
this.core.bot.emit("error:unhandled:interaction", err);
|
|
3173
3002
|
});
|
|
3003
|
+
this.inFlight.add(run);
|
|
3004
|
+
run.finally(() => this.inFlight.delete(run));
|
|
3174
3005
|
});
|
|
3175
3006
|
}
|
|
3007
|
+
stopAccepting() {
|
|
3008
|
+
this.draining = true;
|
|
3009
|
+
}
|
|
3010
|
+
drain(timeoutMs) {
|
|
3011
|
+
return settleWithin(Promise.allSettled(this.inFlight), timeoutMs);
|
|
3012
|
+
}
|
|
3176
3013
|
async handleCustomIdInteraction(interaction, getMap, interactionType) {
|
|
3177
3014
|
if ([...this.keysToIgnore].some((matcher) => matcher.owns(interaction.customId))) return;
|
|
3178
3015
|
const prefix = prefixOf(interaction.customId);
|
|
3179
3016
|
if (!prefix) return this.logger.warn(`${interactionType} has invalid customId: ${interaction.customId}`);
|
|
3180
3017
|
await this.processInteraction(interaction, () => prefix, (key) => getMap().get(key));
|
|
3181
3018
|
}
|
|
3182
|
-
async processInteraction(interaction, extractKey, getHandler) {
|
|
3019
|
+
async processInteraction(interaction, extractKey, getHandler, fallback = UnhandledRepliable) {
|
|
3183
3020
|
const key = extractKey(interaction);
|
|
3021
|
+
let sender;
|
|
3184
3022
|
try {
|
|
3185
3023
|
if (!interaction.isAutocomplete()) for (const { ctor } of this.middlewares) await new ctor(interaction, this.core).execute();
|
|
3186
3024
|
let HandlerCtor = getHandler(key);
|
|
3187
3025
|
if (!HandlerCtor) {
|
|
3188
|
-
this.logger.warn(`No handler found for key ${paint.sky.bold(key)}. Falling back to
|
|
3189
|
-
HandlerCtor =
|
|
3026
|
+
this.logger.warn(`No handler found for key ${paint.sky.bold(key)}. Falling back to ${fallback.name}.`);
|
|
3027
|
+
HandlerCtor = fallback;
|
|
3190
3028
|
}
|
|
3191
3029
|
this.logger.debug(`Processing ${paint.mint.bold(key)} with ${paint.mute(HandlerCtor.name)}`);
|
|
3192
3030
|
const dispatch = new DispatchContext(routeIdOf(HandlerCtor));
|
|
3193
3031
|
const handler = new HandlerCtor(interaction, this.core, dispatch);
|
|
3194
|
-
if (
|
|
3032
|
+
if (handler instanceof RepliableHandler) sender = handler.getSender();
|
|
3033
|
+
if (!interaction.isAutocomplete()) {
|
|
3034
|
+
const monitor = slowGateMonitor();
|
|
3035
|
+
try {
|
|
3036
|
+
await runHandlerGates(HandlerCtor, interactionGateContext(interaction, this.core), dispatch.routeId ?? void 0, monitor?.observe);
|
|
3037
|
+
} finally {
|
|
3038
|
+
monitor?.report(dispatch.routeId);
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3195
3041
|
await handler.execute();
|
|
3196
3042
|
} catch (caught) {
|
|
3197
|
-
await handleInteractionFault(caught, interaction, this.core);
|
|
3043
|
+
await handleInteractionFault(caught, interaction, this.core, sender);
|
|
3198
3044
|
}
|
|
3199
3045
|
}
|
|
3200
3046
|
async handleInteraction(interaction) {
|
|
@@ -3270,12 +3116,7 @@ var InteractionDispatcher = class {
|
|
|
3270
3116
|
}
|
|
3271
3117
|
async handleAutocomplete(interaction) {
|
|
3272
3118
|
const route = slashRouteOf(interaction);
|
|
3273
|
-
|
|
3274
|
-
this.logger.warn(`No autocomplete handler for ${paint.sky.bold(route)}.`);
|
|
3275
|
-
await interaction.respond([]);
|
|
3276
|
-
return;
|
|
3277
|
-
}
|
|
3278
|
-
await this.processInteraction(interaction, () => route, (key) => this.autocompleteMap.get(key));
|
|
3119
|
+
await this.processInteraction(interaction, () => route, (key) => this.autocompleteMap.get(key), UnhandledAutocomplete);
|
|
3279
3120
|
}
|
|
3280
3121
|
};
|
|
3281
3122
|
|
|
@@ -3296,23 +3137,25 @@ function __decorate(decorators, target, key, desc) {
|
|
|
3296
3137
|
|
|
3297
3138
|
//#endregion
|
|
3298
3139
|
//#region src/bot/Bot.ts
|
|
3140
|
+
const DISPATCH_DRAIN_TIMEOUT_MS = 5e3;
|
|
3141
|
+
const DRAIN_TASK_TIMEOUT_MS = 6e3;
|
|
3142
|
+
const UNBIND_TIMEOUT_MS = 2e3;
|
|
3143
|
+
const LOGOUT_TIMEOUT_MS = 2e3;
|
|
3299
3144
|
/**
|
|
3300
|
-
* Discord
|
|
3301
|
-
*
|
|
3302
|
-
* Don't instantiate this class directly. Use `core.bot` instead.
|
|
3145
|
+
* The Discord client and its controllers. Access it through `core.bot`.
|
|
3303
3146
|
*/
|
|
3304
|
-
var Bot = class extends
|
|
3147
|
+
var Bot = class extends TypedEventEmitter {
|
|
3305
3148
|
/** @internal */
|
|
3306
3149
|
logger = new Logger("Bot");
|
|
3307
3150
|
isInitialized = false;
|
|
3308
3151
|
_client;
|
|
3309
3152
|
interactions;
|
|
3310
3153
|
events;
|
|
3311
|
-
commands;
|
|
3312
3154
|
emojiInjector;
|
|
3155
|
+
commands;
|
|
3313
3156
|
emojis = Emojis;
|
|
3314
3157
|
mentions = CommandMentions;
|
|
3315
|
-
/** @internal
|
|
3158
|
+
/** @internal */
|
|
3316
3159
|
async onHmr(event) {
|
|
3317
3160
|
if (this.interactions) await this.interactions.onHmr(event);
|
|
3318
3161
|
if (this.events) await this.events.onHmr(event);
|
|
@@ -3320,7 +3163,7 @@ var Bot = class extends Plugin {
|
|
|
3320
3163
|
}
|
|
3321
3164
|
/** @internal */
|
|
3322
3165
|
constructor(core) {
|
|
3323
|
-
super(
|
|
3166
|
+
super();
|
|
3324
3167
|
this._client = new Client(core.config.bot.clientOptions);
|
|
3325
3168
|
if (core.config.bot.interactions.path) this.interactions = new InteractionDispatcher(core);
|
|
3326
3169
|
if (core.config.bot.events.path) this.events = new EventDispatcher(core);
|
|
@@ -3331,12 +3174,24 @@ var Bot = class extends Plugin {
|
|
|
3331
3174
|
});
|
|
3332
3175
|
}
|
|
3333
3176
|
this.emojiInjector = new EmojiInjector(core);
|
|
3334
|
-
|
|
3177
|
+
this.registerShutdownTasks(core);
|
|
3335
3178
|
}
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3179
|
+
registerShutdownTasks(core) {
|
|
3180
|
+
core.shutdown.addTask(ShutdownPhase.Unbind, "stop-dispatch", () => {
|
|
3181
|
+
this.stopAccepting();
|
|
3182
|
+
return Promise.resolve();
|
|
3183
|
+
}, UNBIND_TIMEOUT_MS);
|
|
3184
|
+
core.shutdown.addTask(ShutdownPhase.Drain, "drain-dispatch", () => this.drain(), DRAIN_TASK_TIMEOUT_MS);
|
|
3185
|
+
core.shutdown.addTask(ShutdownPhase.Logout, "stop-bot", async () => await this.stop(), LOGOUT_TIMEOUT_MS);
|
|
3186
|
+
}
|
|
3187
|
+
stopAccepting() {
|
|
3188
|
+
this.interactions?.stopAccepting();
|
|
3189
|
+
this.events?.stopAccepting();
|
|
3190
|
+
}
|
|
3191
|
+
async drain() {
|
|
3192
|
+
await Promise.allSettled([this.interactions?.drain(DISPATCH_DRAIN_TIMEOUT_MS), this.events?.drain(DISPATCH_DRAIN_TIMEOUT_MS)]);
|
|
3193
|
+
}
|
|
3194
|
+
/** @internal */
|
|
3340
3195
|
async init() {
|
|
3341
3196
|
if (this.isInitialized) return;
|
|
3342
3197
|
this.isInitialized = true;
|
|
@@ -3352,25 +3207,18 @@ var Bot = class extends Plugin {
|
|
|
3352
3207
|
this.interactions?.warnUnhandledContextMenuRoutes(this.commands.contextMenuLeaves());
|
|
3353
3208
|
}
|
|
3354
3209
|
}
|
|
3355
|
-
/**
|
|
3356
|
-
* Stops the bot and cleans up connections
|
|
3357
|
-
* @internal
|
|
3358
|
-
*/
|
|
3210
|
+
/** @internal */
|
|
3359
3211
|
async stop() {
|
|
3360
3212
|
this._client.removeAllListeners();
|
|
3361
3213
|
await this.logout();
|
|
3362
3214
|
}
|
|
3363
|
-
/**
|
|
3364
|
-
* Logs the bot into Discord using the configured token
|
|
3365
|
-
*/
|
|
3366
3215
|
async login(token) {
|
|
3367
|
-
|
|
3216
|
+
this._client.once(Events.ClientReady, () => this.emit("ready"));
|
|
3217
|
+
this._client.login(token);
|
|
3218
|
+
await this.waitFor("ready");
|
|
3368
3219
|
this.logger.info(`Logged in as ${chalk.bold.magenta(this._client.user?.username)}!`);
|
|
3369
3220
|
return this;
|
|
3370
3221
|
}
|
|
3371
|
-
/**
|
|
3372
|
-
* Logs out and destroys the Discord client connection
|
|
3373
|
-
*/
|
|
3374
3222
|
async logout() {
|
|
3375
3223
|
await this._client.destroy();
|
|
3376
3224
|
this.logger.info(chalk.bold.red("Logged out of Discord!"));
|
|
@@ -3378,6 +3226,12 @@ var Bot = class extends Plugin {
|
|
|
3378
3226
|
get client() {
|
|
3379
3227
|
return this._client;
|
|
3380
3228
|
}
|
|
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
|
+
*/
|
|
3381
3235
|
emit(event, ...args) {
|
|
3382
3236
|
return super.emit(event, ...args);
|
|
3383
3237
|
}
|
|
@@ -3390,157 +3244,70 @@ var Bot = class extends Plugin {
|
|
|
3390
3244
|
};
|
|
3391
3245
|
__decorate([Envapt("DISCORD_BOT_TOKEN", { converter: (raw) => validateDiscordToken(raw) }), __decorateMetadata("design:type", String)], Bot.prototype, "botToken", void 0);
|
|
3392
3246
|
|
|
3393
|
-
//#endregion
|
|
3394
|
-
//#region src/subscribers/bases/webhookHelpers.ts
|
|
3395
|
-
const DISCORD_WEBHOOK_REGEX = new RegExp(String.raw`^https?:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w$-]+$`);
|
|
3396
|
-
/**
|
|
3397
|
-
* Whether a string is a well-formed Discord webhook URL.
|
|
3398
|
-
*
|
|
3399
|
-
* @internal
|
|
3400
|
-
*/
|
|
3401
|
-
function isDiscordWebhookUrl(value) {
|
|
3402
|
-
return URL.canParse(value) && DISCORD_WEBHOOK_REGEX.test(value);
|
|
3403
|
-
}
|
|
3404
|
-
/**
|
|
3405
|
-
* Serializes a value to a JSON attachment, made circular-safe first.
|
|
3406
|
-
*
|
|
3407
|
-
* @internal
|
|
3408
|
-
*/
|
|
3409
|
-
function jsonAttachment(name, description, data) {
|
|
3410
|
-
const content = filterCirculars(data);
|
|
3411
|
-
return new AttachmentBuilder(Buffer.from(JSON.stringify(content, void 0, 2), "utf8"), {
|
|
3412
|
-
name,
|
|
3413
|
-
description
|
|
3414
|
-
});
|
|
3415
|
-
}
|
|
3416
|
-
/**
|
|
3417
|
-
* The small divider the default webhook reporters place between sections.
|
|
3418
|
-
*
|
|
3419
|
-
* @internal
|
|
3420
|
-
*/
|
|
3421
|
-
var WebhookSeparator = class extends BuilderComponent {
|
|
3422
|
-
constructor() {
|
|
3423
|
-
super("separator");
|
|
3424
|
-
this.instance.setSpacing(SeparatorSpacingSize.Small).setDivider(true);
|
|
3425
|
-
}
|
|
3426
|
-
};
|
|
3427
|
-
|
|
3428
3247
|
//#endregion
|
|
3429
3248
|
//#region src/subscribers/default/HandledException.ts
|
|
3430
|
-
var _HandledException;
|
|
3431
|
-
function webhookUrlValidator$1(raw) {
|
|
3432
|
-
if (raw !== null && typeof raw !== "string") throw new SeedcordError(SeedcordErrorCode.ConfigHandledExceptionWebhookInvalid);
|
|
3433
|
-
const value = raw?.trim() ?? "";
|
|
3434
|
-
if (value === "") throw new SeedcordError(SeedcordErrorCode.ConfigHandledExceptionWebhookMissing);
|
|
3435
|
-
if (!isDiscordWebhookUrl(value)) throw new SeedcordError(SeedcordErrorCode.ConfigHandledExceptionWebhookInvalid);
|
|
3436
|
-
return value;
|
|
3437
|
-
}
|
|
3438
3249
|
let HandledException = class HandledException extends WebhookLog {
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
try {
|
|
3446
|
-
await this.webhook.send({
|
|
3447
|
-
flags: flagsFor(false),
|
|
3448
|
-
withComponents: true,
|
|
3449
|
-
username: "Handled Exception",
|
|
3450
|
-
components: [new HandledExceptionContainer(this.data).component],
|
|
3451
|
-
files: [jsonAttachment("source.json", "The raw source the fault came from", source.raw)]
|
|
3452
|
-
});
|
|
3453
|
-
} catch (error) {
|
|
3454
|
-
this.logger.error("Failed to send handled exception webhook", error);
|
|
3455
|
-
}
|
|
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
|
+
};
|
|
3456
3256
|
}
|
|
3457
3257
|
};
|
|
3458
|
-
__decorate([
|
|
3459
|
-
HandledException = _HandledException = __decorate([Subscribe("handledException")], HandledException);
|
|
3258
|
+
HandledException = __decorate([Subscribe("handledException"), WebhookUrl("HANDLED_EXCEPTION_WEBHOOK_URL")], HandledException);
|
|
3460
3259
|
var HandledExceptionContainer = class extends BuilderComponent {
|
|
3461
3260
|
constructor(data) {
|
|
3462
3261
|
super("container");
|
|
3463
3262
|
const { denial, uuid, source } = data;
|
|
3464
|
-
this.instance.addTextDisplayComponents((text) => text.setContent(faultSummary(denial, source))).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### UUID \`${uuid}\`\n\`\`\`${causeStack(denial)}\`\`\``)).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent("### Source")).addFileComponents((file) => file.setURL("attachment://source.json"));
|
|
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"));
|
|
3465
3264
|
}
|
|
3466
3265
|
};
|
|
3467
|
-
/**
|
|
3468
|
-
* The summary block of a fault report, narrowed per source kind so an interaction fault shows its
|
|
3469
|
-
* command and customId while an event fault shows its event name and handler.
|
|
3470
|
-
*
|
|
3471
|
-
* @internal
|
|
3472
|
-
*/
|
|
3473
3266
|
function faultSummary(denial, source) {
|
|
3474
|
-
const head = `### A handled fault was reported: \`${denial.name}\`\n**Message:** ${denial.message}\n
|
|
3267
|
+
const head = stripAnsi(`### A handled fault was reported: \`${denial.name}\`\n**Message:** ${denial.message}\n`);
|
|
3475
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"}\``;
|
|
3476
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}\``;
|
|
3477
3270
|
}
|
|
3478
|
-
/**
|
|
3479
|
-
* The text shown for a denial's cause in the report. Tolerates a non-serializable cause (a bigint or a
|
|
3480
|
-
* circular object) so building the report never throws.
|
|
3481
|
-
*
|
|
3482
|
-
* @internal
|
|
3483
|
-
*/
|
|
3484
3271
|
function causeStack(denial) {
|
|
3485
3272
|
const { cause } = denial;
|
|
3486
|
-
if (Error.isError(cause)) return cause.stack ?? cause.message;
|
|
3487
|
-
if (typeof cause === "string") return cause;
|
|
3273
|
+
if (Error.isError(cause)) return stripAnsi(cause.stack ?? cause.message);
|
|
3274
|
+
if (typeof cause === "string") return stripAnsi(cause);
|
|
3488
3275
|
if (typeof cause === "bigint" || typeof cause === "symbol" || typeof cause === "function") return cause.toString();
|
|
3489
3276
|
if (cause !== void 0) try {
|
|
3490
3277
|
return JSON.stringify(cause);
|
|
3491
3278
|
} catch {
|
|
3492
3279
|
return "[unserializable cause]";
|
|
3493
3280
|
}
|
|
3494
|
-
return denial.stack ?? "No cause recorded.";
|
|
3281
|
+
return stripAnsi(denial.stack ?? "No cause recorded.");
|
|
3495
3282
|
}
|
|
3496
3283
|
|
|
3497
3284
|
//#endregion
|
|
3498
3285
|
//#region src/subscribers/default/UnknownException.ts
|
|
3499
|
-
var _UnknownException;
|
|
3500
|
-
function webhookUrlValidator(raw) {
|
|
3501
|
-
if (raw !== null && typeof raw !== "string") throw new SeedcordError(SeedcordErrorCode.ConfigUnknownExceptionWebhookInvalid);
|
|
3502
|
-
const value = raw?.trim() ?? "";
|
|
3503
|
-
if (value === "") throw new SeedcordError(SeedcordErrorCode.ConfigUnknownExceptionWebhookMissing);
|
|
3504
|
-
if (!isDiscordWebhookUrl(value)) throw new SeedcordError(SeedcordErrorCode.ConfigUnknownExceptionWebhookInvalid);
|
|
3505
|
-
return value;
|
|
3506
|
-
}
|
|
3507
3286
|
let UnknownException = class UnknownException extends WebhookLog {
|
|
3508
|
-
|
|
3509
|
-
_UnknownException = this;
|
|
3510
|
-
}
|
|
3511
|
-
webhook = new WebhookClient({ url: _UnknownException.unknownExceptionWebhookUrl });
|
|
3512
|
-
async execute() {
|
|
3287
|
+
report() {
|
|
3513
3288
|
const { metadata } = this.data;
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
username: "Unknown Exception",
|
|
3520
|
-
components: [new UnhandledErrorContainer(this.data).component],
|
|
3521
|
-
files: metadataFile ? [metadataFile] : []
|
|
3522
|
-
});
|
|
3523
|
-
} catch (error) {
|
|
3524
|
-
this.logger.error("Failed to send unknown exception webhook", error);
|
|
3525
|
-
}
|
|
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
|
+
};
|
|
3526
3294
|
}
|
|
3527
3295
|
};
|
|
3528
|
-
__decorate([
|
|
3529
|
-
UnknownException = _UnknownException = __decorate([Subscribe("unknownException")], UnknownException);
|
|
3296
|
+
UnknownException = __decorate([Subscribe("unknownException"), WebhookUrl("UNKNOWN_EXCEPTION_WEBHOOK_URL")], UnknownException);
|
|
3530
3297
|
var UnhandledErrorContainer = class extends BuilderComponent {
|
|
3531
3298
|
constructor(data) {
|
|
3532
3299
|
super("container");
|
|
3533
3300
|
const { uuid, error, guild, user, metadata } = data;
|
|
3534
|
-
this.instance.addTextDisplayComponents((text) => text.setContent(`### An unknown exception was thrown\n**Guild ID:** \`${guild?.id ?? "Not used in a guild"}\`\n**Guild Name:** ${guild?.name ?? "Not used in a guild"}\n**User ID:** \`${user?.id ?? "Missing user info"}\`\n**Username:** ${user?.username ?? "Missing user info"}\n`)).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### UUID \`${uuid}\`\n\`\`\`${error
|
|
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)}\`\`\``));
|
|
3535
3302
|
this.addTimestampsIfAvailable(error);
|
|
3536
3303
|
this.addMetadataIfAvailable(metadata);
|
|
3537
3304
|
}
|
|
3538
3305
|
addTimestampsIfAvailable(error) {
|
|
3539
|
-
if (!(error instanceof DiscordAPIError)) return;
|
|
3306
|
+
if (!(error instanceof DiscordAPIError$1)) return;
|
|
3540
3307
|
const now = Date.now();
|
|
3541
3308
|
const snowflake = error.url.match(/\/interactions\/(\d+)\//)?.[1];
|
|
3542
3309
|
if (!snowflake) return void 0;
|
|
3543
|
-
const interactionTs =
|
|
3310
|
+
const interactionTs = timestampFromSnowflake(snowflake);
|
|
3544
3311
|
const diff = now - interactionTs;
|
|
3545
3312
|
const seconds = Math.floor(diff / 1e3);
|
|
3546
3313
|
const millis = diff % 1e3;
|
|
@@ -3555,24 +3322,19 @@ var UnhandledErrorContainer = class extends BuilderComponent {
|
|
|
3555
3322
|
//#endregion
|
|
3556
3323
|
//#region src/subscribers/Bus.ts
|
|
3557
3324
|
/**
|
|
3558
|
-
*
|
|
3559
|
-
*
|
|
3560
|
-
* Provides a centralized system for registering and executing custom subscribers
|
|
3561
|
-
* throughout the application lifecycle. Bus subscribers are loaded from configured directories
|
|
3562
|
-
* and can be triggered programmatically or by framework events. Accessed via `core.bus`. Do not
|
|
3563
|
-
* construct it directly.
|
|
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.
|
|
3564
3327
|
*/
|
|
3565
|
-
var Bus = class extends
|
|
3328
|
+
var Bus = class extends TypedEventEmitter {
|
|
3566
3329
|
core;
|
|
3567
3330
|
logger = new Logger("Subscribers");
|
|
3568
|
-
/** @internal */
|
|
3569
|
-
name = "Subscribers";
|
|
3570
3331
|
isInitialized = false;
|
|
3571
3332
|
subscribersMap = /* @__PURE__ */ new Map();
|
|
3572
3333
|
executedOnceHandlers = /* @__PURE__ */ new Set();
|
|
3334
|
+
webhookProbes = /* @__PURE__ */ new Map();
|
|
3573
3335
|
hmrHandler;
|
|
3574
3336
|
constructor(core) {
|
|
3575
|
-
super(
|
|
3337
|
+
super();
|
|
3576
3338
|
this.core = core;
|
|
3577
3339
|
if (this.core.config.subscribers.path) {
|
|
3578
3340
|
if (!Envapter.isDevelopment && !Envapter.isTest) return;
|
|
@@ -3595,7 +3357,6 @@ var Bus = class extends Plugin {
|
|
|
3595
3357
|
this.isInitialized = true;
|
|
3596
3358
|
this.registerSubscriber(UnknownException);
|
|
3597
3359
|
this.registerSubscriber(HandledException);
|
|
3598
|
-
Envapter.require("UNKNOWN_EXCEPTION_WEBHOOK_URL", "HANDLED_EXCEPTION_WEBHOOK_URL");
|
|
3599
3360
|
const subscribersDir = this.core.config.subscribers.path;
|
|
3600
3361
|
if (subscribersDir) {
|
|
3601
3362
|
this.logger.info(chalk.bold(subscribersDir));
|
|
@@ -3603,8 +3364,19 @@ var Bus = class extends Plugin {
|
|
|
3603
3364
|
const totalSubscribers = [...this.subscribersMap.values()].reduce((acc, handlers) => acc + handlers.length, 0);
|
|
3604
3365
|
this.logger.utils.list([`${chalk.bold.magenta(totalSubscribers)} subscribers`], chalk.bold.green("Loaded"));
|
|
3605
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(", ")]);
|
|
3606
3378
|
}
|
|
3607
|
-
/** @internal
|
|
3379
|
+
/** @internal */
|
|
3608
3380
|
async onHmr(event) {
|
|
3609
3381
|
if (this.hmrHandler) await this.hmrHandler.handle(event);
|
|
3610
3382
|
}
|
|
@@ -3621,6 +3393,17 @@ var Bus = class extends Plugin {
|
|
|
3621
3393
|
}, this.logger);
|
|
3622
3394
|
}
|
|
3623
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
|
+
}
|
|
3624
3407
|
const options = Reflect.getMetadata(SubscribeMetadataKey, handler);
|
|
3625
3408
|
let handlers = this.subscribersMap.get(options.subscriber);
|
|
3626
3409
|
if (!handlers) {
|
|
@@ -3680,15 +3463,13 @@ var Bus = class extends Plugin {
|
|
|
3680
3463
|
//#endregion
|
|
3681
3464
|
//#region src/version.ts
|
|
3682
3465
|
/** Package version */
|
|
3683
|
-
const version = "0.1.0-next.
|
|
3466
|
+
const version = "0.1.0-next.4";
|
|
3684
3467
|
|
|
3685
3468
|
//#endregion
|
|
3686
3469
|
//#region src/Seedcord.ts
|
|
3687
3470
|
/**
|
|
3688
|
-
*
|
|
3689
|
-
*
|
|
3690
|
-
* Primary entry point for creating Discord bots with Seedcord.
|
|
3691
|
-
* Manages component lifecycle and provides plugin support.
|
|
3471
|
+
* The gateway bot host. Opens a discord.js gateway session, discovers handlers, and runs
|
|
3472
|
+
* coordinated startup and shutdown. Attach plugins with `attach()`.
|
|
3692
3473
|
*/
|
|
3693
3474
|
var Seedcord = class Seedcord extends Pluggable {
|
|
3694
3475
|
config;
|
|
@@ -3698,7 +3479,6 @@ var Seedcord = class Seedcord extends Pluggable {
|
|
|
3698
3479
|
augmentTarget = "@seedcord/gateway";
|
|
3699
3480
|
/** @internal */
|
|
3700
3481
|
version = version;
|
|
3701
|
-
static isInstantiated = false;
|
|
3702
3482
|
healthCheck;
|
|
3703
3483
|
hmrManager;
|
|
3704
3484
|
/** @see {@link CoordinatedShutdown} */
|
|
@@ -3712,15 +3492,11 @@ var Seedcord = class Seedcord extends Pluggable {
|
|
|
3712
3492
|
/** @see {@link IRateLimiter} */
|
|
3713
3493
|
rateLimiter;
|
|
3714
3494
|
/**
|
|
3715
|
-
* Creates a new Seedcord instance
|
|
3716
|
-
*
|
|
3717
3495
|
* @param config - Bot configuration including paths and Discord client options
|
|
3718
3496
|
* @throws A **SeedcordError** When attempting to create multiple instances (singleton)
|
|
3719
3497
|
*/
|
|
3720
3498
|
constructor(config) {
|
|
3721
|
-
|
|
3722
|
-
Seedcord.isInstantiated = true;
|
|
3723
|
-
const shutdown = new CoordinatedShutdown(config.shutdownEnabled);
|
|
3499
|
+
const shutdown = new CoordinatedShutdown();
|
|
3724
3500
|
const startup = new CoordinatedStartup();
|
|
3725
3501
|
super(shutdown, startup);
|
|
3726
3502
|
this.config = config;
|
|
@@ -3733,15 +3509,22 @@ var Seedcord = class Seedcord extends Pluggable {
|
|
|
3733
3509
|
this.bus = new Bus(this);
|
|
3734
3510
|
this.bot = new Bot(this);
|
|
3735
3511
|
this.rateLimiter = config.store ?? new MemoryRateLimiter();
|
|
3736
|
-
this.healthCheck =
|
|
3512
|
+
this.healthCheck = HealthCheck.fromOption(this.shutdown, config.healthCheck);
|
|
3737
3513
|
this.registerStartupTasks();
|
|
3738
3514
|
}
|
|
3739
3515
|
/** The bot's discord username, populated after login. */
|
|
3740
3516
|
get username() {
|
|
3741
3517
|
return this.bot.client.user?.username;
|
|
3742
3518
|
}
|
|
3519
|
+
pluginCapabilities() {
|
|
3520
|
+
return {
|
|
3521
|
+
client: this.bot.client,
|
|
3522
|
+
token: this.bot.botToken,
|
|
3523
|
+
rest: this.bot.client.rest
|
|
3524
|
+
};
|
|
3525
|
+
}
|
|
3743
3526
|
static reset() {
|
|
3744
|
-
|
|
3527
|
+
super.reset();
|
|
3745
3528
|
LoggerChannelRegistry.instance.reset();
|
|
3746
3529
|
}
|
|
3747
3530
|
registerStartupTasks() {
|
|
@@ -3751,24 +3534,31 @@ var Seedcord = class Seedcord extends Pluggable {
|
|
|
3751
3534
|
await this.bus.init();
|
|
3752
3535
|
this.bus.logger.utils.initialization("Subscribers", "end");
|
|
3753
3536
|
});
|
|
3754
|
-
this.startup.addTask(StartupPhase.
|
|
3537
|
+
this.startup.addTask(StartupPhase.Login, "Bot Initialization", async () => {
|
|
3755
3538
|
this.bot.logger.utils.initialization("Bot", "start");
|
|
3756
3539
|
await this.bot.init();
|
|
3757
3540
|
this.bot.logger.utils.initialization("Bot", "end");
|
|
3758
3541
|
});
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3542
|
+
const { healthCheck } = this;
|
|
3543
|
+
if (healthCheck) this.startup.addTask(StartupPhase.Ready, "Health Check", async () => {
|
|
3544
|
+
healthCheck.logger.utils.initialization("HealthCheck", "start");
|
|
3545
|
+
await healthCheck.init();
|
|
3546
|
+
healthCheck.logger.utils.initialization("HealthCheck", "end");
|
|
3763
3547
|
});
|
|
3764
3548
|
}
|
|
3765
3549
|
/**
|
|
3766
3550
|
* Starts the bot and runs all initialization tasks
|
|
3767
3551
|
*
|
|
3768
|
-
* @returns This Seedcord instance when
|
|
3552
|
+
* @returns This Seedcord instance when initialized
|
|
3769
3553
|
*/
|
|
3770
3554
|
async start() {
|
|
3771
|
-
|
|
3555
|
+
try {
|
|
3556
|
+
await super.init();
|
|
3557
|
+
} catch (caught) {
|
|
3558
|
+
await this.shutdown.run(1, false);
|
|
3559
|
+
Seedcord.reset();
|
|
3560
|
+
throw caught;
|
|
3561
|
+
}
|
|
3772
3562
|
return this;
|
|
3773
3563
|
}
|
|
3774
3564
|
registerHmrAwareModules() {
|
|
@@ -3782,5 +3572,5 @@ var Seedcord = class Seedcord extends Pluggable {
|
|
|
3782
3572
|
};
|
|
3783
3573
|
|
|
3784
3574
|
//#endregion
|
|
3785
|
-
export { ArraySource, AutocompleteHandler,
|
|
3575
|
+
export { ArraySource, AutocompleteHandler, ButtonHandler, CommandMentions, ContextMenuHandler, CursorSource, Emojis, EventHandler, EventMiddleware, Gated, IgnoreBots, InteractionHandler, InteractionMiddleware, Middleware, MiddlewareType, ModalHandler, Nsfw, Paginator, RegisterEvent, RepliableHandler, Seedcord, SelectMenuHandler, SlashHandler, Subscribe, Subscriber, WebhookLog, WebhookUrl, checkBotPermissions, checkPermissions, fetchGuildMember, fetchManyGuildMembers, fetchManyUsers, fetchRole, fetchText, fetchUser, getBotRole, getConfirmation, hasPermsToAssign, updateMemberRoles, version };
|
|
3786
3576
|
//# sourceMappingURL=index.mjs.map
|