@seedcord/gateway 0.1.0-next.2 → 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 +803 -948
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +365 -642
- package/dist/index.mjs +803 -940
- package/dist/index.mjs.map +1 -1
- package/package.json +13 -12
package/dist/index.mjs
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
1
|
import "reflect-metadata";
|
|
2
2
|
import { installNodeDefaults } from "@seedcord/logger/node";
|
|
3
|
-
import {
|
|
4
|
-
import { CommandMetadataKey, ComponentDefsKey, EventMetadataKey, GateNotice, GatedMetadataKey, HmrManager, InteractionMetadataKey, InteractionRouteKeys, InteractionRoutes, MiddlewareMetadataKey, NotInGuild, NoticeCard, PAGE_MAX, SubscribeMetadataKey, decodeFor, getDevChannel, pageCursor, pickNotice, prefixOf, routeIdOf, runHandlerGates, setBotColor } from "@seedcord/core/internal";
|
|
5
|
-
import { ApplicationCommandType, AttachmentBuilder, ButtonStyle, Client, Collection, ComponentType, DiscordAPIError, Events, Guild, GuildMember, Message, MessageFlags, PermissionFlagsBits, RESTJSONErrorCodes, Role, SeparatorSpacingSize, SnowflakeUtil, TextChannel, User, WebhookClient, chatInputApplicationCommandMention } from "discord.js";
|
|
3
|
+
import { BaseReplySender, CommandMetadataKey, EventMetadataKey, GateNotice, GatedMetadataKey, HmrManager, InteractionMetadataKey, InteractionRouteKeys, InteractionRoutes, MiddlewareMetadataKey, MissingPermissions, NoticeCard, PAGE_MAX, PermissionNames, SubscribeMetadataKey, WebhookUrlMetadataKey, areRoutes, decodeComponentRoute, deferFlags, getDevChannel, pageCursor, pickNotice, prefixOf, routeIdOf, runHandlerGates, sendFlags, setBotColor, slowGateMonitor } from "@seedcord/core/internal";
|
|
6
4
|
import { SeedcordErrorCode } from "@seedcord/errors";
|
|
7
5
|
import { SeedcordError, SeedcordRangeError, SeedcordTypeError, validateDiscordToken } from "@seedcord/errors/internal";
|
|
8
|
-
import {
|
|
6
|
+
import { BaseHandler, BuilderComponent, CustomId, DispatchContext, Fault, Notice, RepliableHandler as RepliableHandler$1, RowComponent, Silence, defineGate, paginate } from "@seedcord/core";
|
|
9
7
|
import { Logger, LoggerChannelRegistry, paint } from "@seedcord/logger";
|
|
10
8
|
import { ActionRowBuilder, ButtonBuilder, ContextMenuCommandBuilder, SlashCommandBuilder, TextDisplayBuilder } from "@discordjs/builders";
|
|
11
9
|
import { buildSlashRoute, routeLeavesOf } from "@seedcord/utils/internal";
|
|
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 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";
|
|
13
19
|
import { MemoryRateLimiter } from "@seedcord/rate-limiter";
|
|
14
|
-
import { CoordinatedShutdown, CoordinatedStartup, HealthCheck, ShutdownPhase, StartupPhase } from "@seedcord/services";
|
|
15
20
|
import { SeedcordBrand } from "@seedcord/types/internal";
|
|
16
|
-
import { Envapter } from "envapt";
|
|
17
|
-
import chalk from "chalk";
|
|
18
21
|
import { Envapt } from "envapt/legacy";
|
|
19
22
|
import { resolve } from "node:path";
|
|
20
23
|
import { HmrModuleHandler } from "@seedcord/core/hmr";
|
|
21
24
|
import { traverseDirectory } from "@seedcord/utils/node";
|
|
22
|
-
import * as crypto from "node:crypto";
|
|
23
|
-
|
|
24
|
-
export * from "@seedcord/core"
|
|
25
25
|
|
|
26
26
|
export * from "@seedcord/errors"
|
|
27
27
|
|
|
28
|
-
export * from "@seedcord/
|
|
28
|
+
export * from "@seedcord/core"
|
|
29
29
|
|
|
30
30
|
export * from "@seedcord/logger"
|
|
31
31
|
|
|
32
32
|
export * from "@seedcord/event-emitter"
|
|
33
33
|
|
|
34
|
-
export * from "@seedcord/
|
|
34
|
+
export * from "@seedcord/utils"
|
|
35
35
|
|
|
36
|
-
export * from "@seedcord/
|
|
36
|
+
export * from "@seedcord/rate-limiter"
|
|
37
37
|
|
|
38
38
|
//#region src/logger-bootstrap.ts
|
|
39
39
|
installNodeDefaults();
|
|
@@ -131,190 +131,6 @@ function Gated(...gates) {
|
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
//#endregion
|
|
135
|
-
//#region src/miscellaneous/areRoutes.ts
|
|
136
|
-
/**
|
|
137
|
-
* Checks if the provided value is an array of strings.
|
|
138
|
-
*
|
|
139
|
-
* @internal
|
|
140
|
-
*/
|
|
141
|
-
function areRoutes(routes) {
|
|
142
|
-
return Array.isArray(routes) && routes.every((r) => typeof r === "string");
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
//#endregion
|
|
146
|
-
//#region src/bot/decorators/Interactions.ts
|
|
147
|
-
/**
|
|
148
|
-
* Types of select menus supported for routing
|
|
149
|
-
*/
|
|
150
|
-
let SelectMenuKind = /* @__PURE__ */ function(SelectMenuKind) {
|
|
151
|
-
SelectMenuKind["String"] = "string";
|
|
152
|
-
SelectMenuKind["User"] = "user";
|
|
153
|
-
SelectMenuKind["Role"] = "role";
|
|
154
|
-
SelectMenuKind["Channel"] = "channel";
|
|
155
|
-
SelectMenuKind["Mentionable"] = "mentionable";
|
|
156
|
-
return SelectMenuKind;
|
|
157
|
-
}({});
|
|
158
|
-
/**
|
|
159
|
-
* Routes one or more slash commands to a {@link SlashHandler}.
|
|
160
|
-
*
|
|
161
|
-
* Pass the same route string(s) as the handler's generic. A single command reads `this.options`, several
|
|
162
|
-
* commands branch with `this.match`. The decorator routes and the generic must match exactly, so listing
|
|
163
|
-
* fewer or more than the handler declares is a compile error. Subcommands use a slash path.
|
|
164
|
-
*
|
|
165
|
-
* @param routes - The route string(s) this handler serves, e.g. `'ban'`, `'ban', 'kick'`, or `'demo/setup'`.
|
|
166
|
-
* @decorator
|
|
167
|
-
* @example
|
|
168
|
-
* ```ts
|
|
169
|
-
* \@SlashRoute('ban')
|
|
170
|
-
* class BanHandler extends SlashHandler<'ban'> {
|
|
171
|
-
* async execute() {
|
|
172
|
-
* const target = this.options.getUser('target');
|
|
173
|
-
* }
|
|
174
|
-
* }
|
|
175
|
-
* ```
|
|
176
|
-
*
|
|
177
|
-
* @example
|
|
178
|
-
* ```ts
|
|
179
|
-
* \@SlashRoute('ban', 'kick')
|
|
180
|
-
* class ModerationHandler extends SlashHandler<'ban' | 'kick'> {
|
|
181
|
-
* async execute() {
|
|
182
|
-
* await this.match({ ban: (o) => o.getUser('target'), kick: (o) => o.getUser('member') });
|
|
183
|
-
* }
|
|
184
|
-
* }
|
|
185
|
-
* ```
|
|
186
|
-
*/
|
|
187
|
-
function SlashRoute(...routes) {
|
|
188
|
-
return function(constructor) {
|
|
189
|
-
storeMetadata(InteractionRoutes.Slash, routes, constructor);
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Routes button interactions to handler classes.
|
|
194
|
-
*
|
|
195
|
-
* Pass the {@link CustomId} definition(s) this handler decodes and list the same ones in the handler's
|
|
196
|
-
* generic. Passing different definitions to the decorator and the generic is a compile error. Routing
|
|
197
|
-
* matches the stable prefix, so a wire minted from an older shape still reaches the handler, where
|
|
198
|
-
* reading this.params throws StaleCustomId and the controller boundary turns it into a reply.
|
|
199
|
-
*
|
|
200
|
-
* @param defs - The customId definition(s) this handler decodes, one per route.
|
|
201
|
-
* @decorator
|
|
202
|
-
*/
|
|
203
|
-
function ButtonRoute(...defs) {
|
|
204
|
-
return function(constructor) {
|
|
205
|
-
storeComponentRoute(InteractionRoutes.Button, defs, constructor);
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Routes modal submissions to handler classes.
|
|
210
|
-
*
|
|
211
|
-
* Pass the {@link CustomId} definition(s) this handler decodes and list the same ones in the handler's
|
|
212
|
-
* generic. Passing different definitions to the decorator and the generic is a compile error. Routing
|
|
213
|
-
* matches the stable prefix, so an older-shape wire still reaches the handler and throws StaleCustomId
|
|
214
|
-
* on read, the same as a button route.
|
|
215
|
-
*
|
|
216
|
-
* @param defs - The customId definition(s) this handler decodes, one per route.
|
|
217
|
-
* @decorator
|
|
218
|
-
*/
|
|
219
|
-
function ModalRoute(...defs) {
|
|
220
|
-
return function(constructor) {
|
|
221
|
-
storeComponentRoute(InteractionRoutes.Modal, defs, constructor);
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* Routes one or more context-menu commands to a {@link ContextMenuHandler}.
|
|
226
|
-
*
|
|
227
|
-
* Pass the kind (`ApplicationCommandType.User` or `ApplicationCommandType.Message`) and the command name(s),
|
|
228
|
-
* each checked against that kind's registry, so a typo is a compile error. The same kind must sit on the
|
|
229
|
-
* handler's generic, cross-checked both directions. Context menus carry no options, so a multi-name handler
|
|
230
|
-
* reads `this.target` uniformly with no branch.
|
|
231
|
-
*
|
|
232
|
-
* @param kind - The context-menu kind, `ApplicationCommandType.User` or `ApplicationCommandType.Message`.
|
|
233
|
-
* @param names - The command name(s) this handler serves, keyed off the matching registry.
|
|
234
|
-
* @decorator
|
|
235
|
-
* @example
|
|
236
|
-
* ```ts
|
|
237
|
-
* \@ContextMenuRoute(ApplicationCommandType.User, 'View Profile')
|
|
238
|
-
* class ViewProfile extends ContextMenuHandler<ApplicationCommandType.User> {
|
|
239
|
-
* async execute() {
|
|
240
|
-
* const user = this.target;
|
|
241
|
-
* }
|
|
242
|
-
* }
|
|
243
|
-
* ```
|
|
244
|
-
*/
|
|
245
|
-
function ContextMenuRoute(kind, ...names) {
|
|
246
|
-
return function(constructor) {
|
|
247
|
-
storeMetadata(kind === ApplicationCommandType.User ? InteractionRoutes.UserContextMenu : InteractionRoutes.MessageContextMenu, names, constructor);
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Routes one or more commands' autocomplete to an {@link AutocompleteHandler}.
|
|
252
|
-
*
|
|
253
|
-
* Pass the same command route string(s) as the handler's generic. One command branches with `this.match`
|
|
254
|
-
* over its autocompletable fields, several commands share one handler whose arms span every command's
|
|
255
|
-
* fields. The decorator routes and the generic must match exactly, so listing fewer or more than the
|
|
256
|
-
* handler declares is a compile error.
|
|
257
|
-
*
|
|
258
|
-
* @param routes - The command route string(s) this handler serves, e.g. `'search'` or `'search', 'find'`.
|
|
259
|
-
* @decorator
|
|
260
|
-
* @example
|
|
261
|
-
* ```ts
|
|
262
|
-
* \@AutocompleteRoute('search')
|
|
263
|
-
* class SearchAutocomplete extends AutocompleteHandler<'search'> {
|
|
264
|
-
* async execute() {
|
|
265
|
-
* await this.match({ query: (value, respond) => respond([{ name: value, value }]) });
|
|
266
|
-
* }
|
|
267
|
-
* }
|
|
268
|
-
* ```
|
|
269
|
-
*/
|
|
270
|
-
function AutocompleteRoute(...routes) {
|
|
271
|
-
return function(constructor) {
|
|
272
|
-
storeMetadata(InteractionRoutes.Autocomplete, routes, constructor);
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* Routes select menu interactions to handler classes.
|
|
277
|
-
*
|
|
278
|
-
* Pass the select kind and the {@link CustomId} definition(s) this handler decodes. The handler's
|
|
279
|
-
* generic must list the same definitions, and its second generic argument must be the matching select
|
|
280
|
-
* interaction type, or it is a compile error.
|
|
281
|
-
*
|
|
282
|
-
* @param type - Select menu kind from {@link SelectMenuKind}.
|
|
283
|
-
* @param defs - The customId definition(s) this handler decodes, one per route.
|
|
284
|
-
* @decorator
|
|
285
|
-
*
|
|
286
|
-
* @example
|
|
287
|
-
* ```typescript
|
|
288
|
-
* \@SelectMenuRoute(SelectMenuKind.User, AssignId)
|
|
289
|
-
* class AssignSelect extends SelectMenuHandler<SelectMenuKind.User, [typeof AssignId]> {
|
|
290
|
-
* // handles user select menus minted from AssignId
|
|
291
|
-
* }
|
|
292
|
-
* ```
|
|
293
|
-
*/
|
|
294
|
-
function SelectMenuRoute(type, ...defs) {
|
|
295
|
-
return function(constructor) {
|
|
296
|
-
storeComponentRoute({
|
|
297
|
-
["string"]: InteractionRoutes.StringMenu,
|
|
298
|
-
["user"]: InteractionRoutes.UserMenu,
|
|
299
|
-
["role"]: InteractionRoutes.RoleMenu,
|
|
300
|
-
["channel"]: InteractionRoutes.ChannelMenu,
|
|
301
|
-
["mentionable"]: InteractionRoutes.MentionableMenu
|
|
302
|
-
}[type], defs, constructor);
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
function storeComponentRoute(route, defs, constructor) {
|
|
306
|
-
storeMetadata(route, defs.map((def) => def.prefix), constructor);
|
|
307
|
-
Reflect.defineMetadata(ComponentDefsKey, defs, constructor);
|
|
308
|
-
}
|
|
309
|
-
function storeMetadata(route, routes, constructor) {
|
|
310
|
-
const key = InteractionRouteKeys[route];
|
|
311
|
-
const savedRoutes = Reflect.getMetadata(key, constructor);
|
|
312
|
-
const existing = areRoutes(savedRoutes) ? savedRoutes : [];
|
|
313
|
-
const toStore = Array.isArray(routes) ? routes : [routes];
|
|
314
|
-
Reflect.defineMetadata(key, [...existing, ...toStore], constructor);
|
|
315
|
-
Reflect.defineMetadata(InteractionMetadataKey, true, constructor);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
134
|
//#endregion
|
|
319
135
|
//#region src/bot/decorators/Middlewares.ts
|
|
320
136
|
/**
|
|
@@ -383,21 +199,6 @@ const IgnoreBots = defineGate("IgnoreBots", (ctx) => {
|
|
|
383
199
|
if (ctx.user?.bot) throw new Silence("actor is a bot");
|
|
384
200
|
});
|
|
385
201
|
|
|
386
|
-
//#endregion
|
|
387
|
-
//#region src/bot/notices/utils.ts
|
|
388
|
-
function mentionFor(subject) {
|
|
389
|
-
if (subject instanceof Role) return `<@&${subject.id}>`;
|
|
390
|
-
if (subject instanceof TextChannel) return `<#${subject.id}>`;
|
|
391
|
-
if (subject instanceof GuildMember) return `<@${subject.id}>`;
|
|
392
|
-
return `\`${subject.name}\``;
|
|
393
|
-
}
|
|
394
|
-
function labelFor(subject) {
|
|
395
|
-
if (subject instanceof Role) return "role";
|
|
396
|
-
if (subject instanceof TextChannel) return "channel";
|
|
397
|
-
if (subject instanceof GuildMember) return "member";
|
|
398
|
-
return "guild";
|
|
399
|
-
}
|
|
400
|
-
|
|
401
202
|
//#endregion
|
|
402
203
|
//#region src/bot/notices/index.ts
|
|
403
204
|
var NotNsfw = class extends GateNotice {
|
|
@@ -405,13 +206,6 @@ var NotNsfw = class extends GateNotice {
|
|
|
405
206
|
super(message);
|
|
406
207
|
}
|
|
407
208
|
};
|
|
408
|
-
var MissingRole = class extends GateNotice {
|
|
409
|
-
role;
|
|
410
|
-
constructor(message, role) {
|
|
411
|
-
super(message ?? (role ? `You need the ${role.name} role to use this.` : "You do not have the required role."));
|
|
412
|
-
this.role = role;
|
|
413
|
-
}
|
|
414
|
-
};
|
|
415
209
|
var RoleHigherThanMe = class extends Notice {
|
|
416
210
|
role;
|
|
417
211
|
botRole;
|
|
@@ -432,30 +226,19 @@ var CannotAssignBotRole = class extends Notice {
|
|
|
432
226
|
return { components: [new NoticeCard("I cannot assign a managed role.").component] };
|
|
433
227
|
}
|
|
434
228
|
};
|
|
435
|
-
var MissingPermissions = class extends Notice {
|
|
436
|
-
where;
|
|
437
|
-
missingPerms;
|
|
438
|
-
constructor(message, where, missingPerms) {
|
|
439
|
-
super(message);
|
|
440
|
-
this.where = where;
|
|
441
|
-
this.missingPerms = missingPerms;
|
|
442
|
-
}
|
|
443
|
-
render() {
|
|
444
|
-
const bullets = this.missingPerms.map((perm) => `• ${perm}`).join("\n");
|
|
445
|
-
return { components: [new NoticeCard(`The ${labelFor(this.where)} ${mentionFor(this.where)} is missing the following permission entries:\n\n${bullets}`).component] };
|
|
446
|
-
}
|
|
447
|
-
};
|
|
448
229
|
var HasDangerousPermissions = class extends Notice {
|
|
449
|
-
|
|
230
|
+
subject;
|
|
450
231
|
dangerousPerms;
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
232
|
+
customLead;
|
|
233
|
+
constructor(message, subject, dangerousPerms) {
|
|
234
|
+
super(message ?? "A dangerous permission is enabled.");
|
|
235
|
+
this.subject = subject;
|
|
454
236
|
this.dangerousPerms = dangerousPerms;
|
|
237
|
+
this.customLead = message;
|
|
455
238
|
}
|
|
456
239
|
render() {
|
|
457
240
|
const bullets = this.dangerousPerms.map((perm) => `• ${perm}`).join("\n");
|
|
458
|
-
return { components: [new NoticeCard(
|
|
241
|
+
return { components: [new NoticeCard(`${this.customLead ?? `${this.subject} has the following permission entries that must not be enabled:`}\n\n${bullets}`).component] };
|
|
459
242
|
}
|
|
460
243
|
};
|
|
461
244
|
var UserNotFound = class extends Notice {
|
|
@@ -531,129 +314,6 @@ function Nsfw(options) {
|
|
|
531
314
|
});
|
|
532
315
|
}
|
|
533
316
|
|
|
534
|
-
//#endregion
|
|
535
|
-
//#region src/bot/utilities/permissions/checkPermissions.ts
|
|
536
|
-
/**
|
|
537
|
-
* Map of permission bits to their prettified human readable names.
|
|
538
|
-
*/
|
|
539
|
-
const PermissionNames = new Map(Object.entries(PermissionFlagsBits).map(([key, bit]) => [bit, prettify(key)]));
|
|
540
|
-
function checkPermissions(a, b, c, d, e) {
|
|
541
|
-
const { for: pFor, in: pIn, scope, inverse = false, missing: missingCtor, dangerous: dangerousCtor } = a instanceof Role || a instanceof GuildMember ? {
|
|
542
|
-
for: a,
|
|
543
|
-
in: b,
|
|
544
|
-
scope: c,
|
|
545
|
-
inverse: d ?? false,
|
|
546
|
-
...e
|
|
547
|
-
} : a;
|
|
548
|
-
const Missing = missingCtor ?? MissingPermissions;
|
|
549
|
-
const Dangerous = dangerousCtor ?? HasDangerousPermissions;
|
|
550
|
-
const perms = pIn instanceof Guild ? pFor.permissions : pIn.permissionsFor(pFor, true);
|
|
551
|
-
const names = (bits) => bits.map((bit) => PermissionNames.get(bit) ?? String(bit));
|
|
552
|
-
const present = scope.filter((bit) => perms.has(bit, true));
|
|
553
|
-
const presentNames = names(present);
|
|
554
|
-
if (inverse) {
|
|
555
|
-
if (present.length > 0) throw new Dangerous(`${pFor instanceof Role ? "Role" : "Member"} has dangerous permissions in this ${pIn instanceof Guild ? "guild" : "channel"}`, pFor, presentNames);
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
|
-
const missingBits = scope.filter((bit) => !perms.has(bit, true));
|
|
559
|
-
if (missingBits.length > 0) throw new Missing("Missing Any/All/No Permissions", pFor, names(missingBits));
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
//#endregion
|
|
563
|
-
//#region src/bot/gates/catalog/permissions.ts
|
|
564
|
-
const UNCACHED_MEMBER = "Your server member data could not be resolved. Try again.";
|
|
565
|
-
/**
|
|
566
|
-
* Requires the caller to hold every permission in `scope`, via `checkPermissions`. Refuses outside a guild.
|
|
567
|
-
*
|
|
568
|
-
* Interaction-only and excludes ModalSubmit, since a modal lacks a reliable cached caller member.
|
|
569
|
-
*
|
|
570
|
-
* @param scope - The permission flag bits the caller must all hold.
|
|
571
|
-
* @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-permissions one with `missing`.
|
|
572
|
-
*
|
|
573
|
-
* @see {@link Gated}
|
|
574
|
-
*
|
|
575
|
-
* @example
|
|
576
|
-
* ```ts
|
|
577
|
-
* import { PermissionFlagsBits } from 'discord.js';
|
|
578
|
-
*
|
|
579
|
-
* \@Gated(RequirePermissions([PermissionFlagsBits.BanMembers]))
|
|
580
|
-
* \@SlashRoute('ban')
|
|
581
|
-
* class BanHandler extends SlashHandler<'ban'> {
|
|
582
|
-
* async execute() {
|
|
583
|
-
* const target = this.options.getUser('target');
|
|
584
|
-
* }
|
|
585
|
-
* }
|
|
586
|
-
* ```
|
|
587
|
-
*/
|
|
588
|
-
function RequirePermissions(scope, options) {
|
|
589
|
-
return defineGate("RequirePermissions", (ctx) => {
|
|
590
|
-
if (!ctx.guild) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message));
|
|
591
|
-
if (!ctx.member) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message ?? UNCACHED_MEMBER));
|
|
592
|
-
checkPermissions(ctx.member, ctx.guild, scope, false, options?.missing ? { missing: options.missing } : void 0);
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
/**
|
|
596
|
-
* Requires the bot to hold every permission in `scope`, via `checkPermissions`. Refuses outside a guild.
|
|
597
|
-
*
|
|
598
|
-
* It reads the bot's own member, so it attaches to modal handlers as well.
|
|
599
|
-
*
|
|
600
|
-
* @param scope - The permission flag bits the bot must all hold.
|
|
601
|
-
* @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-permissions one with `missing`.
|
|
602
|
-
*
|
|
603
|
-
* @see {@link Gated}
|
|
604
|
-
*
|
|
605
|
-
* @example
|
|
606
|
-
* ```ts
|
|
607
|
-
* import { PermissionFlagsBits } from 'discord.js';
|
|
608
|
-
*
|
|
609
|
-
* \@Gated(RequireBotPermissions([PermissionFlagsBits.BanMembers]))
|
|
610
|
-
* \@ModalRoute(MyModalId)
|
|
611
|
-
* class MyModalHandler extends ModalHandler<[typeof MyModalId]> {
|
|
612
|
-
* async execute() {
|
|
613
|
-
* // ...
|
|
614
|
-
* }
|
|
615
|
-
* }
|
|
616
|
-
* ```
|
|
617
|
-
*/
|
|
618
|
-
function RequireBotPermissions(scope, options) {
|
|
619
|
-
return defineGate("RequireBotPermissions", (ctx) => {
|
|
620
|
-
const botMember = ctx.guild?.members.me;
|
|
621
|
-
if (!ctx.guild || !botMember) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message));
|
|
622
|
-
checkPermissions(botMember, ctx.guild, scope, false, options?.missing ? { missing: options.missing } : void 0);
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
/**
|
|
626
|
-
* Requires the caller to have `roleId`, read from the member's role cache. Refuses outside a guild first, then
|
|
627
|
-
* when the role is missing.
|
|
628
|
-
*
|
|
629
|
-
* Interaction-only and excludes ModalSubmit.
|
|
630
|
-
*
|
|
631
|
-
* @param roleId - The role snowflake the caller must hold, read from the member's role cache.
|
|
632
|
-
* @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-role one with `missingRole`.
|
|
633
|
-
*
|
|
634
|
-
* @see {@link Gated}
|
|
635
|
-
*
|
|
636
|
-
* @example
|
|
637
|
-
* ```ts
|
|
638
|
-
* \@Gated(RequireRole('123456789012345678'))
|
|
639
|
-
* \@SlashRoute('vip')
|
|
640
|
-
* class VipHandler extends SlashHandler<'vip'> {
|
|
641
|
-
* async execute() {
|
|
642
|
-
* // ...
|
|
643
|
-
* }
|
|
644
|
-
* }
|
|
645
|
-
* ```
|
|
646
|
-
*/
|
|
647
|
-
function RequireRole(roleId, options) {
|
|
648
|
-
return defineGate("RequireRole", (ctx) => {
|
|
649
|
-
if (!ctx.guild) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message));
|
|
650
|
-
if (!ctx.member) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message ?? UNCACHED_MEMBER));
|
|
651
|
-
if (ctx.member.roles.cache.has(roleId)) return;
|
|
652
|
-
const role = ctx.guild.roles.cache.get(roleId) ?? null;
|
|
653
|
-
throw pickNotice(options?.missingRole, (message) => new MissingRole(message, role));
|
|
654
|
-
});
|
|
655
|
-
}
|
|
656
|
-
|
|
657
317
|
//#endregion
|
|
658
318
|
//#region src/bot/injectors/EmojiInjector.ts
|
|
659
319
|
const emojiStorage = {};
|
|
@@ -842,6 +502,37 @@ async function fetchText(client, channelId) {
|
|
|
842
502
|
throw new CouldNotFindChannel("Channel not found or not a text channel", channelId);
|
|
843
503
|
}
|
|
844
504
|
|
|
505
|
+
//#endregion
|
|
506
|
+
//#region src/bot/notices/utils.ts
|
|
507
|
+
function mentionFor(subject) {
|
|
508
|
+
if (subject instanceof Role) return `<@&${subject.id}>`;
|
|
509
|
+
return `<@${subject.id}>`;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
//#endregion
|
|
513
|
+
//#region src/bot/utilities/permissions/checkPermissions.ts
|
|
514
|
+
function checkPermissions(a, b, c, d, e) {
|
|
515
|
+
const { for: pFor, in: pIn, scope, inverse = false, missingNotice: missingCtor, dangerousNotice: dangerousCtor } = a instanceof Role || a instanceof GuildMember ? {
|
|
516
|
+
for: a,
|
|
517
|
+
in: b,
|
|
518
|
+
scope: c,
|
|
519
|
+
inverse: d ?? false,
|
|
520
|
+
...e
|
|
521
|
+
} : a;
|
|
522
|
+
const Missing = missingCtor ?? MissingPermissions;
|
|
523
|
+
const Dangerous = dangerousCtor ?? HasDangerousPermissions;
|
|
524
|
+
const subject = mentionFor(pFor);
|
|
525
|
+
const perms = pIn instanceof Guild ? pFor.permissions : pIn.permissionsFor(pFor, true);
|
|
526
|
+
const names = (bits) => bits.map((bit) => PermissionNames.get(bit) ?? String(bit));
|
|
527
|
+
if (inverse) {
|
|
528
|
+
const present = scope.filter((bit) => perms.has(bit, true));
|
|
529
|
+
if (present.length > 0) throw new Dangerous(void 0, subject, names(present));
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const missingBits = scope.filter((bit) => !perms.has(bit, true));
|
|
533
|
+
if (missingBits.length > 0) throw new Missing(void 0, subject, names(missingBits));
|
|
534
|
+
}
|
|
535
|
+
|
|
845
536
|
//#endregion
|
|
846
537
|
//#region src/bot/utilities/permissions/checkBotPermissions.ts
|
|
847
538
|
/**
|
|
@@ -870,7 +561,7 @@ function checkBotPermissions(target, scope, inverse = false, errors) {
|
|
|
870
561
|
const me = target.members.me;
|
|
871
562
|
if (!me) {
|
|
872
563
|
const names = scope.map((bit) => PermissionNames.get(bit) ?? String(bit));
|
|
873
|
-
throw new ((errors?.
|
|
564
|
+
throw new ((errors?.missingNotice) ?? MissingPermissions)(void 0, "The bot", names);
|
|
874
565
|
}
|
|
875
566
|
checkPermissions(me, target, scope, inverse, errors);
|
|
876
567
|
return;
|
|
@@ -879,7 +570,7 @@ function checkBotPermissions(target, scope, inverse = false, errors) {
|
|
|
879
570
|
if (!me) {
|
|
880
571
|
new Logger("checkBotPermissions").warn(`Bot member is unavailable in guild ${target.guild.id} while checking permissions in channel ${target.id}`);
|
|
881
572
|
const names = scope.map((bit) => PermissionNames.get(bit) ?? String(bit));
|
|
882
|
-
throw new ((errors?.
|
|
573
|
+
throw new ((errors?.missingNotice) ?? MissingPermissions)(void 0, "The bot", names);
|
|
883
574
|
}
|
|
884
575
|
checkPermissions(me, target, scope, inverse, errors);
|
|
885
576
|
}
|
|
@@ -916,26 +607,26 @@ function getBotRole(guild) {
|
|
|
916
607
|
* // Check with custom error handling
|
|
917
608
|
* hasPermsToAssign({
|
|
918
609
|
* targetRole: role,
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
*
|
|
922
|
-
*
|
|
610
|
+
* noticeOverrides: {
|
|
611
|
+
* higherNotice: CustomHigherNotice,
|
|
612
|
+
* managedNotice: CustomManagedNotice,
|
|
613
|
+
* missingNotice: CustomMissingPermissionsNotice
|
|
923
614
|
* }
|
|
924
615
|
* });
|
|
925
616
|
* ```
|
|
926
617
|
*/
|
|
927
618
|
function hasPermsToAssign(roleOrOptions) {
|
|
928
|
-
const { targetRole, errors } = roleOrOptions instanceof Role ? {
|
|
619
|
+
const { targetRole, noticeOverrides: errors } = roleOrOptions instanceof Role ? {
|
|
929
620
|
targetRole: roleOrOptions,
|
|
930
|
-
|
|
621
|
+
noticeOverrides: void 0
|
|
931
622
|
} : roleOrOptions;
|
|
932
|
-
const
|
|
933
|
-
const
|
|
934
|
-
const
|
|
623
|
+
const HigherNotice = errors?.higherNotice ?? RoleHigherThanMe;
|
|
624
|
+
const ManagedNotice = errors?.managedNotice ?? CannotAssignBotRole;
|
|
625
|
+
const MissingNotice = errors?.missingNotice ?? MissingPermissions;
|
|
935
626
|
const botRole = getBotRole(targetRole.guild);
|
|
936
|
-
if (targetRole.comparePositionTo(botRole) >= 0) throw new
|
|
937
|
-
if (targetRole.managed) throw new
|
|
938
|
-
checkBotPermissions(targetRole.guild, [PermissionFlagsBits.ManageRoles], false, {
|
|
627
|
+
if (targetRole.comparePositionTo(botRole) >= 0) throw new HigherNotice("Role is higher than me", targetRole, botRole);
|
|
628
|
+
if (targetRole.managed) throw new ManagedNotice(`Cannot assign managed role ${targetRole.name}`);
|
|
629
|
+
checkBotPermissions(targetRole.guild, [PermissionFlagsBits.ManageRoles], false, { missingNotice: MissingNotice });
|
|
939
630
|
}
|
|
940
631
|
|
|
941
632
|
//#endregion
|
|
@@ -1063,113 +754,308 @@ async function updateMemberRoles(rolesToAdd, rolesToRemove, member) {
|
|
|
1063
754
|
}
|
|
1064
755
|
|
|
1065
756
|
//#endregion
|
|
1066
|
-
//#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
|
+
}
|
|
1067
764
|
/**
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1070
|
-
* the original throw, and {@link ReplySender} drops them on its own send.
|
|
1071
|
-
*
|
|
1072
|
-
* @internal
|
|
765
|
+
* Writes interaction responses through the discord.js interaction. Construction is internal to the repliable
|
|
766
|
+
* handler bases and the dispatcher.
|
|
1073
767
|
*/
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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
|
+
};
|
|
1079
862
|
|
|
1080
863
|
//#endregion
|
|
1081
|
-
//#region src/miscellaneous/
|
|
864
|
+
//#region src/bot/utilities/miscellaneous/slashRouteOf.ts
|
|
1082
865
|
/**
|
|
1083
|
-
*
|
|
1084
|
-
*
|
|
1085
|
-
*
|
|
1086
|
-
* @param ephemeral - Whether the reply is ephemeral.
|
|
1087
|
-
* @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.
|
|
1088
869
|
*
|
|
1089
870
|
* @internal
|
|
1090
871
|
*/
|
|
1091
|
-
function
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
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);
|
|
1095
876
|
}
|
|
1096
877
|
|
|
1097
878
|
//#endregion
|
|
1098
|
-
//#region src/
|
|
879
|
+
//#region src/miscellaneous/FaultThrottle.ts
|
|
1099
880
|
/**
|
|
1100
|
-
*
|
|
1101
|
-
*
|
|
1102
|
-
*
|
|
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
|
|
1103
886
|
*/
|
|
1104
|
-
var
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
* Sends the reply and returns the sent message so a caller can attach a collector to it. Never throws,
|
|
1112
|
-
* a failed send is logged and dropped and resolves to `undefined`.
|
|
1113
|
-
*
|
|
1114
|
-
* @param response - The ComponentsV2 reply to show.
|
|
1115
|
-
* @param ephemeral - Whether the reply is ephemeral. {@default `true`}
|
|
1116
|
-
* @returns The sent {@link Message}, or `undefined` when the send was swallowed.
|
|
1117
|
-
*/
|
|
1118
|
-
async send(response, ephemeral = true) {
|
|
1119
|
-
try {
|
|
1120
|
-
return await this.dispatch(response, ephemeral);
|
|
1121
|
-
} catch (error) {
|
|
1122
|
-
this.logSwallowed("send", error);
|
|
1123
|
-
return;
|
|
1124
|
-
}
|
|
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;
|
|
1125
894
|
}
|
|
1126
|
-
/**
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
async edit(message, response) {
|
|
1134
|
-
try {
|
|
1135
|
-
await this.interaction.webhook.editMessage(message, this.editBody(response));
|
|
1136
|
-
} catch (error) {
|
|
1137
|
-
this.logSwallowed("edit", error);
|
|
1138
|
-
}
|
|
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;
|
|
1139
902
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
if (this.interaction.deferred) {
|
|
1143
|
-
if (this.interaction.ephemeral === null) return await this.interaction.followUp(this.replyOptions(response, ephemeral));
|
|
1144
|
-
return await this.interaction.editReply(this.editBody(response));
|
|
1145
|
-
}
|
|
1146
|
-
await this.interaction.reply(this.replyOptions(response, ephemeral));
|
|
1147
|
-
return await this.interaction.fetchReply();
|
|
903
|
+
markReported(key) {
|
|
904
|
+
this.lastReportedAt.set(key, this.now());
|
|
1148
905
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
components: response.components,
|
|
1152
|
-
flags: flagsFor(ephemeral),
|
|
1153
|
-
...response.allowedMentions && { allowedMentions: response.allowedMentions },
|
|
1154
|
-
...response.files && { files: response.files }
|
|
1155
|
-
};
|
|
906
|
+
clear() {
|
|
907
|
+
this.lastReportedAt.clear();
|
|
1156
908
|
}
|
|
1157
|
-
|
|
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);
|
|
1158
932
|
return {
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
...response.allowedMentions && { allowedMentions: response.allowedMentions },
|
|
1162
|
-
...response.files && { files: response.files }
|
|
933
|
+
uuid,
|
|
934
|
+
response: error.render(ctx)
|
|
1163
935
|
};
|
|
1164
936
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
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;
|
|
1171
949
|
}
|
|
1172
|
-
|
|
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
|
+
}
|
|
1173
1059
|
|
|
1174
1060
|
//#endregion
|
|
1175
1061
|
//#region src/bot/confirm/reserved.ts
|
|
@@ -1215,9 +1101,9 @@ async function collectChoice(message, userId, timeoutMs) {
|
|
|
1215
1101
|
return null;
|
|
1216
1102
|
}
|
|
1217
1103
|
}
|
|
1218
|
-
async function settle(sender,
|
|
1104
|
+
async function settle(sender, message, outcome) {
|
|
1219
1105
|
if (outcome === void 0) {
|
|
1220
|
-
await
|
|
1106
|
+
await sender.delete(message).catch(() => void 0);
|
|
1221
1107
|
return;
|
|
1222
1108
|
}
|
|
1223
1109
|
await sender.edit(message, typeof outcome === "function" ? await outcome() : outcome);
|
|
@@ -1225,17 +1111,16 @@ async function settle(sender, interaction, message, outcome) {
|
|
|
1225
1111
|
async function getConfirmation(interaction, prompt, options) {
|
|
1226
1112
|
const { ephemeral = true, timeoutMs = DEFAULT_TIMEOUT_MS } = options ?? {};
|
|
1227
1113
|
const response = typeof prompt === "string" ? defaultPrompt(prompt, options) : await prompt(CONFIRM_IDS);
|
|
1228
|
-
const sender = new ReplySender(interaction);
|
|
1229
|
-
const message = await sender.send(response, ephemeral);
|
|
1230
|
-
if (!message) return false;
|
|
1114
|
+
const sender = new ReplySender(interaction, interactionRoute(interaction));
|
|
1115
|
+
const message = await sender.send(response, { ephemeral });
|
|
1231
1116
|
const winner = await collectChoice(message, interaction.user.id, timeoutMs);
|
|
1232
1117
|
if (!winner) {
|
|
1233
|
-
await settle(sender,
|
|
1118
|
+
await settle(sender, message, options?.onTimeout);
|
|
1234
1119
|
return false;
|
|
1235
1120
|
}
|
|
1236
1121
|
await winner.deferUpdate().catch(() => void 0);
|
|
1237
1122
|
const confirmed = winner.customId === CONFIRM_IDS.confirm;
|
|
1238
|
-
await settle(sender,
|
|
1123
|
+
await settle(sender, message, confirmed ? options?.onConfirm : options?.onCancel);
|
|
1239
1124
|
return confirmed;
|
|
1240
1125
|
}
|
|
1241
1126
|
|
|
@@ -1348,19 +1233,10 @@ var Pluggable = class Pluggable extends TypedEventEmitter {
|
|
|
1348
1233
|
//#endregion
|
|
1349
1234
|
//#region src/handlers/BaseHandler.ts
|
|
1350
1235
|
/**
|
|
1351
|
-
*
|
|
1236
|
+
* The gateway base, fixes the core base's `TCore` to the gateway {@link Core} and adds the `getEvent`
|
|
1237
|
+
* gate hook.
|
|
1352
1238
|
*/
|
|
1353
|
-
var BaseHandler = class {
|
|
1354
|
-
core;
|
|
1355
|
-
event;
|
|
1356
|
-
logger;
|
|
1357
|
-
dispatch;
|
|
1358
|
-
constructor(event, core, dispatch) {
|
|
1359
|
-
this.core = core;
|
|
1360
|
-
this.event = event;
|
|
1361
|
-
this.logger = new Logger(this.constructor.name);
|
|
1362
|
-
this.dispatch = dispatch;
|
|
1363
|
-
}
|
|
1239
|
+
var BaseHandler$1 = class extends BaseHandler {
|
|
1364
1240
|
/** @internal */
|
|
1365
1241
|
getEvent() {
|
|
1366
1242
|
return this.event;
|
|
@@ -1377,43 +1253,56 @@ var BaseHandler = class {
|
|
|
1377
1253
|
*
|
|
1378
1254
|
* @typeParam Repliable - The interaction type this middleware processes
|
|
1379
1255
|
*/
|
|
1380
|
-
var InteractionMiddleware = class extends BaseHandler {
|
|
1256
|
+
var InteractionMiddleware = class extends BaseHandler$1 {
|
|
1381
1257
|
constructor(event, core) {
|
|
1382
1258
|
super(event, core);
|
|
1383
1259
|
}
|
|
1384
1260
|
};
|
|
1385
1261
|
|
|
1386
1262
|
//#endregion
|
|
1387
|
-
//#region src/handlers/
|
|
1263
|
+
//#region src/handlers/RepliableHandler.ts
|
|
1388
1264
|
/**
|
|
1389
|
-
* Shared base the
|
|
1265
|
+
* Shared base the repliable interaction handlers extend.
|
|
1390
1266
|
*
|
|
1391
|
-
* Not a public entry point.
|
|
1392
|
-
* or {@link
|
|
1393
|
-
*
|
|
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.
|
|
1394
1270
|
*
|
|
1395
|
-
* @typeParam
|
|
1271
|
+
* @typeParam Event - The repliable interaction type this handler processes
|
|
1396
1272
|
*/
|
|
1397
|
-
var
|
|
1398
|
-
|
|
1399
|
-
|
|
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);
|
|
1400
1280
|
}
|
|
1401
1281
|
};
|
|
1402
1282
|
|
|
1403
1283
|
//#endregion
|
|
1404
|
-
//#region src/
|
|
1284
|
+
//#region src/handlers/interaction/InteractionHandler.ts
|
|
1405
1285
|
/**
|
|
1406
|
-
*
|
|
1407
|
-
* `seedcord codegen` both assemble route keys through the same {@link buildSlashRoute}, so a dispatched
|
|
1408
|
-
* interaction and a generated registry key always match.
|
|
1286
|
+
* Shared base the typed interaction handlers extend.
|
|
1409
1287
|
*
|
|
1410
|
-
* @
|
|
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.
|
|
1291
|
+
*
|
|
1292
|
+
* @typeParam Repliable - The interaction type this handler processes
|
|
1411
1293
|
*/
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1294
|
+
var InteractionHandler = class extends RepliableHandler {
|
|
1295
|
+
constructor(event, core, dispatch) {
|
|
1296
|
+
super(event, core, dispatch);
|
|
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
|
+
}
|
|
1305
|
+
};
|
|
1417
1306
|
|
|
1418
1307
|
//#endregion
|
|
1419
1308
|
//#region src/handlers/interaction/SlashHandler.ts
|
|
@@ -1449,7 +1338,7 @@ var SlashHandler = class extends InteractionHandler {
|
|
|
1449
1338
|
}
|
|
1450
1339
|
/**
|
|
1451
1340
|
* Run the arm for whichever command fired. Use this only when the handler is registered for several
|
|
1452
|
-
* routes.
|
|
1341
|
+
* routes.
|
|
1453
1342
|
*
|
|
1454
1343
|
* Provide one arm per registered route, keyed by its route string, and each arm receives that route's
|
|
1455
1344
|
* own narrowed options. The arms must cover every route in the generic, a missing route or an unknown
|
|
@@ -1473,7 +1362,7 @@ var SlashHandler = class extends InteractionHandler {
|
|
|
1473
1362
|
*/
|
|
1474
1363
|
async match(arms) {
|
|
1475
1364
|
const route = slashRouteOf(this.event);
|
|
1476
|
-
const arm = arms[route];
|
|
1365
|
+
const arm = Object.hasOwn(arms, route) ? arms[route] : void 0;
|
|
1477
1366
|
if (!arm) throw new SeedcordError(SeedcordErrorCode.SlashMatchArmMissing, [route]);
|
|
1478
1367
|
return await arm(this.options);
|
|
1479
1368
|
}
|
|
@@ -1540,15 +1429,15 @@ var ContextMenuHandler = class extends InteractionHandler {
|
|
|
1540
1429
|
* }
|
|
1541
1430
|
* ```
|
|
1542
1431
|
*/
|
|
1543
|
-
var AutocompleteHandler = class extends BaseHandler {
|
|
1432
|
+
var AutocompleteHandler = class extends BaseHandler$1 {
|
|
1544
1433
|
constructor(event, core, dispatch) {
|
|
1545
1434
|
super(event, core, dispatch);
|
|
1546
1435
|
}
|
|
1436
|
+
decodedFocused;
|
|
1547
1437
|
/**
|
|
1548
1438
|
* The focused option for this interaction. `value` is always the raw partial string the user is typing,
|
|
1549
1439
|
* even for an integer or number option, so coerce it yourself when you need a number.
|
|
1550
1440
|
*/
|
|
1551
|
-
decodedFocused;
|
|
1552
1441
|
get focused() {
|
|
1553
1442
|
if (this.decodedFocused) return this.decodedFocused;
|
|
1554
1443
|
const raw = this.event.options.getFocused(true);
|
|
@@ -1564,7 +1453,7 @@ var AutocompleteHandler = class extends BaseHandler {
|
|
|
1564
1453
|
* match is how you read it. There is no single-field shortcut the way slash/component handlers have one.
|
|
1565
1454
|
*
|
|
1566
1455
|
* Provide one arm per autocompletable field across the registered commands, keyed by field name. Each
|
|
1567
|
-
* arm receives the focused partial value and a `respond`
|
|
1456
|
+
* arm receives the focused partial value and a `respond` typed to that field's choice value. The
|
|
1568
1457
|
* arms must cover every autocompletable field, a missing field or an unknown key is a compile error. A
|
|
1569
1458
|
* focused field with no arm, only reachable from a stale-deployed command, throws at runtime.
|
|
1570
1459
|
*
|
|
@@ -1586,11 +1475,15 @@ var AutocompleteHandler = class extends BaseHandler {
|
|
|
1586
1475
|
*/
|
|
1587
1476
|
async match(arms) {
|
|
1588
1477
|
const { name, value } = this.focused;
|
|
1589
|
-
const respond = (choices) => this.
|
|
1590
|
-
const arm = arms[name];
|
|
1478
|
+
const respond = (choices) => this.respond(choices);
|
|
1479
|
+
const arm = Object.hasOwn(arms, name) ? arms[name] : void 0;
|
|
1591
1480
|
if (!arm) throw new SeedcordError(SeedcordErrorCode.AutocompleteMatchArmMissing, [name]);
|
|
1592
1481
|
return await arm(value, respond);
|
|
1593
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
|
+
}
|
|
1594
1487
|
/** The firing command route, for a field whose completion differs per registered command. */
|
|
1595
1488
|
get route() {
|
|
1596
1489
|
return slashRouteOf(this.event);
|
|
@@ -1598,7 +1491,7 @@ var AutocompleteHandler = class extends BaseHandler {
|
|
|
1598
1491
|
/**
|
|
1599
1492
|
* The already-entered options on this command, restricted to the kinds Discord resolves during autocomplete
|
|
1600
1493
|
* (string, integer, number, boolean) with every read nullable, since a sibling is partial while the user
|
|
1601
|
-
* types the focused field.
|
|
1494
|
+
* types the focused field. The focused field is on `this.focused`.
|
|
1602
1495
|
*/
|
|
1603
1496
|
get options() {
|
|
1604
1497
|
return this.event.options;
|
|
@@ -1610,25 +1503,25 @@ var AutocompleteHandler = class extends BaseHandler {
|
|
|
1610
1503
|
/**
|
|
1611
1504
|
* Shared base the customId-routed component handlers extend.
|
|
1612
1505
|
*
|
|
1613
|
-
* Not a public entry point.
|
|
1614
|
-
*
|
|
1615
|
-
* 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.
|
|
1616
1508
|
*
|
|
1617
1509
|
* @typeParam Event - The component interaction type this handler processes
|
|
1618
1510
|
* @typeParam Defs - The customId route definitions registered on the concrete handler
|
|
1619
1511
|
*/
|
|
1620
1512
|
var ComponentHandler = class extends InteractionHandler {
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
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();
|
|
1625
1520
|
}
|
|
1626
1521
|
decoded;
|
|
1627
1522
|
get route() {
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
this.decoded = decoded;
|
|
1631
|
-
return decoded;
|
|
1523
|
+
this.decoded ??= decodeComponentRoute(this.constructor, this.event.customId);
|
|
1524
|
+
return this.decoded;
|
|
1632
1525
|
}
|
|
1633
1526
|
/**
|
|
1634
1527
|
* The decoded params of the single route this handler is registered for.
|
|
@@ -1647,10 +1540,11 @@ var ComponentHandler = class extends InteractionHandler {
|
|
|
1647
1540
|
* handler `this.params` is `never`, so match is the only way to read the decoded params.
|
|
1648
1541
|
*
|
|
1649
1542
|
* Provide one arm per registered route, keyed by its prefix, and each arm receives that route's own
|
|
1650
|
-
* decoded params. The arms
|
|
1651
|
-
*
|
|
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.
|
|
1652
1546
|
*
|
|
1653
|
-
* @param arms - One
|
|
1547
|
+
* @param arms - One callback per registered route, keyed by prefix.
|
|
1654
1548
|
* @returns The result of the arm that ran.
|
|
1655
1549
|
*
|
|
1656
1550
|
* @example
|
|
@@ -1659,8 +1553,8 @@ var ComponentHandler = class extends InteractionHandler {
|
|
|
1659
1553
|
* class ReviewButtons extends ButtonHandler<[typeof Approve, typeof Reject]> {
|
|
1660
1554
|
* async execute() {
|
|
1661
1555
|
* await this.match({
|
|
1662
|
-
* approve: ({ userId }) => this.
|
|
1663
|
-
* reject: ({ userId }) => this.
|
|
1556
|
+
* approve: ({ userId }) => this.reply(`approved <@${userId}>`),
|
|
1557
|
+
* reject: ({ userId }) => this.reply(`rejected <@${userId}>`)
|
|
1664
1558
|
* });
|
|
1665
1559
|
* }
|
|
1666
1560
|
* }
|
|
@@ -1668,7 +1562,7 @@ var ComponentHandler = class extends InteractionHandler {
|
|
|
1668
1562
|
*/
|
|
1669
1563
|
async match(arms) {
|
|
1670
1564
|
const { prefix, params } = this.route;
|
|
1671
|
-
const arm = arms[prefix];
|
|
1565
|
+
const arm = Object.hasOwn(arms, prefix) ? arms[prefix] : void 0;
|
|
1672
1566
|
if (!arm) throw new SeedcordError(SeedcordErrorCode.CustomIdMatchArmMissing, [prefix]);
|
|
1673
1567
|
return await arm(params);
|
|
1674
1568
|
}
|
|
@@ -1680,8 +1574,9 @@ var ComponentHandler = class extends InteractionHandler {
|
|
|
1680
1574
|
* Base class for a button interaction handler.
|
|
1681
1575
|
*
|
|
1682
1576
|
* Register the customId definitions this handler decodes with `@ButtonRoute`, list the same ones in the
|
|
1683
|
-
* generic, then read `this.params` for a single route or `this.match` for several.
|
|
1684
|
-
*
|
|
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.
|
|
1685
1580
|
*
|
|
1686
1581
|
* @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ApproveId]`.
|
|
1687
1582
|
* @typeParam Cache - The interaction cache state, `'cached'` by default.
|
|
@@ -1692,7 +1587,7 @@ var ComponentHandler = class extends InteractionHandler {
|
|
|
1692
1587
|
* class ApproveButton extends ButtonHandler<[typeof ApproveId]> {
|
|
1693
1588
|
* async execute() {
|
|
1694
1589
|
* const { userId } = this.params;
|
|
1695
|
-
* await this.
|
|
1590
|
+
* await this.reply(`approved <@${userId}>`);
|
|
1696
1591
|
* }
|
|
1697
1592
|
* }
|
|
1698
1593
|
* ```
|
|
@@ -1706,7 +1601,8 @@ var ButtonHandler = class extends ComponentHandler {};
|
|
|
1706
1601
|
*
|
|
1707
1602
|
* Register the customId definitions this handler decodes with `@ModalRoute`, list the same ones in the
|
|
1708
1603
|
* generic, then read `this.params` for a single route or `this.match` for several. Read the submitted
|
|
1709
|
-
* 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.
|
|
1710
1606
|
*
|
|
1711
1607
|
* @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ConfigId]`.
|
|
1712
1608
|
* @typeParam Cache - The interaction cache state, `'cached'` by default.
|
|
@@ -1718,7 +1614,7 @@ var ButtonHandler = class extends ComponentHandler {};
|
|
|
1718
1614
|
* async execute() {
|
|
1719
1615
|
* const { guildId } = this.params;
|
|
1720
1616
|
* const name = this.event.fields.getTextInputValue('name');
|
|
1721
|
-
* await this.
|
|
1617
|
+
* await this.reply(`saved ${name} for ${guildId}`);
|
|
1722
1618
|
* }
|
|
1723
1619
|
* }
|
|
1724
1620
|
* ```
|
|
@@ -1732,7 +1628,8 @@ var ModalHandler = class extends ComponentHandler {};
|
|
|
1732
1628
|
*
|
|
1733
1629
|
* Pass the select kind first and the customId definitions second, the same order as `@SelectMenuRoute`,
|
|
1734
1630
|
* so `this.event` and `this.event.values` are narrowed to that kind. Read `this.params` for a single
|
|
1735
|
-
* route or `this.match` for several
|
|
1631
|
+
* route or `this.match` for several, and reply through the handler members or rewrite the source message
|
|
1632
|
+
* with `this.update`.
|
|
1736
1633
|
*
|
|
1737
1634
|
* @typeParam Kind - The select kind from {@link SelectMenuKind}, e.g. `SelectMenuKind.User`.
|
|
1738
1635
|
* @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof AssignId]`.
|
|
@@ -1744,7 +1641,7 @@ var ModalHandler = class extends ComponentHandler {};
|
|
|
1744
1641
|
* class AssignSelect extends SelectMenuHandler<SelectMenuKind.User, [typeof AssignId]> {
|
|
1745
1642
|
* async execute() {
|
|
1746
1643
|
* const { roleId } = this.params;
|
|
1747
|
-
* await this.
|
|
1644
|
+
* await this.reply(`assigning ${this.event.values.length} member(s) to <@&${roleId}>`);
|
|
1748
1645
|
* }
|
|
1749
1646
|
* }
|
|
1750
1647
|
* ```
|
|
@@ -1775,7 +1672,7 @@ var SelectMenuHandler = class extends ComponentHandler {};
|
|
|
1775
1672
|
* }
|
|
1776
1673
|
* ```
|
|
1777
1674
|
*/
|
|
1778
|
-
var EventHandler = class extends BaseHandler {
|
|
1675
|
+
var EventHandler = class extends BaseHandler$1 {
|
|
1779
1676
|
firedEvent;
|
|
1780
1677
|
constructor(event, core, eventName) {
|
|
1781
1678
|
super(event, core);
|
|
@@ -1828,7 +1725,7 @@ var EventHandler = class extends BaseHandler {
|
|
|
1828
1725
|
*
|
|
1829
1726
|
* @typeParam EventName - One or more `ClientEvents` keys. Defaults to every event (catchall).
|
|
1830
1727
|
*/
|
|
1831
|
-
var EventMiddleware = class extends BaseHandler {
|
|
1728
|
+
var EventMiddleware = class extends BaseHandler$1 {
|
|
1832
1729
|
firedEvent;
|
|
1833
1730
|
constructor(event, core, eventName) {
|
|
1834
1731
|
super(event, core);
|
|
@@ -2010,16 +1907,16 @@ var Paginator = class {
|
|
|
2010
1907
|
const loadPage = (ctx, n) => this.page(ctx, n);
|
|
2011
1908
|
this.Handler = class Nav extends ButtonHandler {
|
|
2012
1909
|
async execute() {
|
|
2013
|
-
await this.
|
|
1910
|
+
await this.deferUpdate();
|
|
2014
1911
|
const response = await loadPage(contextOf(this.event, this.core), this.params.page);
|
|
2015
|
-
await
|
|
1912
|
+
await this.update(response);
|
|
2016
1913
|
}
|
|
2017
1914
|
};
|
|
2018
1915
|
}
|
|
2019
1916
|
/** Render page 0 and send it, picking reply or followUp from the interaction's state. */
|
|
2020
1917
|
async start(interaction, core) {
|
|
2021
1918
|
const response = await this.page(contextOf(interaction, core), 0);
|
|
2022
|
-
return new ReplySender(interaction).send(response, this.config.ephemeral ?? false);
|
|
1919
|
+
return new ReplySender(interaction, interactionRoute(interaction)).send(response, { ephemeral: this.config.ephemeral ?? false });
|
|
2023
1920
|
}
|
|
2024
1921
|
/** Render a page as a {@link ReplyResponse}. To post it elsewhere, add `flags: MessageFlags.IsComponentsV2`. */
|
|
2025
1922
|
async page(ctx, n) {
|
|
@@ -2143,19 +2040,184 @@ function Subscribe(subscriber, options) {
|
|
|
2143
2040
|
};
|
|
2144
2041
|
}
|
|
2145
2042
|
|
|
2043
|
+
//#endregion
|
|
2044
|
+
//#region src/subscribers/decorators/WebhookUrl.ts
|
|
2045
|
+
/**
|
|
2046
|
+
* Declares the environment variable a {@link WebhookLog} reporter reads its webhook url from.
|
|
2047
|
+
*
|
|
2048
|
+
* An unset variable disables the reporter with a boot warning. A set but malformed value throws
|
|
2049
|
+
* at boot.
|
|
2050
|
+
*
|
|
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
|
+
* ```
|
|
2063
|
+
*/
|
|
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);
|
|
2110
|
+
}
|
|
2111
|
+
};
|
|
2112
|
+
|
|
2113
|
+
//#endregion
|
|
2114
|
+
//#region src/subscribers/bases/WebhookSender.ts
|
|
2115
|
+
const HTTP_NOT_FOUND = 404;
|
|
2116
|
+
const HTTP_UNAUTHORIZED = 401;
|
|
2117
|
+
/**
|
|
2118
|
+
* Sends component messages to a Discord webhook through `@discordjs/rest`.
|
|
2119
|
+
*
|
|
2120
|
+
* @throws A `SeedcordError` when the url does not carry a webhook id and token.
|
|
2121
|
+
*/
|
|
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]);
|
|
2129
|
+
}
|
|
2130
|
+
/**
|
|
2131
|
+
* Fetches the webhook object, sending no message. Returns `missing` on a 404 or 401 from
|
|
2132
|
+
* Discord and `unreachable` on any other failure.
|
|
2133
|
+
*/
|
|
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";
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
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
|
+
});
|
|
2165
|
+
}
|
|
2166
|
+
};
|
|
2167
|
+
|
|
2146
2168
|
//#endregion
|
|
2147
2169
|
//#region src/subscribers/bases/WebhookLog.ts
|
|
2148
2170
|
/**
|
|
2149
|
-
*
|
|
2171
|
+
* Base class for subscribers that deliver their event to a Discord webhook.
|
|
2150
2172
|
*
|
|
2151
|
-
*
|
|
2152
|
-
*
|
|
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.
|
|
2153
2176
|
*
|
|
2154
|
-
* @typeParam KeyOfSubscribers - The
|
|
2177
|
+
* @typeParam KeyOfSubscribers - The subscription key this reporter receives
|
|
2155
2178
|
*/
|
|
2156
|
-
var WebhookLog = class extends Subscriber {
|
|
2157
|
-
|
|
2158
|
-
|
|
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;
|
|
2186
|
+
}
|
|
2187
|
+
const { username, components, files } = await this.report();
|
|
2188
|
+
try {
|
|
2189
|
+
await WebhookLog.senderFor(url).send({
|
|
2190
|
+
flags: flagsFor(false),
|
|
2191
|
+
username: username ?? this.constructor.name,
|
|
2192
|
+
components,
|
|
2193
|
+
files
|
|
2194
|
+
});
|
|
2195
|
+
} catch (error) {
|
|
2196
|
+
this.logger.error("Failed to send webhook report", error);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
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);
|
|
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;
|
|
2159
2221
|
}
|
|
2160
2222
|
};
|
|
2161
2223
|
|
|
@@ -2371,261 +2433,94 @@ var CommandRegistry = class {
|
|
|
2371
2433
|
const tag = commands.length === 1 ? "command" : "commands";
|
|
2372
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");
|
|
2373
2435
|
}
|
|
2374
|
-
this.onDeployed?.(result);
|
|
2375
|
-
return result;
|
|
2376
|
-
}
|
|
2377
|
-
/** The deduplicated slash route keys across every global and guild command. @internal */
|
|
2378
|
-
routeLeaves() {
|
|
2379
|
-
return slashRouteLeaves([...this.globalCommands, ...this.guildCommands.values()].flat());
|
|
2380
|
-
}
|
|
2381
|
-
/** The registered context-menu command names, split by kind, across every global and guild command. @internal */
|
|
2382
|
-
contextMenuLeaves() {
|
|
2383
|
-
return contextMenuLeaves([...this.globalCommands, ...this.guildCommands.values()].flat());
|
|
2384
|
-
}
|
|
2385
|
-
};
|
|
2386
|
-
|
|
2387
|
-
//#endregion
|
|
2388
|
-
//#region src/miscellaneous/deriveEventActor.ts
|
|
2389
|
-
function deriveEventActor(args) {
|
|
2390
|
-
const tuple = Array.isArray(args) ? args : [args];
|
|
2391
|
-
for (const arg of tuple) {
|
|
2392
|
-
if (arg instanceof Message) return {
|
|
2393
|
-
guild: arg.guild,
|
|
2394
|
-
member: arg.member,
|
|
2395
|
-
user: arg.author,
|
|
2396
|
-
channelId: arg.channelId
|
|
2397
|
-
};
|
|
2398
|
-
if (arg instanceof GuildMember) return {
|
|
2399
|
-
guild: arg.guild,
|
|
2400
|
-
member: arg,
|
|
2401
|
-
user: arg.user,
|
|
2402
|
-
channelId: null
|
|
2403
|
-
};
|
|
2404
|
-
if (arg instanceof User) return {
|
|
2405
|
-
guild: null,
|
|
2406
|
-
member: null,
|
|
2407
|
-
user: arg,
|
|
2408
|
-
channelId: null
|
|
2409
|
-
};
|
|
2410
|
-
}
|
|
2411
|
-
return {
|
|
2412
|
-
guild: null,
|
|
2413
|
-
member: null,
|
|
2414
|
-
user: null,
|
|
2415
|
-
channelId: null
|
|
2416
|
-
};
|
|
2417
|
-
}
|
|
2418
|
-
|
|
2419
|
-
//#endregion
|
|
2420
|
-
//#region src/bot/gates/runGates.ts
|
|
2421
|
-
function interactionGateContext(interaction, core) {
|
|
2422
|
-
const rawMember = interaction.member;
|
|
2423
|
-
const memberRoleIds = rawMember instanceof GuildMember ? [...rawMember.roles.cache.keys()].filter((id) => id !== interaction.guildId) : rawMember?.roles ?? [];
|
|
2424
|
-
return {
|
|
2425
|
-
kind: "interaction",
|
|
2426
|
-
interaction,
|
|
2427
|
-
core,
|
|
2428
|
-
user: interaction.user,
|
|
2429
|
-
guild: interaction.guild,
|
|
2430
|
-
member: rawMember instanceof GuildMember ? rawMember : null,
|
|
2431
|
-
userId: interaction.user.id,
|
|
2432
|
-
guildId: interaction.guildId,
|
|
2433
|
-
channelId: interaction.channelId,
|
|
2434
|
-
memberRoleIds,
|
|
2435
|
-
memberPermissions: interaction.memberPermissions?.bitfield ?? null,
|
|
2436
|
-
routeId: null
|
|
2437
|
-
};
|
|
2438
|
-
}
|
|
2439
|
-
function eventGateContext(eventName, args, core) {
|
|
2440
|
-
const actor = deriveEventActor(args);
|
|
2441
|
-
return {
|
|
2442
|
-
kind: "event",
|
|
2443
|
-
core,
|
|
2444
|
-
eventName,
|
|
2445
|
-
payload: args,
|
|
2446
|
-
user: actor.user,
|
|
2447
|
-
guild: actor.guild,
|
|
2448
|
-
member: actor.member,
|
|
2449
|
-
userId: actor.user?.id ?? null,
|
|
2450
|
-
guildId: actor.guild?.id ?? null,
|
|
2451
|
-
channelId: actor.channelId,
|
|
2452
|
-
memberRoleIds: actor.member ? [...actor.member.roles.cache.keys()].filter((id) => id !== actor.guild?.id) : [],
|
|
2453
|
-
memberPermissions: actor.member?.permissions.bitfield ?? null,
|
|
2454
|
-
routeId: null
|
|
2455
|
-
};
|
|
2456
|
-
}
|
|
2457
|
-
|
|
2458
|
-
//#endregion
|
|
2459
|
-
//#region src/miscellaneous/FaultThrottle.ts
|
|
2460
|
-
/**
|
|
2461
|
-
* Drops duplicate fault reports inside a fixed window so a recurring fault (a database outage on a
|
|
2462
|
-
* hot path) reports once per window instead of flooding the webhook. Keyed on a structural string, so
|
|
2463
|
-
* the map stays small, and a stale entry is evicted on the next read of its key.
|
|
2464
|
-
*
|
|
2465
|
-
* @internal
|
|
2466
|
-
*/
|
|
2467
|
-
var FaultThrottle = class {
|
|
2468
|
-
windowMs;
|
|
2469
|
-
now;
|
|
2470
|
-
lastReportedAt = /* @__PURE__ */ new Map();
|
|
2471
|
-
constructor(windowMs = 6e4, now = () => Date.now()) {
|
|
2472
|
-
this.windowMs = windowMs;
|
|
2473
|
-
this.now = now;
|
|
2474
|
-
}
|
|
2475
|
-
/** Evicts the entry for this key if it has aged out, so the map never holds stale keys forever. */
|
|
2476
|
-
shouldReport(key) {
|
|
2477
|
-
const last = this.lastReportedAt.get(key);
|
|
2478
|
-
if (last === void 0) return true;
|
|
2479
|
-
if (this.now() - last < this.windowMs) return false;
|
|
2480
|
-
this.lastReportedAt.delete(key);
|
|
2481
|
-
return true;
|
|
2482
|
-
}
|
|
2483
|
-
markReported(key) {
|
|
2484
|
-
this.lastReportedAt.set(key, this.now());
|
|
2485
|
-
}
|
|
2486
|
-
clear() {
|
|
2487
|
-
this.lastReportedAt.clear();
|
|
2488
|
-
}
|
|
2489
|
-
};
|
|
2490
|
-
|
|
2491
|
-
//#endregion
|
|
2492
|
-
//#region src/miscellaneous/extractErrorResponse.ts
|
|
2493
|
-
const logger$2 = new Logger("ErrorsHandling");
|
|
2494
|
-
/**
|
|
2495
|
-
* The single throttle for every fault publish, so a recurring fault reports once per window across both
|
|
2496
|
-
* the interaction and event paths. Exported so tests reset it between cases.
|
|
2497
|
-
*
|
|
2498
|
-
* @internal
|
|
2499
|
-
*/
|
|
2500
|
-
const faultThrottle = new FaultThrottle();
|
|
2501
|
-
/**
|
|
2502
|
-
* Processes an error into a user-facing reply and routes its report.
|
|
2503
|
-
*
|
|
2504
|
-
* A {@link Notice} always renders its own reply. A reporting denial (`report: true`) additionally
|
|
2505
|
-
* publishes `handledException` on the interaction path. A raw, non-denial throw publishes
|
|
2506
|
-
* `unknownException` and renders the configured `defaultError` (or a generic {@link Fault}). One uuid is
|
|
2507
|
-
* threaded into both the reply and the bus payload.
|
|
2508
|
-
*
|
|
2509
|
-
* @internal
|
|
2510
|
-
*/
|
|
2511
|
-
function extractErrorResponse(error, core, origin) {
|
|
2512
|
-
const uuid = crypto.randomUUID();
|
|
2513
|
-
const developerUsername = core.config.notifications?.developerUsername;
|
|
2514
|
-
const ctx = developerUsername === void 0 ? { uuid } : {
|
|
2515
|
-
uuid,
|
|
2516
|
-
developerUsername
|
|
2517
|
-
};
|
|
2518
|
-
if (error instanceof Notice) {
|
|
2519
|
-
if (error.report) reportFault(error, core, origin, uuid);
|
|
2520
|
-
return {
|
|
2521
|
-
uuid,
|
|
2522
|
-
response: error.render(ctx)
|
|
2523
|
-
};
|
|
2436
|
+
this.onDeployed?.(result);
|
|
2437
|
+
return result;
|
|
2524
2438
|
}
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2439
|
+
/** The deduplicated slash route keys across every global and guild command. @internal */
|
|
2440
|
+
routeLeaves() {
|
|
2441
|
+
return slashRouteLeaves([...this.globalCommands, ...this.guildCommands.values()].flat());
|
|
2442
|
+
}
|
|
2443
|
+
/** The registered context-menu command names, split by kind, across every global and guild command. @internal */
|
|
2444
|
+
contextMenuLeaves() {
|
|
2445
|
+
return contextMenuLeaves([...this.globalCommands, ...this.guildCommands.values()].flat());
|
|
2446
|
+
}
|
|
2447
|
+
};
|
|
2448
|
+
|
|
2449
|
+
//#endregion
|
|
2450
|
+
//#region src/miscellaneous/deriveEventActor.ts
|
|
2451
|
+
function deriveEventActor(args) {
|
|
2452
|
+
const tuple = Array.isArray(args) ? args : [args];
|
|
2453
|
+
for (const arg of tuple) {
|
|
2454
|
+
if (arg instanceof Message) return {
|
|
2455
|
+
guild: arg.guild,
|
|
2456
|
+
member: arg.member,
|
|
2457
|
+
user: arg.author,
|
|
2458
|
+
channelId: arg.channelId
|
|
2459
|
+
};
|
|
2460
|
+
if (arg instanceof GuildMember) return {
|
|
2461
|
+
guild: arg.guild,
|
|
2462
|
+
member: arg,
|
|
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
|
+
};
|
|
2537
2472
|
}
|
|
2538
|
-
publish();
|
|
2539
|
-
faultThrottle.markReported(key);
|
|
2540
|
-
}
|
|
2541
|
-
function reportFault(denial, core, origin, uuid) {
|
|
2542
|
-
withThrottle(origin, denial, () => {
|
|
2543
|
-
logger$2.error(`${denial.name}: ${uuid}`, denial);
|
|
2544
|
-
if (origin.interaction) core.bus.publish("handledException", {
|
|
2545
|
-
denial,
|
|
2546
|
-
uuid,
|
|
2547
|
-
source: buildInteractionSource(origin.interaction)
|
|
2548
|
-
});
|
|
2549
|
-
else if (origin.event) core.bus.publish("handledException", {
|
|
2550
|
-
denial,
|
|
2551
|
-
uuid,
|
|
2552
|
-
source: buildEventSource(origin.event, origin)
|
|
2553
|
-
});
|
|
2554
|
-
else core.bus.publish("unknownException", {
|
|
2555
|
-
uuid,
|
|
2556
|
-
error: denial,
|
|
2557
|
-
guild: origin.guild,
|
|
2558
|
-
user: origin.user,
|
|
2559
|
-
metadata: metadataFor(origin)
|
|
2560
|
-
});
|
|
2561
|
-
});
|
|
2562
|
-
}
|
|
2563
|
-
function reportRawFault(error, core, origin, uuid) {
|
|
2564
|
-
withThrottle(origin, error, () => {
|
|
2565
|
-
if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
|
|
2566
|
-
else logger$2.error(`${uuid} | ${error.message}`);
|
|
2567
|
-
core.bus.publish("unknownException", {
|
|
2568
|
-
uuid,
|
|
2569
|
-
error,
|
|
2570
|
-
guild: origin.guild,
|
|
2571
|
-
user: origin.user,
|
|
2572
|
-
metadata: metadataFor(origin)
|
|
2573
|
-
});
|
|
2574
|
-
});
|
|
2575
|
-
}
|
|
2576
|
-
function metadataFor(origin) {
|
|
2577
|
-
if (origin.event) return {
|
|
2578
|
-
eventName: origin.event.name,
|
|
2579
|
-
handler: origin.event.handler,
|
|
2580
|
-
args: origin.event.args
|
|
2581
|
-
};
|
|
2582
|
-
return origin.metadata;
|
|
2583
|
-
}
|
|
2584
|
-
function buildEventSource(event, origin) {
|
|
2585
2473
|
return {
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
guildId: origin.guild?.id ?? null,
|
|
2591
|
-
channelId: event.channelId,
|
|
2592
|
-
raw: event.args
|
|
2474
|
+
guild: null,
|
|
2475
|
+
member: null,
|
|
2476
|
+
user: null,
|
|
2477
|
+
channelId: null
|
|
2593
2478
|
};
|
|
2594
2479
|
}
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
}
|
|
2602
|
-
function interactionRoute(interaction) {
|
|
2603
|
-
if (interaction.isChatInputCommand()) return slashRouteOf(interaction);
|
|
2604
|
-
if (interaction.isContextMenuCommand()) return interaction.commandName;
|
|
2605
|
-
if (interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit()) return prefixOf(interaction.customId) || interaction.customId;
|
|
2606
|
-
return "interaction";
|
|
2607
|
-
}
|
|
2608
|
-
function buildInteractionSource(interaction) {
|
|
2609
|
-
const command = interaction.isChatInputCommand() ? slashRouteOf(interaction) : interaction.isContextMenuCommand() ? interaction.commandName : null;
|
|
2610
|
-
const customId = interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit() ? interaction.customId : null;
|
|
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 ?? [];
|
|
2611
2486
|
return {
|
|
2612
2487
|
kind: "interaction",
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2488
|
+
interaction,
|
|
2489
|
+
core,
|
|
2490
|
+
user: interaction.user,
|
|
2491
|
+
guild: interaction.guild,
|
|
2492
|
+
member: rawMember instanceof GuildMember ? rawMember : null,
|
|
2616
2493
|
userId: interaction.user.id,
|
|
2617
2494
|
guildId: interaction.guildId,
|
|
2618
2495
|
channelId: interaction.channelId,
|
|
2619
|
-
|
|
2620
|
-
|
|
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
|
|
2621
2502
|
};
|
|
2622
2503
|
}
|
|
2623
|
-
function
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
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
|
+
};
|
|
2629
2524
|
}
|
|
2630
2525
|
|
|
2631
2526
|
//#endregion
|
|
@@ -2640,7 +2535,7 @@ const logger$1 = new Logger("EventBoundary");
|
|
|
2640
2535
|
*/
|
|
2641
2536
|
function handleEventFault(caught, eventName, handlerName, args, core) {
|
|
2642
2537
|
if (caught instanceof Silence) {
|
|
2643
|
-
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}`);
|
|
2644
2539
|
return;
|
|
2645
2540
|
}
|
|
2646
2541
|
if (!Error.isError(caught)) throw caught;
|
|
@@ -2885,35 +2780,55 @@ var EventDispatcher = class {
|
|
|
2885
2780
|
};
|
|
2886
2781
|
|
|
2887
2782
|
//#endregion
|
|
2888
|
-
//#region src/bot/defaults/
|
|
2783
|
+
//#region src/bot/defaults/UnhandledRepliable.ts
|
|
2889
2784
|
/**
|
|
2890
|
-
* Default handler for unhandled interactions.
|
|
2891
|
-
* raw fault, so it needs no decorator of its own.
|
|
2892
|
-
*
|
|
2893
|
-
* @internal
|
|
2785
|
+
* @internal Default handler for unhandled repliable interactions.
|
|
2894
2786
|
*/
|
|
2895
|
-
var
|
|
2787
|
+
var UnhandledRepliable = class extends InteractionHandler {
|
|
2896
2788
|
async execute() {
|
|
2897
|
-
await this.
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
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([]);
|
|
2901
2801
|
}
|
|
2902
2802
|
};
|
|
2903
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
|
+
|
|
2904
2818
|
//#endregion
|
|
2905
2819
|
//#region src/bot/handleInteractionFault.ts
|
|
2906
2820
|
const logger = new Logger("InteractionBoundary");
|
|
2907
2821
|
/**
|
|
2908
2822
|
* The interaction controller boundary. Sorts a throw from any handler-lifecycle phase into the right
|
|
2909
|
-
* user reply plus the right bus event
|
|
2910
|
-
*
|
|
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.
|
|
2911
2826
|
*
|
|
2912
2827
|
* @internal
|
|
2913
2828
|
*/
|
|
2914
|
-
async function handleInteractionFault(caught, interaction, core) {
|
|
2829
|
+
async function handleInteractionFault(caught, interaction, core, sender) {
|
|
2915
2830
|
if (caught instanceof Silence) {
|
|
2916
|
-
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}`);
|
|
2917
2832
|
return;
|
|
2918
2833
|
}
|
|
2919
2834
|
if (!Error.isError(caught)) throw caught;
|
|
@@ -2937,7 +2852,18 @@ async function handleInteractionFault(caught, interaction, core) {
|
|
|
2937
2852
|
user: interaction.user,
|
|
2938
2853
|
metadata: interaction
|
|
2939
2854
|
});
|
|
2940
|
-
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
|
+
}
|
|
2941
2867
|
}
|
|
2942
2868
|
|
|
2943
2869
|
//#endregion
|
|
@@ -3006,7 +2932,7 @@ var InteractionDispatcher = class {
|
|
|
3006
2932
|
}
|
|
3007
2933
|
/**
|
|
3008
2934
|
* Warns for each command route with no registered `@SlashRoute` handler, which would fall through to
|
|
3009
|
-
*
|
|
2935
|
+
* UnhandledRepliable at runtime. A bot may route commands outside the registry, so this does not throw.
|
|
3010
2936
|
*
|
|
3011
2937
|
* @internal
|
|
3012
2938
|
*/
|
|
@@ -3014,7 +2940,7 @@ var InteractionDispatcher = class {
|
|
|
3014
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.`);
|
|
3015
2941
|
}
|
|
3016
2942
|
/**
|
|
3017
|
-
* 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
|
|
3018
2944
|
* at runtime. Checked per kind since a user and a message command can share a name.
|
|
3019
2945
|
*
|
|
3020
2946
|
* @internal
|
|
@@ -3179,22 +3105,31 @@ var InteractionDispatcher = class {
|
|
|
3179
3105
|
if (!prefix) return this.logger.warn(`${interactionType} has invalid customId: ${interaction.customId}`);
|
|
3180
3106
|
await this.processInteraction(interaction, () => prefix, (key) => getMap().get(key));
|
|
3181
3107
|
}
|
|
3182
|
-
async processInteraction(interaction, extractKey, getHandler) {
|
|
3108
|
+
async processInteraction(interaction, extractKey, getHandler, fallback = UnhandledRepliable) {
|
|
3183
3109
|
const key = extractKey(interaction);
|
|
3110
|
+
let sender;
|
|
3184
3111
|
try {
|
|
3185
3112
|
if (!interaction.isAutocomplete()) for (const { ctor } of this.middlewares) await new ctor(interaction, this.core).execute();
|
|
3186
3113
|
let HandlerCtor = getHandler(key);
|
|
3187
3114
|
if (!HandlerCtor) {
|
|
3188
|
-
this.logger.warn(`No handler found for key ${paint.sky.bold(key)}. Falling back to
|
|
3189
|
-
HandlerCtor =
|
|
3115
|
+
this.logger.warn(`No handler found for key ${paint.sky.bold(key)}. Falling back to ${fallback.name}.`);
|
|
3116
|
+
HandlerCtor = fallback;
|
|
3190
3117
|
}
|
|
3191
3118
|
this.logger.debug(`Processing ${paint.mint.bold(key)} with ${paint.mute(HandlerCtor.name)}`);
|
|
3192
3119
|
const dispatch = new DispatchContext(routeIdOf(HandlerCtor));
|
|
3193
3120
|
const handler = new HandlerCtor(interaction, this.core, dispatch);
|
|
3194
|
-
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
|
+
}
|
|
3195
3130
|
await handler.execute();
|
|
3196
3131
|
} catch (caught) {
|
|
3197
|
-
await handleInteractionFault(caught, interaction, this.core);
|
|
3132
|
+
await handleInteractionFault(caught, interaction, this.core, sender);
|
|
3198
3133
|
}
|
|
3199
3134
|
}
|
|
3200
3135
|
async handleInteraction(interaction) {
|
|
@@ -3270,12 +3205,7 @@ var InteractionDispatcher = class {
|
|
|
3270
3205
|
}
|
|
3271
3206
|
async handleAutocomplete(interaction) {
|
|
3272
3207
|
const route = slashRouteOf(interaction);
|
|
3273
|
-
|
|
3274
|
-
this.logger.warn(`No autocomplete handler for ${paint.sky.bold(route)}.`);
|
|
3275
|
-
await interaction.respond([]);
|
|
3276
|
-
return;
|
|
3277
|
-
}
|
|
3278
|
-
await this.processInteraction(interaction, () => route, (key) => this.autocompleteMap.get(key));
|
|
3208
|
+
await this.processInteraction(interaction, () => route, (key) => this.autocompleteMap.get(key), UnhandledAutocomplete);
|
|
3279
3209
|
}
|
|
3280
3210
|
};
|
|
3281
3211
|
|
|
@@ -3331,7 +3261,7 @@ var Bot = class extends Plugin {
|
|
|
3331
3261
|
});
|
|
3332
3262
|
}
|
|
3333
3263
|
this.emojiInjector = new EmojiInjector(core);
|
|
3334
|
-
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);
|
|
3335
3265
|
}
|
|
3336
3266
|
/**
|
|
3337
3267
|
* Initializes Discord client and all controllers
|
|
@@ -3364,7 +3294,9 @@ var Bot = class extends Plugin {
|
|
|
3364
3294
|
* Logs the bot into Discord using the configured token
|
|
3365
3295
|
*/
|
|
3366
3296
|
async login(token) {
|
|
3367
|
-
|
|
3297
|
+
this._client.once(Events.ClientReady, () => this.emit("ready"));
|
|
3298
|
+
this._client.login(token);
|
|
3299
|
+
await this.waitFor("ready");
|
|
3368
3300
|
this.logger.info(`Logged in as ${chalk.bold.magenta(this._client.user?.username)}!`);
|
|
3369
3301
|
return this;
|
|
3370
3302
|
}
|
|
@@ -3390,157 +3322,70 @@ var Bot = class extends Plugin {
|
|
|
3390
3322
|
};
|
|
3391
3323
|
__decorate([Envapt("DISCORD_BOT_TOKEN", { converter: (raw) => validateDiscordToken(raw) }), __decorateMetadata("design:type", String)], Bot.prototype, "botToken", void 0);
|
|
3392
3324
|
|
|
3393
|
-
//#endregion
|
|
3394
|
-
//#region src/subscribers/bases/webhookHelpers.ts
|
|
3395
|
-
const DISCORD_WEBHOOK_REGEX = new RegExp(String.raw`^https?:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w$-]+$`);
|
|
3396
|
-
/**
|
|
3397
|
-
* Whether a string is a well-formed Discord webhook URL.
|
|
3398
|
-
*
|
|
3399
|
-
* @internal
|
|
3400
|
-
*/
|
|
3401
|
-
function isDiscordWebhookUrl(value) {
|
|
3402
|
-
return URL.canParse(value) && DISCORD_WEBHOOK_REGEX.test(value);
|
|
3403
|
-
}
|
|
3404
|
-
/**
|
|
3405
|
-
* Serializes a value to a JSON attachment, made circular-safe first.
|
|
3406
|
-
*
|
|
3407
|
-
* @internal
|
|
3408
|
-
*/
|
|
3409
|
-
function jsonAttachment(name, description, data) {
|
|
3410
|
-
const content = filterCirculars(data);
|
|
3411
|
-
return new AttachmentBuilder(Buffer.from(JSON.stringify(content, void 0, 2), "utf8"), {
|
|
3412
|
-
name,
|
|
3413
|
-
description
|
|
3414
|
-
});
|
|
3415
|
-
}
|
|
3416
|
-
/**
|
|
3417
|
-
* The small divider the default webhook reporters place between sections.
|
|
3418
|
-
*
|
|
3419
|
-
* @internal
|
|
3420
|
-
*/
|
|
3421
|
-
var WebhookSeparator = class extends BuilderComponent {
|
|
3422
|
-
constructor() {
|
|
3423
|
-
super("separator");
|
|
3424
|
-
this.instance.setSpacing(SeparatorSpacingSize.Small).setDivider(true);
|
|
3425
|
-
}
|
|
3426
|
-
};
|
|
3427
|
-
|
|
3428
3325
|
//#endregion
|
|
3429
3326
|
//#region src/subscribers/default/HandledException.ts
|
|
3430
|
-
var _HandledException;
|
|
3431
|
-
function webhookUrlValidator$1(raw) {
|
|
3432
|
-
if (raw !== null && typeof raw !== "string") throw new SeedcordError(SeedcordErrorCode.ConfigHandledExceptionWebhookInvalid);
|
|
3433
|
-
const value = raw?.trim() ?? "";
|
|
3434
|
-
if (value === "") throw new SeedcordError(SeedcordErrorCode.ConfigHandledExceptionWebhookMissing);
|
|
3435
|
-
if (!isDiscordWebhookUrl(value)) throw new SeedcordError(SeedcordErrorCode.ConfigHandledExceptionWebhookInvalid);
|
|
3436
|
-
return value;
|
|
3437
|
-
}
|
|
3438
3327
|
let HandledException = class HandledException extends WebhookLog {
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
try {
|
|
3446
|
-
await this.webhook.send({
|
|
3447
|
-
flags: flagsFor(false),
|
|
3448
|
-
withComponents: true,
|
|
3449
|
-
username: "Handled Exception",
|
|
3450
|
-
components: [new HandledExceptionContainer(this.data).component],
|
|
3451
|
-
files: [jsonAttachment("source.json", "The raw source the fault came from", source.raw)]
|
|
3452
|
-
});
|
|
3453
|
-
} catch (error) {
|
|
3454
|
-
this.logger.error("Failed to send handled exception webhook", error);
|
|
3455
|
-
}
|
|
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
|
+
};
|
|
3456
3334
|
}
|
|
3457
3335
|
};
|
|
3458
|
-
__decorate([
|
|
3459
|
-
HandledException = _HandledException = __decorate([Subscribe("handledException")], HandledException);
|
|
3336
|
+
HandledException = __decorate([Subscribe("handledException"), WebhookUrl("HANDLED_EXCEPTION_WEBHOOK_URL")], HandledException);
|
|
3460
3337
|
var HandledExceptionContainer = class extends BuilderComponent {
|
|
3461
3338
|
constructor(data) {
|
|
3462
3339
|
super("container");
|
|
3463
3340
|
const { denial, uuid, source } = data;
|
|
3464
|
-
this.instance.addTextDisplayComponents((text) => text.setContent(faultSummary(denial, source))).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### UUID \`${uuid}\`\n\`\`\`${causeStack(denial)}\`\`\``)).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent("### Source")).addFileComponents((file) => file.setURL("attachment://source.json"));
|
|
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"));
|
|
3465
3342
|
}
|
|
3466
3343
|
};
|
|
3467
|
-
/**
|
|
3468
|
-
* The summary block of a fault report, narrowed per source kind so an interaction fault shows its
|
|
3469
|
-
* command and customId while an event fault shows its event name and handler.
|
|
3470
|
-
*
|
|
3471
|
-
* @internal
|
|
3472
|
-
*/
|
|
3473
3344
|
function faultSummary(denial, source) {
|
|
3474
|
-
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`);
|
|
3475
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"}\``;
|
|
3476
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}\``;
|
|
3477
3348
|
}
|
|
3478
|
-
/**
|
|
3479
|
-
* The text shown for a denial's cause in the report. Tolerates a non-serializable cause (a bigint or a
|
|
3480
|
-
* circular object) so building the report never throws.
|
|
3481
|
-
*
|
|
3482
|
-
* @internal
|
|
3483
|
-
*/
|
|
3484
3349
|
function causeStack(denial) {
|
|
3485
3350
|
const { cause } = denial;
|
|
3486
|
-
if (Error.isError(cause)) return cause.stack ?? cause.message;
|
|
3487
|
-
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);
|
|
3488
3353
|
if (typeof cause === "bigint" || typeof cause === "symbol" || typeof cause === "function") return cause.toString();
|
|
3489
3354
|
if (cause !== void 0) try {
|
|
3490
3355
|
return JSON.stringify(cause);
|
|
3491
3356
|
} catch {
|
|
3492
3357
|
return "[unserializable cause]";
|
|
3493
3358
|
}
|
|
3494
|
-
return denial.stack ?? "No cause recorded.";
|
|
3359
|
+
return stripAnsi(denial.stack ?? "No cause recorded.");
|
|
3495
3360
|
}
|
|
3496
3361
|
|
|
3497
3362
|
//#endregion
|
|
3498
3363
|
//#region src/subscribers/default/UnknownException.ts
|
|
3499
|
-
var _UnknownException;
|
|
3500
|
-
function webhookUrlValidator(raw) {
|
|
3501
|
-
if (raw !== null && typeof raw !== "string") throw new SeedcordError(SeedcordErrorCode.ConfigUnknownExceptionWebhookInvalid);
|
|
3502
|
-
const value = raw?.trim() ?? "";
|
|
3503
|
-
if (value === "") throw new SeedcordError(SeedcordErrorCode.ConfigUnknownExceptionWebhookMissing);
|
|
3504
|
-
if (!isDiscordWebhookUrl(value)) throw new SeedcordError(SeedcordErrorCode.ConfigUnknownExceptionWebhookInvalid);
|
|
3505
|
-
return value;
|
|
3506
|
-
}
|
|
3507
3364
|
let UnknownException = class UnknownException extends WebhookLog {
|
|
3508
|
-
|
|
3509
|
-
_UnknownException = this;
|
|
3510
|
-
}
|
|
3511
|
-
webhook = new WebhookClient({ url: _UnknownException.unknownExceptionWebhookUrl });
|
|
3512
|
-
async execute() {
|
|
3365
|
+
report() {
|
|
3513
3366
|
const { metadata } = this.data;
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
username: "Unknown Exception",
|
|
3520
|
-
components: [new UnhandledErrorContainer(this.data).component],
|
|
3521
|
-
files: metadataFile ? [metadataFile] : []
|
|
3522
|
-
});
|
|
3523
|
-
} catch (error) {
|
|
3524
|
-
this.logger.error("Failed to send unknown exception webhook", error);
|
|
3525
|
-
}
|
|
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
|
+
};
|
|
3526
3372
|
}
|
|
3527
3373
|
};
|
|
3528
|
-
__decorate([
|
|
3529
|
-
UnknownException = _UnknownException = __decorate([Subscribe("unknownException")], UnknownException);
|
|
3374
|
+
UnknownException = __decorate([Subscribe("unknownException"), WebhookUrl("UNKNOWN_EXCEPTION_WEBHOOK_URL")], UnknownException);
|
|
3530
3375
|
var UnhandledErrorContainer = class extends BuilderComponent {
|
|
3531
3376
|
constructor(data) {
|
|
3532
3377
|
super("container");
|
|
3533
3378
|
const { uuid, error, guild, user, metadata } = data;
|
|
3534
|
-
this.instance.addTextDisplayComponents((text) => text.setContent(`### An unknown exception was thrown\n**Guild ID:** \`${guild?.id ?? "Not used in a guild"}\`\n**Guild Name:** ${guild?.name ?? "Not used in a guild"}\n**User ID:** \`${user?.id ?? "Missing user info"}\`\n**Username:** ${user?.username ?? "Missing user info"}\n`)).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### UUID \`${uuid}\`\n\`\`\`${error
|
|
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)}\`\`\``));
|
|
3535
3380
|
this.addTimestampsIfAvailable(error);
|
|
3536
3381
|
this.addMetadataIfAvailable(metadata);
|
|
3537
3382
|
}
|
|
3538
3383
|
addTimestampsIfAvailable(error) {
|
|
3539
|
-
if (!(error instanceof DiscordAPIError)) return;
|
|
3384
|
+
if (!(error instanceof DiscordAPIError$1)) return;
|
|
3540
3385
|
const now = Date.now();
|
|
3541
3386
|
const snowflake = error.url.match(/\/interactions\/(\d+)\//)?.[1];
|
|
3542
3387
|
if (!snowflake) return void 0;
|
|
3543
|
-
const interactionTs =
|
|
3388
|
+
const interactionTs = timestampFromSnowflake(snowflake);
|
|
3544
3389
|
const diff = now - interactionTs;
|
|
3545
3390
|
const seconds = Math.floor(diff / 1e3);
|
|
3546
3391
|
const millis = diff % 1e3;
|
|
@@ -3555,12 +3400,8 @@ var UnhandledErrorContainer = class extends BuilderComponent {
|
|
|
3555
3400
|
//#endregion
|
|
3556
3401
|
//#region src/subscribers/Bus.ts
|
|
3557
3402
|
/**
|
|
3558
|
-
*
|
|
3559
|
-
*
|
|
3560
|
-
* Provides a centralized system for registering and executing custom subscribers
|
|
3561
|
-
* throughout the application lifecycle. Bus subscribers are loaded from configured directories
|
|
3562
|
-
* and can be triggered programmatically or by framework events. Accessed via `core.bus`. Do not
|
|
3563
|
-
* construct it directly.
|
|
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.
|
|
3564
3405
|
*/
|
|
3565
3406
|
var Bus = class extends Plugin {
|
|
3566
3407
|
core;
|
|
@@ -3570,6 +3411,7 @@ var Bus = class extends Plugin {
|
|
|
3570
3411
|
isInitialized = false;
|
|
3571
3412
|
subscribersMap = /* @__PURE__ */ new Map();
|
|
3572
3413
|
executedOnceHandlers = /* @__PURE__ */ new Set();
|
|
3414
|
+
webhookProbes = /* @__PURE__ */ new Map();
|
|
3573
3415
|
hmrHandler;
|
|
3574
3416
|
constructor(core) {
|
|
3575
3417
|
super(core);
|
|
@@ -3595,7 +3437,6 @@ var Bus = class extends Plugin {
|
|
|
3595
3437
|
this.isInitialized = true;
|
|
3596
3438
|
this.registerSubscriber(UnknownException);
|
|
3597
3439
|
this.registerSubscriber(HandledException);
|
|
3598
|
-
Envapter.require("UNKNOWN_EXCEPTION_WEBHOOK_URL", "HANDLED_EXCEPTION_WEBHOOK_URL");
|
|
3599
3440
|
const subscribersDir = this.core.config.subscribers.path;
|
|
3600
3441
|
if (subscribersDir) {
|
|
3601
3442
|
this.logger.info(chalk.bold(subscribersDir));
|
|
@@ -3603,8 +3444,19 @@ var Bus = class extends Plugin {
|
|
|
3603
3444
|
const totalSubscribers = [...this.subscribersMap.values()].reduce((acc, handlers) => acc + handlers.length, 0);
|
|
3604
3445
|
this.logger.utils.list([`${chalk.bold.magenta(totalSubscribers)} subscribers`], chalk.bold.green("Loaded"));
|
|
3605
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(", ")]);
|
|
3606
3458
|
}
|
|
3607
|
-
/** @internal
|
|
3459
|
+
/** @internal */
|
|
3608
3460
|
async onHmr(event) {
|
|
3609
3461
|
if (this.hmrHandler) await this.hmrHandler.handle(event);
|
|
3610
3462
|
}
|
|
@@ -3621,6 +3473,17 @@ var Bus = class extends Plugin {
|
|
|
3621
3473
|
}, this.logger);
|
|
3622
3474
|
}
|
|
3623
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
|
+
}
|
|
3624
3487
|
const options = Reflect.getMetadata(SubscribeMetadataKey, handler);
|
|
3625
3488
|
let handlers = this.subscribersMap.get(options.subscriber);
|
|
3626
3489
|
if (!handlers) {
|
|
@@ -3680,7 +3543,7 @@ var Bus = class extends Plugin {
|
|
|
3680
3543
|
//#endregion
|
|
3681
3544
|
//#region src/version.ts
|
|
3682
3545
|
/** Package version */
|
|
3683
|
-
const version = "0.1.0-next.
|
|
3546
|
+
const version = "0.1.0-next.3";
|
|
3684
3547
|
|
|
3685
3548
|
//#endregion
|
|
3686
3549
|
//#region src/Seedcord.ts
|
|
@@ -3746,17 +3609,17 @@ var Seedcord = class Seedcord extends Pluggable {
|
|
|
3746
3609
|
}
|
|
3747
3610
|
registerStartupTasks() {
|
|
3748
3611
|
if (Envapter.isDevelopment || Envapter.isTest) this.registerHmrAwareModules();
|
|
3749
|
-
this.startup.addTask(StartupPhase.Configuration, "Bus Initialization", async () => {
|
|
3612
|
+
this.startup.addTask(StartupPhase$1.Configuration, "Bus Initialization", async () => {
|
|
3750
3613
|
this.bus.logger.utils.initialization("Subscribers", "start");
|
|
3751
3614
|
await this.bus.init();
|
|
3752
3615
|
this.bus.logger.utils.initialization("Subscribers", "end");
|
|
3753
3616
|
});
|
|
3754
|
-
this.startup.addTask(StartupPhase.Instantiation, "Bot Initialization", async () => {
|
|
3617
|
+
this.startup.addTask(StartupPhase$1.Instantiation, "Bot Initialization", async () => {
|
|
3755
3618
|
this.bot.logger.utils.initialization("Bot", "start");
|
|
3756
3619
|
await this.bot.init();
|
|
3757
3620
|
this.bot.logger.utils.initialization("Bot", "end");
|
|
3758
3621
|
});
|
|
3759
|
-
this.startup.addTask(StartupPhase.Ready, "Health Check", async () => {
|
|
3622
|
+
this.startup.addTask(StartupPhase$1.Ready, "Health Check", async () => {
|
|
3760
3623
|
this.healthCheck.logger.utils.initialization("HealthCheck", "start");
|
|
3761
3624
|
await this.healthCheck.init();
|
|
3762
3625
|
this.healthCheck.logger.utils.initialization("HealthCheck", "end");
|
|
@@ -3772,7 +3635,7 @@ var Seedcord = class Seedcord extends Pluggable {
|
|
|
3772
3635
|
return this;
|
|
3773
3636
|
}
|
|
3774
3637
|
registerHmrAwareModules() {
|
|
3775
|
-
this.startup.addTask(StartupPhase.Configuration, "HMR Registration", async () => {
|
|
3638
|
+
this.startup.addTask(StartupPhase$1.Configuration, "HMR Registration", async () => {
|
|
3776
3639
|
this.hmrManager.register(this.bot);
|
|
3777
3640
|
this.hmrManager.register(this.bus);
|
|
3778
3641
|
for (const plugin of this.plugins) this.hmrManager.register(plugin);
|
|
@@ -3782,5 +3645,5 @@ var Seedcord = class Seedcord extends Pluggable {
|
|
|
3782
3645
|
};
|
|
3783
3646
|
|
|
3784
3647
|
//#endregion
|
|
3785
|
-
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 };
|
|
3786
3649
|
//# sourceMappingURL=index.mjs.map
|