@seedcord/gateway 0.1.0-next.1 → 0.1.0-next.3
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 +989 -1268
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +373 -723
- package/dist/index.mjs +914 -1189
- package/dist/index.mjs.map +1 -1
- package/package.json +13 -12
package/dist/index.mjs
CHANGED
|
@@ -1,35 +1,44 @@
|
|
|
1
1
|
import "reflect-metadata";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
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";
|
|
2
|
+
import { installNodeDefaults } from "@seedcord/logger/node";
|
|
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";
|
|
5
4
|
import { SeedcordErrorCode } from "@seedcord/errors";
|
|
6
5
|
import { SeedcordError, SeedcordRangeError, SeedcordTypeError, validateDiscordToken } from "@seedcord/errors/internal";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import chalk from "chalk";
|
|
6
|
+
import { BaseHandler, BuilderComponent, CustomId, DispatchContext, Fault, Notice, RepliableHandler as RepliableHandler$1, RowComponent, Silence, defineGate, paginate } from "@seedcord/core";
|
|
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";
|
|
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
12
|
import { TypedEventEmitter } from "@seedcord/event-emitter";
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
13
|
+
import chalk from "chalk";
|
|
14
|
+
import { Converters, Envapter } from "envapt";
|
|
15
|
+
import { MessageFlags as MessageFlags$1, Routes, SeparatorSpacingSize } from "discord-api-types/v10";
|
|
16
|
+
import { filterCirculars, formatFilePath, hasKeys, stripAnsi, timestampFromSnowflake } from "@seedcord/utils";
|
|
17
|
+
import { DiscordAPIError as DiscordAPIError$1, REST } from "@discordjs/rest";
|
|
18
|
+
import { CoordinatedShutdown, CoordinatedStartup, HealthCheck, ShutdownPhase, ShutdownPhase as ShutdownPhase$1, StartupPhase, StartupPhase as StartupPhase$1 } from "@seedcord/core/node";
|
|
16
19
|
import { MemoryRateLimiter } from "@seedcord/rate-limiter";
|
|
17
|
-
import { SeedcordBrand
|
|
18
|
-
import { Envapt
|
|
19
|
-
import
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
import { SeedcordBrand } from "@seedcord/types/internal";
|
|
21
|
+
import { Envapt } from "envapt/legacy";
|
|
22
|
+
import { resolve } from "node:path";
|
|
23
|
+
import { HmrModuleHandler } from "@seedcord/core/hmr";
|
|
24
|
+
import { traverseDirectory } from "@seedcord/utils/node";
|
|
22
25
|
|
|
23
26
|
export * from "@seedcord/errors"
|
|
24
27
|
|
|
25
|
-
export * from "@seedcord/
|
|
28
|
+
export * from "@seedcord/core"
|
|
26
29
|
|
|
27
|
-
export * from "@seedcord/
|
|
30
|
+
export * from "@seedcord/logger"
|
|
28
31
|
|
|
29
32
|
export * from "@seedcord/event-emitter"
|
|
30
33
|
|
|
34
|
+
export * from "@seedcord/utils"
|
|
35
|
+
|
|
31
36
|
export * from "@seedcord/rate-limiter"
|
|
32
37
|
|
|
38
|
+
//#region src/logger-bootstrap.ts
|
|
39
|
+
installNodeDefaults();
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
33
42
|
//#region src/bot/decorators/Events.ts
|
|
34
43
|
/**
|
|
35
44
|
* Registers an event handler class with one or more Discord.js events.
|
|
@@ -122,190 +131,6 @@ function Gated(...gates) {
|
|
|
122
131
|
};
|
|
123
132
|
}
|
|
124
133
|
|
|
125
|
-
//#endregion
|
|
126
|
-
//#region src/miscellaneous/areRoutes.ts
|
|
127
|
-
/**
|
|
128
|
-
* Checks if the provided value is an array of strings.
|
|
129
|
-
*
|
|
130
|
-
* @internal
|
|
131
|
-
*/
|
|
132
|
-
function areRoutes(routes) {
|
|
133
|
-
return Array.isArray(routes) && routes.every((r) => typeof r === "string");
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
//#endregion
|
|
137
|
-
//#region src/bot/decorators/Interactions.ts
|
|
138
|
-
/**
|
|
139
|
-
* Types of select menus supported for routing
|
|
140
|
-
*/
|
|
141
|
-
let SelectMenuKind = /* @__PURE__ */ function(SelectMenuKind) {
|
|
142
|
-
SelectMenuKind["String"] = "string";
|
|
143
|
-
SelectMenuKind["User"] = "user";
|
|
144
|
-
SelectMenuKind["Role"] = "role";
|
|
145
|
-
SelectMenuKind["Channel"] = "channel";
|
|
146
|
-
SelectMenuKind["Mentionable"] = "mentionable";
|
|
147
|
-
return SelectMenuKind;
|
|
148
|
-
}({});
|
|
149
|
-
/**
|
|
150
|
-
* Routes one or more slash commands to a {@link SlashHandler}.
|
|
151
|
-
*
|
|
152
|
-
* Pass the same route string(s) as the handler's generic. A single command reads `this.options`, several
|
|
153
|
-
* commands branch with `this.match`. The decorator routes and the generic must match exactly, so listing
|
|
154
|
-
* fewer or more than the handler declares is a compile error. Subcommands use a slash path.
|
|
155
|
-
*
|
|
156
|
-
* @param routes - The route string(s) this handler serves, e.g. `'ban'`, `'ban', 'kick'`, or `'demo/setup'`.
|
|
157
|
-
* @decorator
|
|
158
|
-
* @example
|
|
159
|
-
* ```ts
|
|
160
|
-
* \@SlashRoute('ban')
|
|
161
|
-
* class BanHandler extends SlashHandler<'ban'> {
|
|
162
|
-
* async execute() {
|
|
163
|
-
* const target = this.options.getUser('target');
|
|
164
|
-
* }
|
|
165
|
-
* }
|
|
166
|
-
* ```
|
|
167
|
-
*
|
|
168
|
-
* @example
|
|
169
|
-
* ```ts
|
|
170
|
-
* \@SlashRoute('ban', 'kick')
|
|
171
|
-
* class ModerationHandler extends SlashHandler<'ban' | 'kick'> {
|
|
172
|
-
* async execute() {
|
|
173
|
-
* await this.match({ ban: (o) => o.getUser('target'), kick: (o) => o.getUser('member') });
|
|
174
|
-
* }
|
|
175
|
-
* }
|
|
176
|
-
* ```
|
|
177
|
-
*/
|
|
178
|
-
function SlashRoute(...routes) {
|
|
179
|
-
return function(constructor) {
|
|
180
|
-
storeMetadata(InteractionRoutes.Slash, routes, constructor);
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Routes button interactions to handler classes.
|
|
185
|
-
*
|
|
186
|
-
* Pass the {@link CustomId} definition(s) this handler decodes and list the same ones in the handler's
|
|
187
|
-
* generic. Passing different definitions to the decorator and the generic is a compile error. Routing
|
|
188
|
-
* matches the stable prefix, so a wire minted from an older shape still reaches the handler, where
|
|
189
|
-
* reading this.params throws StaleCustomId and the controller boundary turns it into a reply.
|
|
190
|
-
*
|
|
191
|
-
* @param defs - The customId definition(s) this handler decodes, one per route.
|
|
192
|
-
* @decorator
|
|
193
|
-
*/
|
|
194
|
-
function ButtonRoute(...defs) {
|
|
195
|
-
return function(constructor) {
|
|
196
|
-
storeComponentRoute(InteractionRoutes.Button, defs, constructor);
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
/**
|
|
200
|
-
* Routes modal submissions to handler classes.
|
|
201
|
-
*
|
|
202
|
-
* Pass the {@link CustomId} definition(s) this handler decodes and list the same ones in the handler's
|
|
203
|
-
* generic. Passing different definitions to the decorator and the generic is a compile error. Routing
|
|
204
|
-
* matches the stable prefix, so an older-shape wire still reaches the handler and throws StaleCustomId
|
|
205
|
-
* on read, the same as a button route.
|
|
206
|
-
*
|
|
207
|
-
* @param defs - The customId definition(s) this handler decodes, one per route.
|
|
208
|
-
* @decorator
|
|
209
|
-
*/
|
|
210
|
-
function ModalRoute(...defs) {
|
|
211
|
-
return function(constructor) {
|
|
212
|
-
storeComponentRoute(InteractionRoutes.Modal, defs, constructor);
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
/**
|
|
216
|
-
* Routes one or more context-menu commands to a {@link ContextMenuHandler}.
|
|
217
|
-
*
|
|
218
|
-
* Pass the kind (`ApplicationCommandType.User` or `ApplicationCommandType.Message`) and the command name(s),
|
|
219
|
-
* each checked against that kind's registry, so a typo is a compile error. The same kind must sit on the
|
|
220
|
-
* handler's generic, cross-checked both directions. Context menus carry no options, so a multi-name handler
|
|
221
|
-
* reads `this.target` uniformly with no branch.
|
|
222
|
-
*
|
|
223
|
-
* @param kind - The context-menu kind, `ApplicationCommandType.User` or `ApplicationCommandType.Message`.
|
|
224
|
-
* @param names - The command name(s) this handler serves, keyed off the matching registry.
|
|
225
|
-
* @decorator
|
|
226
|
-
* @example
|
|
227
|
-
* ```ts
|
|
228
|
-
* \@ContextMenuRoute(ApplicationCommandType.User, 'View Profile')
|
|
229
|
-
* class ViewProfile extends ContextMenuHandler<ApplicationCommandType.User> {
|
|
230
|
-
* async execute() {
|
|
231
|
-
* const user = this.target;
|
|
232
|
-
* }
|
|
233
|
-
* }
|
|
234
|
-
* ```
|
|
235
|
-
*/
|
|
236
|
-
function ContextMenuRoute(kind, ...names) {
|
|
237
|
-
return function(constructor) {
|
|
238
|
-
storeMetadata(kind === ApplicationCommandType.User ? InteractionRoutes.UserContextMenu : InteractionRoutes.MessageContextMenu, names, constructor);
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
/**
|
|
242
|
-
* Routes one or more commands' autocomplete to an {@link AutocompleteHandler}.
|
|
243
|
-
*
|
|
244
|
-
* Pass the same command route string(s) as the handler's generic. One command branches with `this.match`
|
|
245
|
-
* over its autocompletable fields, several commands share one handler whose arms span every command's
|
|
246
|
-
* fields. The decorator routes and the generic must match exactly, so listing fewer or more than the
|
|
247
|
-
* handler declares is a compile error.
|
|
248
|
-
*
|
|
249
|
-
* @param routes - The command route string(s) this handler serves, e.g. `'search'` or `'search', 'find'`.
|
|
250
|
-
* @decorator
|
|
251
|
-
* @example
|
|
252
|
-
* ```ts
|
|
253
|
-
* \@AutocompleteRoute('search')
|
|
254
|
-
* class SearchAutocomplete extends AutocompleteHandler<'search'> {
|
|
255
|
-
* async execute() {
|
|
256
|
-
* await this.match({ query: (value, respond) => respond([{ name: value, value }]) });
|
|
257
|
-
* }
|
|
258
|
-
* }
|
|
259
|
-
* ```
|
|
260
|
-
*/
|
|
261
|
-
function AutocompleteRoute(...routes) {
|
|
262
|
-
return function(constructor) {
|
|
263
|
-
storeMetadata(InteractionRoutes.Autocomplete, routes, constructor);
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
/**
|
|
267
|
-
* Routes select menu interactions to handler classes.
|
|
268
|
-
*
|
|
269
|
-
* Pass the select kind and the {@link CustomId} definition(s) this handler decodes. The handler's
|
|
270
|
-
* generic must list the same definitions, and its second generic argument must be the matching select
|
|
271
|
-
* interaction type, or it is a compile error.
|
|
272
|
-
*
|
|
273
|
-
* @param type - Select menu kind from {@link SelectMenuKind}.
|
|
274
|
-
* @param defs - The customId definition(s) this handler decodes, one per route.
|
|
275
|
-
* @decorator
|
|
276
|
-
*
|
|
277
|
-
* @example
|
|
278
|
-
* ```typescript
|
|
279
|
-
* \@SelectMenuRoute(SelectMenuKind.User, AssignId)
|
|
280
|
-
* class AssignSelect extends SelectMenuHandler<SelectMenuKind.User, [typeof AssignId]> {
|
|
281
|
-
* // handles user select menus minted from AssignId
|
|
282
|
-
* }
|
|
283
|
-
* ```
|
|
284
|
-
*/
|
|
285
|
-
function SelectMenuRoute(type, ...defs) {
|
|
286
|
-
return function(constructor) {
|
|
287
|
-
storeComponentRoute({
|
|
288
|
-
["string"]: InteractionRoutes.StringMenu,
|
|
289
|
-
["user"]: InteractionRoutes.UserMenu,
|
|
290
|
-
["role"]: InteractionRoutes.RoleMenu,
|
|
291
|
-
["channel"]: InteractionRoutes.ChannelMenu,
|
|
292
|
-
["mentionable"]: InteractionRoutes.MentionableMenu
|
|
293
|
-
}[type], defs, constructor);
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
function storeComponentRoute(route, defs, constructor) {
|
|
297
|
-
storeMetadata(route, defs.map((def) => def.prefix), constructor);
|
|
298
|
-
Reflect.defineMetadata(ComponentDefsKey, defs, constructor);
|
|
299
|
-
}
|
|
300
|
-
function storeMetadata(route, routes, constructor) {
|
|
301
|
-
const key = InteractionRouteKeys[route];
|
|
302
|
-
const savedRoutes = Reflect.getMetadata(key, constructor);
|
|
303
|
-
const existing = areRoutes(savedRoutes) ? savedRoutes : [];
|
|
304
|
-
const toStore = Array.isArray(routes) ? routes : [routes];
|
|
305
|
-
Reflect.defineMetadata(key, [...existing, ...toStore], constructor);
|
|
306
|
-
Reflect.defineMetadata(InteractionMetadataKey, true, constructor);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
134
|
//#endregion
|
|
310
135
|
//#region src/bot/decorators/Middlewares.ts
|
|
311
136
|
/**
|
|
@@ -374,21 +199,6 @@ const IgnoreBots = defineGate("IgnoreBots", (ctx) => {
|
|
|
374
199
|
if (ctx.user?.bot) throw new Silence("actor is a bot");
|
|
375
200
|
});
|
|
376
201
|
|
|
377
|
-
//#endregion
|
|
378
|
-
//#region src/bot/notices/utils.ts
|
|
379
|
-
function mentionFor(subject) {
|
|
380
|
-
if (subject instanceof Role) return `<@&${subject.id}>`;
|
|
381
|
-
if (subject instanceof TextChannel) return `<#${subject.id}>`;
|
|
382
|
-
if (subject instanceof GuildMember) return `<@${subject.id}>`;
|
|
383
|
-
return `\`${subject.name}\``;
|
|
384
|
-
}
|
|
385
|
-
function labelFor(subject) {
|
|
386
|
-
if (subject instanceof Role) return "role";
|
|
387
|
-
if (subject instanceof TextChannel) return "channel";
|
|
388
|
-
if (subject instanceof GuildMember) return "member";
|
|
389
|
-
return "guild";
|
|
390
|
-
}
|
|
391
|
-
|
|
392
202
|
//#endregion
|
|
393
203
|
//#region src/bot/notices/index.ts
|
|
394
204
|
var NotNsfw = class extends GateNotice {
|
|
@@ -396,13 +206,6 @@ var NotNsfw = class extends GateNotice {
|
|
|
396
206
|
super(message);
|
|
397
207
|
}
|
|
398
208
|
};
|
|
399
|
-
var MissingRole = class extends GateNotice {
|
|
400
|
-
role;
|
|
401
|
-
constructor(message, role) {
|
|
402
|
-
super(message ?? (role ? `You need the ${role.name} role to use this.` : "You do not have the required role."));
|
|
403
|
-
this.role = role;
|
|
404
|
-
}
|
|
405
|
-
};
|
|
406
209
|
var RoleHigherThanMe = class extends Notice {
|
|
407
210
|
role;
|
|
408
211
|
botRole;
|
|
@@ -423,30 +226,19 @@ var CannotAssignBotRole = class extends Notice {
|
|
|
423
226
|
return { components: [new NoticeCard("I cannot assign a managed role.").component] };
|
|
424
227
|
}
|
|
425
228
|
};
|
|
426
|
-
var MissingPermissions = class extends Notice {
|
|
427
|
-
where;
|
|
428
|
-
missingPerms;
|
|
429
|
-
constructor(message, where, missingPerms) {
|
|
430
|
-
super(message);
|
|
431
|
-
this.where = where;
|
|
432
|
-
this.missingPerms = missingPerms;
|
|
433
|
-
}
|
|
434
|
-
render() {
|
|
435
|
-
const bullets = this.missingPerms.map((perm) => `• ${perm}`).join("\n");
|
|
436
|
-
return { components: [new NoticeCard(`The ${labelFor(this.where)} ${mentionFor(this.where)} is missing the following permission entries:\n\n${bullets}`).component] };
|
|
437
|
-
}
|
|
438
|
-
};
|
|
439
229
|
var HasDangerousPermissions = class extends Notice {
|
|
440
|
-
|
|
230
|
+
subject;
|
|
441
231
|
dangerousPerms;
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
232
|
+
customLead;
|
|
233
|
+
constructor(message, subject, dangerousPerms) {
|
|
234
|
+
super(message ?? "A dangerous permission is enabled.");
|
|
235
|
+
this.subject = subject;
|
|
445
236
|
this.dangerousPerms = dangerousPerms;
|
|
237
|
+
this.customLead = message;
|
|
446
238
|
}
|
|
447
239
|
render() {
|
|
448
240
|
const bullets = this.dangerousPerms.map((perm) => `• ${perm}`).join("\n");
|
|
449
|
-
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] };
|
|
450
242
|
}
|
|
451
243
|
};
|
|
452
244
|
var UserNotFound = class extends Notice {
|
|
@@ -522,129 +314,6 @@ function Nsfw(options) {
|
|
|
522
314
|
});
|
|
523
315
|
}
|
|
524
316
|
|
|
525
|
-
//#endregion
|
|
526
|
-
//#region src/bot/utilities/permissions/checkPermissions.ts
|
|
527
|
-
/**
|
|
528
|
-
* Map of permission bits to their prettified human readable names.
|
|
529
|
-
*/
|
|
530
|
-
const PermissionNames = new Map(Object.entries(PermissionFlagsBits).map(([key, bit]) => [bit, prettify(key)]));
|
|
531
|
-
function checkPermissions(a, b, c, d, e) {
|
|
532
|
-
const { for: pFor, in: pIn, scope, inverse = false, missing: missingCtor, dangerous: dangerousCtor } = a instanceof Role || a instanceof GuildMember ? {
|
|
533
|
-
for: a,
|
|
534
|
-
in: b,
|
|
535
|
-
scope: c,
|
|
536
|
-
inverse: d ?? false,
|
|
537
|
-
...e
|
|
538
|
-
} : a;
|
|
539
|
-
const Missing = missingCtor ?? MissingPermissions;
|
|
540
|
-
const Dangerous = dangerousCtor ?? HasDangerousPermissions;
|
|
541
|
-
const perms = pIn instanceof Guild ? pFor.permissions : pIn.permissionsFor(pFor, true);
|
|
542
|
-
const names = (bits) => bits.map((bit) => PermissionNames.get(bit) ?? String(bit));
|
|
543
|
-
const present = scope.filter((bit) => perms.has(bit, true));
|
|
544
|
-
const presentNames = names(present);
|
|
545
|
-
if (inverse) {
|
|
546
|
-
if (present.length > 0) throw new Dangerous(`${pFor instanceof Role ? "Role" : "Member"} has dangerous permissions in this ${pIn instanceof Guild ? "guild" : "channel"}`, pFor, presentNames);
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
const missingBits = scope.filter((bit) => !perms.has(bit, true));
|
|
550
|
-
if (missingBits.length > 0) throw new Missing("Missing Any/All/No Permissions", pFor, names(missingBits));
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
//#endregion
|
|
554
|
-
//#region src/bot/gates/catalog/permissions.ts
|
|
555
|
-
const UNCACHED_MEMBER = "Your server member data could not be resolved. Try again.";
|
|
556
|
-
/**
|
|
557
|
-
* Requires the caller to hold every permission in `scope`, via `checkPermissions`. Refuses outside a guild.
|
|
558
|
-
*
|
|
559
|
-
* Interaction-only and excludes ModalSubmit, since a modal lacks a reliable cached caller member.
|
|
560
|
-
*
|
|
561
|
-
* @param scope - The permission flag bits the caller must all hold.
|
|
562
|
-
* @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-permissions one with `missing`.
|
|
563
|
-
*
|
|
564
|
-
* @see {@link Gated}
|
|
565
|
-
*
|
|
566
|
-
* @example
|
|
567
|
-
* ```ts
|
|
568
|
-
* import { PermissionFlagsBits } from 'discord.js';
|
|
569
|
-
*
|
|
570
|
-
* \@Gated(RequirePermissions([PermissionFlagsBits.BanMembers]))
|
|
571
|
-
* \@SlashRoute('ban')
|
|
572
|
-
* class BanHandler extends SlashHandler<'ban'> {
|
|
573
|
-
* async execute() {
|
|
574
|
-
* const target = this.options.getUser('target');
|
|
575
|
-
* }
|
|
576
|
-
* }
|
|
577
|
-
* ```
|
|
578
|
-
*/
|
|
579
|
-
function RequirePermissions(scope, options) {
|
|
580
|
-
return defineGate("RequirePermissions", (ctx) => {
|
|
581
|
-
if (!ctx.guild) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message));
|
|
582
|
-
if (!ctx.member) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message ?? UNCACHED_MEMBER));
|
|
583
|
-
checkPermissions(ctx.member, ctx.guild, scope, false, options?.missing ? { missing: options.missing } : void 0);
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
/**
|
|
587
|
-
* Requires the bot to hold every permission in `scope`, via `checkPermissions`. Refuses outside a guild.
|
|
588
|
-
*
|
|
589
|
-
* It reads the bot's own member, so it attaches to modal handlers as well.
|
|
590
|
-
*
|
|
591
|
-
* @param scope - The permission flag bits the bot must all hold.
|
|
592
|
-
* @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-permissions one with `missing`.
|
|
593
|
-
*
|
|
594
|
-
* @see {@link Gated}
|
|
595
|
-
*
|
|
596
|
-
* @example
|
|
597
|
-
* ```ts
|
|
598
|
-
* import { PermissionFlagsBits } from 'discord.js';
|
|
599
|
-
*
|
|
600
|
-
* \@Gated(RequireBotPermissions([PermissionFlagsBits.BanMembers]))
|
|
601
|
-
* \@ModalRoute(MyModalId)
|
|
602
|
-
* class MyModalHandler extends ModalHandler<[typeof MyModalId]> {
|
|
603
|
-
* async execute() {
|
|
604
|
-
* // ...
|
|
605
|
-
* }
|
|
606
|
-
* }
|
|
607
|
-
* ```
|
|
608
|
-
*/
|
|
609
|
-
function RequireBotPermissions(scope, options) {
|
|
610
|
-
return defineGate("RequireBotPermissions", (ctx) => {
|
|
611
|
-
const botMember = ctx.guild?.members.me;
|
|
612
|
-
if (!ctx.guild || !botMember) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message));
|
|
613
|
-
checkPermissions(botMember, ctx.guild, scope, false, options?.missing ? { missing: options.missing } : void 0);
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
/**
|
|
617
|
-
* Requires the caller to have `roleId`, read from the member's role cache. Refuses outside a guild first, then
|
|
618
|
-
* when the role is missing.
|
|
619
|
-
*
|
|
620
|
-
* Interaction-only and excludes ModalSubmit.
|
|
621
|
-
*
|
|
622
|
-
* @param roleId - The role snowflake the caller must hold, read from the member's role cache.
|
|
623
|
-
* @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-role one with `missingRole`.
|
|
624
|
-
*
|
|
625
|
-
* @see {@link Gated}
|
|
626
|
-
*
|
|
627
|
-
* @example
|
|
628
|
-
* ```ts
|
|
629
|
-
* \@Gated(RequireRole('123456789012345678'))
|
|
630
|
-
* \@SlashRoute('vip')
|
|
631
|
-
* class VipHandler extends SlashHandler<'vip'> {
|
|
632
|
-
* async execute() {
|
|
633
|
-
* // ...
|
|
634
|
-
* }
|
|
635
|
-
* }
|
|
636
|
-
* ```
|
|
637
|
-
*/
|
|
638
|
-
function RequireRole(roleId, options) {
|
|
639
|
-
return defineGate("RequireRole", (ctx) => {
|
|
640
|
-
if (!ctx.guild) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message));
|
|
641
|
-
if (!ctx.member) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message ?? UNCACHED_MEMBER));
|
|
642
|
-
if (ctx.member.roles.cache.has(roleId)) return;
|
|
643
|
-
const role = ctx.guild.roles.cache.get(roleId) ?? null;
|
|
644
|
-
throw pickNotice(options?.missingRole, (message) => new MissingRole(message, role));
|
|
645
|
-
});
|
|
646
|
-
}
|
|
647
|
-
|
|
648
317
|
//#endregion
|
|
649
318
|
//#region src/bot/injectors/EmojiInjector.ts
|
|
650
319
|
const emojiStorage = {};
|
|
@@ -673,7 +342,7 @@ var EmojiInjector = class {
|
|
|
673
342
|
async init() {
|
|
674
343
|
this.clearEmojis();
|
|
675
344
|
if (!this.core.config.bot.emojis || Object.keys(this.core.config.bot.emojis).length === 0) {
|
|
676
|
-
this.logger.debug(
|
|
345
|
+
this.logger.debug("No emojis configured, skipping emoji injection.");
|
|
677
346
|
return;
|
|
678
347
|
}
|
|
679
348
|
const configEmojis = this.core.config.bot.emojis;
|
|
@@ -833,6 +502,37 @@ async function fetchText(client, channelId) {
|
|
|
833
502
|
throw new CouldNotFindChannel("Channel not found or not a text channel", channelId);
|
|
834
503
|
}
|
|
835
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
|
+
|
|
836
536
|
//#endregion
|
|
837
537
|
//#region src/bot/utilities/permissions/checkBotPermissions.ts
|
|
838
538
|
/**
|
|
@@ -861,7 +561,7 @@ function checkBotPermissions(target, scope, inverse = false, errors) {
|
|
|
861
561
|
const me = target.members.me;
|
|
862
562
|
if (!me) {
|
|
863
563
|
const names = scope.map((bit) => PermissionNames.get(bit) ?? String(bit));
|
|
864
|
-
throw new ((errors?.
|
|
564
|
+
throw new ((errors?.missingNotice) ?? MissingPermissions)(void 0, "The bot", names);
|
|
865
565
|
}
|
|
866
566
|
checkPermissions(me, target, scope, inverse, errors);
|
|
867
567
|
return;
|
|
@@ -870,7 +570,7 @@ function checkBotPermissions(target, scope, inverse = false, errors) {
|
|
|
870
570
|
if (!me) {
|
|
871
571
|
new Logger("checkBotPermissions").warn(`Bot member is unavailable in guild ${target.guild.id} while checking permissions in channel ${target.id}`);
|
|
872
572
|
const names = scope.map((bit) => PermissionNames.get(bit) ?? String(bit));
|
|
873
|
-
throw new ((errors?.
|
|
573
|
+
throw new ((errors?.missingNotice) ?? MissingPermissions)(void 0, "The bot", names);
|
|
874
574
|
}
|
|
875
575
|
checkPermissions(me, target, scope, inverse, errors);
|
|
876
576
|
}
|
|
@@ -907,26 +607,26 @@ function getBotRole(guild) {
|
|
|
907
607
|
* // Check with custom error handling
|
|
908
608
|
* hasPermsToAssign({
|
|
909
609
|
* targetRole: role,
|
|
910
|
-
*
|
|
911
|
-
*
|
|
912
|
-
*
|
|
913
|
-
*
|
|
610
|
+
* noticeOverrides: {
|
|
611
|
+
* higherNotice: CustomHigherNotice,
|
|
612
|
+
* managedNotice: CustomManagedNotice,
|
|
613
|
+
* missingNotice: CustomMissingPermissionsNotice
|
|
914
614
|
* }
|
|
915
615
|
* });
|
|
916
616
|
* ```
|
|
917
617
|
*/
|
|
918
618
|
function hasPermsToAssign(roleOrOptions) {
|
|
919
|
-
const { targetRole, errors } = roleOrOptions instanceof Role ? {
|
|
619
|
+
const { targetRole, noticeOverrides: errors } = roleOrOptions instanceof Role ? {
|
|
920
620
|
targetRole: roleOrOptions,
|
|
921
|
-
|
|
621
|
+
noticeOverrides: void 0
|
|
922
622
|
} : roleOrOptions;
|
|
923
|
-
const
|
|
924
|
-
const
|
|
925
|
-
const
|
|
623
|
+
const HigherNotice = errors?.higherNotice ?? RoleHigherThanMe;
|
|
624
|
+
const ManagedNotice = errors?.managedNotice ?? CannotAssignBotRole;
|
|
625
|
+
const MissingNotice = errors?.missingNotice ?? MissingPermissions;
|
|
926
626
|
const botRole = getBotRole(targetRole.guild);
|
|
927
|
-
if (targetRole.comparePositionTo(botRole) >= 0) throw new
|
|
928
|
-
if (targetRole.managed) throw new
|
|
929
|
-
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 });
|
|
930
630
|
}
|
|
931
631
|
|
|
932
632
|
//#endregion
|
|
@@ -1054,113 +754,308 @@ async function updateMemberRoles(rolesToAdd, rolesToRemove, member) {
|
|
|
1054
754
|
}
|
|
1055
755
|
|
|
1056
756
|
//#endregion
|
|
1057
|
-
//#region src/bot/
|
|
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
|
+
}
|
|
1058
764
|
/**
|
|
1059
|
-
*
|
|
1060
|
-
*
|
|
1061
|
-
* the original throw, and {@link ReplySender} drops them on its own send.
|
|
1062
|
-
*
|
|
1063
|
-
* @internal
|
|
765
|
+
* Writes interaction responses through the discord.js interaction. Construction is internal to the repliable
|
|
766
|
+
* handler bases and the dispatcher.
|
|
1064
767
|
*/
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
768
|
+
var ReplySender = class extends BaseReplySender {
|
|
769
|
+
interaction;
|
|
770
|
+
constructor(interaction, routeId) {
|
|
771
|
+
super(routeId, seedState(interaction));
|
|
772
|
+
this.interaction = interaction;
|
|
773
|
+
}
|
|
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);
|
|
780
|
+
}
|
|
781
|
+
async writeDefer(opts) {
|
|
782
|
+
await this.interaction.deferReply({ flags: deferFlags(opts) });
|
|
783
|
+
}
|
|
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));
|
|
799
|
+
}
|
|
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);
|
|
833
|
+
return {
|
|
834
|
+
components: reply.components,
|
|
835
|
+
flags,
|
|
836
|
+
...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
|
|
837
|
+
...reply.files && { files: [...reply.files] }
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
editOptions(response) {
|
|
841
|
+
const reply = this.serialize(response);
|
|
842
|
+
return {
|
|
843
|
+
components: reply.components,
|
|
844
|
+
flags: MessageFlags.IsComponentsV2,
|
|
845
|
+
...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
|
|
846
|
+
...reply.files && { files: [...reply.files] }
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
updateOptions(response) {
|
|
850
|
+
const reply = this.serialize(response);
|
|
851
|
+
return {
|
|
852
|
+
components: reply.components,
|
|
853
|
+
flags: MessageFlags.IsComponentsV2,
|
|
854
|
+
...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
|
|
855
|
+
...reply.files && { files: [...reply.files] }
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
createdMessage(result) {
|
|
859
|
+
return result.resource?.message ?? void 0;
|
|
860
|
+
}
|
|
861
|
+
};
|
|
1070
862
|
|
|
1071
863
|
//#endregion
|
|
1072
|
-
//#region src/miscellaneous/
|
|
864
|
+
//#region src/bot/utilities/miscellaneous/slashRouteOf.ts
|
|
1073
865
|
/**
|
|
1074
|
-
*
|
|
1075
|
-
*
|
|
1076
|
-
*
|
|
1077
|
-
* @param ephemeral - Whether the reply is ephemeral.
|
|
1078
|
-
* @returns The OR'd `MessageFlags` value as a plain number.
|
|
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.
|
|
1079
869
|
*
|
|
1080
870
|
* @internal
|
|
1081
871
|
*/
|
|
1082
|
-
function
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
return
|
|
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);
|
|
1086
876
|
}
|
|
1087
877
|
|
|
1088
878
|
//#endregion
|
|
1089
|
-
//#region src/
|
|
879
|
+
//#region src/miscellaneous/FaultThrottle.ts
|
|
1090
880
|
/**
|
|
1091
|
-
*
|
|
1092
|
-
*
|
|
1093
|
-
*
|
|
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
|
|
1094
886
|
*/
|
|
1095
|
-
var
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
* Sends the reply and returns the sent message so a caller can attach a collector to it. Never throws,
|
|
1103
|
-
* a failed send is logged and dropped and resolves to `undefined`.
|
|
1104
|
-
*
|
|
1105
|
-
* @param response - The ComponentsV2 reply to show.
|
|
1106
|
-
* @param ephemeral - Whether the reply is ephemeral. {@default `true`}
|
|
1107
|
-
* @returns The sent {@link Message}, or `undefined` when the send was swallowed.
|
|
1108
|
-
*/
|
|
1109
|
-
async send(response, ephemeral = true) {
|
|
1110
|
-
try {
|
|
1111
|
-
return await this.dispatch(response, ephemeral);
|
|
1112
|
-
} catch (error) {
|
|
1113
|
-
this.logSwallowed("send", error);
|
|
1114
|
-
return;
|
|
1115
|
-
}
|
|
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;
|
|
1116
894
|
}
|
|
1117
|
-
/**
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
async edit(message, response) {
|
|
1125
|
-
try {
|
|
1126
|
-
await this.interaction.webhook.editMessage(message, this.editBody(response));
|
|
1127
|
-
} catch (error) {
|
|
1128
|
-
this.logSwallowed("edit", error);
|
|
1129
|
-
}
|
|
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;
|
|
1130
902
|
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
if (this.interaction.deferred) {
|
|
1134
|
-
if (this.interaction.ephemeral === null) return await this.interaction.followUp(this.replyOptions(response, ephemeral));
|
|
1135
|
-
return await this.interaction.editReply(this.editBody(response));
|
|
1136
|
-
}
|
|
1137
|
-
await this.interaction.reply(this.replyOptions(response, ephemeral));
|
|
1138
|
-
return await this.interaction.fetchReply();
|
|
903
|
+
markReported(key) {
|
|
904
|
+
this.lastReportedAt.set(key, this.now());
|
|
1139
905
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
components: response.components,
|
|
1143
|
-
flags: flagsFor(ephemeral),
|
|
1144
|
-
...response.allowedMentions && { allowedMentions: response.allowedMentions },
|
|
1145
|
-
...response.files && { files: response.files }
|
|
1146
|
-
};
|
|
906
|
+
clear() {
|
|
907
|
+
this.lastReportedAt.clear();
|
|
1147
908
|
}
|
|
1148
|
-
|
|
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);
|
|
1149
932
|
return {
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
...response.allowedMentions && { allowedMentions: response.allowedMentions },
|
|
1153
|
-
...response.files && { files: response.files }
|
|
933
|
+
uuid,
|
|
934
|
+
response: error.render(ctx)
|
|
1154
935
|
};
|
|
1155
936
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
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;
|
|
1162
949
|
}
|
|
1163
|
-
|
|
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
|
+
}
|
|
1164
1059
|
|
|
1165
1060
|
//#endregion
|
|
1166
1061
|
//#region src/bot/confirm/reserved.ts
|
|
@@ -1206,9 +1101,9 @@ async function collectChoice(message, userId, timeoutMs) {
|
|
|
1206
1101
|
return null;
|
|
1207
1102
|
}
|
|
1208
1103
|
}
|
|
1209
|
-
async function settle(sender,
|
|
1104
|
+
async function settle(sender, message, outcome) {
|
|
1210
1105
|
if (outcome === void 0) {
|
|
1211
|
-
await
|
|
1106
|
+
await sender.delete(message).catch(() => void 0);
|
|
1212
1107
|
return;
|
|
1213
1108
|
}
|
|
1214
1109
|
await sender.edit(message, typeof outcome === "function" ? await outcome() : outcome);
|
|
@@ -1216,30 +1111,19 @@ async function settle(sender, interaction, message, outcome) {
|
|
|
1216
1111
|
async function getConfirmation(interaction, prompt, options) {
|
|
1217
1112
|
const { ephemeral = true, timeoutMs = DEFAULT_TIMEOUT_MS } = options ?? {};
|
|
1218
1113
|
const response = typeof prompt === "string" ? defaultPrompt(prompt, options) : await prompt(CONFIRM_IDS);
|
|
1219
|
-
const sender = new ReplySender(interaction);
|
|
1220
|
-
const message = await sender.send(response, ephemeral);
|
|
1221
|
-
if (!message) return false;
|
|
1114
|
+
const sender = new ReplySender(interaction, interactionRoute(interaction));
|
|
1115
|
+
const message = await sender.send(response, { ephemeral });
|
|
1222
1116
|
const winner = await collectChoice(message, interaction.user.id, timeoutMs);
|
|
1223
1117
|
if (!winner) {
|
|
1224
|
-
await settle(sender,
|
|
1118
|
+
await settle(sender, message, options?.onTimeout);
|
|
1225
1119
|
return false;
|
|
1226
1120
|
}
|
|
1227
1121
|
await winner.deferUpdate().catch(() => void 0);
|
|
1228
1122
|
const confirmed = winner.customId === CONFIRM_IDS.confirm;
|
|
1229
|
-
await settle(sender,
|
|
1123
|
+
await settle(sender, message, confirmed ? options?.onConfirm : options?.onCancel);
|
|
1230
1124
|
return confirmed;
|
|
1231
1125
|
}
|
|
1232
1126
|
|
|
1233
|
-
//#endregion
|
|
1234
|
-
//#region src/hmr/devChannel.ts
|
|
1235
|
-
let channel;
|
|
1236
|
-
function setDevChannel(next) {
|
|
1237
|
-
channel = next;
|
|
1238
|
-
}
|
|
1239
|
-
function getDevChannel() {
|
|
1240
|
-
return channel;
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
1127
|
//#endregion
|
|
1244
1128
|
//#region src/interfaces/Plugin.ts
|
|
1245
1129
|
/**
|
|
@@ -1349,19 +1233,10 @@ var Pluggable = class Pluggable extends TypedEventEmitter {
|
|
|
1349
1233
|
//#endregion
|
|
1350
1234
|
//#region src/handlers/BaseHandler.ts
|
|
1351
1235
|
/**
|
|
1352
|
-
*
|
|
1236
|
+
* The gateway base, fixes the core base's `TCore` to the gateway {@link Core} and adds the `getEvent`
|
|
1237
|
+
* gate hook.
|
|
1353
1238
|
*/
|
|
1354
|
-
var BaseHandler = class {
|
|
1355
|
-
core;
|
|
1356
|
-
event;
|
|
1357
|
-
logger;
|
|
1358
|
-
dispatch;
|
|
1359
|
-
constructor(event, core, dispatch) {
|
|
1360
|
-
this.core = core;
|
|
1361
|
-
this.event = event;
|
|
1362
|
-
this.logger = new Logger(this.constructor.name);
|
|
1363
|
-
this.dispatch = dispatch;
|
|
1364
|
-
}
|
|
1239
|
+
var BaseHandler$1 = class extends BaseHandler {
|
|
1365
1240
|
/** @internal */
|
|
1366
1241
|
getEvent() {
|
|
1367
1242
|
return this.event;
|
|
@@ -1378,44 +1253,57 @@ var BaseHandler = class {
|
|
|
1378
1253
|
*
|
|
1379
1254
|
* @typeParam Repliable - The interaction type this middleware processes
|
|
1380
1255
|
*/
|
|
1381
|
-
var InteractionMiddleware = class extends BaseHandler {
|
|
1256
|
+
var InteractionMiddleware = class extends BaseHandler$1 {
|
|
1382
1257
|
constructor(event, core) {
|
|
1383
1258
|
super(event, core);
|
|
1384
1259
|
}
|
|
1385
1260
|
};
|
|
1386
1261
|
|
|
1262
|
+
//#endregion
|
|
1263
|
+
//#region src/handlers/RepliableHandler.ts
|
|
1264
|
+
/**
|
|
1265
|
+
* Shared base the repliable interaction handlers extend.
|
|
1266
|
+
*
|
|
1267
|
+
* Not a public entry point. Extend {@link SlashHandler}, {@link ButtonHandler}, {@link ModalHandler},
|
|
1268
|
+
* {@link SelectMenuHandler}, or {@link ContextMenuHandler} instead. This class binds the core reply base
|
|
1269
|
+
* to the gateway sender.
|
|
1270
|
+
*
|
|
1271
|
+
* @typeParam Event - The repliable interaction type this handler processes
|
|
1272
|
+
*/
|
|
1273
|
+
var RepliableHandler = class extends RepliableHandler$1 {
|
|
1274
|
+
/** @internal */
|
|
1275
|
+
getEvent() {
|
|
1276
|
+
return this.event;
|
|
1277
|
+
}
|
|
1278
|
+
buildSender(event, _core, routeId) {
|
|
1279
|
+
return new ReplySender(event, routeId);
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1387
1283
|
//#endregion
|
|
1388
1284
|
//#region src/handlers/interaction/InteractionHandler.ts
|
|
1389
1285
|
/**
|
|
1390
1286
|
* Shared base the typed interaction handlers extend.
|
|
1391
1287
|
*
|
|
1392
|
-
* Not a public entry point.
|
|
1393
|
-
* or {@link SelectMenuHandler} instead. This class
|
|
1394
|
-
*
|
|
1288
|
+
* Not a public entry point. Extend {@link SlashHandler}, {@link ButtonHandler}, {@link ModalHandler},
|
|
1289
|
+
* or {@link SelectMenuHandler} instead. This class adds `showModal` on top of the reply members those bases
|
|
1290
|
+
* share.
|
|
1395
1291
|
*
|
|
1396
1292
|
* @typeParam Repliable - The interaction type this handler processes
|
|
1397
1293
|
*/
|
|
1398
|
-
var InteractionHandler = class extends
|
|
1294
|
+
var InteractionHandler = class extends RepliableHandler {
|
|
1399
1295
|
constructor(event, core, dispatch) {
|
|
1400
1296
|
super(event, core, dispatch);
|
|
1401
1297
|
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Open a modal. Must be the initial response to this interaction. The modal kind rejects this call at
|
|
1300
|
+
* compile time (Discord forbids a modal in response to a modal).
|
|
1301
|
+
*/
|
|
1302
|
+
showModal(modal) {
|
|
1303
|
+
return this.sender.showModal(modal);
|
|
1304
|
+
}
|
|
1402
1305
|
};
|
|
1403
1306
|
|
|
1404
|
-
//#endregion
|
|
1405
|
-
//#region src/bot/utilities/miscellaneous/slashRouteOf.ts
|
|
1406
|
-
/**
|
|
1407
|
-
* Computes the dispatch route for a chat-input or autocomplete interaction. The controller and
|
|
1408
|
-
* `seedcord codegen` both assemble route keys through the same {@link buildSlashRoute}, so a dispatched
|
|
1409
|
-
* interaction and a generated registry key always match.
|
|
1410
|
-
*
|
|
1411
|
-
* @internal
|
|
1412
|
-
*/
|
|
1413
|
-
function slashRouteOf(interaction) {
|
|
1414
|
-
const group = interaction.options.getSubcommandGroup(false) ?? void 0;
|
|
1415
|
-
const subcommand = interaction.options.getSubcommand(false) ?? void 0;
|
|
1416
|
-
return buildSlashRoute(interaction.commandName, subcommand, group);
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
1307
|
//#endregion
|
|
1420
1308
|
//#region src/handlers/interaction/SlashHandler.ts
|
|
1421
1309
|
/**
|
|
@@ -1450,7 +1338,7 @@ var SlashHandler = class extends InteractionHandler {
|
|
|
1450
1338
|
}
|
|
1451
1339
|
/**
|
|
1452
1340
|
* Run the arm for whichever command fired. Use this only when the handler is registered for several
|
|
1453
|
-
* routes.
|
|
1341
|
+
* routes.
|
|
1454
1342
|
*
|
|
1455
1343
|
* Provide one arm per registered route, keyed by its route string, and each arm receives that route's
|
|
1456
1344
|
* own narrowed options. The arms must cover every route in the generic, a missing route or an unknown
|
|
@@ -1474,7 +1362,7 @@ var SlashHandler = class extends InteractionHandler {
|
|
|
1474
1362
|
*/
|
|
1475
1363
|
async match(arms) {
|
|
1476
1364
|
const route = slashRouteOf(this.event);
|
|
1477
|
-
const arm = arms[route];
|
|
1365
|
+
const arm = Object.hasOwn(arms, route) ? arms[route] : void 0;
|
|
1478
1366
|
if (!arm) throw new SeedcordError(SeedcordErrorCode.SlashMatchArmMissing, [route]);
|
|
1479
1367
|
return await arm(this.options);
|
|
1480
1368
|
}
|
|
@@ -1541,15 +1429,15 @@ var ContextMenuHandler = class extends InteractionHandler {
|
|
|
1541
1429
|
* }
|
|
1542
1430
|
* ```
|
|
1543
1431
|
*/
|
|
1544
|
-
var AutocompleteHandler = class extends BaseHandler {
|
|
1432
|
+
var AutocompleteHandler = class extends BaseHandler$1 {
|
|
1545
1433
|
constructor(event, core, dispatch) {
|
|
1546
1434
|
super(event, core, dispatch);
|
|
1547
1435
|
}
|
|
1436
|
+
decodedFocused;
|
|
1548
1437
|
/**
|
|
1549
1438
|
* The focused option for this interaction. `value` is always the raw partial string the user is typing,
|
|
1550
1439
|
* even for an integer or number option, so coerce it yourself when you need a number.
|
|
1551
1440
|
*/
|
|
1552
|
-
decodedFocused;
|
|
1553
1441
|
get focused() {
|
|
1554
1442
|
if (this.decodedFocused) return this.decodedFocused;
|
|
1555
1443
|
const raw = this.event.options.getFocused(true);
|
|
@@ -1565,7 +1453,7 @@ var AutocompleteHandler = class extends BaseHandler {
|
|
|
1565
1453
|
* match is how you read it. There is no single-field shortcut the way slash/component handlers have one.
|
|
1566
1454
|
*
|
|
1567
1455
|
* Provide one arm per autocompletable field across the registered commands, keyed by field name. Each
|
|
1568
|
-
* arm receives the focused partial value and a `respond`
|
|
1456
|
+
* arm receives the focused partial value and a `respond` typed to that field's choice value. The
|
|
1569
1457
|
* arms must cover every autocompletable field, a missing field or an unknown key is a compile error. A
|
|
1570
1458
|
* focused field with no arm, only reachable from a stale-deployed command, throws at runtime.
|
|
1571
1459
|
*
|
|
@@ -1587,11 +1475,15 @@ var AutocompleteHandler = class extends BaseHandler {
|
|
|
1587
1475
|
*/
|
|
1588
1476
|
async match(arms) {
|
|
1589
1477
|
const { name, value } = this.focused;
|
|
1590
|
-
const respond = (choices) => this.
|
|
1591
|
-
const arm = arms[name];
|
|
1478
|
+
const respond = (choices) => this.respond(choices);
|
|
1479
|
+
const arm = Object.hasOwn(arms, name) ? arms[name] : void 0;
|
|
1592
1480
|
if (!arm) throw new SeedcordError(SeedcordErrorCode.AutocompleteMatchArmMissing, [name]);
|
|
1593
1481
|
return await arm(value, respond);
|
|
1594
1482
|
}
|
|
1483
|
+
/** Send autocomplete suggestions, callback type 8. Prefer {@link match}, which restricts each field's choices to its declared type. */
|
|
1484
|
+
async respond(choices) {
|
|
1485
|
+
await this.event.respond(choices);
|
|
1486
|
+
}
|
|
1595
1487
|
/** The firing command route, for a field whose completion differs per registered command. */
|
|
1596
1488
|
get route() {
|
|
1597
1489
|
return slashRouteOf(this.event);
|
|
@@ -1599,7 +1491,7 @@ var AutocompleteHandler = class extends BaseHandler {
|
|
|
1599
1491
|
/**
|
|
1600
1492
|
* The already-entered options on this command, restricted to the kinds Discord resolves during autocomplete
|
|
1601
1493
|
* (string, integer, number, boolean) with every read nullable, since a sibling is partial while the user
|
|
1602
|
-
* types the focused field.
|
|
1494
|
+
* types the focused field. The focused field is on `this.focused`.
|
|
1603
1495
|
*/
|
|
1604
1496
|
get options() {
|
|
1605
1497
|
return this.event.options;
|
|
@@ -1611,25 +1503,25 @@ var AutocompleteHandler = class extends BaseHandler {
|
|
|
1611
1503
|
/**
|
|
1612
1504
|
* Shared base the customId-routed component handlers extend.
|
|
1613
1505
|
*
|
|
1614
|
-
* Not a public entry point.
|
|
1615
|
-
*
|
|
1616
|
-
* those bases share, so DO NOT use it directly.
|
|
1506
|
+
* Not a public entry point. Extend {@link ButtonHandler}, {@link SelectMenuHandler}, or {@link ModalHandler}
|
|
1507
|
+
* instead. This class defines the customId decode and the route matching those bases share.
|
|
1617
1508
|
*
|
|
1618
1509
|
* @typeParam Event - The component interaction type this handler processes
|
|
1619
1510
|
* @typeParam Defs - The customId route definitions registered on the concrete handler
|
|
1620
1511
|
*/
|
|
1621
1512
|
var ComponentHandler = class extends InteractionHandler {
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1513
|
+
/** Rewrite the source message this component interaction came from. */
|
|
1514
|
+
update(response) {
|
|
1515
|
+
return this.sender.update(response);
|
|
1516
|
+
}
|
|
1517
|
+
/** Acknowledge the component without changing the source message. */
|
|
1518
|
+
deferUpdate() {
|
|
1519
|
+
return this.sender.deferUpdate();
|
|
1626
1520
|
}
|
|
1627
1521
|
decoded;
|
|
1628
1522
|
get route() {
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
this.decoded = decoded;
|
|
1632
|
-
return decoded;
|
|
1523
|
+
this.decoded ??= decodeComponentRoute(this.constructor, this.event.customId);
|
|
1524
|
+
return this.decoded;
|
|
1633
1525
|
}
|
|
1634
1526
|
/**
|
|
1635
1527
|
* The decoded params of the single route this handler is registered for.
|
|
@@ -1648,10 +1540,11 @@ var ComponentHandler = class extends InteractionHandler {
|
|
|
1648
1540
|
* handler `this.params` is `never`, so match is the only way to read the decoded params.
|
|
1649
1541
|
*
|
|
1650
1542
|
* Provide one arm per registered route, keyed by its prefix, and each arm receives that route's own
|
|
1651
|
-
* decoded params. The arms
|
|
1652
|
-
*
|
|
1543
|
+
* decoded params. The arms cover every registered route prefix, checked at compile time, and a prefix
|
|
1544
|
+
* unmatched at runtime throws `CustomIdMatchArmMissing`. Decoding runs before any arm, so a stale or
|
|
1545
|
+
* corrupt wire throws before an arm body executes.
|
|
1653
1546
|
*
|
|
1654
|
-
* @param arms - One
|
|
1547
|
+
* @param arms - One callback per registered route, keyed by prefix.
|
|
1655
1548
|
* @returns The result of the arm that ran.
|
|
1656
1549
|
*
|
|
1657
1550
|
* @example
|
|
@@ -1660,8 +1553,8 @@ var ComponentHandler = class extends InteractionHandler {
|
|
|
1660
1553
|
* class ReviewButtons extends ButtonHandler<[typeof Approve, typeof Reject]> {
|
|
1661
1554
|
* async execute() {
|
|
1662
1555
|
* await this.match({
|
|
1663
|
-
* approve: ({ userId }) => this.
|
|
1664
|
-
* reject: ({ userId }) => this.
|
|
1556
|
+
* approve: ({ userId }) => this.reply(`approved <@${userId}>`),
|
|
1557
|
+
* reject: ({ userId }) => this.reply(`rejected <@${userId}>`)
|
|
1665
1558
|
* });
|
|
1666
1559
|
* }
|
|
1667
1560
|
* }
|
|
@@ -1669,7 +1562,7 @@ var ComponentHandler = class extends InteractionHandler {
|
|
|
1669
1562
|
*/
|
|
1670
1563
|
async match(arms) {
|
|
1671
1564
|
const { prefix, params } = this.route;
|
|
1672
|
-
const arm = arms[prefix];
|
|
1565
|
+
const arm = Object.hasOwn(arms, prefix) ? arms[prefix] : void 0;
|
|
1673
1566
|
if (!arm) throw new SeedcordError(SeedcordErrorCode.CustomIdMatchArmMissing, [prefix]);
|
|
1674
1567
|
return await arm(params);
|
|
1675
1568
|
}
|
|
@@ -1681,8 +1574,9 @@ var ComponentHandler = class extends InteractionHandler {
|
|
|
1681
1574
|
* Base class for a button interaction handler.
|
|
1682
1575
|
*
|
|
1683
1576
|
* Register the customId definitions this handler decodes with `@ButtonRoute`, list the same ones in the
|
|
1684
|
-
* generic, then read `this.params` for a single route or `this.match` for several.
|
|
1685
|
-
*
|
|
1577
|
+
* generic, then read `this.params` for a single route or `this.match` for several. Reply through the handler
|
|
1578
|
+
* members, or rewrite the source message with `this.update`. Passing different definitions to the decorator
|
|
1579
|
+
* and the generic is a compile error.
|
|
1686
1580
|
*
|
|
1687
1581
|
* @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ApproveId]`.
|
|
1688
1582
|
* @typeParam Cache - The interaction cache state, `'cached'` by default.
|
|
@@ -1693,7 +1587,7 @@ var ComponentHandler = class extends InteractionHandler {
|
|
|
1693
1587
|
* class ApproveButton extends ButtonHandler<[typeof ApproveId]> {
|
|
1694
1588
|
* async execute() {
|
|
1695
1589
|
* const { userId } = this.params;
|
|
1696
|
-
* await this.
|
|
1590
|
+
* await this.reply(`approved <@${userId}>`);
|
|
1697
1591
|
* }
|
|
1698
1592
|
* }
|
|
1699
1593
|
* ```
|
|
@@ -1707,7 +1601,8 @@ var ButtonHandler = class extends ComponentHandler {};
|
|
|
1707
1601
|
*
|
|
1708
1602
|
* Register the customId definitions this handler decodes with `@ModalRoute`, list the same ones in the
|
|
1709
1603
|
* generic, then read `this.params` for a single route or `this.match` for several. Read the submitted
|
|
1710
|
-
* inputs from `this.event.fields
|
|
1604
|
+
* inputs from `this.event.fields`, and reply through the handler members. Discord forbids opening a modal in
|
|
1605
|
+
* response to a modal, so `showModal` fails compilation on this kind.
|
|
1711
1606
|
*
|
|
1712
1607
|
* @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ConfigId]`.
|
|
1713
1608
|
* @typeParam Cache - The interaction cache state, `'cached'` by default.
|
|
@@ -1719,7 +1614,7 @@ var ButtonHandler = class extends ComponentHandler {};
|
|
|
1719
1614
|
* async execute() {
|
|
1720
1615
|
* const { guildId } = this.params;
|
|
1721
1616
|
* const name = this.event.fields.getTextInputValue('name');
|
|
1722
|
-
* await this.
|
|
1617
|
+
* await this.reply(`saved ${name} for ${guildId}`);
|
|
1723
1618
|
* }
|
|
1724
1619
|
* }
|
|
1725
1620
|
* ```
|
|
@@ -1733,7 +1628,8 @@ var ModalHandler = class extends ComponentHandler {};
|
|
|
1733
1628
|
*
|
|
1734
1629
|
* Pass the select kind first and the customId definitions second, the same order as `@SelectMenuRoute`,
|
|
1735
1630
|
* so `this.event` and `this.event.values` are narrowed to that kind. Read `this.params` for a single
|
|
1736
|
-
* route or `this.match` for several
|
|
1631
|
+
* route or `this.match` for several, and reply through the handler members or rewrite the source message
|
|
1632
|
+
* with `this.update`.
|
|
1737
1633
|
*
|
|
1738
1634
|
* @typeParam Kind - The select kind from {@link SelectMenuKind}, e.g. `SelectMenuKind.User`.
|
|
1739
1635
|
* @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof AssignId]`.
|
|
@@ -1745,7 +1641,7 @@ var ModalHandler = class extends ComponentHandler {};
|
|
|
1745
1641
|
* class AssignSelect extends SelectMenuHandler<SelectMenuKind.User, [typeof AssignId]> {
|
|
1746
1642
|
* async execute() {
|
|
1747
1643
|
* const { roleId } = this.params;
|
|
1748
|
-
* await this.
|
|
1644
|
+
* await this.reply(`assigning ${this.event.values.length} member(s) to <@&${roleId}>`);
|
|
1749
1645
|
* }
|
|
1750
1646
|
* }
|
|
1751
1647
|
* ```
|
|
@@ -1776,7 +1672,7 @@ var SelectMenuHandler = class extends ComponentHandler {};
|
|
|
1776
1672
|
* }
|
|
1777
1673
|
* ```
|
|
1778
1674
|
*/
|
|
1779
|
-
var EventHandler = class extends BaseHandler {
|
|
1675
|
+
var EventHandler = class extends BaseHandler$1 {
|
|
1780
1676
|
firedEvent;
|
|
1781
1677
|
constructor(event, core, eventName) {
|
|
1782
1678
|
super(event, core);
|
|
@@ -1829,7 +1725,7 @@ var EventHandler = class extends BaseHandler {
|
|
|
1829
1725
|
*
|
|
1830
1726
|
* @typeParam EventName - One or more `ClientEvents` keys. Defaults to every event (catchall).
|
|
1831
1727
|
*/
|
|
1832
|
-
var EventMiddleware = class extends BaseHandler {
|
|
1728
|
+
var EventMiddleware = class extends BaseHandler$1 {
|
|
1833
1729
|
firedEvent;
|
|
1834
1730
|
constructor(event, core, eventName) {
|
|
1835
1731
|
super(event, core);
|
|
@@ -2011,16 +1907,16 @@ var Paginator = class {
|
|
|
2011
1907
|
const loadPage = (ctx, n) => this.page(ctx, n);
|
|
2012
1908
|
this.Handler = class Nav extends ButtonHandler {
|
|
2013
1909
|
async execute() {
|
|
2014
|
-
await this.
|
|
1910
|
+
await this.deferUpdate();
|
|
2015
1911
|
const response = await loadPage(contextOf(this.event, this.core), this.params.page);
|
|
2016
|
-
await
|
|
1912
|
+
await this.update(response);
|
|
2017
1913
|
}
|
|
2018
1914
|
};
|
|
2019
1915
|
}
|
|
2020
1916
|
/** Render page 0 and send it, picking reply or followUp from the interaction's state. */
|
|
2021
1917
|
async start(interaction, core) {
|
|
2022
1918
|
const response = await this.page(contextOf(interaction, core), 0);
|
|
2023
|
-
return new ReplySender(interaction).send(response, this.config.ephemeral ?? false);
|
|
1919
|
+
return new ReplySender(interaction, interactionRoute(interaction)).send(response, { ephemeral: this.config.ephemeral ?? false });
|
|
2024
1920
|
}
|
|
2025
1921
|
/** Render a page as a {@link ReplyResponse}. To post it elsewhere, add `flags: MessageFlags.IsComponentsV2`. */
|
|
2026
1922
|
async page(ctx, n) {
|
|
@@ -2145,183 +2041,183 @@ function Subscribe(subscriber, options) {
|
|
|
2145
2041
|
}
|
|
2146
2042
|
|
|
2147
2043
|
//#endregion
|
|
2148
|
-
//#region src/subscribers/
|
|
2044
|
+
//#region src/subscribers/decorators/WebhookUrl.ts
|
|
2149
2045
|
/**
|
|
2150
|
-
*
|
|
2046
|
+
* Declares the environment variable a {@link WebhookLog} reporter reads its webhook url from.
|
|
2151
2047
|
*
|
|
2152
|
-
*
|
|
2153
|
-
*
|
|
2048
|
+
* An unset variable disables the reporter with a boot warning. A set but malformed value throws
|
|
2049
|
+
* at boot.
|
|
2154
2050
|
*
|
|
2155
|
-
* @
|
|
2051
|
+
* @param envKey - The environment variable name holding the webhook url
|
|
2052
|
+
* @decorator
|
|
2053
|
+
* @example
|
|
2054
|
+
* ```ts
|
|
2055
|
+
* \@Subscribe('memberBanned')
|
|
2056
|
+
* \@WebhookUrl('BAN_LOG_WEBHOOK_URL')
|
|
2057
|
+
* class BanLog extends WebhookLog<'memberBanned'> {
|
|
2058
|
+
* report() {
|
|
2059
|
+
* return { components: [new BanCard(this.data).component] };
|
|
2060
|
+
* }
|
|
2061
|
+
* }
|
|
2062
|
+
* ```
|
|
2156
2063
|
*/
|
|
2157
|
-
|
|
2158
|
-
constructor
|
|
2159
|
-
|
|
2064
|
+
function WebhookUrl(envKey) {
|
|
2065
|
+
return function(constructor) {
|
|
2066
|
+
Reflect.defineMetadata(WebhookUrlMetadataKey, envKey, constructor);
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
//#endregion
|
|
2071
|
+
//#region src/miscellaneous/flagsFor.ts
|
|
2072
|
+
/** @internal */
|
|
2073
|
+
function flagsFor(ephemeral) {
|
|
2074
|
+
let flags = MessageFlags$1.IsComponentsV2;
|
|
2075
|
+
if (ephemeral) flags |= MessageFlags$1.Ephemeral;
|
|
2076
|
+
return flags;
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
//#endregion
|
|
2080
|
+
//#region src/subscribers/bases/webhookHelpers.ts
|
|
2081
|
+
const DISCORD_WEBHOOK_REGEX = new RegExp(String.raw`^https?:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w$-]+$`);
|
|
2082
|
+
function isDiscordWebhookUrl(value) {
|
|
2083
|
+
return URL.canParse(value) && DISCORD_WEBHOOK_REGEX.test(value);
|
|
2084
|
+
}
|
|
2085
|
+
function jsonAttachment(name, description, data) {
|
|
2086
|
+
const content = filterCirculars(data);
|
|
2087
|
+
return {
|
|
2088
|
+
name,
|
|
2089
|
+
description,
|
|
2090
|
+
data: Buffer.from(JSON.stringify(content, void 0, 2), "utf8")
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
function neutralizeFences(text) {
|
|
2094
|
+
return text.replaceAll("```", "```");
|
|
2095
|
+
}
|
|
2096
|
+
/** An error's stack with its direct cause appended (one level), ANSI-stripped for the webhook payload. */
|
|
2097
|
+
function errorReport(error) {
|
|
2098
|
+
let report = error.stack ?? `${error.name}: ${error.message}`;
|
|
2099
|
+
if (Error.isError(error.cause)) {
|
|
2100
|
+
const cause = error.cause.stack ?? `${error.cause.name}: ${error.cause.message}`;
|
|
2101
|
+
report += `\n\nCaused by:\n${cause}`;
|
|
2102
|
+
}
|
|
2103
|
+
const stripped = neutralizeFences(stripAnsi(report));
|
|
2104
|
+
return stripped.length > 1800 ? `${stripped.slice(0, 1799)}…` : stripped;
|
|
2105
|
+
}
|
|
2106
|
+
var WebhookSeparator = class extends BuilderComponent {
|
|
2107
|
+
constructor() {
|
|
2108
|
+
super("separator");
|
|
2109
|
+
this.instance.setSpacing(SeparatorSpacingSize.Small).setDivider(true);
|
|
2160
2110
|
}
|
|
2161
2111
|
};
|
|
2162
2112
|
|
|
2163
2113
|
//#endregion
|
|
2164
|
-
//#region src/
|
|
2114
|
+
//#region src/subscribers/bases/WebhookSender.ts
|
|
2115
|
+
const HTTP_NOT_FOUND = 404;
|
|
2116
|
+
const HTTP_UNAUTHORIZED = 401;
|
|
2165
2117
|
/**
|
|
2166
|
-
*
|
|
2167
|
-
*
|
|
2168
|
-
* You can either implement a custom `onHmr()` method in your class by extending `HmrAware`, or if it's a plugin, override the existing `onHmr()` with fully custom logic. Or, use this class's `handle()` method inside your `onHmr()` to get standard HMR handling for your modules based on the provided options you give to the constructor.
|
|
2118
|
+
* Sends component messages to a Discord webhook through `@discordjs/rest`.
|
|
2169
2119
|
*
|
|
2170
|
-
*
|
|
2120
|
+
* @throws A `SeedcordError` when the url does not carry a webhook id and token.
|
|
2171
2121
|
*/
|
|
2172
|
-
var
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
constructor(
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
};
|
|
2180
|
-
this.store = {
|
|
2181
|
-
fileToHandlers: /* @__PURE__ */ new Map(),
|
|
2182
|
-
fileToMiddlewares: /* @__PURE__ */ new Map(),
|
|
2183
|
-
handlerArtifacts: /* @__PURE__ */ new Map()
|
|
2184
|
-
};
|
|
2185
|
-
}
|
|
2186
|
-
/**
|
|
2187
|
-
* Handles an HMR update event by reloading affected modules based on the event details and config. It receives an {@link HmrUpdateEvent}, then checks the type of update.
|
|
2188
|
-
*
|
|
2189
|
-
* - If it's a deletion, it unloads the module.
|
|
2190
|
-
* - If it's an update/creation, it checks if the file is tracked, unloads it if so, then reloads the file if it is in the handlers or middlewares directory and is a valid handler/middleware.
|
|
2191
|
-
*/
|
|
2192
|
-
async handle(event) {
|
|
2193
|
-
const { file, affectedModules, type } = event;
|
|
2194
|
-
const { logger, handlersDir, middlewaresDir } = this.options;
|
|
2195
|
-
if (type === "delete" || type === "deleteDir") {
|
|
2196
|
-
this.unload(file);
|
|
2197
|
-
return;
|
|
2198
|
-
}
|
|
2199
|
-
if (type === "update" && !existsSync(file)) {
|
|
2200
|
-
if (this.isTracked(file)) {
|
|
2201
|
-
logger.info(`Received update for non-existent file: ${formatFilePath(file)}, treating as delete`);
|
|
2202
|
-
this.unload(file);
|
|
2203
|
-
}
|
|
2204
|
-
return;
|
|
2205
|
-
}
|
|
2206
|
-
const rollbackEnabled = event.rollback ?? true;
|
|
2207
|
-
const filesToReload = affectedModules && affectedModules.length > 0 ? affectedModules : [file];
|
|
2208
|
-
const absHandlersDir = resolve(process.cwd(), handlersDir);
|
|
2209
|
-
const absMiddlewaresDir = middlewaresDir ? resolve(process.cwd(), middlewaresDir) : null;
|
|
2210
|
-
for (const fileToReload of filesToReload) {
|
|
2211
|
-
const isHandler = fileToReload.startsWith(absHandlersDir);
|
|
2212
|
-
const isMiddleware = absMiddlewaresDir ? fileToReload.startsWith(absMiddlewaresDir) : false;
|
|
2213
|
-
if (!isHandler && !isMiddleware) continue;
|
|
2214
|
-
await this.reloadWithRollback(fileToReload, rollbackEnabled);
|
|
2215
|
-
}
|
|
2216
|
-
}
|
|
2217
|
-
async reloadWithRollback(file, rollbackEnabled) {
|
|
2218
|
-
const { logger } = this.options;
|
|
2219
|
-
const snapshot = this.snapshotUnits(file);
|
|
2220
|
-
this.unload(file);
|
|
2221
|
-
if (await this.reloadFile(file)) return;
|
|
2222
|
-
this.unload(file);
|
|
2223
|
-
if (!rollbackEnabled) return;
|
|
2224
|
-
this.restoreUnits(file, snapshot);
|
|
2225
|
-
logger.warn(`${chalk.yellow.bold("Rolled back")} ${chalk.gray(formatFilePath(file))} to the last-good version`);
|
|
2122
|
+
var WebhookSender = class {
|
|
2123
|
+
rest = new REST();
|
|
2124
|
+
route;
|
|
2125
|
+
constructor(url) {
|
|
2126
|
+
const match = /\/webhooks\/(\d+)\/([\w$-]+)$/.exec(url);
|
|
2127
|
+
if (!match?.[1] || !match[2]) throw new SeedcordError(SeedcordErrorCode.ConfigWebhookUrlInvalid, ["The url"]);
|
|
2128
|
+
this.route = Routes.webhook(match[1], match[2]);
|
|
2226
2129
|
}
|
|
2227
2130
|
/**
|
|
2228
|
-
*
|
|
2229
|
-
*
|
|
2230
|
-
* @param handler - The handler module instance.
|
|
2131
|
+
* Fetches the webhook object, sending no message. Returns `missing` on a 404 or 401 from
|
|
2132
|
+
* Discord and `unreachable` on any other failure.
|
|
2231
2133
|
*/
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
set.add(handler);
|
|
2239
|
-
if (this.options.getArtifacts) {
|
|
2240
|
-
const artifacts = this.options.getArtifacts(handler);
|
|
2241
|
-
this.store.handlerArtifacts.set(handler, artifacts);
|
|
2134
|
+
async verify() {
|
|
2135
|
+
try {
|
|
2136
|
+
await this.rest.get(this.route, { auth: false });
|
|
2137
|
+
return "ok";
|
|
2138
|
+
} catch (error) {
|
|
2139
|
+
return error instanceof DiscordAPIError$1 && (error.status === HTTP_NOT_FOUND || error.status === HTTP_UNAUTHORIZED) ? "missing" : "unreachable";
|
|
2242
2140
|
}
|
|
2243
2141
|
}
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
this.store.handlerArtifacts.delete(handler);
|
|
2268
|
-
const name = handler.name ?? "Handler";
|
|
2269
|
-
logger.info(`${chalk.red.bold("Unloaded")} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`);
|
|
2270
|
-
}
|
|
2271
|
-
this.store.fileToHandlers.delete(file);
|
|
2272
|
-
}
|
|
2273
|
-
const middlewares = this.store.fileToMiddlewares.get(file);
|
|
2274
|
-
if (middlewares) {
|
|
2275
|
-
for (const middleware of middlewares) {
|
|
2276
|
-
this.options.unregisterMiddleware?.(middleware);
|
|
2277
|
-
const name = middleware.name ?? "Middleware";
|
|
2278
|
-
logger.info(`${chalk.red.bold("Unloaded")} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`);
|
|
2279
|
-
}
|
|
2280
|
-
this.store.fileToMiddlewares.delete(file);
|
|
2281
|
-
}
|
|
2142
|
+
async send(options) {
|
|
2143
|
+
const { flags, username, components, files } = options;
|
|
2144
|
+
await this.rest.post(this.route, {
|
|
2145
|
+
auth: false,
|
|
2146
|
+
query: new URLSearchParams({
|
|
2147
|
+
with_components: "true",
|
|
2148
|
+
wait: "true"
|
|
2149
|
+
}),
|
|
2150
|
+
body: {
|
|
2151
|
+
flags,
|
|
2152
|
+
username,
|
|
2153
|
+
components: components.map((component) => component.toJSON()),
|
|
2154
|
+
...files?.length && { attachments: files.map((file, id) => ({
|
|
2155
|
+
id,
|
|
2156
|
+
filename: file.name,
|
|
2157
|
+
description: file.description
|
|
2158
|
+
})) }
|
|
2159
|
+
},
|
|
2160
|
+
...files?.length && { files: files.map((file) => ({
|
|
2161
|
+
name: file.name,
|
|
2162
|
+
data: file.data
|
|
2163
|
+
})) }
|
|
2164
|
+
});
|
|
2282
2165
|
}
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2166
|
+
};
|
|
2167
|
+
|
|
2168
|
+
//#endregion
|
|
2169
|
+
//#region src/subscribers/bases/WebhookLog.ts
|
|
2170
|
+
/**
|
|
2171
|
+
* Base class for subscribers that deliver their event to a Discord webhook.
|
|
2172
|
+
*
|
|
2173
|
+
* Declare the url's environment variable with `@WebhookUrl` and implement {@link report}. Url
|
|
2174
|
+
* resolution, validation, sender reuse, and sending run in the base. When the variable is unset
|
|
2175
|
+
* the reporter is skipped at registration with a boot warning.
|
|
2176
|
+
*
|
|
2177
|
+
* @typeParam KeyOfSubscribers - The subscription key this reporter receives
|
|
2178
|
+
*/
|
|
2179
|
+
var WebhookLog = class WebhookLog extends Subscriber {
|
|
2180
|
+
static senders = /* @__PURE__ */ new Map();
|
|
2181
|
+
async execute() {
|
|
2182
|
+
const url = WebhookLog.urlOf(WebhookLog.envKeyOf(this.constructor));
|
|
2183
|
+
if (url === null) {
|
|
2184
|
+
this.logger.warn(`${chalk.bold(this.constructor.name)} has no webhook url set, this reporter is disabled`);
|
|
2185
|
+
return;
|
|
2288
2186
|
}
|
|
2187
|
+
const { username, components, files } = await this.report();
|
|
2289
2188
|
try {
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
const name = val.name ?? "Handler";
|
|
2297
|
-
logger.info(`${chalk.blue.bold("Reloaded")} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`);
|
|
2298
|
-
} else if (isMiddleware && registerMiddleware && isMiddleware(val)) {
|
|
2299
|
-
registerMiddleware(val, file);
|
|
2300
|
-
this.trackMiddleware(file, val);
|
|
2301
|
-
const name = val.name ?? "Middleware";
|
|
2302
|
-
logger.info(`${chalk.blue.bold("Reloaded")} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`);
|
|
2303
|
-
}
|
|
2304
|
-
return true;
|
|
2189
|
+
await WebhookLog.senderFor(url).send({
|
|
2190
|
+
flags: flagsFor(false),
|
|
2191
|
+
username: username ?? this.constructor.name,
|
|
2192
|
+
components,
|
|
2193
|
+
files
|
|
2194
|
+
});
|
|
2305
2195
|
} catch (error) {
|
|
2306
|
-
logger.error(
|
|
2307
|
-
return false;
|
|
2196
|
+
this.logger.error("Failed to send webhook report", error);
|
|
2308
2197
|
}
|
|
2309
2198
|
}
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
restoreUnits(file, snapshot) {
|
|
2317
|
-
for (const handler of snapshot.handlers) {
|
|
2318
|
-
this.options.registerHandler(handler, file);
|
|
2319
|
-
this.trackHandler(file, handler);
|
|
2320
|
-
}
|
|
2321
|
-
if (this.options.registerMiddleware) for (const middleware of snapshot.middlewares) {
|
|
2322
|
-
this.options.registerMiddleware(middleware, file);
|
|
2323
|
-
this.trackMiddleware(file, middleware);
|
|
2199
|
+
/** @internal */
|
|
2200
|
+
static senderFor(url) {
|
|
2201
|
+
let sender = WebhookLog.senders.get(url);
|
|
2202
|
+
if (!sender) {
|
|
2203
|
+
sender = new WebhookSender(url);
|
|
2204
|
+
WebhookLog.senders.set(url, sender);
|
|
2324
2205
|
}
|
|
2206
|
+
return sender;
|
|
2207
|
+
}
|
|
2208
|
+
/** @internal */
|
|
2209
|
+
static envKeyOf(ctor) {
|
|
2210
|
+
const envKey = Reflect.getMetadata(WebhookUrlMetadataKey, ctor);
|
|
2211
|
+
if (!envKey) throw new SeedcordError(SeedcordErrorCode.DecoratorWebhookUrlMissing, [ctor.name]);
|
|
2212
|
+
return envKey;
|
|
2213
|
+
}
|
|
2214
|
+
/** @internal Returns null when the env var is unset, throws when set but malformed. */
|
|
2215
|
+
static urlOf(envKey) {
|
|
2216
|
+
if (!Envapter.has(envKey)) return null;
|
|
2217
|
+
const raw = Envapter.getRequired(envKey, Converters.String).trim();
|
|
2218
|
+
if (raw === "") return null;
|
|
2219
|
+
if (!isDiscordWebhookUrl(raw)) throw new SeedcordError(SeedcordErrorCode.ConfigWebhookUrlInvalid, [envKey]);
|
|
2220
|
+
return raw;
|
|
2325
2221
|
}
|
|
2326
2222
|
};
|
|
2327
2223
|
|
|
@@ -2385,6 +2281,8 @@ var CommandRegistry = class {
|
|
|
2385
2281
|
globalCommands = [];
|
|
2386
2282
|
guildCommands = new Collection();
|
|
2387
2283
|
ctorToCommand = /* @__PURE__ */ new Map();
|
|
2284
|
+
loading = false;
|
|
2285
|
+
loadedCommands = [];
|
|
2388
2286
|
hmrHandler;
|
|
2389
2287
|
pendingEvents = /* @__PURE__ */ new Map();
|
|
2390
2288
|
constructor(core, onDeployed) {
|
|
@@ -2411,16 +2309,33 @@ var CommandRegistry = class {
|
|
|
2411
2309
|
this.isInitialised = true;
|
|
2412
2310
|
const commandsDir = this.core.config.bot.commands.path;
|
|
2413
2311
|
if (!commandsDir) return;
|
|
2414
|
-
this.
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2312
|
+
this.loading = true;
|
|
2313
|
+
this.loadedCommands.length = 0;
|
|
2314
|
+
try {
|
|
2315
|
+
await this.loadCommands(commandsDir);
|
|
2316
|
+
} finally {
|
|
2317
|
+
this.loading = false;
|
|
2318
|
+
}
|
|
2319
|
+
this.reportLoad();
|
|
2420
2320
|
getDevChannel()?.on("seedcord:refresh-commands", (data) => {
|
|
2421
2321
|
this.refresh(data.shouldRefresh);
|
|
2422
2322
|
});
|
|
2423
2323
|
}
|
|
2324
|
+
reportLoad() {
|
|
2325
|
+
const { utils } = this.logger;
|
|
2326
|
+
utils.summary("Loaded commands", {
|
|
2327
|
+
global: this.globalCommands.length,
|
|
2328
|
+
"guild groups": this.guildCommands.size
|
|
2329
|
+
});
|
|
2330
|
+
let slash = 0;
|
|
2331
|
+
let menu = 0;
|
|
2332
|
+
for (const command of this.loadedCommands) if (command.kind === "slash command") slash++;
|
|
2333
|
+
else menu++;
|
|
2334
|
+
utils.block("Loaded commands", [...utils.entries(this.loadedCommands), ...utils.counts({
|
|
2335
|
+
"slash commands": slash,
|
|
2336
|
+
"context menus": menu
|
|
2337
|
+
})]);
|
|
2338
|
+
}
|
|
2424
2339
|
/** @internal */
|
|
2425
2340
|
async refresh(shouldRefresh = true) {
|
|
2426
2341
|
if (!shouldRefresh) {
|
|
@@ -2458,25 +2373,27 @@ var CommandRegistry = class {
|
|
|
2458
2373
|
const meta = Reflect.getMetadata(CommandMetadataKey, ctor);
|
|
2459
2374
|
if (!meta) return;
|
|
2460
2375
|
const comp = new ctor().component;
|
|
2461
|
-
const
|
|
2462
|
-
if (meta.scope === "global")
|
|
2463
|
-
|
|
2464
|
-
this.
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
for (const g of meta.guilds) {
|
|
2468
|
-
const arr = this.guildCommands.get(g) ?? [];
|
|
2469
|
-
arr.push(comp);
|
|
2470
|
-
this.guildCommands.set(g, arr);
|
|
2471
|
-
}
|
|
2472
|
-
this.logger.utils.registration(ctor.name, rel);
|
|
2473
|
-
this.logger.utils.item(`Guild ${commandType}: ${chalk.bold.cyan(comp.name)} for ${chalk.magenta.bold(meta.guilds.length)} guild(s)`);
|
|
2376
|
+
const kind = comp instanceof SlashCommandBuilder ? "slash command" : "context menu";
|
|
2377
|
+
if (meta.scope === "global") this.globalCommands.push(comp);
|
|
2378
|
+
else for (const g of meta.guilds) {
|
|
2379
|
+
const arr = this.guildCommands.get(g) ?? [];
|
|
2380
|
+
arr.push(comp);
|
|
2381
|
+
this.guildCommands.set(g, arr);
|
|
2474
2382
|
}
|
|
2475
2383
|
this.ctorToCommand.set(ctor, {
|
|
2476
2384
|
name: comp.name,
|
|
2477
2385
|
scope: meta.scope,
|
|
2478
2386
|
...meta.scope === "guild" && { guilds: meta.guilds }
|
|
2479
2387
|
});
|
|
2388
|
+
if (this.loading) {
|
|
2389
|
+
this.loadedCommands.push({
|
|
2390
|
+
name: comp.name,
|
|
2391
|
+
from: formatFilePath(rel),
|
|
2392
|
+
kind
|
|
2393
|
+
});
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
this.logger.utils.registration(comp.name, rel, `${meta.scope} ${kind}`);
|
|
2480
2397
|
}
|
|
2481
2398
|
unregisterCommand(ctor, artifacts) {
|
|
2482
2399
|
const info = artifacts ?? this.ctorToCommand.get(ctor);
|
|
@@ -2503,8 +2420,7 @@ var CommandRegistry = class {
|
|
|
2503
2420
|
const deployed = await this.core.bot.client.application?.commands.set(this.globalCommands);
|
|
2504
2421
|
if (deployed) result.global = deployed;
|
|
2505
2422
|
const tag = this.globalCommands.length === 1 ? "command" : "commands";
|
|
2506
|
-
this.logger.utils.
|
|
2507
|
-
this.logger.utils.item(`${this.globalCommands.map((command) => chalk.bold.cyan(command.name)).join(", ")}`);
|
|
2423
|
+
this.logger.utils.block(`Deployed ${this.globalCommands.length} global ${tag}`, this.logger.utils.wrap(this.globalCommands.map((command) => command.name)), "info");
|
|
2508
2424
|
}
|
|
2509
2425
|
for (const [guildId, commands] of this.guildCommands.entries()) {
|
|
2510
2426
|
const guild = this.core.bot.client.guilds.cache.get(guildId);
|
|
@@ -2515,8 +2431,7 @@ var CommandRegistry = class {
|
|
|
2515
2431
|
const deployed = await guild.commands.set(commands);
|
|
2516
2432
|
result.guilds.set(guildId, deployed);
|
|
2517
2433
|
const tag = commands.length === 1 ? "command" : "commands";
|
|
2518
|
-
this.logger.utils.
|
|
2519
|
-
this.logger.utils.item(`${commands.map((command) => chalk.bold.cyan(command.name)).join(", ")}`);
|
|
2434
|
+
this.logger.utils.block(`Deployed ${commands.length} ${tag} to ${paint.amber.bold(guild.name)}`, this.logger.utils.wrap(commands.map((command) => command.name)), "info");
|
|
2520
2435
|
}
|
|
2521
2436
|
this.onDeployed?.(result);
|
|
2522
2437
|
return result;
|
|
@@ -2545,234 +2460,67 @@ function deriveEventActor(args) {
|
|
|
2545
2460
|
if (arg instanceof GuildMember) return {
|
|
2546
2461
|
guild: arg.guild,
|
|
2547
2462
|
member: arg,
|
|
2548
|
-
user: arg.user,
|
|
2549
|
-
channelId: null
|
|
2550
|
-
};
|
|
2551
|
-
if (arg instanceof User) return {
|
|
2552
|
-
guild: null,
|
|
2553
|
-
member: null,
|
|
2554
|
-
user: arg,
|
|
2555
|
-
channelId: null
|
|
2556
|
-
};
|
|
2557
|
-
}
|
|
2558
|
-
return {
|
|
2559
|
-
guild: null,
|
|
2560
|
-
member: null,
|
|
2561
|
-
user: null,
|
|
2562
|
-
channelId: null
|
|
2563
|
-
};
|
|
2564
|
-
}
|
|
2565
|
-
|
|
2566
|
-
//#endregion
|
|
2567
|
-
//#region src/bot/gates/runGates.ts
|
|
2568
|
-
function interactionGateContext(interaction, core) {
|
|
2569
|
-
const rawMember = interaction.member;
|
|
2570
|
-
const memberRoleIds = rawMember instanceof GuildMember ? [...rawMember.roles.cache.keys()].filter((id) => id !== interaction.guildId) : rawMember?.roles ?? [];
|
|
2571
|
-
return {
|
|
2572
|
-
kind: "interaction",
|
|
2573
|
-
interaction,
|
|
2574
|
-
core,
|
|
2575
|
-
user: interaction.user,
|
|
2576
|
-
guild: interaction.guild,
|
|
2577
|
-
member: rawMember instanceof GuildMember ? rawMember : null,
|
|
2578
|
-
userId: interaction.user.id,
|
|
2579
|
-
guildId: interaction.guildId,
|
|
2580
|
-
channelId: interaction.channelId,
|
|
2581
|
-
memberRoleIds,
|
|
2582
|
-
memberPermissions: interaction.memberPermissions?.bitfield ?? null,
|
|
2583
|
-
routeId: null
|
|
2584
|
-
};
|
|
2585
|
-
}
|
|
2586
|
-
function eventGateContext(eventName, args, core) {
|
|
2587
|
-
const actor = deriveEventActor(args);
|
|
2588
|
-
return {
|
|
2589
|
-
kind: "event",
|
|
2590
|
-
core,
|
|
2591
|
-
eventName,
|
|
2592
|
-
payload: args,
|
|
2593
|
-
user: actor.user,
|
|
2594
|
-
guild: actor.guild,
|
|
2595
|
-
member: actor.member,
|
|
2596
|
-
userId: actor.user?.id ?? null,
|
|
2597
|
-
guildId: actor.guild?.id ?? null,
|
|
2598
|
-
channelId: actor.channelId,
|
|
2599
|
-
memberRoleIds: actor.member ? [...actor.member.roles.cache.keys()].filter((id) => id !== actor.guild?.id) : [],
|
|
2600
|
-
memberPermissions: actor.member?.permissions.bitfield ?? null,
|
|
2601
|
-
routeId: null
|
|
2602
|
-
};
|
|
2603
|
-
}
|
|
2604
|
-
|
|
2605
|
-
//#endregion
|
|
2606
|
-
//#region src/miscellaneous/FaultThrottle.ts
|
|
2607
|
-
/**
|
|
2608
|
-
* Drops duplicate fault reports inside a fixed window so a recurring fault (a database outage on a
|
|
2609
|
-
* hot path) reports once per window instead of flooding the webhook. Keyed on a structural string, so
|
|
2610
|
-
* the map stays small, and a stale entry is evicted on the next read of its key.
|
|
2611
|
-
*
|
|
2612
|
-
* @internal
|
|
2613
|
-
*/
|
|
2614
|
-
var FaultThrottle = class {
|
|
2615
|
-
windowMs;
|
|
2616
|
-
now;
|
|
2617
|
-
lastReportedAt = /* @__PURE__ */ new Map();
|
|
2618
|
-
constructor(windowMs = 6e4, now = () => Date.now()) {
|
|
2619
|
-
this.windowMs = windowMs;
|
|
2620
|
-
this.now = now;
|
|
2621
|
-
}
|
|
2622
|
-
/** Evicts the entry for this key if it has aged out, so the map never holds stale keys forever. */
|
|
2623
|
-
shouldReport(key) {
|
|
2624
|
-
const last = this.lastReportedAt.get(key);
|
|
2625
|
-
if (last === void 0) return true;
|
|
2626
|
-
if (this.now() - last < this.windowMs) return false;
|
|
2627
|
-
this.lastReportedAt.delete(key);
|
|
2628
|
-
return true;
|
|
2629
|
-
}
|
|
2630
|
-
markReported(key) {
|
|
2631
|
-
this.lastReportedAt.set(key, this.now());
|
|
2632
|
-
}
|
|
2633
|
-
clear() {
|
|
2634
|
-
this.lastReportedAt.clear();
|
|
2635
|
-
}
|
|
2636
|
-
};
|
|
2637
|
-
|
|
2638
|
-
//#endregion
|
|
2639
|
-
//#region src/miscellaneous/extractErrorResponse.ts
|
|
2640
|
-
const logger$2 = new Logger("ErrorsHandling");
|
|
2641
|
-
/**
|
|
2642
|
-
* The single throttle for every fault publish, so a recurring fault reports once per window across both
|
|
2643
|
-
* the interaction and event paths. Exported so tests reset it between cases.
|
|
2644
|
-
*
|
|
2645
|
-
* @internal
|
|
2646
|
-
*/
|
|
2647
|
-
const faultThrottle = new FaultThrottle();
|
|
2648
|
-
/**
|
|
2649
|
-
* Processes an error into a user-facing reply and routes its report.
|
|
2650
|
-
*
|
|
2651
|
-
* A {@link Notice} always renders its own reply. A reporting denial (`report: true`) additionally
|
|
2652
|
-
* publishes `handledException` on the interaction path. A raw, non-denial throw publishes
|
|
2653
|
-
* `unknownException` and renders the configured `defaultError` (or a generic {@link Fault}). One uuid is
|
|
2654
|
-
* threaded into both the reply and the bus payload.
|
|
2655
|
-
*
|
|
2656
|
-
* @internal
|
|
2657
|
-
*/
|
|
2658
|
-
function extractErrorResponse(error, core, origin) {
|
|
2659
|
-
const uuid = crypto.randomUUID();
|
|
2660
|
-
const developerUsername = core.config.notifications?.developerUsername;
|
|
2661
|
-
const ctx = developerUsername === void 0 ? { uuid } : {
|
|
2662
|
-
uuid,
|
|
2663
|
-
developerUsername
|
|
2664
|
-
};
|
|
2665
|
-
if (error instanceof Notice) {
|
|
2666
|
-
if (error.report) reportFault(error, core, origin, uuid);
|
|
2667
|
-
return {
|
|
2668
|
-
uuid,
|
|
2669
|
-
response: error.render(ctx)
|
|
2670
|
-
};
|
|
2671
|
-
}
|
|
2672
|
-
reportRawFault(error, core, origin, uuid);
|
|
2673
|
-
const override = core.config.errors?.defaultError;
|
|
2674
|
-
return {
|
|
2675
|
-
uuid,
|
|
2676
|
-
response: override ? new override(uuid).render(ctx) : new Fault().render(ctx)
|
|
2677
|
-
};
|
|
2678
|
-
}
|
|
2679
|
-
function withThrottle(origin, error, publish) {
|
|
2680
|
-
const key = faultKey(origin, error);
|
|
2681
|
-
if (!faultThrottle.shouldReport(key)) {
|
|
2682
|
-
logger$2.info(`throttled duplicate fault ${key}`);
|
|
2683
|
-
return;
|
|
2463
|
+
user: arg.user,
|
|
2464
|
+
channelId: null
|
|
2465
|
+
};
|
|
2466
|
+
if (arg instanceof User) return {
|
|
2467
|
+
guild: null,
|
|
2468
|
+
member: null,
|
|
2469
|
+
user: arg,
|
|
2470
|
+
channelId: null
|
|
2471
|
+
};
|
|
2684
2472
|
}
|
|
2685
|
-
publish();
|
|
2686
|
-
faultThrottle.markReported(key);
|
|
2687
|
-
}
|
|
2688
|
-
function reportFault(denial, core, origin, uuid) {
|
|
2689
|
-
withThrottle(origin, denial, () => {
|
|
2690
|
-
logger$2.error(`${denial.name}: ${uuid}`, denial);
|
|
2691
|
-
if (origin.interaction) core.bus.publish("handledException", {
|
|
2692
|
-
denial,
|
|
2693
|
-
uuid,
|
|
2694
|
-
source: buildInteractionSource(origin.interaction)
|
|
2695
|
-
});
|
|
2696
|
-
else if (origin.event) core.bus.publish("handledException", {
|
|
2697
|
-
denial,
|
|
2698
|
-
uuid,
|
|
2699
|
-
source: buildEventSource(origin.event, origin)
|
|
2700
|
-
});
|
|
2701
|
-
else core.bus.publish("unknownException", {
|
|
2702
|
-
uuid,
|
|
2703
|
-
error: denial,
|
|
2704
|
-
guild: origin.guild,
|
|
2705
|
-
user: origin.user,
|
|
2706
|
-
metadata: metadataFor(origin)
|
|
2707
|
-
});
|
|
2708
|
-
});
|
|
2709
|
-
}
|
|
2710
|
-
function reportRawFault(error, core, origin, uuid) {
|
|
2711
|
-
withThrottle(origin, error, () => {
|
|
2712
|
-
if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
|
|
2713
|
-
else logger$2.error(`${uuid} | ${error.message}`);
|
|
2714
|
-
core.bus.publish("unknownException", {
|
|
2715
|
-
uuid,
|
|
2716
|
-
error,
|
|
2717
|
-
guild: origin.guild,
|
|
2718
|
-
user: origin.user,
|
|
2719
|
-
metadata: metadataFor(origin)
|
|
2720
|
-
});
|
|
2721
|
-
});
|
|
2722
|
-
}
|
|
2723
|
-
function metadataFor(origin) {
|
|
2724
|
-
if (origin.event) return {
|
|
2725
|
-
eventName: origin.event.name,
|
|
2726
|
-
handler: origin.event.handler,
|
|
2727
|
-
args: origin.event.args
|
|
2728
|
-
};
|
|
2729
|
-
return origin.metadata;
|
|
2730
|
-
}
|
|
2731
|
-
function buildEventSource(event, origin) {
|
|
2732
2473
|
return {
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
guildId: origin.guild?.id ?? null,
|
|
2738
|
-
channelId: event.channelId,
|
|
2739
|
-
raw: event.args
|
|
2474
|
+
guild: null,
|
|
2475
|
+
member: null,
|
|
2476
|
+
user: null,
|
|
2477
|
+
channelId: null
|
|
2740
2478
|
};
|
|
2741
2479
|
}
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
}
|
|
2749
|
-
function interactionRoute(interaction) {
|
|
2750
|
-
if (interaction.isChatInputCommand()) return slashRouteOf(interaction);
|
|
2751
|
-
if (interaction.isContextMenuCommand()) return interaction.commandName;
|
|
2752
|
-
if (interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit()) return prefixOf(interaction.customId) || interaction.customId;
|
|
2753
|
-
return "interaction";
|
|
2754
|
-
}
|
|
2755
|
-
function buildInteractionSource(interaction) {
|
|
2756
|
-
const command = interaction.isChatInputCommand() ? slashRouteOf(interaction) : interaction.isContextMenuCommand() ? interaction.commandName : null;
|
|
2757
|
-
const customId = interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit() ? interaction.customId : null;
|
|
2480
|
+
|
|
2481
|
+
//#endregion
|
|
2482
|
+
//#region src/bot/gates/runGates.ts
|
|
2483
|
+
function interactionGateContext(interaction, core) {
|
|
2484
|
+
const rawMember = interaction.member;
|
|
2485
|
+
const memberRoleIds = rawMember instanceof GuildMember ? [...rawMember.roles.cache.keys()].filter((id) => id !== interaction.guildId) : rawMember?.roles ?? [];
|
|
2758
2486
|
return {
|
|
2759
2487
|
kind: "interaction",
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2488
|
+
interaction,
|
|
2489
|
+
core,
|
|
2490
|
+
user: interaction.user,
|
|
2491
|
+
guild: interaction.guild,
|
|
2492
|
+
member: rawMember instanceof GuildMember ? rawMember : null,
|
|
2763
2493
|
userId: interaction.user.id,
|
|
2764
2494
|
guildId: interaction.guildId,
|
|
2765
2495
|
channelId: interaction.channelId,
|
|
2766
|
-
|
|
2767
|
-
|
|
2496
|
+
memberRoleIds,
|
|
2497
|
+
memberPermissions: interaction.memberPermissions?.bitfield ?? null,
|
|
2498
|
+
appPermissions: interaction.appPermissions.bitfield,
|
|
2499
|
+
memberGuildPermissions: rawMember instanceof GuildMember ? rawMember.permissions.bitfield : null,
|
|
2500
|
+
appGuildPermissions: interaction.guild?.members.me?.permissions.bitfield ?? null,
|
|
2501
|
+
routeId: null
|
|
2768
2502
|
};
|
|
2769
2503
|
}
|
|
2770
|
-
function
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2504
|
+
function eventGateContext(eventName, args, core) {
|
|
2505
|
+
const actor = deriveEventActor(args);
|
|
2506
|
+
return {
|
|
2507
|
+
kind: "event",
|
|
2508
|
+
core,
|
|
2509
|
+
eventName,
|
|
2510
|
+
payload: args,
|
|
2511
|
+
user: actor.user,
|
|
2512
|
+
guild: actor.guild,
|
|
2513
|
+
member: actor.member,
|
|
2514
|
+
userId: actor.user?.id ?? null,
|
|
2515
|
+
guildId: actor.guild?.id ?? null,
|
|
2516
|
+
channelId: actor.channelId,
|
|
2517
|
+
memberRoleIds: actor.member ? [...actor.member.roles.cache.keys()].filter((id) => id !== actor.guild?.id) : [],
|
|
2518
|
+
memberPermissions: actor.member?.permissions.bitfield ?? null,
|
|
2519
|
+
appPermissions: null,
|
|
2520
|
+
memberGuildPermissions: actor.member?.permissions.bitfield ?? null,
|
|
2521
|
+
appGuildPermissions: actor.guild?.members.me?.permissions.bitfield ?? null,
|
|
2522
|
+
routeId: null
|
|
2523
|
+
};
|
|
2776
2524
|
}
|
|
2777
2525
|
|
|
2778
2526
|
//#endregion
|
|
@@ -2787,7 +2535,7 @@ const logger$1 = new Logger("EventBoundary");
|
|
|
2787
2535
|
*/
|
|
2788
2536
|
function handleEventFault(caught, eventName, handlerName, args, core) {
|
|
2789
2537
|
if (caught instanceof Silence) {
|
|
2790
|
-
if (caught.reason !== void 0) logger$1.debug(`Silence: ${caught.reason}`);
|
|
2538
|
+
if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger$1.debug(`Silence: ${caught.reason}`);
|
|
2791
2539
|
return;
|
|
2792
2540
|
}
|
|
2793
2541
|
if (!Error.isError(caught)) throw caught;
|
|
@@ -2828,6 +2576,9 @@ var EventDispatcher = class {
|
|
|
2828
2576
|
middlewares = [];
|
|
2829
2577
|
executedOnceHandlers = /* @__PURE__ */ new Set();
|
|
2830
2578
|
attachedEvents = /* @__PURE__ */ new Set();
|
|
2579
|
+
loading = false;
|
|
2580
|
+
loadedHandlers = [];
|
|
2581
|
+
loadedMiddlewares = [];
|
|
2831
2582
|
hmrHandler;
|
|
2832
2583
|
constructor(core) {
|
|
2833
2584
|
this.core = core;
|
|
@@ -2857,20 +2608,29 @@ var EventDispatcher = class {
|
|
|
2857
2608
|
this.isInitialized = true;
|
|
2858
2609
|
const handlersDir = this.core.config.bot.events.path;
|
|
2859
2610
|
if (!handlersDir) return;
|
|
2860
|
-
this.logger.info(chalk.bold(handlersDir));
|
|
2861
2611
|
const middlewareDir = hasKeys(this.core.config.bot.events, ["middlewares"]) ? this.core.config.bot.events.middlewares : void 0;
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2612
|
+
this.loading = true;
|
|
2613
|
+
this.loadedHandlers.length = 0;
|
|
2614
|
+
this.loadedMiddlewares.length = 0;
|
|
2615
|
+
try {
|
|
2616
|
+
if (middlewareDir) await this.loadMiddlewares(middlewareDir);
|
|
2617
|
+
await this.loadHandlers(handlersDir);
|
|
2618
|
+
} finally {
|
|
2619
|
+
this.loading = false;
|
|
2865
2620
|
}
|
|
2866
|
-
await this.loadHandlers(handlersDir);
|
|
2867
2621
|
this.attachToClient();
|
|
2868
|
-
this.
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2622
|
+
this.reportLoad();
|
|
2623
|
+
}
|
|
2624
|
+
reportLoad() {
|
|
2625
|
+
const { utils } = this.logger;
|
|
2626
|
+
utils.summary("Loaded", {
|
|
2627
|
+
"event handlers": this.loadedHandlers.length,
|
|
2628
|
+
middlewares: this.loadedMiddlewares.length
|
|
2872
2629
|
});
|
|
2873
|
-
this.
|
|
2630
|
+
if (this.loadedMiddlewares.length > 0) utils.block("Loaded event middlewares", utils.entries(this.loadedMiddlewares));
|
|
2631
|
+
const perEvent = {};
|
|
2632
|
+
for (const [event, handlers] of this.eventMap) perEvent[event] = handlers.length;
|
|
2633
|
+
utils.block("Loaded event handlers", [...utils.entries(this.loadedHandlers), ...utils.counts(perEvent)]);
|
|
2874
2634
|
}
|
|
2875
2635
|
async loadHandlers(dir) {
|
|
2876
2636
|
await traverseDirectory(dir, (fullPath, relativePath, imported) => {
|
|
@@ -2920,7 +2680,14 @@ var EventDispatcher = class {
|
|
|
2920
2680
|
...metadata.events && { events: metadata.events }
|
|
2921
2681
|
});
|
|
2922
2682
|
this.middlewares.sort((a, b) => a.priority - b.priority);
|
|
2923
|
-
|
|
2683
|
+
if (this.loading) {
|
|
2684
|
+
this.loadedMiddlewares.push({
|
|
2685
|
+
name: `${middlewareCtor.name} (${metadata.priority})`,
|
|
2686
|
+
from: formatFilePath(relativePath)
|
|
2687
|
+
});
|
|
2688
|
+
return;
|
|
2689
|
+
}
|
|
2690
|
+
this.logger.utils.registration(`${middlewareCtor.name} ${paint.mute(`(${metadata.priority})`)}`, relativePath, "event middleware");
|
|
2924
2691
|
}
|
|
2925
2692
|
async runMiddlewares(eventName, args) {
|
|
2926
2693
|
for (const { ctor, events } of this.middlewares) {
|
|
@@ -2961,7 +2728,12 @@ var EventDispatcher = class {
|
|
|
2961
2728
|
const names = areRoutes(raw) ? raw : typeof raw === "string" ? [raw] : [];
|
|
2962
2729
|
for (const name of names) register(name, "on");
|
|
2963
2730
|
}
|
|
2964
|
-
|
|
2731
|
+
const from = formatFilePath(relativePath);
|
|
2732
|
+
if (this.loading) this.loadedHandlers.push({
|
|
2733
|
+
name: handlerClass.name,
|
|
2734
|
+
from
|
|
2735
|
+
});
|
|
2736
|
+
else this.logger.utils.registration(handlerClass.name, from);
|
|
2965
2737
|
}
|
|
2966
2738
|
attachToClient() {
|
|
2967
2739
|
for (const [eventName] of this.eventMap) this.attachListener(eventName);
|
|
@@ -2970,12 +2742,12 @@ var EventDispatcher = class {
|
|
|
2970
2742
|
if (this.attachedEvents.has(eventName)) return;
|
|
2971
2743
|
this.attachedEvents.add(eventName);
|
|
2972
2744
|
const handlerEntries = this.eventMap.get(eventName);
|
|
2973
|
-
this.logger.debug(`Attaching ${
|
|
2745
|
+
this.logger.debug(`Attaching ${paint.mint.bold(eventName)} to the client with ${paint.mute(handlerEntries?.length ?? 0)} handler(s)`);
|
|
2974
2746
|
this.core.bot.client.on(eventName, (...args) => {
|
|
2975
2747
|
this.core.bot.emitSafe("any:event", eventName, ...args);
|
|
2976
2748
|
(async () => {
|
|
2977
2749
|
await this.processEvent(eventName, args).catch((err) => {
|
|
2978
|
-
this.logger.error(`[${
|
|
2750
|
+
this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
|
|
2979
2751
|
this.core.bot.emit("error:unhandled:event", err);
|
|
2980
2752
|
});
|
|
2981
2753
|
})();
|
|
@@ -2997,7 +2769,7 @@ var EventDispatcher = class {
|
|
|
2997
2769
|
}
|
|
2998
2770
|
async processHandler(eventName, ctor, args) {
|
|
2999
2771
|
try {
|
|
3000
|
-
this.logger.debug(`Processing ${
|
|
2772
|
+
this.logger.debug(`Processing ${paint.mint.bold(eventName)} with ${paint.mute(ctor.name)}`);
|
|
3001
2773
|
const handler = new ctor(args, this.core, eventName);
|
|
3002
2774
|
await runHandlerGates(ctor, eventGateContext(eventName, args, this.core));
|
|
3003
2775
|
await handler.execute();
|
|
@@ -3008,35 +2780,55 @@ var EventDispatcher = class {
|
|
|
3008
2780
|
};
|
|
3009
2781
|
|
|
3010
2782
|
//#endregion
|
|
3011
|
-
//#region src/bot/defaults/
|
|
2783
|
+
//#region src/bot/defaults/UnhandledRepliable.ts
|
|
3012
2784
|
/**
|
|
3013
|
-
* Default handler for unhandled interactions.
|
|
3014
|
-
* raw fault, so it needs no decorator of its own.
|
|
3015
|
-
*
|
|
3016
|
-
* @internal
|
|
2785
|
+
* @internal Default handler for unhandled repliable interactions.
|
|
3017
2786
|
*/
|
|
3018
|
-
var
|
|
2787
|
+
var UnhandledRepliable = class extends InteractionHandler {
|
|
3019
2788
|
async execute() {
|
|
3020
|
-
await this.
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
2789
|
+
await this.reply("Feature not implemented yet.");
|
|
2790
|
+
}
|
|
2791
|
+
};
|
|
2792
|
+
|
|
2793
|
+
//#endregion
|
|
2794
|
+
//#region src/bot/defaults/UnhandledAutocomplete.ts
|
|
2795
|
+
/**
|
|
2796
|
+
* @internal Default handler for an autocomplete with no registered handler.
|
|
2797
|
+
*/
|
|
2798
|
+
var UnhandledAutocomplete = class extends AutocompleteHandler {
|
|
2799
|
+
async execute() {
|
|
2800
|
+
await this.respond([]);
|
|
3024
2801
|
}
|
|
3025
2802
|
};
|
|
3026
2803
|
|
|
2804
|
+
//#endregion
|
|
2805
|
+
//#region src/bot/harmlessApiCodes.ts
|
|
2806
|
+
/**
|
|
2807
|
+
* Discord API error codes that mean a dead end on an interaction, a dead token, a double ack, or a gone
|
|
2808
|
+
* message. Nothing to show the user and no bug to report, so the boundary's fault-card send drops them.
|
|
2809
|
+
*
|
|
2810
|
+
* @internal
|
|
2811
|
+
*/
|
|
2812
|
+
const HARMLESS_API_CODES = new Set([
|
|
2813
|
+
RESTJSONErrorCodes.UnknownInteraction,
|
|
2814
|
+
RESTJSONErrorCodes.InteractionHasAlreadyBeenAcknowledged,
|
|
2815
|
+
RESTJSONErrorCodes.UnknownMessage
|
|
2816
|
+
]);
|
|
2817
|
+
|
|
3027
2818
|
//#endregion
|
|
3028
2819
|
//#region src/bot/handleInteractionFault.ts
|
|
3029
2820
|
const logger = new Logger("InteractionBoundary");
|
|
3030
2821
|
/**
|
|
3031
2822
|
* The interaction controller boundary. Sorts a throw from any handler-lifecycle phase into the right
|
|
3032
|
-
* user reply plus the right bus event
|
|
3033
|
-
*
|
|
2823
|
+
* user reply plus the right bus event. When a handler is built its sender is passed in, so the boundary
|
|
2824
|
+
* sends from the current ack state. A middleware or pre-construction throw arrives with no sender, so the
|
|
2825
|
+
* boundary builds one from the interaction flags.
|
|
3034
2826
|
*
|
|
3035
2827
|
* @internal
|
|
3036
2828
|
*/
|
|
3037
|
-
async function handleInteractionFault(caught, interaction, core) {
|
|
2829
|
+
async function handleInteractionFault(caught, interaction, core, sender) {
|
|
3038
2830
|
if (caught instanceof Silence) {
|
|
3039
|
-
if (caught.reason !== void 0) logger.debug(`Silence: ${caught.reason}`);
|
|
2831
|
+
if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger.debug(`Silence: ${caught.reason}`);
|
|
3040
2832
|
return;
|
|
3041
2833
|
}
|
|
3042
2834
|
if (!Error.isError(caught)) throw caught;
|
|
@@ -3060,7 +2852,18 @@ async function handleInteractionFault(caught, interaction, core) {
|
|
|
3060
2852
|
user: interaction.user,
|
|
3061
2853
|
metadata: interaction
|
|
3062
2854
|
});
|
|
3063
|
-
await new ReplySender(interaction)
|
|
2855
|
+
await sendGuarded(sender ?? new ReplySender(interaction, interactionRoute(interaction)), response, caught instanceof Notice ? caught.ephemeral : true);
|
|
2856
|
+
}
|
|
2857
|
+
async function sendGuarded(sender, response, ephemeral) {
|
|
2858
|
+
try {
|
|
2859
|
+
await sender.send(response, { ephemeral });
|
|
2860
|
+
} catch (error) {
|
|
2861
|
+
if (error instanceof DiscordAPIError && HARMLESS_API_CODES.has(error.code)) {
|
|
2862
|
+
logger.debug(`reply send hit harmless code ${error.code}`);
|
|
2863
|
+
return;
|
|
2864
|
+
}
|
|
2865
|
+
logger.error("reply send failed", error);
|
|
2866
|
+
}
|
|
3064
2867
|
}
|
|
3065
2868
|
|
|
3066
2869
|
//#endregion
|
|
@@ -3089,6 +2892,9 @@ var InteractionDispatcher = class {
|
|
|
3089
2892
|
autocompleteMap = /* @__PURE__ */ new Map();
|
|
3090
2893
|
keysToIgnore = /* @__PURE__ */ new Set();
|
|
3091
2894
|
middlewares = [];
|
|
2895
|
+
loading = false;
|
|
2896
|
+
loadedHandlers = [];
|
|
2897
|
+
loadedMiddlewares = [];
|
|
3092
2898
|
hmrHandler;
|
|
3093
2899
|
routeTypes = [
|
|
3094
2900
|
[InteractionRoutes.Slash, this.slashMap],
|
|
@@ -3126,22 +2932,22 @@ var InteractionDispatcher = class {
|
|
|
3126
2932
|
}
|
|
3127
2933
|
/**
|
|
3128
2934
|
* Warns for each command route with no registered `@SlashRoute` handler, which would fall through to
|
|
3129
|
-
*
|
|
2935
|
+
* UnhandledRepliable at runtime. A bot may route commands outside the registry, so this does not throw.
|
|
3130
2936
|
*
|
|
3131
2937
|
* @internal
|
|
3132
2938
|
*/
|
|
3133
2939
|
warnUnhandledRoutes(commandLeaves) {
|
|
3134
|
-
for (const route of commandLeaves) if (!this.slashMap.has(route)) this.logger.warn(`Slash route ${
|
|
2940
|
+
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.`);
|
|
3135
2941
|
}
|
|
3136
2942
|
/**
|
|
3137
|
-
* Warns for each context-menu command with no matching handler, which would fall through to
|
|
2943
|
+
* Warns for each context-menu command with no matching handler, which would fall through to UnhandledRepliable
|
|
3138
2944
|
* at runtime. Checked per kind since a user and a message command can share a name.
|
|
3139
2945
|
*
|
|
3140
2946
|
* @internal
|
|
3141
2947
|
*/
|
|
3142
2948
|
warnUnhandledContextMenuRoutes(leaves) {
|
|
3143
|
-
for (const name of leaves.user) if (!this.userContextMenuMap.has(name)) this.logger.warn(`User context menu ${
|
|
3144
|
-
for (const name of leaves.message) if (!this.messageContextMenuMap.has(name)) this.logger.warn(`Message context menu ${
|
|
2949
|
+
for (const name of leaves.user) if (!this.userContextMenuMap.has(name)) this.logger.warn(`User context menu ${paint.sky.bold(name)} has no registered ${chalk.bold("@ContextMenuRoute")} handler.`);
|
|
2950
|
+
for (const name of leaves.message) if (!this.messageContextMenuMap.has(name)) this.logger.warn(`Message context menu ${paint.sky.bold(name)} has no registered ${chalk.bold("@ContextMenuRoute")} handler.`);
|
|
3145
2951
|
}
|
|
3146
2952
|
getArtifacts(handlerClass) {
|
|
3147
2953
|
const artifacts = [];
|
|
@@ -3159,29 +2965,39 @@ var InteractionDispatcher = class {
|
|
|
3159
2965
|
this.isInitialized = true;
|
|
3160
2966
|
const handlersDir = this.core.config.bot.interactions.path;
|
|
3161
2967
|
if (!handlersDir) return;
|
|
3162
|
-
this.logger.info(chalk.bold(handlersDir));
|
|
3163
2968
|
const middlewareDir = hasKeys(this.core.config.bot.interactions, ["middlewares"]) ? this.core.config.bot.interactions.middlewares : void 0;
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
2969
|
+
this.loading = true;
|
|
2970
|
+
this.loadedHandlers.length = 0;
|
|
2971
|
+
this.loadedMiddlewares.length = 0;
|
|
2972
|
+
try {
|
|
2973
|
+
if (middlewareDir) await this.loadMiddlewares(middlewareDir);
|
|
2974
|
+
await this.loadHandlers(handlersDir);
|
|
2975
|
+
} finally {
|
|
2976
|
+
this.loading = false;
|
|
3167
2977
|
}
|
|
3168
|
-
await this.loadHandlers(handlersDir);
|
|
3169
2978
|
this.attachToClient();
|
|
3170
|
-
this.
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
2979
|
+
this.reportLoad();
|
|
2980
|
+
}
|
|
2981
|
+
reportLoad() {
|
|
2982
|
+
const { utils } = this.logger;
|
|
2983
|
+
utils.summary("Loaded", {
|
|
2984
|
+
"interaction handlers": this.loadedHandlers.length,
|
|
2985
|
+
middlewares: this.loadedMiddlewares.length
|
|
2986
|
+
});
|
|
2987
|
+
if (this.loadedMiddlewares.length > 0) utils.block("Loaded middlewares", utils.entries(this.loadedMiddlewares));
|
|
2988
|
+
utils.block("Loaded interaction handlers", [...utils.entries(this.loadedHandlers), ...utils.counts({
|
|
2989
|
+
slash: this.slashMap.size,
|
|
2990
|
+
buttons: this.buttonMap.size,
|
|
2991
|
+
modals: this.modalMap.size,
|
|
2992
|
+
"string selects": this.stringSelectMap.size,
|
|
2993
|
+
"user selects": this.userSelectMap.size,
|
|
2994
|
+
"role selects": this.roleSelectMap.size,
|
|
2995
|
+
"channel selects": this.channelSelectMap.size,
|
|
2996
|
+
"mentionable selects": this.mentionableSelectMap.size,
|
|
2997
|
+
"message menus": this.messageContextMenuMap.size,
|
|
2998
|
+
"user menus": this.userContextMenuMap.size,
|
|
2999
|
+
autocomplete: this.autocompleteMap.size
|
|
3000
|
+
})]);
|
|
3185
3001
|
}
|
|
3186
3002
|
async loadHandlers(dir) {
|
|
3187
3003
|
await traverseDirectory(dir, (fullPath, relativePath, imported) => {
|
|
@@ -3211,7 +3027,14 @@ var InteractionDispatcher = class {
|
|
|
3211
3027
|
priority: metadata.priority
|
|
3212
3028
|
});
|
|
3213
3029
|
this.middlewares.sort((a, b) => a.priority - b.priority);
|
|
3214
|
-
|
|
3030
|
+
if (this.loading) {
|
|
3031
|
+
this.loadedMiddlewares.push({
|
|
3032
|
+
name: `${middlewareCtor.name} (${metadata.priority})`,
|
|
3033
|
+
from: formatFilePath(relativePath)
|
|
3034
|
+
});
|
|
3035
|
+
return;
|
|
3036
|
+
}
|
|
3037
|
+
this.logger.utils.registration(`${middlewareCtor.name} ${paint.mute(`(${metadata.priority})`)}`, relativePath, "middleware");
|
|
3215
3038
|
}
|
|
3216
3039
|
isHandlerClass(obj) {
|
|
3217
3040
|
if (typeof obj !== "function") return false;
|
|
@@ -3235,9 +3058,15 @@ var InteractionDispatcher = class {
|
|
|
3235
3058
|
]);
|
|
3236
3059
|
writes.push([map, route]);
|
|
3237
3060
|
}
|
|
3238
|
-
this.logger.utils.registration(handlerClass.name, formatFilePath(relativePath));
|
|
3239
3061
|
}
|
|
3062
|
+
if (writes.length === 0) return;
|
|
3240
3063
|
for (const [map, route] of writes) map.set(route, handlerClass);
|
|
3064
|
+
const from = formatFilePath(relativePath);
|
|
3065
|
+
if (this.loading) this.loadedHandlers.push({
|
|
3066
|
+
name: handlerClass.name,
|
|
3067
|
+
from
|
|
3068
|
+
});
|
|
3069
|
+
else this.logger.utils.registration(handlerClass.name, from);
|
|
3241
3070
|
}
|
|
3242
3071
|
/** @internal For use in dev mode */
|
|
3243
3072
|
async onHmr(event) {
|
|
@@ -3265,7 +3094,7 @@ var InteractionDispatcher = class {
|
|
|
3265
3094
|
this.core.bot.client.on(Events.InteractionCreate, (interaction) => {
|
|
3266
3095
|
this.core.bot.emitSafe("any:interaction", interaction);
|
|
3267
3096
|
this.handleInteraction(interaction).catch((err) => {
|
|
3268
|
-
this.logger.error(`[${
|
|
3097
|
+
this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
|
|
3269
3098
|
this.core.bot.emit("error:unhandled:interaction", err);
|
|
3270
3099
|
});
|
|
3271
3100
|
});
|
|
@@ -3276,22 +3105,31 @@ var InteractionDispatcher = class {
|
|
|
3276
3105
|
if (!prefix) return this.logger.warn(`${interactionType} has invalid customId: ${interaction.customId}`);
|
|
3277
3106
|
await this.processInteraction(interaction, () => prefix, (key) => getMap().get(key));
|
|
3278
3107
|
}
|
|
3279
|
-
async processInteraction(interaction, extractKey, getHandler) {
|
|
3108
|
+
async processInteraction(interaction, extractKey, getHandler, fallback = UnhandledRepliable) {
|
|
3280
3109
|
const key = extractKey(interaction);
|
|
3110
|
+
let sender;
|
|
3281
3111
|
try {
|
|
3282
3112
|
if (!interaction.isAutocomplete()) for (const { ctor } of this.middlewares) await new ctor(interaction, this.core).execute();
|
|
3283
3113
|
let HandlerCtor = getHandler(key);
|
|
3284
3114
|
if (!HandlerCtor) {
|
|
3285
|
-
this.logger.warn(`No handler found for key ${
|
|
3286
|
-
HandlerCtor =
|
|
3115
|
+
this.logger.warn(`No handler found for key ${paint.sky.bold(key)}. Falling back to ${fallback.name}.`);
|
|
3116
|
+
HandlerCtor = fallback;
|
|
3287
3117
|
}
|
|
3288
|
-
this.logger.debug(`Processing ${
|
|
3118
|
+
this.logger.debug(`Processing ${paint.mint.bold(key)} with ${paint.mute(HandlerCtor.name)}`);
|
|
3289
3119
|
const dispatch = new DispatchContext(routeIdOf(HandlerCtor));
|
|
3290
3120
|
const handler = new HandlerCtor(interaction, this.core, dispatch);
|
|
3291
|
-
if (
|
|
3121
|
+
if (handler instanceof RepliableHandler) sender = handler.getSender();
|
|
3122
|
+
if (!interaction.isAutocomplete()) {
|
|
3123
|
+
const monitor = slowGateMonitor();
|
|
3124
|
+
try {
|
|
3125
|
+
await runHandlerGates(HandlerCtor, interactionGateContext(interaction, this.core), dispatch.routeId ?? void 0, monitor?.observe);
|
|
3126
|
+
} finally {
|
|
3127
|
+
monitor?.report(dispatch.routeId);
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3292
3130
|
await handler.execute();
|
|
3293
3131
|
} catch (caught) {
|
|
3294
|
-
await handleInteractionFault(caught, interaction, this.core);
|
|
3132
|
+
await handleInteractionFault(caught, interaction, this.core, sender);
|
|
3295
3133
|
}
|
|
3296
3134
|
}
|
|
3297
3135
|
async handleInteraction(interaction) {
|
|
@@ -3367,12 +3205,7 @@ var InteractionDispatcher = class {
|
|
|
3367
3205
|
}
|
|
3368
3206
|
async handleAutocomplete(interaction) {
|
|
3369
3207
|
const route = slashRouteOf(interaction);
|
|
3370
|
-
|
|
3371
|
-
this.logger.warn(`No autocomplete handler for ${chalk.bold.cyan(route)}.`);
|
|
3372
|
-
await interaction.respond([]);
|
|
3373
|
-
return;
|
|
3374
|
-
}
|
|
3375
|
-
await this.processInteraction(interaction, () => route, (key) => this.autocompleteMap.get(key));
|
|
3208
|
+
await this.processInteraction(interaction, () => route, (key) => this.autocompleteMap.get(key), UnhandledAutocomplete);
|
|
3376
3209
|
}
|
|
3377
3210
|
};
|
|
3378
3211
|
|
|
@@ -3428,7 +3261,7 @@ var Bot = class extends Plugin {
|
|
|
3428
3261
|
});
|
|
3429
3262
|
}
|
|
3430
3263
|
this.emojiInjector = new EmojiInjector(core);
|
|
3431
|
-
core.shutdown.addTask(ShutdownPhase.DiscordCleanup, "stop-bot", async () => await this.stop(), 2e3);
|
|
3264
|
+
core.shutdown.addTask(ShutdownPhase$1.DiscordCleanup, "stop-bot", async () => await this.stop(), 2e3);
|
|
3432
3265
|
}
|
|
3433
3266
|
/**
|
|
3434
3267
|
* Initializes Discord client and all controllers
|
|
@@ -3461,7 +3294,9 @@ var Bot = class extends Plugin {
|
|
|
3461
3294
|
* Logs the bot into Discord using the configured token
|
|
3462
3295
|
*/
|
|
3463
3296
|
async login(token) {
|
|
3464
|
-
|
|
3297
|
+
this._client.once(Events.ClientReady, () => this.emit("ready"));
|
|
3298
|
+
this._client.login(token);
|
|
3299
|
+
await this.waitFor("ready");
|
|
3465
3300
|
this.logger.info(`Logged in as ${chalk.bold.magenta(this._client.user?.username)}!`);
|
|
3466
3301
|
return this;
|
|
3467
3302
|
}
|
|
@@ -3487,197 +3322,70 @@ var Bot = class extends Plugin {
|
|
|
3487
3322
|
};
|
|
3488
3323
|
__decorate([Envapt("DISCORD_BOT_TOKEN", { converter: (raw) => validateDiscordToken(raw) }), __decorateMetadata("design:type", String)], Bot.prototype, "botToken", void 0);
|
|
3489
3324
|
|
|
3490
|
-
//#endregion
|
|
3491
|
-
//#region src/hmr/HmrManager.ts
|
|
3492
|
-
var HmrManager = class {
|
|
3493
|
-
logger = new Logger("HMR", { channel: "hmr" });
|
|
3494
|
-
listeners = /* @__PURE__ */ new Set();
|
|
3495
|
-
constructor() {}
|
|
3496
|
-
/** @internal */
|
|
3497
|
-
init() {
|
|
3498
|
-
if (!import.meta.hot) return;
|
|
3499
|
-
setDevChannel(wrapHot(import.meta.hot));
|
|
3500
|
-
if (Envapter.isDevelopment || Envapter.isTest) {
|
|
3501
|
-
this.logger.info("Enabled");
|
|
3502
|
-
getDevChannel()?.on("seedcord:hmr", (payload) => {
|
|
3503
|
-
const affected = payload.affectedModules?.length ?? 0;
|
|
3504
|
-
this.logger.info(`${chalk.bold("1")} module changed, ${chalk.bold(affected)} affected modules`);
|
|
3505
|
-
if (payload.affectedModules) this.logger.utils.list(payload.affectedModules.map((mod) => formatFilePath(mod)), "Affected modules: ");
|
|
3506
|
-
this.handleUpdate(payload);
|
|
3507
|
-
});
|
|
3508
|
-
}
|
|
3509
|
-
}
|
|
3510
|
-
/** @internal */
|
|
3511
|
-
register(listener) {
|
|
3512
|
-
this.listeners.add(listener);
|
|
3513
|
-
}
|
|
3514
|
-
/** @internal */
|
|
3515
|
-
unregister(listener) {
|
|
3516
|
-
this.listeners.delete(listener);
|
|
3517
|
-
}
|
|
3518
|
-
async handleUpdate(event) {
|
|
3519
|
-
const promises = [...this.listeners].map(async (listener) => {
|
|
3520
|
-
try {
|
|
3521
|
-
await listener.onHmr(event);
|
|
3522
|
-
} catch (error) {
|
|
3523
|
-
this.logger.error(`Error handling HMR update in ${chalk.bold(listener.name)}: `, error);
|
|
3524
|
-
}
|
|
3525
|
-
});
|
|
3526
|
-
await Promise.all(promises);
|
|
3527
|
-
}
|
|
3528
|
-
};
|
|
3529
|
-
|
|
3530
|
-
//#endregion
|
|
3531
|
-
//#region src/subscribers/bases/webhookHelpers.ts
|
|
3532
|
-
const DISCORD_WEBHOOK_REGEX = new RegExp(String.raw`^https?:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w$-]+$`);
|
|
3533
|
-
/**
|
|
3534
|
-
* Whether a string is a well-formed Discord webhook URL.
|
|
3535
|
-
*
|
|
3536
|
-
* @internal
|
|
3537
|
-
*/
|
|
3538
|
-
function isDiscordWebhookUrl(value) {
|
|
3539
|
-
return URL.canParse(value) && DISCORD_WEBHOOK_REGEX.test(value);
|
|
3540
|
-
}
|
|
3541
|
-
/**
|
|
3542
|
-
* Serializes a value to a JSON attachment, made circular-safe first.
|
|
3543
|
-
*
|
|
3544
|
-
* @internal
|
|
3545
|
-
*/
|
|
3546
|
-
function jsonAttachment(name, description, data) {
|
|
3547
|
-
const content = filterCirculars(data);
|
|
3548
|
-
return new AttachmentBuilder(Buffer.from(JSON.stringify(content, void 0, 2), "utf8"), {
|
|
3549
|
-
name,
|
|
3550
|
-
description
|
|
3551
|
-
});
|
|
3552
|
-
}
|
|
3553
|
-
/**
|
|
3554
|
-
* The small divider the default webhook reporters place between sections.
|
|
3555
|
-
*
|
|
3556
|
-
* @internal
|
|
3557
|
-
*/
|
|
3558
|
-
var WebhookSeparator = class extends BuilderComponent {
|
|
3559
|
-
constructor() {
|
|
3560
|
-
super("separator");
|
|
3561
|
-
this.instance.setSpacing(SeparatorSpacingSize.Small).setDivider(true);
|
|
3562
|
-
}
|
|
3563
|
-
};
|
|
3564
|
-
|
|
3565
3325
|
//#endregion
|
|
3566
3326
|
//#region src/subscribers/default/HandledException.ts
|
|
3567
|
-
var _HandledException;
|
|
3568
|
-
function webhookUrlValidator$1(raw) {
|
|
3569
|
-
if (raw !== null && typeof raw !== "string") throw new SeedcordError(SeedcordErrorCode.ConfigHandledExceptionWebhookInvalid);
|
|
3570
|
-
const value = raw?.trim() ?? "";
|
|
3571
|
-
if (value === "") throw new SeedcordError(SeedcordErrorCode.ConfigHandledExceptionWebhookMissing);
|
|
3572
|
-
if (!isDiscordWebhookUrl(value)) throw new SeedcordError(SeedcordErrorCode.ConfigHandledExceptionWebhookInvalid);
|
|
3573
|
-
return value;
|
|
3574
|
-
}
|
|
3575
3327
|
let HandledException = class HandledException extends WebhookLog {
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
try {
|
|
3583
|
-
await this.webhook.send({
|
|
3584
|
-
flags: flagsFor(false),
|
|
3585
|
-
withComponents: true,
|
|
3586
|
-
username: "Handled Exception",
|
|
3587
|
-
components: [new HandledExceptionContainer(this.data).component],
|
|
3588
|
-
files: [jsonAttachment("source.json", "The raw source the fault came from", source.raw)]
|
|
3589
|
-
});
|
|
3590
|
-
} catch (error) {
|
|
3591
|
-
this.logger.error("Failed to send handled exception webhook", error);
|
|
3592
|
-
}
|
|
3328
|
+
report() {
|
|
3329
|
+
return {
|
|
3330
|
+
username: "Handled Exception",
|
|
3331
|
+
components: [new HandledExceptionContainer(this.data).component],
|
|
3332
|
+
files: [jsonAttachment("source.json", "The raw source the fault came from", this.data.source.raw)]
|
|
3333
|
+
};
|
|
3593
3334
|
}
|
|
3594
3335
|
};
|
|
3595
|
-
__decorate([
|
|
3596
|
-
HandledException = _HandledException = __decorate([Subscribe("handledException")], HandledException);
|
|
3336
|
+
HandledException = __decorate([Subscribe("handledException"), WebhookUrl("HANDLED_EXCEPTION_WEBHOOK_URL")], HandledException);
|
|
3597
3337
|
var HandledExceptionContainer = class extends BuilderComponent {
|
|
3598
3338
|
constructor(data) {
|
|
3599
3339
|
super("container");
|
|
3600
3340
|
const { denial, uuid, source } = data;
|
|
3601
|
-
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"));
|
|
3341
|
+
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"));
|
|
3602
3342
|
}
|
|
3603
3343
|
};
|
|
3604
|
-
/**
|
|
3605
|
-
* The summary block of a fault report, narrowed per source kind so an interaction fault shows its
|
|
3606
|
-
* command and customId while an event fault shows its event name and handler.
|
|
3607
|
-
*
|
|
3608
|
-
* @internal
|
|
3609
|
-
*/
|
|
3610
3344
|
function faultSummary(denial, source) {
|
|
3611
|
-
const head = `### A handled fault was reported: \`${denial.name}\`\n**Message:** ${denial.message}\n
|
|
3345
|
+
const head = stripAnsi(`### A handled fault was reported: \`${denial.name}\`\n**Message:** ${denial.message}\n`);
|
|
3612
3346
|
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"}\``;
|
|
3613
3347
|
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}\``;
|
|
3614
3348
|
}
|
|
3615
|
-
/**
|
|
3616
|
-
* The text shown for a denial's cause in the report. Tolerates a non-serializable cause (a bigint or a
|
|
3617
|
-
* circular object) so building the report never throws.
|
|
3618
|
-
*
|
|
3619
|
-
* @internal
|
|
3620
|
-
*/
|
|
3621
3349
|
function causeStack(denial) {
|
|
3622
3350
|
const { cause } = denial;
|
|
3623
|
-
if (Error.isError(cause)) return cause.stack ?? cause.message;
|
|
3624
|
-
if (typeof cause === "string") return cause;
|
|
3351
|
+
if (Error.isError(cause)) return stripAnsi(cause.stack ?? cause.message);
|
|
3352
|
+
if (typeof cause === "string") return stripAnsi(cause);
|
|
3625
3353
|
if (typeof cause === "bigint" || typeof cause === "symbol" || typeof cause === "function") return cause.toString();
|
|
3626
3354
|
if (cause !== void 0) try {
|
|
3627
3355
|
return JSON.stringify(cause);
|
|
3628
3356
|
} catch {
|
|
3629
3357
|
return "[unserializable cause]";
|
|
3630
3358
|
}
|
|
3631
|
-
return denial.stack ?? "No cause recorded.";
|
|
3359
|
+
return stripAnsi(denial.stack ?? "No cause recorded.");
|
|
3632
3360
|
}
|
|
3633
3361
|
|
|
3634
3362
|
//#endregion
|
|
3635
3363
|
//#region src/subscribers/default/UnknownException.ts
|
|
3636
|
-
var _UnknownException;
|
|
3637
|
-
function webhookUrlValidator(raw) {
|
|
3638
|
-
if (raw !== null && typeof raw !== "string") throw new SeedcordError(SeedcordErrorCode.ConfigUnknownExceptionWebhookInvalid);
|
|
3639
|
-
const value = raw?.trim() ?? "";
|
|
3640
|
-
if (value === "") throw new SeedcordError(SeedcordErrorCode.ConfigUnknownExceptionWebhookMissing);
|
|
3641
|
-
if (!isDiscordWebhookUrl(value)) throw new SeedcordError(SeedcordErrorCode.ConfigUnknownExceptionWebhookInvalid);
|
|
3642
|
-
return value;
|
|
3643
|
-
}
|
|
3644
3364
|
let UnknownException = class UnknownException extends WebhookLog {
|
|
3645
|
-
|
|
3646
|
-
_UnknownException = this;
|
|
3647
|
-
}
|
|
3648
|
-
webhook = new WebhookClient({ url: _UnknownException.unknownExceptionWebhookUrl });
|
|
3649
|
-
async execute() {
|
|
3365
|
+
report() {
|
|
3650
3366
|
const { metadata } = this.data;
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
username: "Unknown Exception",
|
|
3657
|
-
components: [new UnhandledErrorContainer(this.data).component],
|
|
3658
|
-
files: metadataFile ? [metadataFile] : []
|
|
3659
|
-
});
|
|
3660
|
-
} catch (error) {
|
|
3661
|
-
this.logger.error("Failed to send unknown exception webhook", error);
|
|
3662
|
-
}
|
|
3367
|
+
return {
|
|
3368
|
+
username: "Unknown Exception",
|
|
3369
|
+
components: [new UnhandledErrorContainer(this.data).component],
|
|
3370
|
+
files: metadata ? [jsonAttachment("metadata.json", "Metadata associated with the error", metadata)] : void 0
|
|
3371
|
+
};
|
|
3663
3372
|
}
|
|
3664
3373
|
};
|
|
3665
|
-
__decorate([
|
|
3666
|
-
UnknownException = _UnknownException = __decorate([Subscribe("unknownException")], UnknownException);
|
|
3374
|
+
UnknownException = __decorate([Subscribe("unknownException"), WebhookUrl("UNKNOWN_EXCEPTION_WEBHOOK_URL")], UnknownException);
|
|
3667
3375
|
var UnhandledErrorContainer = class extends BuilderComponent {
|
|
3668
3376
|
constructor(data) {
|
|
3669
3377
|
super("container");
|
|
3670
3378
|
const { uuid, error, guild, user, metadata } = data;
|
|
3671
|
-
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
|
|
3379
|
+
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)}\`\`\``));
|
|
3672
3380
|
this.addTimestampsIfAvailable(error);
|
|
3673
3381
|
this.addMetadataIfAvailable(metadata);
|
|
3674
3382
|
}
|
|
3675
3383
|
addTimestampsIfAvailable(error) {
|
|
3676
|
-
if (!(error instanceof DiscordAPIError)) return;
|
|
3384
|
+
if (!(error instanceof DiscordAPIError$1)) return;
|
|
3677
3385
|
const now = Date.now();
|
|
3678
3386
|
const snowflake = error.url.match(/\/interactions\/(\d+)\//)?.[1];
|
|
3679
3387
|
if (!snowflake) return void 0;
|
|
3680
|
-
const interactionTs =
|
|
3388
|
+
const interactionTs = timestampFromSnowflake(snowflake);
|
|
3681
3389
|
const diff = now - interactionTs;
|
|
3682
3390
|
const seconds = Math.floor(diff / 1e3);
|
|
3683
3391
|
const millis = diff % 1e3;
|
|
@@ -3692,12 +3400,8 @@ var UnhandledErrorContainer = class extends BuilderComponent {
|
|
|
3692
3400
|
//#endregion
|
|
3693
3401
|
//#region src/subscribers/Bus.ts
|
|
3694
3402
|
/**
|
|
3695
|
-
*
|
|
3696
|
-
*
|
|
3697
|
-
* Provides a centralized system for registering and executing custom subscribers
|
|
3698
|
-
* throughout the application lifecycle. Bus subscribers are loaded from configured directories
|
|
3699
|
-
* and can be triggered programmatically or by framework events. Accessed via `core.bus`. Do not
|
|
3700
|
-
* construct it directly.
|
|
3403
|
+
* Registers and dispatches subscribers. Subscribers load from the configured directories and run
|
|
3404
|
+
* on a programmatic or framework-event publish. Accessed via `core.bus`. Do not construct it directly.
|
|
3701
3405
|
*/
|
|
3702
3406
|
var Bus = class extends Plugin {
|
|
3703
3407
|
core;
|
|
@@ -3707,6 +3411,7 @@ var Bus = class extends Plugin {
|
|
|
3707
3411
|
isInitialized = false;
|
|
3708
3412
|
subscribersMap = /* @__PURE__ */ new Map();
|
|
3709
3413
|
executedOnceHandlers = /* @__PURE__ */ new Set();
|
|
3414
|
+
webhookProbes = /* @__PURE__ */ new Map();
|
|
3710
3415
|
hmrHandler;
|
|
3711
3416
|
constructor(core) {
|
|
3712
3417
|
super(core);
|
|
@@ -3732,7 +3437,6 @@ var Bus = class extends Plugin {
|
|
|
3732
3437
|
this.isInitialized = true;
|
|
3733
3438
|
this.registerSubscriber(UnknownException);
|
|
3734
3439
|
this.registerSubscriber(HandledException);
|
|
3735
|
-
Envapter.require("UNKNOWN_EXCEPTION_WEBHOOK_URL", "HANDLED_EXCEPTION_WEBHOOK_URL");
|
|
3736
3440
|
const subscribersDir = this.core.config.subscribers.path;
|
|
3737
3441
|
if (subscribersDir) {
|
|
3738
3442
|
this.logger.info(chalk.bold(subscribersDir));
|
|
@@ -3740,8 +3444,19 @@ var Bus = class extends Plugin {
|
|
|
3740
3444
|
const totalSubscribers = [...this.subscribersMap.values()].reduce((acc, handlers) => acc + handlers.length, 0);
|
|
3741
3445
|
this.logger.utils.list([`${chalk.bold.magenta(totalSubscribers)} subscribers`], chalk.bold.green("Loaded"));
|
|
3742
3446
|
}
|
|
3447
|
+
if (!Envapter.isTest) await this.verifyWebhooks();
|
|
3448
|
+
}
|
|
3449
|
+
/** @internal Exposed for tests. */
|
|
3450
|
+
async verifyWebhooks() {
|
|
3451
|
+
const missing = [];
|
|
3452
|
+
await Promise.all([...this.webhookProbes].map(async ([url, envKeys]) => {
|
|
3453
|
+
const result = await WebhookLog.senderFor(url).verify();
|
|
3454
|
+
if (result === "missing") missing.push(...envKeys);
|
|
3455
|
+
else if (result === "unreachable") this.logger.warn(`could not verify webhook at ${paint.sky.bold(envKeys.join(", "))}`);
|
|
3456
|
+
}));
|
|
3457
|
+
if (missing.length > 0) throw new SeedcordError(SeedcordErrorCode.ConfigWebhookNotFound, [missing.join(", ")]);
|
|
3743
3458
|
}
|
|
3744
|
-
/** @internal
|
|
3459
|
+
/** @internal */
|
|
3745
3460
|
async onHmr(event) {
|
|
3746
3461
|
if (this.hmrHandler) await this.hmrHandler.handle(event);
|
|
3747
3462
|
}
|
|
@@ -3758,6 +3473,17 @@ var Bus = class extends Plugin {
|
|
|
3758
3473
|
}, this.logger);
|
|
3759
3474
|
}
|
|
3760
3475
|
registerSubscriber(handler) {
|
|
3476
|
+
if (handler.prototype instanceof WebhookLog) {
|
|
3477
|
+
const envKey = WebhookLog.envKeyOf(handler);
|
|
3478
|
+
const url = WebhookLog.urlOf(envKey);
|
|
3479
|
+
if (url === null) {
|
|
3480
|
+
this.logger.warn(`${chalk.bold(handler.name)} disabled, ${paint.sky.bold(envKey)} is not set`);
|
|
3481
|
+
return;
|
|
3482
|
+
}
|
|
3483
|
+
const envKeys = this.webhookProbes.get(url) ?? [];
|
|
3484
|
+
if (!envKeys.includes(envKey)) envKeys.push(envKey);
|
|
3485
|
+
this.webhookProbes.set(url, envKeys);
|
|
3486
|
+
}
|
|
3761
3487
|
const options = Reflect.getMetadata(SubscribeMetadataKey, handler);
|
|
3762
3488
|
let handlers = this.subscribersMap.get(options.subscriber);
|
|
3763
3489
|
if (!handlers) {
|
|
@@ -3817,7 +3543,7 @@ var Bus = class extends Plugin {
|
|
|
3817
3543
|
//#endregion
|
|
3818
3544
|
//#region src/version.ts
|
|
3819
3545
|
/** Package version */
|
|
3820
|
-
const version = "0.1.0-next.
|
|
3546
|
+
const version = "0.1.0-next.3";
|
|
3821
3547
|
|
|
3822
3548
|
//#endregion
|
|
3823
3549
|
//#region src/Seedcord.ts
|
|
@@ -3836,6 +3562,8 @@ var Seedcord = class Seedcord extends Pluggable {
|
|
|
3836
3562
|
/** @internal */
|
|
3837
3563
|
version = version;
|
|
3838
3564
|
static isInstantiated = false;
|
|
3565
|
+
healthCheck;
|
|
3566
|
+
hmrManager;
|
|
3839
3567
|
/** @see {@link CoordinatedShutdown} */
|
|
3840
3568
|
shutdown;
|
|
3841
3569
|
/** @see {@link CoordinatedStartup} */
|
|
@@ -3846,10 +3574,6 @@ var Seedcord = class Seedcord extends Pluggable {
|
|
|
3846
3574
|
bot;
|
|
3847
3575
|
/** @see {@link IRateLimiter} */
|
|
3848
3576
|
rateLimiter;
|
|
3849
|
-
/** @see {@link HealthCheck} */
|
|
3850
|
-
healthCheck;
|
|
3851
|
-
/** @see {@link HmrManager} */
|
|
3852
|
-
hmrManager;
|
|
3853
3577
|
/**
|
|
3854
3578
|
* Creates a new Seedcord instance
|
|
3855
3579
|
*
|
|
@@ -3863,6 +3587,7 @@ var Seedcord = class Seedcord extends Pluggable {
|
|
|
3863
3587
|
const startup = new CoordinatedStartup();
|
|
3864
3588
|
super(shutdown, startup);
|
|
3865
3589
|
this.config = config;
|
|
3590
|
+
installNodeDefaults(config.logger);
|
|
3866
3591
|
setBotColor(config.botColor);
|
|
3867
3592
|
this.shutdown = shutdown;
|
|
3868
3593
|
this.startup = startup;
|
|
@@ -3874,27 +3599,27 @@ var Seedcord = class Seedcord extends Pluggable {
|
|
|
3874
3599
|
this.healthCheck = new HealthCheck(this.shutdown, config.healthCheck);
|
|
3875
3600
|
this.registerStartupTasks();
|
|
3876
3601
|
}
|
|
3877
|
-
/**
|
|
3602
|
+
/** The bot's discord username, populated after login. */
|
|
3603
|
+
get username() {
|
|
3604
|
+
return this.bot.client.user?.username;
|
|
3605
|
+
}
|
|
3878
3606
|
static reset() {
|
|
3879
3607
|
Seedcord.isInstantiated = false;
|
|
3608
|
+
LoggerChannelRegistry.instance.reset();
|
|
3880
3609
|
}
|
|
3881
|
-
/**
|
|
3882
|
-
* Registers default startup tasks
|
|
3883
|
-
* @internal
|
|
3884
|
-
*/
|
|
3885
3610
|
registerStartupTasks() {
|
|
3886
3611
|
if (Envapter.isDevelopment || Envapter.isTest) this.registerHmrAwareModules();
|
|
3887
|
-
this.startup.addTask(StartupPhase.Configuration, "Bus Initialization", async () => {
|
|
3612
|
+
this.startup.addTask(StartupPhase$1.Configuration, "Bus Initialization", async () => {
|
|
3888
3613
|
this.bus.logger.utils.initialization("Subscribers", "start");
|
|
3889
3614
|
await this.bus.init();
|
|
3890
3615
|
this.bus.logger.utils.initialization("Subscribers", "end");
|
|
3891
3616
|
});
|
|
3892
|
-
this.startup.addTask(StartupPhase.Instantiation, "Bot Initialization", async () => {
|
|
3617
|
+
this.startup.addTask(StartupPhase$1.Instantiation, "Bot Initialization", async () => {
|
|
3893
3618
|
this.bot.logger.utils.initialization("Bot", "start");
|
|
3894
3619
|
await this.bot.init();
|
|
3895
3620
|
this.bot.logger.utils.initialization("Bot", "end");
|
|
3896
3621
|
});
|
|
3897
|
-
this.startup.addTask(StartupPhase.Ready, "Health Check", async () => {
|
|
3622
|
+
this.startup.addTask(StartupPhase$1.Ready, "Health Check", async () => {
|
|
3898
3623
|
this.healthCheck.logger.utils.initialization("HealthCheck", "start");
|
|
3899
3624
|
await this.healthCheck.init();
|
|
3900
3625
|
this.healthCheck.logger.utils.initialization("HealthCheck", "end");
|
|
@@ -3910,7 +3635,7 @@ var Seedcord = class Seedcord extends Pluggable {
|
|
|
3910
3635
|
return this;
|
|
3911
3636
|
}
|
|
3912
3637
|
registerHmrAwareModules() {
|
|
3913
|
-
this.startup.addTask(StartupPhase.Configuration, "HMR Registration", async () => {
|
|
3638
|
+
this.startup.addTask(StartupPhase$1.Configuration, "HMR Registration", async () => {
|
|
3914
3639
|
this.hmrManager.register(this.bot);
|
|
3915
3640
|
this.hmrManager.register(this.bus);
|
|
3916
3641
|
for (const plugin of this.plugins) this.hmrManager.register(plugin);
|
|
@@ -3920,5 +3645,5 @@ var Seedcord = class Seedcord extends Pluggable {
|
|
|
3920
3645
|
};
|
|
3921
3646
|
|
|
3922
3647
|
//#endregion
|
|
3923
|
-
export { ArraySource, AutocompleteHandler,
|
|
3648
|
+
export { ArraySource, AutocompleteHandler, ButtonHandler, CommandMentions, ContextMenuHandler, CursorSource, Emojis, EventHandler, EventMiddleware, Gated, IgnoreBots, InteractionHandler, InteractionMiddleware, Middleware, MiddlewareType, ModalHandler, Nsfw, Paginator, Pluggable, Plugin, RegisterEvent, RepliableHandler, Seedcord, SelectMenuHandler, ShutdownPhase, SlashHandler, StartupPhase, Subscribe, Subscriber, WebhookLog, WebhookUrl, checkBotPermissions, checkPermissions, fetchGuildMember, fetchManyGuildMembers, fetchManyUsers, fetchRole, fetchText, fetchUser, getBotRole, getConfirmation, hasPermsToAssign, updateMemberRoles, version };
|
|
3924
3649
|
//# sourceMappingURL=index.mjs.map
|