@seedcord/gateway 0.0.1 → 0.1.0-next.0
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/README.md +7 -3
- package/dist/index.cjs +4023 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +2528 -2
- package/dist/index.mjs +3914 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +94 -52
package/dist/index.cjs
CHANGED
|
@@ -1,9 +1,4030 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
12
|
+
key = keys[i];
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
14
|
+
__defProp(to, key, {
|
|
15
|
+
get: ((k) => from[k]).bind(null, key),
|
|
16
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
24
|
+
value: mod,
|
|
25
|
+
enumerable: true
|
|
26
|
+
}) : target, mod));
|
|
2
27
|
|
|
3
|
-
//#
|
|
28
|
+
//#endregion
|
|
29
|
+
require("reflect-metadata");
|
|
30
|
+
let _seedcord_core = require("@seedcord/core");
|
|
31
|
+
let _seedcord_core_internal = require("@seedcord/core/internal");
|
|
32
|
+
let discord_js = require("discord.js");
|
|
33
|
+
let _seedcord_errors = require("@seedcord/errors");
|
|
34
|
+
let _seedcord_errors_internal = require("@seedcord/errors/internal");
|
|
35
|
+
let _seedcord_utils = require("@seedcord/utils");
|
|
36
|
+
let _seedcord_services = require("@seedcord/services");
|
|
37
|
+
let chalk = require("chalk");
|
|
38
|
+
chalk = __toESM(chalk, 1);
|
|
39
|
+
let _discordjs_builders = require("@discordjs/builders");
|
|
40
|
+
let _seedcord_utils_internal = require("@seedcord/utils/internal");
|
|
41
|
+
let node_fs = require("node:fs");
|
|
42
|
+
let node_path = require("node:path");
|
|
43
|
+
let node_url = require("node:url");
|
|
44
|
+
let _seedcord_rate_limiter = require("@seedcord/rate-limiter");
|
|
45
|
+
let _seedcord_types_internal = require("@seedcord/types/internal");
|
|
46
|
+
let envapt = require("envapt");
|
|
47
|
+
let node_crypto = require("node:crypto");
|
|
48
|
+
node_crypto = __toESM(node_crypto, 1);
|
|
49
|
+
|
|
50
|
+
//#region src/bot/decorators/Events.ts
|
|
51
|
+
/**
|
|
52
|
+
* Registers an event handler class with one or more Discord.js events.
|
|
53
|
+
*
|
|
54
|
+
* Associates the decorated class with the provided Discord client events for automatic registration and execution when those events are emitted.
|
|
55
|
+
*
|
|
56
|
+
* Supply any number of event tuples. Each tuple can include per event options.
|
|
57
|
+
*
|
|
58
|
+
* @typeParam EventKey - Union of Discord.js ClientEvents keys to register for
|
|
59
|
+
* @param defs - One or more tuples of [event, options]
|
|
60
|
+
* @decorator
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* \@RegisterEvent([Events.MessageCreate])
|
|
64
|
+
* class MessageHandler extends EventHandler<Events.MessageCreate> {
|
|
65
|
+
* async execute() {
|
|
66
|
+
* // Handle message creation
|
|
67
|
+
* }
|
|
68
|
+
* }
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* \@RegisterEvent([Events.MessageCreate], [Events.MessageUpdate])
|
|
74
|
+
* class MessageHandler extends EventHandler<Events.MessageCreate | Events.MessageUpdate> {
|
|
75
|
+
* async execute() {
|
|
76
|
+
* // Handle message creation or update
|
|
77
|
+
* }
|
|
78
|
+
* }
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* \@RegisterEvent([Events.MessageCreate, { frequency: 'once' }], [Events.MessageUpdate])
|
|
84
|
+
* class OneTimeMessageHandler extends EventHandler<Events.MessageCreate | Events.MessageUpdate> {
|
|
85
|
+
* async execute() {
|
|
86
|
+
* // Handle only the first message creation, and message update normally
|
|
87
|
+
* }
|
|
88
|
+
* }
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
function RegisterEvent(...defs) {
|
|
92
|
+
return function(constructor) {
|
|
93
|
+
const existing = Reflect.getMetadata(_seedcord_core_internal.EventMetadataKey, constructor) ?? [];
|
|
94
|
+
const entries = defs.map(([event, options]) => ({
|
|
95
|
+
event,
|
|
96
|
+
frequency: options?.frequency ?? "on"
|
|
97
|
+
}));
|
|
98
|
+
Reflect.defineMetadata(_seedcord_core_internal.EventMetadataKey, [...existing, ...entries], constructor);
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/bot/decorators/Gated.ts
|
|
104
|
+
/**
|
|
105
|
+
* Attaches gates to a handler. The gates run before `execute`, and a gate refusing stops the handler with
|
|
106
|
+
* the reply or drop the gate threw. Multiple gates are ANDed, so each must pass. A gate that requires a
|
|
107
|
+
* context the handler does not provide (a button gate on a slash handler, an interaction gate on an event
|
|
108
|
+
* handler) is a compile error at this line, and the error names the gate and the handler kind. Place it
|
|
109
|
+
* above the route decorator.
|
|
110
|
+
*
|
|
111
|
+
* Combine gates with {@link and} or {@link or} to build a single gate from multiple arms.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* import { PermissionFlagsBits } from 'discord.js';
|
|
116
|
+
*
|
|
117
|
+
* \@Gated(GuildOnly(), RequirePermissions([PermissionFlagsBits.Administrator]))
|
|
118
|
+
* \@SlashRoute('maintenance')
|
|
119
|
+
* class Maintenance extends SlashHandler<'maintenance'> {
|
|
120
|
+
* async execute() {
|
|
121
|
+
* // both gates passed, so this runs in a guild with an admin caller
|
|
122
|
+
* }
|
|
123
|
+
* }
|
|
124
|
+
* ```
|
|
125
|
+
*
|
|
126
|
+
* @typeParam Gates - The non-empty tuple of gates to run, in order, against the handler's context.
|
|
127
|
+
* @typeParam TCtor - The decorated handler's constructor, checked against every gate's required context.
|
|
128
|
+
* @param gates - One or more gates to attach, each ANDed so the handler runs only when all pass.
|
|
129
|
+
* @param ctor - The handler class being decorated, rejected at compile time when a gate needs a context it lacks.
|
|
130
|
+
*
|
|
131
|
+
* @see {@link defineGate}
|
|
132
|
+
* @see {@link defineEffectGate}
|
|
133
|
+
* @decorator
|
|
134
|
+
*/
|
|
135
|
+
function Gated(...gates) {
|
|
136
|
+
return function(ctor) {
|
|
137
|
+
const existing = Reflect.getMetadata(_seedcord_core_internal.GatedMetadataKey, ctor) ?? [];
|
|
138
|
+
Reflect.defineMetadata(_seedcord_core_internal.GatedMetadataKey, [...existing, ...gates], ctor);
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/miscellaneous/areRoutes.ts
|
|
144
|
+
/**
|
|
145
|
+
* Checks if the provided value is an array of strings.
|
|
146
|
+
*
|
|
147
|
+
* @internal
|
|
148
|
+
*/
|
|
149
|
+
function areRoutes(routes) {
|
|
150
|
+
return Array.isArray(routes) && routes.every((r) => typeof r === "string");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region src/bot/decorators/Interactions.ts
|
|
155
|
+
/**
|
|
156
|
+
* Types of select menus supported for routing
|
|
157
|
+
*/
|
|
158
|
+
let SelectMenuKind = /* @__PURE__ */ function(SelectMenuKind) {
|
|
159
|
+
SelectMenuKind["String"] = "string";
|
|
160
|
+
SelectMenuKind["User"] = "user";
|
|
161
|
+
SelectMenuKind["Role"] = "role";
|
|
162
|
+
SelectMenuKind["Channel"] = "channel";
|
|
163
|
+
SelectMenuKind["Mentionable"] = "mentionable";
|
|
164
|
+
return SelectMenuKind;
|
|
165
|
+
}({});
|
|
166
|
+
/**
|
|
167
|
+
* Routes one or more slash commands to a {@link SlashHandler}.
|
|
168
|
+
*
|
|
169
|
+
* Pass the same route string(s) as the handler's generic. A single command reads `this.options`, several
|
|
170
|
+
* commands branch with `this.match`. The decorator routes and the generic must match exactly, so listing
|
|
171
|
+
* fewer or more than the handler declares is a compile error. Subcommands use a slash path.
|
|
172
|
+
*
|
|
173
|
+
* @param routes - The route string(s) this handler serves, e.g. `'ban'`, `'ban', 'kick'`, or `'demo/setup'`.
|
|
174
|
+
* @decorator
|
|
175
|
+
* @example
|
|
176
|
+
* ```ts
|
|
177
|
+
* \@SlashRoute('ban')
|
|
178
|
+
* class BanHandler extends SlashHandler<'ban'> {
|
|
179
|
+
* async execute() {
|
|
180
|
+
* const target = this.options.getUser('target');
|
|
181
|
+
* }
|
|
182
|
+
* }
|
|
183
|
+
* ```
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```ts
|
|
187
|
+
* \@SlashRoute('ban', 'kick')
|
|
188
|
+
* class ModerationHandler extends SlashHandler<'ban' | 'kick'> {
|
|
189
|
+
* async execute() {
|
|
190
|
+
* await this.match({ ban: (o) => o.getUser('target'), kick: (o) => o.getUser('member') });
|
|
191
|
+
* }
|
|
192
|
+
* }
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
function SlashRoute(...routes) {
|
|
196
|
+
return function(constructor) {
|
|
197
|
+
storeMetadata(_seedcord_core_internal.InteractionRoutes.Slash, routes, constructor);
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Routes button interactions to handler classes.
|
|
202
|
+
*
|
|
203
|
+
* Pass the {@link CustomId} definition(s) this handler decodes and list the same ones in the handler's
|
|
204
|
+
* generic. Passing different definitions to the decorator and the generic is a compile error. Routing
|
|
205
|
+
* matches the stable prefix, so a wire minted from an older shape still reaches the handler, where
|
|
206
|
+
* reading this.params throws StaleCustomId and the controller boundary turns it into a reply.
|
|
207
|
+
*
|
|
208
|
+
* @param defs - The customId definition(s) this handler decodes, one per route.
|
|
209
|
+
* @decorator
|
|
210
|
+
*/
|
|
211
|
+
function ButtonRoute(...defs) {
|
|
212
|
+
return function(constructor) {
|
|
213
|
+
storeComponentRoute(_seedcord_core_internal.InteractionRoutes.Button, defs, constructor);
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Routes modal submissions to handler classes.
|
|
218
|
+
*
|
|
219
|
+
* Pass the {@link CustomId} definition(s) this handler decodes and list the same ones in the handler's
|
|
220
|
+
* generic. Passing different definitions to the decorator and the generic is a compile error. Routing
|
|
221
|
+
* matches the stable prefix, so an older-shape wire still reaches the handler and throws StaleCustomId
|
|
222
|
+
* on read, the same as a button route.
|
|
223
|
+
*
|
|
224
|
+
* @param defs - The customId definition(s) this handler decodes, one per route.
|
|
225
|
+
* @decorator
|
|
226
|
+
*/
|
|
227
|
+
function ModalRoute(...defs) {
|
|
228
|
+
return function(constructor) {
|
|
229
|
+
storeComponentRoute(_seedcord_core_internal.InteractionRoutes.Modal, defs, constructor);
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Routes one or more context-menu commands to a {@link ContextMenuHandler}.
|
|
234
|
+
*
|
|
235
|
+
* Pass the kind (`ApplicationCommandType.User` or `ApplicationCommandType.Message`) and the command name(s),
|
|
236
|
+
* each checked against that kind's registry, so a typo is a compile error. The same kind must sit on the
|
|
237
|
+
* handler's generic, cross-checked both directions. Context menus carry no options, so a multi-name handler
|
|
238
|
+
* reads `this.target` uniformly with no branch.
|
|
239
|
+
*
|
|
240
|
+
* @param kind - The context-menu kind, `ApplicationCommandType.User` or `ApplicationCommandType.Message`.
|
|
241
|
+
* @param names - The command name(s) this handler serves, keyed off the matching registry.
|
|
242
|
+
* @decorator
|
|
243
|
+
* @example
|
|
244
|
+
* ```ts
|
|
245
|
+
* \@ContextMenuRoute(ApplicationCommandType.User, 'View Profile')
|
|
246
|
+
* class ViewProfile extends ContextMenuHandler<ApplicationCommandType.User> {
|
|
247
|
+
* async execute() {
|
|
248
|
+
* const user = this.target;
|
|
249
|
+
* }
|
|
250
|
+
* }
|
|
251
|
+
* ```
|
|
252
|
+
*/
|
|
253
|
+
function ContextMenuRoute(kind, ...names) {
|
|
254
|
+
return function(constructor) {
|
|
255
|
+
storeMetadata(kind === discord_js.ApplicationCommandType.User ? _seedcord_core_internal.InteractionRoutes.UserContextMenu : _seedcord_core_internal.InteractionRoutes.MessageContextMenu, names, constructor);
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Routes one or more commands' autocomplete to an {@link AutocompleteHandler}.
|
|
260
|
+
*
|
|
261
|
+
* Pass the same command route string(s) as the handler's generic. One command branches with `this.match`
|
|
262
|
+
* over its autocompletable fields, several commands share one handler whose arms span every command's
|
|
263
|
+
* fields. The decorator routes and the generic must match exactly, so listing fewer or more than the
|
|
264
|
+
* handler declares is a compile error.
|
|
265
|
+
*
|
|
266
|
+
* @param routes - The command route string(s) this handler serves, e.g. `'search'` or `'search', 'find'`.
|
|
267
|
+
* @decorator
|
|
268
|
+
* @example
|
|
269
|
+
* ```ts
|
|
270
|
+
* \@AutocompleteRoute('search')
|
|
271
|
+
* class SearchAutocomplete extends AutocompleteHandler<'search'> {
|
|
272
|
+
* async execute() {
|
|
273
|
+
* await this.match({ query: (value, respond) => respond([{ name: value, value }]) });
|
|
274
|
+
* }
|
|
275
|
+
* }
|
|
276
|
+
* ```
|
|
277
|
+
*/
|
|
278
|
+
function AutocompleteRoute(...routes) {
|
|
279
|
+
return function(constructor) {
|
|
280
|
+
storeMetadata(_seedcord_core_internal.InteractionRoutes.Autocomplete, routes, constructor);
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Routes select menu interactions to handler classes.
|
|
285
|
+
*
|
|
286
|
+
* Pass the select kind and the {@link CustomId} definition(s) this handler decodes. The handler's
|
|
287
|
+
* generic must list the same definitions, and its second generic argument must be the matching select
|
|
288
|
+
* interaction type, or it is a compile error.
|
|
289
|
+
*
|
|
290
|
+
* @param type - Select menu kind from {@link SelectMenuKind}.
|
|
291
|
+
* @param defs - The customId definition(s) this handler decodes, one per route.
|
|
292
|
+
* @decorator
|
|
293
|
+
*
|
|
294
|
+
* @example
|
|
295
|
+
* ```typescript
|
|
296
|
+
* \@SelectMenuRoute(SelectMenuKind.User, AssignId)
|
|
297
|
+
* class AssignSelect extends SelectMenuHandler<SelectMenuKind.User, [typeof AssignId]> {
|
|
298
|
+
* // handles user select menus minted from AssignId
|
|
299
|
+
* }
|
|
300
|
+
* ```
|
|
301
|
+
*/
|
|
302
|
+
function SelectMenuRoute(type, ...defs) {
|
|
303
|
+
return function(constructor) {
|
|
304
|
+
storeComponentRoute({
|
|
305
|
+
["string"]: _seedcord_core_internal.InteractionRoutes.StringMenu,
|
|
306
|
+
["user"]: _seedcord_core_internal.InteractionRoutes.UserMenu,
|
|
307
|
+
["role"]: _seedcord_core_internal.InteractionRoutes.RoleMenu,
|
|
308
|
+
["channel"]: _seedcord_core_internal.InteractionRoutes.ChannelMenu,
|
|
309
|
+
["mentionable"]: _seedcord_core_internal.InteractionRoutes.MentionableMenu
|
|
310
|
+
}[type], defs, constructor);
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function storeComponentRoute(route, defs, constructor) {
|
|
314
|
+
storeMetadata(route, defs.map((def) => def.prefix), constructor);
|
|
315
|
+
Reflect.defineMetadata(_seedcord_core_internal.ComponentDefsKey, defs, constructor);
|
|
316
|
+
}
|
|
317
|
+
function storeMetadata(route, routes, constructor) {
|
|
318
|
+
const key = _seedcord_core_internal.InteractionRouteKeys[route];
|
|
319
|
+
const savedRoutes = Reflect.getMetadata(key, constructor);
|
|
320
|
+
const existing = areRoutes(savedRoutes) ? savedRoutes : [];
|
|
321
|
+
const toStore = Array.isArray(routes) ? routes : [routes];
|
|
322
|
+
Reflect.defineMetadata(key, [...existing, ...toStore], constructor);
|
|
323
|
+
Reflect.defineMetadata(_seedcord_core_internal.InteractionMetadataKey, true, constructor);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
//#endregion
|
|
327
|
+
//#region src/bot/decorators/Middlewares.ts
|
|
328
|
+
/**
|
|
329
|
+
* Middleware types supported by Seedcord
|
|
330
|
+
*/
|
|
331
|
+
let MiddlewareType = /* @__PURE__ */ function(MiddlewareType) {
|
|
332
|
+
MiddlewareType["Interaction"] = "interaction";
|
|
333
|
+
MiddlewareType["Event"] = "event";
|
|
334
|
+
return MiddlewareType;
|
|
335
|
+
}({});
|
|
336
|
+
/**
|
|
337
|
+
* Decorator used to register middleware with priority ordering. The lower the priority number, the earlier it runs.
|
|
338
|
+
*
|
|
339
|
+
* Interaction middleware cannot specify event filters.
|
|
340
|
+
*
|
|
341
|
+
* @param type - Middleware kind from {@link MiddlewareType}
|
|
342
|
+
* @param priority - Ordering value where lower runs earlier. {@default `0`}
|
|
343
|
+
* @param options - Additional registration options
|
|
344
|
+
*
|
|
345
|
+
* @decorator
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* ```ts
|
|
349
|
+
* \@Middleware(MiddlewareType.Event, 10, { events: [Events.MessageCreate, Events.MessageUpdate] })
|
|
350
|
+
* class MyEventMiddleware extends EventMiddleware {}
|
|
351
|
+
* ```
|
|
352
|
+
* @throws A **SeedcordTypeError** If priority is not a finite number
|
|
353
|
+
* @throws A **SeedcordError** If interaction middleware specifies event filters
|
|
354
|
+
*/
|
|
355
|
+
function Middleware(type, priority = 0, options = {}) {
|
|
356
|
+
return (ctor) => {
|
|
357
|
+
const normalizedPriority = Number(priority);
|
|
358
|
+
if (!Number.isFinite(normalizedPriority)) throw new _seedcord_errors_internal.SeedcordTypeError(_seedcord_errors.SeedcordErrorCode.DecoratorInvalidMiddlewarePriority);
|
|
359
|
+
if (type === "interaction" && Array.isArray(options.events) && options.events.length > 0) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.DecoratorInteractionEventFilter);
|
|
360
|
+
const metadata = {
|
|
361
|
+
priority: normalizedPriority,
|
|
362
|
+
type,
|
|
363
|
+
...options.events && options.events.length > 0 && { events: options.events }
|
|
364
|
+
};
|
|
365
|
+
Reflect.defineMetadata(_seedcord_core_internal.MiddlewareMetadataKey, metadata, ctor);
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
//#endregion
|
|
370
|
+
//#region src/bot/gates/catalog/IgnoreBots.ts
|
|
371
|
+
/**
|
|
372
|
+
* Drops a client event whose actor is a bot, with a {@link Silence} so nothing is replied. Event-only, because a
|
|
373
|
+
* Silence on an interaction would leave Discord's failed-interaction state. It rejects an interaction handler at
|
|
374
|
+
* the decorator line. Takes no options, so attach it directly without calling it.
|
|
375
|
+
*
|
|
376
|
+
* @see {@link Gated}
|
|
377
|
+
*
|
|
378
|
+
* @example
|
|
379
|
+
* ```ts
|
|
380
|
+
* import { Events } from 'discord.js';
|
|
381
|
+
*
|
|
382
|
+
* \@Gated(IgnoreBots)
|
|
383
|
+
* class OnMessage extends EventHandler<Events.MessageCreate> {
|
|
384
|
+
* async execute() {
|
|
385
|
+
* // ...
|
|
386
|
+
* }
|
|
387
|
+
* }
|
|
388
|
+
* ```
|
|
389
|
+
*/
|
|
390
|
+
const IgnoreBots = (0, _seedcord_core.defineGate)("IgnoreBots", (ctx) => {
|
|
391
|
+
if (ctx.user?.bot) throw new _seedcord_core.Silence("actor is a bot");
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
//#endregion
|
|
395
|
+
//#region src/bot/notices/utils.ts
|
|
396
|
+
function mentionFor(subject) {
|
|
397
|
+
if (subject instanceof discord_js.Role) return `<@&${subject.id}>`;
|
|
398
|
+
if (subject instanceof discord_js.TextChannel) return `<#${subject.id}>`;
|
|
399
|
+
if (subject instanceof discord_js.GuildMember) return `<@${subject.id}>`;
|
|
400
|
+
return `\`${subject.name}\``;
|
|
401
|
+
}
|
|
402
|
+
function labelFor(subject) {
|
|
403
|
+
if (subject instanceof discord_js.Role) return "role";
|
|
404
|
+
if (subject instanceof discord_js.TextChannel) return "channel";
|
|
405
|
+
if (subject instanceof discord_js.GuildMember) return "member";
|
|
406
|
+
return "guild";
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
//#endregion
|
|
410
|
+
//#region src/bot/notices/index.ts
|
|
411
|
+
var NotNsfw = class extends _seedcord_core_internal.GateNotice {
|
|
412
|
+
constructor(message = "This can only be used in an age-restricted channel.") {
|
|
413
|
+
super(message);
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
var MissingRole = class extends _seedcord_core_internal.GateNotice {
|
|
417
|
+
role;
|
|
418
|
+
constructor(message, role) {
|
|
419
|
+
super(message ?? (role ? `You need the ${role.name} role to use this.` : "You do not have the required role."));
|
|
420
|
+
this.role = role;
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
var RoleHigherThanMe = class extends _seedcord_core.Notice {
|
|
424
|
+
role;
|
|
425
|
+
botRole;
|
|
426
|
+
constructor(message, role, botRole) {
|
|
427
|
+
super(message);
|
|
428
|
+
this.role = role;
|
|
429
|
+
this.botRole = botRole;
|
|
430
|
+
}
|
|
431
|
+
render() {
|
|
432
|
+
return { components: [new _seedcord_core_internal.NoticeCard(`I cannot assign a role that is higher than me.\n\nThe role <@&${this.role.id}> is higher than my role <@&${this.botRole.id}> in the hierarchy.`).component] };
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
var CannotAssignBotRole = class extends _seedcord_core.Notice {
|
|
436
|
+
constructor(message = "I cannot assign a managed role.") {
|
|
437
|
+
super(message);
|
|
438
|
+
}
|
|
439
|
+
render() {
|
|
440
|
+
return { components: [new _seedcord_core_internal.NoticeCard("I cannot assign a managed role.").component] };
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
var MissingPermissions = class extends _seedcord_core.Notice {
|
|
444
|
+
where;
|
|
445
|
+
missingPerms;
|
|
446
|
+
constructor(message, where, missingPerms) {
|
|
447
|
+
super(message);
|
|
448
|
+
this.where = where;
|
|
449
|
+
this.missingPerms = missingPerms;
|
|
450
|
+
}
|
|
451
|
+
render() {
|
|
452
|
+
const bullets = this.missingPerms.map((perm) => `• ${perm}`).join("\n");
|
|
453
|
+
return { components: [new _seedcord_core_internal.NoticeCard(`The ${labelFor(this.where)} ${mentionFor(this.where)} is missing the following permission entries:\n\n${bullets}`).component] };
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
var HasDangerousPermissions = class extends _seedcord_core.Notice {
|
|
457
|
+
target;
|
|
458
|
+
dangerousPerms;
|
|
459
|
+
constructor(message, target, dangerousPerms) {
|
|
460
|
+
super(message);
|
|
461
|
+
this.target = target;
|
|
462
|
+
this.dangerousPerms = dangerousPerms;
|
|
463
|
+
}
|
|
464
|
+
render() {
|
|
465
|
+
const bullets = this.dangerousPerms.map((perm) => `• ${perm}`).join("\n");
|
|
466
|
+
return { components: [new _seedcord_core_internal.NoticeCard(`The ${labelFor(this.target)} ${mentionFor(this.target)} has the following permission entries that must not be enabled:\n\n${bullets}`).component] };
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
var UserNotFound = class extends _seedcord_core.Notice {
|
|
470
|
+
userArg;
|
|
471
|
+
constructor(userArg) {
|
|
472
|
+
super(`User not found: ${userArg}`);
|
|
473
|
+
this.userArg = userArg;
|
|
474
|
+
}
|
|
475
|
+
render() {
|
|
476
|
+
return { components: [new _seedcord_core_internal.NoticeCard(`User probably doesn't exist or was deleted.\n**User Argument:** \`${this.userArg}\`\nPlease check the user ID and try again. Only pass valid user IDs as the argument.`, "User Not Found").component] };
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
var UserNotInGuild = class extends _seedcord_core.Notice {
|
|
480
|
+
constructor(message = "User is not in the guild.") {
|
|
481
|
+
super(message);
|
|
482
|
+
}
|
|
483
|
+
render() {
|
|
484
|
+
return { components: [new _seedcord_core_internal.NoticeCard(this.message).component] };
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
var RoleDoesNotExist = class extends _seedcord_core.Notice {
|
|
488
|
+
roleId;
|
|
489
|
+
constructor(message, roleId) {
|
|
490
|
+
super(message);
|
|
491
|
+
this.roleId = roleId;
|
|
492
|
+
}
|
|
493
|
+
render() {
|
|
494
|
+
return { components: [new _seedcord_core_internal.NoticeCard(`The role with ID \`${this.roleId}\` does not exist.`).component] };
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
var CouldNotFindChannel = class extends _seedcord_core.Notice {
|
|
498
|
+
channelId;
|
|
499
|
+
constructor(message, channelId) {
|
|
500
|
+
super(message);
|
|
501
|
+
this.channelId = channelId;
|
|
502
|
+
}
|
|
503
|
+
render() {
|
|
504
|
+
return { components: [new _seedcord_core_internal.NoticeCard(`Could not find channel with ID \`${this.channelId}\`. It could also be that the channel is not a text channel.`).component] };
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
//#endregion
|
|
509
|
+
//#region src/bot/gates/catalog/Nsfw.ts
|
|
510
|
+
function channelIsNsfw(channel) {
|
|
511
|
+
if (!channel) return false;
|
|
512
|
+
if (channel.isThread()) return channel.parent?.nsfw ?? false;
|
|
513
|
+
return "nsfw" in channel ? channel.nsfw : false;
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Requires an age-restricted channel, else refuses. Pass {@link GateNoticeOptions} to reword or replace the refusal.
|
|
517
|
+
*
|
|
518
|
+
* A thread inherits its parent channel's nsfw flag. ModalSubmit has no reliable channel, so it is excluded.
|
|
519
|
+
*
|
|
520
|
+
* @param options - Reword the default refusal with `message`, or replace it with `notice`.
|
|
521
|
+
*
|
|
522
|
+
* @see {@link Gated}
|
|
523
|
+
*
|
|
524
|
+
* @example
|
|
525
|
+
* ```ts
|
|
526
|
+
* \@Gated(Nsfw())
|
|
527
|
+
* \@SlashRoute('nsfwcmd')
|
|
528
|
+
* class NsfwHandler extends SlashHandler<'nsfwcmd'> {
|
|
529
|
+
* async execute() {
|
|
530
|
+
* // ...
|
|
531
|
+
* }
|
|
532
|
+
* }
|
|
533
|
+
* ```
|
|
534
|
+
*/
|
|
535
|
+
function Nsfw(options) {
|
|
536
|
+
return (0, _seedcord_core.defineGate)("Nsfw", (ctx) => {
|
|
537
|
+
if (channelIsNsfw(ctx.interaction.channel)) return;
|
|
538
|
+
throw (0, _seedcord_core_internal.pickNotice)(options, (message) => new NotNsfw(message));
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
//#endregion
|
|
543
|
+
//#region src/bot/utilities/permissions/checkPermissions.ts
|
|
544
|
+
/**
|
|
545
|
+
* Map of permission bits to their prettified human readable names.
|
|
546
|
+
*/
|
|
547
|
+
const PermissionNames = new Map(Object.entries(discord_js.PermissionFlagsBits).map(([key, bit]) => [bit, (0, _seedcord_utils.prettify)(key)]));
|
|
548
|
+
function checkPermissions(a, b, c, d, e) {
|
|
549
|
+
const { for: pFor, in: pIn, scope, inverse = false, missing: missingCtor, dangerous: dangerousCtor } = a instanceof discord_js.Role || a instanceof discord_js.GuildMember ? {
|
|
550
|
+
for: a,
|
|
551
|
+
in: b,
|
|
552
|
+
scope: c,
|
|
553
|
+
inverse: d ?? false,
|
|
554
|
+
...e
|
|
555
|
+
} : a;
|
|
556
|
+
const Missing = missingCtor ?? MissingPermissions;
|
|
557
|
+
const Dangerous = dangerousCtor ?? HasDangerousPermissions;
|
|
558
|
+
const perms = pIn instanceof discord_js.Guild ? pFor.permissions : pIn.permissionsFor(pFor, true);
|
|
559
|
+
const names = (bits) => bits.map((bit) => PermissionNames.get(bit) ?? String(bit));
|
|
560
|
+
const present = scope.filter((bit) => perms.has(bit, true));
|
|
561
|
+
const presentNames = names(present);
|
|
562
|
+
if (inverse) {
|
|
563
|
+
if (present.length > 0) throw new Dangerous(`${pFor instanceof discord_js.Role ? "Role" : "Member"} has dangerous permissions in this ${pIn instanceof discord_js.Guild ? "guild" : "channel"}`, pFor, presentNames);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const missingBits = scope.filter((bit) => !perms.has(bit, true));
|
|
567
|
+
if (missingBits.length > 0) throw new Missing("Missing Any/All/No Permissions", pFor, names(missingBits));
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
//#endregion
|
|
571
|
+
//#region src/bot/gates/catalog/permissions.ts
|
|
572
|
+
const UNCACHED_MEMBER = "Your server member data could not be resolved. Try again.";
|
|
573
|
+
/**
|
|
574
|
+
* Requires the caller to hold every permission in `scope`, via `checkPermissions`. Refuses outside a guild.
|
|
575
|
+
*
|
|
576
|
+
* Interaction-only and excludes ModalSubmit, since a modal lacks a reliable cached caller member.
|
|
577
|
+
*
|
|
578
|
+
* @param scope - The permission flag bits the caller must all hold.
|
|
579
|
+
* @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-permissions one with `missing`.
|
|
580
|
+
*
|
|
581
|
+
* @see {@link Gated}
|
|
582
|
+
*
|
|
583
|
+
* @example
|
|
584
|
+
* ```ts
|
|
585
|
+
* import { PermissionFlagsBits } from 'discord.js';
|
|
586
|
+
*
|
|
587
|
+
* \@Gated(RequirePermissions([PermissionFlagsBits.BanMembers]))
|
|
588
|
+
* \@SlashRoute('ban')
|
|
589
|
+
* class BanHandler extends SlashHandler<'ban'> {
|
|
590
|
+
* async execute() {
|
|
591
|
+
* const target = this.options.getUser('target');
|
|
592
|
+
* }
|
|
593
|
+
* }
|
|
594
|
+
* ```
|
|
595
|
+
*/
|
|
596
|
+
function RequirePermissions(scope, options) {
|
|
597
|
+
return (0, _seedcord_core.defineGate)("RequirePermissions", (ctx) => {
|
|
598
|
+
if (!ctx.guild) throw (0, _seedcord_core_internal.pickNotice)(options?.notInGuild, (message) => new _seedcord_core_internal.NotInGuild(message));
|
|
599
|
+
if (!ctx.member) throw (0, _seedcord_core_internal.pickNotice)(options?.notInGuild, (message) => new _seedcord_core_internal.NotInGuild(message ?? UNCACHED_MEMBER));
|
|
600
|
+
checkPermissions(ctx.member, ctx.guild, scope, false, options?.missing ? { missing: options.missing } : void 0);
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Requires the bot to hold every permission in `scope`, via `checkPermissions`. Refuses outside a guild.
|
|
605
|
+
*
|
|
606
|
+
* It reads the bot's own member, so it attaches to modal handlers as well.
|
|
607
|
+
*
|
|
608
|
+
* @param scope - The permission flag bits the bot must all hold.
|
|
609
|
+
* @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-permissions one with `missing`.
|
|
610
|
+
*
|
|
611
|
+
* @see {@link Gated}
|
|
612
|
+
*
|
|
613
|
+
* @example
|
|
614
|
+
* ```ts
|
|
615
|
+
* import { PermissionFlagsBits } from 'discord.js';
|
|
616
|
+
*
|
|
617
|
+
* \@Gated(RequireBotPermissions([PermissionFlagsBits.BanMembers]))
|
|
618
|
+
* \@ModalRoute(MyModalId)
|
|
619
|
+
* class MyModalHandler extends ModalHandler<[typeof MyModalId]> {
|
|
620
|
+
* async execute() {
|
|
621
|
+
* // ...
|
|
622
|
+
* }
|
|
623
|
+
* }
|
|
624
|
+
* ```
|
|
625
|
+
*/
|
|
626
|
+
function RequireBotPermissions(scope, options) {
|
|
627
|
+
return (0, _seedcord_core.defineGate)("RequireBotPermissions", (ctx) => {
|
|
628
|
+
const botMember = ctx.guild?.members.me;
|
|
629
|
+
if (!ctx.guild || !botMember) throw (0, _seedcord_core_internal.pickNotice)(options?.notInGuild, (message) => new _seedcord_core_internal.NotInGuild(message));
|
|
630
|
+
checkPermissions(botMember, ctx.guild, scope, false, options?.missing ? { missing: options.missing } : void 0);
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Requires the caller to have `roleId`, read from the member's role cache. Refuses outside a guild first, then
|
|
635
|
+
* when the role is missing.
|
|
636
|
+
*
|
|
637
|
+
* Interaction-only and excludes ModalSubmit.
|
|
638
|
+
*
|
|
639
|
+
* @param roleId - The role snowflake the caller must hold, read from the member's role cache.
|
|
640
|
+
* @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-role one with `missingRole`.
|
|
641
|
+
*
|
|
642
|
+
* @see {@link Gated}
|
|
643
|
+
*
|
|
644
|
+
* @example
|
|
645
|
+
* ```ts
|
|
646
|
+
* \@Gated(RequireRole('123456789012345678'))
|
|
647
|
+
* \@SlashRoute('vip')
|
|
648
|
+
* class VipHandler extends SlashHandler<'vip'> {
|
|
649
|
+
* async execute() {
|
|
650
|
+
* // ...
|
|
651
|
+
* }
|
|
652
|
+
* }
|
|
653
|
+
* ```
|
|
654
|
+
*/
|
|
655
|
+
function RequireRole(roleId, options) {
|
|
656
|
+
return (0, _seedcord_core.defineGate)("RequireRole", (ctx) => {
|
|
657
|
+
if (!ctx.guild) throw (0, _seedcord_core_internal.pickNotice)(options?.notInGuild, (message) => new _seedcord_core_internal.NotInGuild(message));
|
|
658
|
+
if (!ctx.member) throw (0, _seedcord_core_internal.pickNotice)(options?.notInGuild, (message) => new _seedcord_core_internal.NotInGuild(message ?? UNCACHED_MEMBER));
|
|
659
|
+
if (ctx.member.roles.cache.has(roleId)) return;
|
|
660
|
+
const role = ctx.guild.roles.cache.get(roleId) ?? null;
|
|
661
|
+
throw (0, _seedcord_core_internal.pickNotice)(options?.missingRole, (message) => new MissingRole(message, role));
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
//#endregion
|
|
666
|
+
//#region src/bot/injectors/EmojiInjector.ts
|
|
667
|
+
const emojiStorage = {};
|
|
668
|
+
function isEmojiTuple(v) {
|
|
669
|
+
return Array.isArray(v) && v.length === 2 && typeof v[0] === "string" && typeof v[1] === "string";
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Global emoji mappings object
|
|
673
|
+
*
|
|
674
|
+
* @see {@link EmojiMap}
|
|
675
|
+
*/
|
|
676
|
+
const Emojis = emojiStorage;
|
|
677
|
+
/**
|
|
678
|
+
* Loads and injects emojis based on bot configuration.
|
|
679
|
+
*
|
|
680
|
+
* For emojis injected from specific guilds, ensure that the Guilds intent is provided in client options.
|
|
681
|
+
*
|
|
682
|
+
* @internal
|
|
683
|
+
*/
|
|
684
|
+
var EmojiInjector = class {
|
|
685
|
+
core;
|
|
686
|
+
logger = new _seedcord_services.Logger("Emojis");
|
|
687
|
+
constructor(core) {
|
|
688
|
+
this.core = core;
|
|
689
|
+
}
|
|
690
|
+
async init() {
|
|
691
|
+
this.clearEmojis();
|
|
692
|
+
if (!this.core.config.bot.emojis || Object.keys(this.core.config.bot.emojis).length === 0) {
|
|
693
|
+
this.logger.debug(chalk.default.bold.yellow("No emojis configured, skipping emoji injection."));
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const configEmojis = this.core.config.bot.emojis;
|
|
697
|
+
await this.core.bot.client.application?.emojis.fetch();
|
|
698
|
+
const failures = [];
|
|
699
|
+
const fetchedGuilds = /* @__PURE__ */ new Set();
|
|
700
|
+
for (const [key, value] of Object.entries(configEmojis)) if (isEmojiTuple(value)) await this.resolveTuple(key, value, failures, fetchedGuilds);
|
|
701
|
+
else if (typeof value === "string") this.resolveString(key, value, failures);
|
|
702
|
+
else failures.push(` - "${key}" has an invalid value (expected a name or [name, guildId])`);
|
|
703
|
+
if (failures.length > 0) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigEmojiUnresolved, [failures.length, failures.join("\n")]);
|
|
704
|
+
this.logger.utils.summary("Loaded emojis", { emojis: Object.keys(emojiStorage).length });
|
|
705
|
+
}
|
|
706
|
+
async resolveTuple(key, value, failures, fetchedGuilds) {
|
|
707
|
+
const [emojiName, guildId] = value;
|
|
708
|
+
const guild = this.core.bot.client.guilds.cache.get(guildId);
|
|
709
|
+
if (!guild) {
|
|
710
|
+
failures.push(` - "${emojiName}" for "${key}" targets guild ${guildId}, which is not available (check the Guilds intent)`);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (!fetchedGuilds.has(guildId)) {
|
|
714
|
+
await guild.emojis.fetch();
|
|
715
|
+
fetchedGuilds.add(guildId);
|
|
716
|
+
}
|
|
717
|
+
const guildEmoji = guild.emojis.cache.find((e) => e.name === emojiName);
|
|
718
|
+
if (!guildEmoji) {
|
|
719
|
+
failures.push(` - "${emojiName}" for "${key}" was not found in guild ${guildId}`);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
emojiStorage[key] = guildEmoji;
|
|
723
|
+
}
|
|
724
|
+
resolveString(key, emojiName, failures) {
|
|
725
|
+
const appEmoji = this.core.bot.client.application?.emojis.cache.find((e) => e.name === emojiName);
|
|
726
|
+
if (!appEmoji) {
|
|
727
|
+
failures.push(` - "${emojiName}" for "${key}" was not found among the application emojis`);
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
emojiStorage[key] = appEmoji;
|
|
731
|
+
}
|
|
732
|
+
clearEmojis() {
|
|
733
|
+
for (const key of Object.keys(emojiStorage)) Reflect.deleteProperty(emojiStorage, key);
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
//#endregion
|
|
738
|
+
//#region src/bot/injectors/CommandMentionInjector.ts
|
|
739
|
+
const mentionStorage = {};
|
|
740
|
+
/**
|
|
741
|
+
* Ready-to-send command mention strings, keyed by {@link SlashOptionRegistry}.
|
|
742
|
+
*
|
|
743
|
+
* A value is a clickable `</name:id>` when the command's id resolved from the deploy, otherwise the plain
|
|
744
|
+
* `/name` text. Populated by {@link CommandMentionInjector} after each deploy.
|
|
745
|
+
*/
|
|
746
|
+
const CommandMentions = mentionStorage;
|
|
747
|
+
const MULTI_GUILD = Symbol("seedcord:commandmentions:multi-guild");
|
|
748
|
+
/**
|
|
749
|
+
* Turns each deployed slash route into a clickable command mention exposed through {@link CommandMentions}.
|
|
750
|
+
*
|
|
751
|
+
* Consumes the ids {@link DeployResult} carries, so it runs after a deploy rather than resolving from a name
|
|
752
|
+
* cache (discord.js offers none for command ids). A global command is clickable everywhere by its app-wide id.
|
|
753
|
+
* A guild command deployed to one guild is clickable by that guild's id. A command deployed to two or more
|
|
754
|
+
* guilds mints a different id per guild, so one accessor key cannot resolve it, and it falls back to plain
|
|
755
|
+
* `/name` text with one warn. On a non-owning shard a guild command also falls back to plain text.
|
|
756
|
+
*
|
|
757
|
+
* @internal
|
|
758
|
+
*/
|
|
759
|
+
var CommandMentionInjector = class {
|
|
760
|
+
core;
|
|
761
|
+
logger = new _seedcord_services.Logger("CommandMentions");
|
|
762
|
+
constructor(core) {
|
|
763
|
+
this.core = core;
|
|
764
|
+
}
|
|
765
|
+
inject(deploy) {
|
|
766
|
+
this.clear();
|
|
767
|
+
const globalIds = this.indexByName(deploy.global);
|
|
768
|
+
const guildIds = this.indexGuilds(deploy.guilds);
|
|
769
|
+
const warned = /* @__PURE__ */ new Set();
|
|
770
|
+
let clickable = 0;
|
|
771
|
+
for (const command of this.allSlashBuilders()) {
|
|
772
|
+
const id = this.resolveId(command.name, globalIds, guildIds, warned);
|
|
773
|
+
for (const leaf of (0, _seedcord_utils_internal.routeLeavesOf)(command.toJSON())) {
|
|
774
|
+
mentionStorage[leaf.route] = id ? this.toMention(leaf.route, id) : this.toPlain(leaf.route);
|
|
775
|
+
if (id) clickable++;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
this.logger.utils.summary("Linked mentions", {
|
|
779
|
+
clickable,
|
|
780
|
+
plain: Object.keys(mentionStorage).length - clickable
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
allSlashBuilders() {
|
|
784
|
+
const registry = this.core.bot.commands;
|
|
785
|
+
if (!registry) return [];
|
|
786
|
+
const slash = [...registry.globalCommands, ...registry.guildCommands.values()].flat().filter((command) => command instanceof _discordjs_builders.SlashCommandBuilder);
|
|
787
|
+
return [...new Set(slash)];
|
|
788
|
+
}
|
|
789
|
+
indexByName(collection) {
|
|
790
|
+
const map = /* @__PURE__ */ new Map();
|
|
791
|
+
for (const command of collection.values()) map.set(command.name, command.id);
|
|
792
|
+
return map;
|
|
793
|
+
}
|
|
794
|
+
indexGuilds(guilds) {
|
|
795
|
+
const map = /* @__PURE__ */ new Map();
|
|
796
|
+
for (const collection of guilds.values()) for (const command of collection.values()) {
|
|
797
|
+
const existing = map.get(command.name);
|
|
798
|
+
if (existing === void 0) map.set(command.name, command.id);
|
|
799
|
+
else if (existing !== command.id) map.set(command.name, MULTI_GUILD);
|
|
800
|
+
}
|
|
801
|
+
return map;
|
|
802
|
+
}
|
|
803
|
+
resolveId(name, globalIds, guildIds, warned) {
|
|
804
|
+
const global = globalIds.get(name);
|
|
805
|
+
if (global) return global;
|
|
806
|
+
const guild = guildIds.get(name);
|
|
807
|
+
if (guild === void 0) return void 0;
|
|
808
|
+
if (guild === MULTI_GUILD) {
|
|
809
|
+
if (!warned.has(name)) {
|
|
810
|
+
this.logger.warn(`${name} is deployed to multiple guilds, falling back to plain text (not clickable).`);
|
|
811
|
+
warned.add(name);
|
|
812
|
+
}
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
return guild;
|
|
816
|
+
}
|
|
817
|
+
toMention(route, id) {
|
|
818
|
+
const [name, middle, sub] = route.split("/");
|
|
819
|
+
if (name && middle && sub) return (0, discord_js.chatInputApplicationCommandMention)(name, middle, sub, id);
|
|
820
|
+
if (name && middle) return (0, discord_js.chatInputApplicationCommandMention)(name, middle, id);
|
|
821
|
+
return (0, discord_js.chatInputApplicationCommandMention)(name ?? route, id);
|
|
822
|
+
}
|
|
823
|
+
toPlain(route) {
|
|
824
|
+
return `/${route.replaceAll("/", " ")}`;
|
|
825
|
+
}
|
|
826
|
+
clear() {
|
|
827
|
+
for (const key of Object.keys(mentionStorage)) Reflect.deleteProperty(mentionStorage, key);
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
//#endregion
|
|
832
|
+
//#region src/bot/utilities/channels/fetchText.ts
|
|
833
|
+
/**
|
|
834
|
+
* Fetches and validates a text channel. Refuses when the channel doesn't exist or isn't a text channel.
|
|
835
|
+
*
|
|
836
|
+
* @param client - The Discord client instance
|
|
837
|
+
* @param channelId - Channel ID or TextChannel instance
|
|
838
|
+
* @returns Promise resolving to the text channel
|
|
839
|
+
*/
|
|
840
|
+
async function fetchText(client, channelId) {
|
|
841
|
+
if (channelId instanceof discord_js.TextChannel) return channelId;
|
|
842
|
+
let channel = client.channels.cache.get(channelId);
|
|
843
|
+
if (!channel) try {
|
|
844
|
+
channel = await client.channels.fetch(channelId);
|
|
845
|
+
} catch (err) {
|
|
846
|
+
if (err instanceof discord_js.DiscordAPIError && err.code === discord_js.RESTJSONErrorCodes.UnknownChannel) throw new CouldNotFindChannel("Channel not found or not a text channel", channelId);
|
|
847
|
+
throw err;
|
|
848
|
+
}
|
|
849
|
+
if (channel?.isTextBased()) return channel;
|
|
850
|
+
throw new CouldNotFindChannel("Channel not found or not a text channel", channelId);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
//#endregion
|
|
854
|
+
//#region src/bot/utilities/permissions/checkBotPermissions.ts
|
|
855
|
+
/**
|
|
856
|
+
* Checks if the bot has required permissions in a {@link Guild} or {@link TextChannel}. Refuses when a
|
|
857
|
+
* required permission is missing.
|
|
858
|
+
*
|
|
859
|
+
* @param target - Guild or text channel to check in
|
|
860
|
+
* @param scope - Permission bits to validate
|
|
861
|
+
* @param inverse - Whether to check for absence of the given permissions
|
|
862
|
+
* @param errors - Optional custom error constructors
|
|
863
|
+
*
|
|
864
|
+
* @example
|
|
865
|
+
* ```ts
|
|
866
|
+
* import { PermissionFlagsBits } from 'discord.js';
|
|
867
|
+
* import { checkBotPermissions } from '@seedcord/gateway';
|
|
868
|
+
*
|
|
869
|
+
* // Check if the bot has SendMessages and ViewChannel permissions in a text channel
|
|
870
|
+
* checkBotPermissions(textChannel, [
|
|
871
|
+
* PermissionFlagsBits.SendMessages,
|
|
872
|
+
* PermissionFlagsBits.ViewChannel
|
|
873
|
+
* ]);
|
|
874
|
+
* ```
|
|
875
|
+
*/
|
|
876
|
+
function checkBotPermissions(target, scope, inverse = false, errors) {
|
|
877
|
+
if (target instanceof discord_js.Guild) {
|
|
878
|
+
const me = target.members.me;
|
|
879
|
+
if (!me) {
|
|
880
|
+
const names = scope.map((bit) => PermissionNames.get(bit) ?? String(bit));
|
|
881
|
+
throw new ((errors?.missing) ?? MissingPermissions)("Missing Permissions", target, names);
|
|
882
|
+
}
|
|
883
|
+
checkPermissions(me, target, scope, inverse, errors);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
const me = target.guild.members.me;
|
|
887
|
+
if (!me) {
|
|
888
|
+
new _seedcord_services.Logger("checkBotPermissions").warn(`Bot member is unavailable in guild ${target.guild.id} while checking permissions in channel ${target.id}`);
|
|
889
|
+
const names = scope.map((bit) => PermissionNames.get(bit) ?? String(bit));
|
|
890
|
+
throw new ((errors?.missing) ?? MissingPermissions)("Missing Permissions", target, names);
|
|
891
|
+
}
|
|
892
|
+
checkPermissions(me, target, scope, inverse, errors);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
//#endregion
|
|
896
|
+
//#region src/bot/utilities/roles/getBotRole.ts
|
|
897
|
+
/**
|
|
898
|
+
* Gets the bot's managed role in a guild.
|
|
899
|
+
*
|
|
900
|
+
* @param guild - The guild to get the bot role from
|
|
901
|
+
* @returns The bot's managed role in the guild
|
|
902
|
+
* @throws A **SeedcordError** if the client user is unavailable or if the bot role is missing
|
|
903
|
+
*/
|
|
904
|
+
function getBotRole(guild) {
|
|
905
|
+
const botRole = guild.roles.botRoleFor(guild.client.user);
|
|
906
|
+
if (!botRole) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CoreBotRoleMissing, [guild.id]);
|
|
907
|
+
return botRole;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
//#endregion
|
|
911
|
+
//#region src/bot/utilities/permissions/hasPermsToAssign.ts
|
|
912
|
+
/**
|
|
913
|
+
* Validates if the bot can assign a target role. Refuses when the target role is above the bot's, is managed, or the bot lacks Manage Roles.
|
|
914
|
+
*
|
|
915
|
+
* @param roleOrOptions - Target role or complete options for the check
|
|
916
|
+
*
|
|
917
|
+
* @example
|
|
918
|
+
* ```ts
|
|
919
|
+
* import { hasPermsToAssign } from '@seedcord/gateway';
|
|
920
|
+
*
|
|
921
|
+
* // Check if the bot can assign a specific role
|
|
922
|
+
* hasPermsToAssign(role);
|
|
923
|
+
*
|
|
924
|
+
* // Check with custom error handling
|
|
925
|
+
* hasPermsToAssign({
|
|
926
|
+
* targetRole: role,
|
|
927
|
+
* errors: {
|
|
928
|
+
* higher: CustomHigherError,
|
|
929
|
+
* managed: CustomManagedError,
|
|
930
|
+
* missing: CustomMissingPermissionsError
|
|
931
|
+
* }
|
|
932
|
+
* });
|
|
933
|
+
* ```
|
|
934
|
+
*/
|
|
935
|
+
function hasPermsToAssign(roleOrOptions) {
|
|
936
|
+
const { targetRole, errors } = roleOrOptions instanceof discord_js.Role ? {
|
|
937
|
+
targetRole: roleOrOptions,
|
|
938
|
+
errors: void 0
|
|
939
|
+
} : roleOrOptions;
|
|
940
|
+
const HigherErr = errors?.higher ?? RoleHigherThanMe;
|
|
941
|
+
const ManagedErr = errors?.managed ?? CannotAssignBotRole;
|
|
942
|
+
const Missing = errors?.missing ?? MissingPermissions;
|
|
943
|
+
const botRole = getBotRole(targetRole.guild);
|
|
944
|
+
if (targetRole.comparePositionTo(botRole) >= 0) throw new HigherErr("Role is higher than me", targetRole, botRole);
|
|
945
|
+
if (targetRole.managed) throw new ManagedErr(`Cannot assign managed role ${targetRole.name}`);
|
|
946
|
+
checkBotPermissions(targetRole.guild, [discord_js.PermissionFlagsBits.ManageRoles], false, { missing: Missing });
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
//#endregion
|
|
950
|
+
//#region src/bot/utilities/roles/fetchRole.ts
|
|
951
|
+
function isUnknownRole(err) {
|
|
952
|
+
return err instanceof discord_js.DiscordAPIError && err.code === discord_js.RESTJSONErrorCodes.UnknownRole;
|
|
953
|
+
}
|
|
954
|
+
async function fetchRoleInGuild(guild, roleId) {
|
|
955
|
+
const cached = guild.roles.cache.get(roleId);
|
|
956
|
+
if (cached) return cached;
|
|
957
|
+
try {
|
|
958
|
+
return await guild.roles.fetch(roleId);
|
|
959
|
+
} catch (err) {
|
|
960
|
+
if (isUnknownRole(err)) throw new RoleDoesNotExist("Role not found in specified guild", roleId);
|
|
961
|
+
throw err;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
async function scanGuildsForRole(client, roleId) {
|
|
965
|
+
const cached = client.guilds.cache.map((guild) => guild.roles.cache.get(roleId)).find(Boolean);
|
|
966
|
+
if (cached) return cached;
|
|
967
|
+
for (const guild of client.guilds.cache.values()) try {
|
|
968
|
+
const role = await guild.roles.fetch(roleId);
|
|
969
|
+
if (role) return role;
|
|
970
|
+
} catch (err) {
|
|
971
|
+
if (isUnknownRole(err)) continue;
|
|
972
|
+
throw err;
|
|
973
|
+
}
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Fetches a role by ID from a client or guild. Refuses when the role doesn't exist.
|
|
978
|
+
*
|
|
979
|
+
* @param clientOrGuild - Discord client or guild instance
|
|
980
|
+
* @param roleId - The role ID to fetch
|
|
981
|
+
* @returns Promise resolving to the role
|
|
982
|
+
*/
|
|
983
|
+
async function fetchRole(clientOrGuild, roleId) {
|
|
984
|
+
if (!roleId) throw new RoleDoesNotExist("Role ID is null or undefined", roleId);
|
|
985
|
+
const role = clientOrGuild instanceof discord_js.Guild ? await fetchRoleInGuild(clientOrGuild, roleId) : await scanGuildsForRole(clientOrGuild, roleId);
|
|
986
|
+
if (!role) throw new RoleDoesNotExist("Role not found", roleId);
|
|
987
|
+
return role;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
//#endregion
|
|
991
|
+
//#region src/bot/utilities/users/fetchGuildMember.ts
|
|
992
|
+
/**
|
|
993
|
+
* Fetches a guild member by user ID. Refuses when the user is not in the guild.
|
|
994
|
+
*
|
|
995
|
+
* @param guild - The guild to fetch the member from
|
|
996
|
+
* @param userId - The Discord user ID
|
|
997
|
+
* @returns Promise resolving to the guild member
|
|
998
|
+
*/
|
|
999
|
+
async function fetchGuildMember(guild, userId) {
|
|
1000
|
+
let user = guild.members.cache.get(userId);
|
|
1001
|
+
user ??= await guild.members.fetch(userId).catch((err) => {
|
|
1002
|
+
if (err instanceof discord_js.DiscordAPIError && err.code === discord_js.RESTJSONErrorCodes.UnknownMember) throw new UserNotInGuild(`User with ID ${userId} not found in guild`);
|
|
1003
|
+
throw err;
|
|
1004
|
+
});
|
|
1005
|
+
return user;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
//#endregion
|
|
1009
|
+
//#region src/bot/utilities/users/fetchManyGuildMembers.ts
|
|
1010
|
+
/**
|
|
1011
|
+
* Fetches multiple guild members by user IDs.
|
|
1012
|
+
*
|
|
1013
|
+
* @param guild - The guild to fetch members from
|
|
1014
|
+
* @param userIds - Array of Discord user IDs
|
|
1015
|
+
* @returns Promise resolving to array of successfully fetched members
|
|
1016
|
+
*/
|
|
1017
|
+
async function fetchManyGuildMembers(guild, userIds) {
|
|
1018
|
+
return (await Promise.allSettled(userIds.map((userId) => fetchGuildMember(guild, userId)))).filter((result) => result.status === "fulfilled").map((result) => result.value);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
//#endregion
|
|
1022
|
+
//#region src/bot/utilities/users/fetchUser.ts
|
|
1023
|
+
/**
|
|
1024
|
+
* Fetches a Discord user by ID. Refuses when the user doesn't exist, with `options.throwAs` or the default.
|
|
1025
|
+
*
|
|
1026
|
+
* @param client - The Discord client instance
|
|
1027
|
+
* @param userId - The Discord user ID
|
|
1028
|
+
* @param options - Optional overrides, including the {@link FetchUserOptions.throwAs} notice
|
|
1029
|
+
* @returns Promise resolving to the user
|
|
1030
|
+
*/
|
|
1031
|
+
async function fetchUser(client, userId, options) {
|
|
1032
|
+
const Throw = options?.throwAs ?? UserNotFound;
|
|
1033
|
+
let user = client.users.cache.get(userId);
|
|
1034
|
+
user ??= await client.users.fetch(userId).catch((err) => {
|
|
1035
|
+
if (err instanceof discord_js.DiscordAPIError && err.code === discord_js.RESTJSONErrorCodes.UnknownUser) throw new Throw(userId);
|
|
1036
|
+
throw err;
|
|
1037
|
+
});
|
|
1038
|
+
return user;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
//#endregion
|
|
1042
|
+
//#region src/bot/utilities/users/fetchManyUsers.ts
|
|
1043
|
+
/**
|
|
1044
|
+
* Fetches multiple Discord users by IDs.
|
|
1045
|
+
*
|
|
1046
|
+
* @param client - The Discord client instance
|
|
1047
|
+
* @param userIds - Array of Discord user IDs
|
|
1048
|
+
* @param options - Optional overrides threaded to each {@link fetchUser} call
|
|
1049
|
+
* @returns Promise resolving to array of successfully fetched users
|
|
1050
|
+
*/
|
|
1051
|
+
async function fetchManyUsers(client, userIds, options) {
|
|
1052
|
+
return (await Promise.allSettled(userIds.map((userId) => fetchUser(client, userId, options)))).filter((result) => result.status === "fulfilled").map((result) => result.value);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
//#endregion
|
|
1056
|
+
//#region src/bot/utilities/users/updateMemberRoles.ts
|
|
1057
|
+
/**
|
|
1058
|
+
* Updates a guild member's roles by adding and removing specified roles.
|
|
1059
|
+
*
|
|
1060
|
+
* @param rolesToAdd - Array of role IDs to add to the member
|
|
1061
|
+
* @param rolesToRemove - Array of role IDs to remove from the member
|
|
1062
|
+
* @param member - The guild member to update
|
|
1063
|
+
* @returns Promise that resolves when roles are updated
|
|
1064
|
+
*/
|
|
1065
|
+
async function updateMemberRoles(rolesToAdd, rolesToRemove, member) {
|
|
1066
|
+
const current = new Set(member.roles.cache.map((r) => r.id));
|
|
1067
|
+
const toAdd = new Set(rolesToAdd);
|
|
1068
|
+
const toRemove = new Set(rolesToRemove);
|
|
1069
|
+
const updated = current.union(toAdd).difference(toRemove);
|
|
1070
|
+
await member.roles.set([...updated]);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
//#endregion
|
|
1074
|
+
//#region src/bot/harmlessApiCodes.ts
|
|
1075
|
+
/**
|
|
1076
|
+
* Discord API error codes that mean a dead end on an interaction, a dead token, a double ack, or a gone
|
|
1077
|
+
* message. There is nothing to show the user and no bug to report. The controller boundary drops them on
|
|
1078
|
+
* the original throw, and {@link ReplySender} drops them on its own send.
|
|
1079
|
+
*
|
|
1080
|
+
* @internal
|
|
1081
|
+
*/
|
|
1082
|
+
const HARMLESS_API_CODES = new Set([
|
|
1083
|
+
discord_js.RESTJSONErrorCodes.UnknownInteraction,
|
|
1084
|
+
discord_js.RESTJSONErrorCodes.InteractionHasAlreadyBeenAcknowledged,
|
|
1085
|
+
discord_js.RESTJSONErrorCodes.UnknownMessage
|
|
1086
|
+
]);
|
|
1087
|
+
|
|
1088
|
+
//#endregion
|
|
1089
|
+
//#region src/miscellaneous/flagsFor.ts
|
|
1090
|
+
/**
|
|
1091
|
+
* Builds the `flags` bitfield for a framework reply. Every framework reply is ComponentsV2, so the
|
|
1092
|
+
* IsComponentsV2 flag is always set.
|
|
1093
|
+
*
|
|
1094
|
+
* @param ephemeral - Whether the reply is ephemeral.
|
|
1095
|
+
* @returns The OR'd `MessageFlags` value as a plain number.
|
|
1096
|
+
*
|
|
1097
|
+
* @internal
|
|
1098
|
+
*/
|
|
1099
|
+
function flagsFor(ephemeral) {
|
|
1100
|
+
let flags = discord_js.MessageFlags.IsComponentsV2;
|
|
1101
|
+
if (ephemeral) flags |= discord_js.MessageFlags.Ephemeral;
|
|
1102
|
+
return flags;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
//#endregion
|
|
1106
|
+
//#region src/bot/ReplySender.ts
|
|
1107
|
+
/**
|
|
1108
|
+
* Sends a {@link ReplyResponse} to an interaction, picking reply, editReply, or followUp from the live
|
|
1109
|
+
* acknowledgement state. Logs and drops its own send failures so a dead token never escapes into the
|
|
1110
|
+
* controller.
|
|
1111
|
+
*/
|
|
1112
|
+
var ReplySender = class {
|
|
1113
|
+
interaction;
|
|
1114
|
+
logger = new _seedcord_services.Logger("ReplySender");
|
|
1115
|
+
constructor(interaction) {
|
|
1116
|
+
this.interaction = interaction;
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* Sends the reply and returns the sent message so a caller can attach a collector to it. Never throws,
|
|
1120
|
+
* a failed send is logged and dropped and resolves to `undefined`.
|
|
1121
|
+
*
|
|
1122
|
+
* @param response - The ComponentsV2 reply to show.
|
|
1123
|
+
* @param ephemeral - Whether the reply is ephemeral. {@default `true`}
|
|
1124
|
+
* @returns The sent {@link Message}, or `undefined` when the send was swallowed.
|
|
1125
|
+
*/
|
|
1126
|
+
async send(response, ephemeral = true) {
|
|
1127
|
+
try {
|
|
1128
|
+
return await this.dispatch(response, ephemeral);
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
this.logSwallowed("send", error);
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Edits an already-sent message to a new {@link ReplyResponse}. Used to swap a confirmation prompt for
|
|
1136
|
+
* its outcome. Never throws, a failed edit is logged and dropped.
|
|
1137
|
+
*
|
|
1138
|
+
* @param message - The message to edit (the handle a prior {@link send} returned).
|
|
1139
|
+
* @param response - The reply to replace it with.
|
|
1140
|
+
*/
|
|
1141
|
+
async edit(message, response) {
|
|
1142
|
+
try {
|
|
1143
|
+
await this.interaction.webhook.editMessage(message, this.editBody(response));
|
|
1144
|
+
} catch (error) {
|
|
1145
|
+
this.logSwallowed("edit", error);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
async dispatch(response, ephemeral) {
|
|
1149
|
+
if (this.interaction.replied) return await this.interaction.followUp(this.replyOptions(response, ephemeral));
|
|
1150
|
+
if (this.interaction.deferred) {
|
|
1151
|
+
if (this.interaction.ephemeral === null) return await this.interaction.followUp(this.replyOptions(response, ephemeral));
|
|
1152
|
+
return await this.interaction.editReply(this.editBody(response));
|
|
1153
|
+
}
|
|
1154
|
+
await this.interaction.reply(this.replyOptions(response, ephemeral));
|
|
1155
|
+
return await this.interaction.fetchReply();
|
|
1156
|
+
}
|
|
1157
|
+
replyOptions(response, ephemeral) {
|
|
1158
|
+
return {
|
|
1159
|
+
components: response.components,
|
|
1160
|
+
flags: flagsFor(ephemeral),
|
|
1161
|
+
...response.allowedMentions && { allowedMentions: response.allowedMentions },
|
|
1162
|
+
...response.files && { files: response.files }
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
editBody(response) {
|
|
1166
|
+
return {
|
|
1167
|
+
components: response.components,
|
|
1168
|
+
flags: discord_js.MessageFlags.IsComponentsV2,
|
|
1169
|
+
...response.allowedMentions && { allowedMentions: response.allowedMentions },
|
|
1170
|
+
...response.files && { files: response.files }
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
logSwallowed(action, error) {
|
|
1174
|
+
if (error instanceof discord_js.DiscordAPIError && HARMLESS_API_CODES.has(error.code)) {
|
|
1175
|
+
this.logger.debug(`reply ${action} hit harmless code ${error.code}`);
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
this.logger.error(`reply ${action} failed`, error);
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1182
|
+
//#endregion
|
|
1183
|
+
//#region src/bot/confirm/reserved.ts
|
|
1184
|
+
const RESERVED_CONFIRM_PREFIX = "__seedcord_confirm";
|
|
1185
|
+
/**
|
|
1186
|
+
* The reserved confirm/cancel codec. It has no route, so the dispatcher ignores its clicks and only the
|
|
1187
|
+
* in-process collector reads them. Do not mint a real component from it.
|
|
1188
|
+
*
|
|
1189
|
+
* @internal
|
|
1190
|
+
*/
|
|
1191
|
+
const CONFIRM_DEF = new _seedcord_core.CustomId(RESERVED_CONFIRM_PREFIX).oneOf("choice", ["confirm", "cancel"]);
|
|
1192
|
+
|
|
1193
|
+
//#endregion
|
|
1194
|
+
//#region src/bot/confirm/getConfirmation.ts
|
|
1195
|
+
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
1196
|
+
const CONFIRM_IDS = {
|
|
1197
|
+
confirm: CONFIRM_DEF.encode({ choice: "confirm" }),
|
|
1198
|
+
cancel: CONFIRM_DEF.encode({ choice: "cancel" })
|
|
1199
|
+
};
|
|
1200
|
+
var ConfirmButtons = class extends _seedcord_core.RowComponent {
|
|
1201
|
+
constructor(options) {
|
|
1202
|
+
super("button");
|
|
1203
|
+
this.instance.addComponents(new _discordjs_builders.ButtonBuilder().setCustomId(CONFIRM_IDS.confirm).setLabel(options?.confirmLabel ?? "Confirm").setStyle(options?.confirmStyle ?? discord_js.ButtonStyle.Danger), new _discordjs_builders.ButtonBuilder().setCustomId(CONFIRM_IDS.cancel).setLabel(options?.cancelLabel ?? "Cancel").setStyle(discord_js.ButtonStyle.Secondary));
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
var ConfirmPromptCard = class extends _seedcord_core.BuilderComponent {
|
|
1207
|
+
constructor(text, options) {
|
|
1208
|
+
super("container");
|
|
1209
|
+
this.instance.addTextDisplayComponents(new _discordjs_builders.TextDisplayBuilder().setContent(text)).addActionRowComponents(new ConfirmButtons(options).component);
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
function defaultPrompt(text, options) {
|
|
1213
|
+
return { components: [new ConfirmPromptCard(text, options).component] };
|
|
1214
|
+
}
|
|
1215
|
+
async function collectChoice(message, userId, timeoutMs) {
|
|
1216
|
+
try {
|
|
1217
|
+
return await message.awaitMessageComponent({
|
|
1218
|
+
componentType: discord_js.ComponentType.Button,
|
|
1219
|
+
time: timeoutMs,
|
|
1220
|
+
filter: (click) => click.user.id === userId && CONFIRM_DEF.owns(click.customId)
|
|
1221
|
+
});
|
|
1222
|
+
} catch {
|
|
1223
|
+
return null;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
async function settle(sender, interaction, message, outcome) {
|
|
1227
|
+
if (outcome === void 0) {
|
|
1228
|
+
await interaction.deleteReply(message).catch(() => void 0);
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
await sender.edit(message, typeof outcome === "function" ? await outcome() : outcome);
|
|
1232
|
+
}
|
|
1233
|
+
async function getConfirmation(interaction, prompt, options) {
|
|
1234
|
+
const { ephemeral = true, timeoutMs = DEFAULT_TIMEOUT_MS } = options ?? {};
|
|
1235
|
+
const response = typeof prompt === "string" ? defaultPrompt(prompt, options) : await prompt(CONFIRM_IDS);
|
|
1236
|
+
const sender = new ReplySender(interaction);
|
|
1237
|
+
const message = await sender.send(response, ephemeral);
|
|
1238
|
+
if (!message) return false;
|
|
1239
|
+
const winner = await collectChoice(message, interaction.user.id, timeoutMs);
|
|
1240
|
+
if (!winner) {
|
|
1241
|
+
await settle(sender, interaction, message, options?.onTimeout);
|
|
1242
|
+
return false;
|
|
1243
|
+
}
|
|
1244
|
+
await winner.deferUpdate().catch(() => void 0);
|
|
1245
|
+
const confirmed = winner.customId === CONFIRM_IDS.confirm;
|
|
1246
|
+
await settle(sender, interaction, message, confirmed ? options?.onConfirm : options?.onCancel);
|
|
1247
|
+
return confirmed;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
//#endregion
|
|
1251
|
+
//#region src/hmr/devChannel.ts
|
|
1252
|
+
let channel;
|
|
1253
|
+
function setDevChannel(next) {
|
|
1254
|
+
channel = next;
|
|
1255
|
+
}
|
|
1256
|
+
function getDevChannel() {
|
|
1257
|
+
return channel;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
//#endregion
|
|
1261
|
+
//#region src/interfaces/Plugin.ts
|
|
1262
|
+
/**
|
|
1263
|
+
* Base class for Seedcord plugins
|
|
1264
|
+
*
|
|
1265
|
+
* Extend this class to create plugins that integrate with the Seedcord lifecycle.
|
|
1266
|
+
* Plugins have access to the core instance and must implement initialization logic.
|
|
1267
|
+
*/
|
|
1268
|
+
var Plugin = class extends _seedcord_services.StrictEventEmitter {
|
|
1269
|
+
pluggable;
|
|
1270
|
+
name = this.constructor.name;
|
|
1271
|
+
constructor(pluggable) {
|
|
1272
|
+
super();
|
|
1273
|
+
this.pluggable = pluggable;
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Reloads plugin state on an HMR update. The base implementation is a no-op, override it in your plugin.
|
|
1277
|
+
* @virtual
|
|
1278
|
+
*/
|
|
1279
|
+
onHmr(_event) {
|
|
1280
|
+
return Promise.resolve();
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Registers critical file patterns that should trigger a full restart when changed in Dev HMR.
|
|
1284
|
+
* @param patterns - Glob patterns relative to the project root
|
|
1285
|
+
*/
|
|
1286
|
+
registerCriticalFiles(patterns) {
|
|
1287
|
+
getDevChannel()?.send("seedcord:register-critical-files", { patterns });
|
|
1288
|
+
}
|
|
1289
|
+
/** @internal */
|
|
1290
|
+
setMaxListeners(n) {
|
|
1291
|
+
return super.setMaxListeners(n);
|
|
1292
|
+
}
|
|
1293
|
+
/** @internal */
|
|
1294
|
+
removeListener(event, listener) {
|
|
1295
|
+
return super.removeListener(event, listener);
|
|
1296
|
+
}
|
|
1297
|
+
/** @internal */
|
|
1298
|
+
removeAllListeners(eventName) {
|
|
1299
|
+
return super.removeAllListeners(eventName);
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
/**
|
|
1303
|
+
* Base class for objects that can have plugins attached.
|
|
1304
|
+
*
|
|
1305
|
+
* Provides plugin attachment capabilities and lifecycle management. Plugins are attached during
|
|
1306
|
+
* configuration and initialized during startup. Not constructed directly, the host is a {@link Seedcord}.
|
|
1307
|
+
*/
|
|
1308
|
+
var Pluggable = class Pluggable extends _seedcord_services.StrictEventEmitter {
|
|
1309
|
+
isInitialized = false;
|
|
1310
|
+
shutdown;
|
|
1311
|
+
startup;
|
|
1312
|
+
plugins = [];
|
|
1313
|
+
static PLUGIN_INIT_TIMEOUT_MS = 15e3;
|
|
1314
|
+
constructor(shutdown, startup) {
|
|
1315
|
+
super();
|
|
1316
|
+
this.shutdown = shutdown;
|
|
1317
|
+
this.startup = startup;
|
|
1318
|
+
}
|
|
1319
|
+
/** @internal */
|
|
1320
|
+
async init() {
|
|
1321
|
+
if (this.isInitialized) return this;
|
|
1322
|
+
await this.startup.run();
|
|
1323
|
+
this.isInitialized = true;
|
|
1324
|
+
return this;
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Attaches a plugin to this instance
|
|
1328
|
+
*
|
|
1329
|
+
* Plugins provide external functionality and are initialized during the specified startup phase.
|
|
1330
|
+
* The plugin instance becomes available as a property in `core` wherever it's available.
|
|
1331
|
+
*
|
|
1332
|
+
* Make sure to augment the {@link Core} interface with the plugin type to ensure TypeScript recognizes it and provides intellisense.
|
|
1333
|
+
*
|
|
1334
|
+
* @typeParam Key - The property name for accessing the plugin
|
|
1335
|
+
* @typeParam Ctor - The plugin constructor type
|
|
1336
|
+
* @param key - Property name to access the plugin instance
|
|
1337
|
+
* @param Plugin - Plugin constructor class
|
|
1338
|
+
* @param startupPhase - When during startup to initialize this plugin ({@link StartupPhase})
|
|
1339
|
+
* @param args - Additional arguments to pass to the plugin constructor
|
|
1340
|
+
* @returns This instance with the plugin attached as a typed property
|
|
1341
|
+
* @throws A **SeedcordError** When called after initialization or if key already exists
|
|
1342
|
+
* @example
|
|
1343
|
+
* ```typescript
|
|
1344
|
+
* seedcord.attach('db', Mongo, StartupPhase.Configuration, { uri: 'mongodb://...', name: 'seedcord', dir: ... })
|
|
1345
|
+
* ```
|
|
1346
|
+
*/
|
|
1347
|
+
attach(key, Plugin, startupPhase, ...args) {
|
|
1348
|
+
if (this.isInitialized) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CorePluginAfterInit);
|
|
1349
|
+
if (key in this) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CorePluginKeyExists, [key]);
|
|
1350
|
+
const instance = new Plugin(this, ...args);
|
|
1351
|
+
this.plugins.push(instance);
|
|
1352
|
+
const entry = { [key]: instance };
|
|
1353
|
+
this.startup.addTask(startupPhase, `Plugin:${key}`, async () => {
|
|
1354
|
+
instance.logger.utils.initialization(key, "start");
|
|
1355
|
+
await instance.init();
|
|
1356
|
+
instance.logger.utils.initialization(key, "end");
|
|
1357
|
+
}, Pluggable.PLUGIN_INIT_TIMEOUT_MS);
|
|
1358
|
+
return Object.assign(this, entry);
|
|
1359
|
+
}
|
|
1360
|
+
/** @internal */
|
|
1361
|
+
setMaxListeners(n) {
|
|
1362
|
+
return super.setMaxListeners(n);
|
|
1363
|
+
}
|
|
1364
|
+
/** @internal */
|
|
1365
|
+
removeListener(event, listener) {
|
|
1366
|
+
return super.removeListener(event, listener);
|
|
1367
|
+
}
|
|
1368
|
+
/** @internal */
|
|
1369
|
+
removeAllListeners(eventName) {
|
|
1370
|
+
return super.removeAllListeners(eventName);
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1374
|
+
//#endregion
|
|
1375
|
+
//#region src/handlers/BaseHandler.ts
|
|
1376
|
+
/**
|
|
1377
|
+
* Base class for a Discord event or interaction handler. Don't register handlers directly, use the more specific handler subclasses.
|
|
1378
|
+
*/
|
|
1379
|
+
var BaseHandler = class {
|
|
1380
|
+
core;
|
|
1381
|
+
event;
|
|
1382
|
+
logger;
|
|
1383
|
+
constructor(event, core) {
|
|
1384
|
+
this.core = core;
|
|
1385
|
+
this.event = event;
|
|
1386
|
+
this.logger = new _seedcord_services.Logger(this.constructor.name);
|
|
1387
|
+
}
|
|
1388
|
+
/** @internal */
|
|
1389
|
+
getEvent() {
|
|
1390
|
+
return this.event;
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
|
|
1394
|
+
//#endregion
|
|
1395
|
+
//#region src/handlers/interaction/InteractionMiddleware.ts
|
|
1396
|
+
/**
|
|
1397
|
+
* Base class for interaction middleware
|
|
1398
|
+
*
|
|
1399
|
+
* Middleware runs before interaction handlers and can modify behavior or block execution.
|
|
1400
|
+
* Unlike handlers, middleware should not send responses directly.
|
|
1401
|
+
*
|
|
1402
|
+
* @typeParam Repliable - The interaction type this middleware processes
|
|
1403
|
+
*/
|
|
1404
|
+
var InteractionMiddleware = class extends BaseHandler {
|
|
1405
|
+
constructor(event, core) {
|
|
1406
|
+
super(event, core);
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
|
|
1410
|
+
//#endregion
|
|
1411
|
+
//#region src/handlers/interaction/InteractionHandler.ts
|
|
1412
|
+
/**
|
|
1413
|
+
* Shared base the typed interaction handlers extend.
|
|
1414
|
+
*
|
|
1415
|
+
* Not a public entry point. You should be using {@link SlashHandler}, {@link ButtonHandler}, {@link ModalHandler},
|
|
1416
|
+
* or {@link SelectMenuHandler} instead. This class only carries the repliable-event plumbing those bases share,
|
|
1417
|
+
* so DO NOT use it directly.
|
|
1418
|
+
*
|
|
1419
|
+
* @typeParam Repliable - The interaction type this handler processes
|
|
1420
|
+
*/
|
|
1421
|
+
var InteractionHandler = class extends BaseHandler {
|
|
1422
|
+
constructor(event, core) {
|
|
1423
|
+
super(event, core);
|
|
1424
|
+
}
|
|
1425
|
+
};
|
|
1426
|
+
|
|
1427
|
+
//#endregion
|
|
1428
|
+
//#region src/bot/utilities/miscellaneous/slashRouteOf.ts
|
|
1429
|
+
/**
|
|
1430
|
+
* Computes the dispatch route for a chat-input or autocomplete interaction. The controller and
|
|
1431
|
+
* `seedcord codegen` both assemble route keys through the same {@link buildSlashRoute}, so a dispatched
|
|
1432
|
+
* interaction and a generated registry key always match.
|
|
1433
|
+
*
|
|
1434
|
+
* @internal
|
|
1435
|
+
*/
|
|
1436
|
+
function slashRouteOf(interaction) {
|
|
1437
|
+
const group = interaction.options.getSubcommandGroup(false) ?? void 0;
|
|
1438
|
+
const subcommand = interaction.options.getSubcommand(false) ?? void 0;
|
|
1439
|
+
return (0, _seedcord_utils_internal.buildSlashRoute)(interaction.commandName, subcommand, group);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
//#endregion
|
|
1443
|
+
//#region src/handlers/interaction/SlashHandler.ts
|
|
1444
|
+
/**
|
|
1445
|
+
* Base class for a chat-input (slash) command handler.
|
|
1446
|
+
*
|
|
1447
|
+
* Pass the route from the generated registry as the generic, the same string as `@SlashRoute`, then read
|
|
1448
|
+
* `this.options` for a single command or `this.match` for several. Command authoring stays plain
|
|
1449
|
+
* discord.js, `seedcord codegen` reads its `toJSON()` to type these options.
|
|
1450
|
+
*
|
|
1451
|
+
* @typeParam Route - One or more route keys from {@link SlashOptionRegistry}, e.g. `'ban'` or `'ban' | 'kick'`.
|
|
1452
|
+
* @typeParam Cache - The interaction cache state, `'cached'` by default.
|
|
1453
|
+
*
|
|
1454
|
+
* @example
|
|
1455
|
+
* ```ts
|
|
1456
|
+
* \@SlashRoute('ban')
|
|
1457
|
+
* class BanHandler extends SlashHandler<'ban'> {
|
|
1458
|
+
* async execute() {
|
|
1459
|
+
* const target = this.options.getUser('target'); // User
|
|
1460
|
+
* const reason = this.options.getString('reason'); // string | null
|
|
1461
|
+
* }
|
|
1462
|
+
* }
|
|
1463
|
+
* ```
|
|
1464
|
+
*/
|
|
1465
|
+
var SlashHandler = class extends InteractionHandler {
|
|
1466
|
+
/**
|
|
1467
|
+
* The typed options for this command's route. Required options drop the null, choices narrow to their
|
|
1468
|
+
* literal union, and only the getters for kinds this command actually uses appear. Use `this.event.options`
|
|
1469
|
+
* directly for anything outside this view, such as narrowing a channel option by type.
|
|
1470
|
+
*/
|
|
1471
|
+
get options() {
|
|
1472
|
+
return this.event.options;
|
|
1473
|
+
}
|
|
1474
|
+
/**
|
|
1475
|
+
* Run the arm for whichever command fired. Use this only when the handler is registered for several
|
|
1476
|
+
* routes. A single-route handler reads `this.options` directly instead, match buys nothing there.
|
|
1477
|
+
*
|
|
1478
|
+
* Provide one arm per registered route, keyed by its route string, and each arm receives that route's
|
|
1479
|
+
* own narrowed options. The arms must cover every route in the generic, a missing route or an unknown
|
|
1480
|
+
* key is a compile error.
|
|
1481
|
+
*
|
|
1482
|
+
* @param arms - One handler per registered route, keyed by route string.
|
|
1483
|
+
* @returns The result of the arm that ran.
|
|
1484
|
+
*
|
|
1485
|
+
* @example
|
|
1486
|
+
* ```ts
|
|
1487
|
+
* \@SlashRoute('ban', 'kick')
|
|
1488
|
+
* class ModHandler extends SlashHandler<'ban' | 'kick'> {
|
|
1489
|
+
* async execute() {
|
|
1490
|
+
* await this.match({
|
|
1491
|
+
* ban: (options) => this.event.guild?.members.ban(options.getUser('target', true).id),
|
|
1492
|
+
* kick: (options) => this.event.guild?.members.kick(options.getUser('member', true).id)
|
|
1493
|
+
* });
|
|
1494
|
+
* }
|
|
1495
|
+
* }
|
|
1496
|
+
* ```
|
|
1497
|
+
*/
|
|
1498
|
+
async match(arms) {
|
|
1499
|
+
const route = slashRouteOf(this.event);
|
|
1500
|
+
const arm = arms[route];
|
|
1501
|
+
if (!arm) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.SlashMatchArmMissing, [route]);
|
|
1502
|
+
return await arm(this.options);
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
|
|
1506
|
+
//#endregion
|
|
1507
|
+
//#region src/handlers/interaction/ContextMenuHandler.ts
|
|
1508
|
+
/**
|
|
1509
|
+
* Base class for a context-menu command handler (right-click a user or a message).
|
|
1510
|
+
*
|
|
1511
|
+
* Pass the kind from `discord.js`'s {@link ApplicationCommandType} as the generic, the same value as
|
|
1512
|
+
* `@ContextMenuRoute`. Read the right-clicked entity from `this.target`, a `User` for a user menu and a
|
|
1513
|
+
* `Message` for a message menu. Context menus carry no options, so a handler registered for several names
|
|
1514
|
+
* reads `this.target` uniformly with no per-name branch.
|
|
1515
|
+
*
|
|
1516
|
+
* @typeParam Kind - `ApplicationCommandType.User` or `ApplicationCommandType.Message`.
|
|
1517
|
+
* @typeParam Cache - The interaction cache state, `'cached'` by default.
|
|
1518
|
+
*
|
|
1519
|
+
* @example
|
|
1520
|
+
* ```ts
|
|
1521
|
+
* \@ContextMenuRoute(ApplicationCommandType.Message, 'Report Message')
|
|
1522
|
+
* class ReportMessage extends ContextMenuHandler<ApplicationCommandType.Message> {
|
|
1523
|
+
* async execute() {
|
|
1524
|
+
* const message = this.target;
|
|
1525
|
+
* }
|
|
1526
|
+
* }
|
|
1527
|
+
* ```
|
|
1528
|
+
*/
|
|
1529
|
+
var ContextMenuHandler = class extends InteractionHandler {
|
|
1530
|
+
get target() {
|
|
1531
|
+
const event = this.event;
|
|
1532
|
+
return "targetUser" in event ? event.targetUser : event.targetMessage;
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* The invoking guild member, resolved only on user menus. Reading it on a message menu is a compile
|
|
1536
|
+
* error since the type is `never` there.
|
|
1537
|
+
*/
|
|
1538
|
+
get targetMember() {
|
|
1539
|
+
return this.event.targetMember;
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1542
|
+
|
|
1543
|
+
//#endregion
|
|
1544
|
+
//#region src/handlers/interaction/AutocompleteHandler.ts
|
|
1545
|
+
/**
|
|
1546
|
+
* Base class for a Discord autocomplete handler.
|
|
1547
|
+
*
|
|
1548
|
+
* Pass the command route(s) from the generated registry as the generic, the same string(s) as
|
|
1549
|
+
* `@AutocompleteRoute`. Branch on the focused field with `this.match`, read already-entered sibling options
|
|
1550
|
+
* with `this.options`, and find which command fired with `this.route`.
|
|
1551
|
+
*
|
|
1552
|
+
* @typeParam Route - One or more route keys from {@link SlashOptionRegistry}, e.g. `'search'` or `'search' | 'find'`.
|
|
1553
|
+
* @typeParam Cache - The interaction cache state, `'cached'` by default.
|
|
1554
|
+
*
|
|
1555
|
+
* @example
|
|
1556
|
+
* ```ts
|
|
1557
|
+
* \@AutocompleteRoute('search')
|
|
1558
|
+
* class SearchAutocomplete extends AutocompleteHandler<'search'> {
|
|
1559
|
+
* async execute() {
|
|
1560
|
+
* await this.match({
|
|
1561
|
+
* query: (value, respond) => respond([{ name: value, value }])
|
|
1562
|
+
* });
|
|
1563
|
+
* }
|
|
1564
|
+
* }
|
|
1565
|
+
* ```
|
|
1566
|
+
*/
|
|
1567
|
+
var AutocompleteHandler = class extends BaseHandler {
|
|
1568
|
+
constructor(event, core) {
|
|
1569
|
+
super(event, core);
|
|
1570
|
+
}
|
|
1571
|
+
/**
|
|
1572
|
+
* The focused option for this interaction. `value` is always the raw partial string the user is typing,
|
|
1573
|
+
* even for an integer or number option, so coerce it yourself when you need a number.
|
|
1574
|
+
*/
|
|
1575
|
+
decodedFocused;
|
|
1576
|
+
get focused() {
|
|
1577
|
+
if (this.decodedFocused) return this.decodedFocused;
|
|
1578
|
+
const raw = this.event.options.getFocused(true);
|
|
1579
|
+
const focused = {
|
|
1580
|
+
name: raw.name,
|
|
1581
|
+
value: raw.value
|
|
1582
|
+
};
|
|
1583
|
+
this.decodedFocused = focused;
|
|
1584
|
+
return focused;
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* Run the arm for the focused field. An autocomplete always dispatches by which field is focused, so
|
|
1588
|
+
* match is how you read it. There is no single-field shortcut the way slash/component handlers have one.
|
|
1589
|
+
*
|
|
1590
|
+
* Provide one arm per autocompletable field across the registered commands, keyed by field name. Each
|
|
1591
|
+
* arm receives the focused partial value and a `respond` pinned to that field's choice value type. The
|
|
1592
|
+
* arms must cover every autocompletable field, a missing field or an unknown key is a compile error. A
|
|
1593
|
+
* focused field with no arm, only reachable from a stale-deployed command, throws at runtime.
|
|
1594
|
+
*
|
|
1595
|
+
* @param arms - One handler per autocompletable field, keyed by field name.
|
|
1596
|
+
* @returns The result of the arm that ran.
|
|
1597
|
+
*
|
|
1598
|
+
* @example
|
|
1599
|
+
* ```ts
|
|
1600
|
+
* \@AutocompleteRoute('search')
|
|
1601
|
+
* class SearchAutocomplete extends AutocompleteHandler<'search'> {
|
|
1602
|
+
* async execute() {
|
|
1603
|
+
* await this.match({
|
|
1604
|
+
* query: (value, respond) => respond(titles(value).map((s) => ({ name: s, value: s }))),
|
|
1605
|
+
* tag: (value, respond) => respond(tags(value).map((s) => ({ name: s, value: s })))
|
|
1606
|
+
* });
|
|
1607
|
+
* }
|
|
1608
|
+
* }
|
|
1609
|
+
* ```
|
|
1610
|
+
*/
|
|
1611
|
+
async match(arms) {
|
|
1612
|
+
const { name, value } = this.focused;
|
|
1613
|
+
const respond = (choices) => this.event.respond(choices);
|
|
1614
|
+
const arm = arms[name];
|
|
1615
|
+
if (!arm) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.AutocompleteMatchArmMissing, [name]);
|
|
1616
|
+
return await arm(value, respond);
|
|
1617
|
+
}
|
|
1618
|
+
/** The firing command route, for a field whose completion differs per registered command. */
|
|
1619
|
+
get route() {
|
|
1620
|
+
return slashRouteOf(this.event);
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* The already-entered options on this command, restricted to the kinds Discord resolves during autocomplete
|
|
1624
|
+
* (string, integer, number, boolean) with every read nullable, since a sibling is partial while the user
|
|
1625
|
+
* types the focused field. Read the focused field from `this.focused`, not here.
|
|
1626
|
+
*/
|
|
1627
|
+
get options() {
|
|
1628
|
+
return this.event.options;
|
|
1629
|
+
}
|
|
1630
|
+
};
|
|
1631
|
+
|
|
1632
|
+
//#endregion
|
|
1633
|
+
//#region src/handlers/interaction/components/ComponentHandler.ts
|
|
1634
|
+
/**
|
|
1635
|
+
* Shared base the customId-routed component handlers extend.
|
|
1636
|
+
*
|
|
1637
|
+
* Not a public entry point. You should be using {@link ButtonHandler}, {@link SelectMenuHandler}, or
|
|
1638
|
+
* {@link ModalHandler} instead. This class only carries the customId decode and route-matching plumbing
|
|
1639
|
+
* those bases share, so DO NOT use it directly.
|
|
1640
|
+
*
|
|
1641
|
+
* @typeParam Event - The component interaction type this handler processes
|
|
1642
|
+
* @typeParam Defs - The customId route definitions registered on the concrete handler
|
|
1643
|
+
*/
|
|
1644
|
+
var ComponentHandler = class extends InteractionHandler {
|
|
1645
|
+
get registeredDefs() {
|
|
1646
|
+
const defs = Reflect.getMetadata(_seedcord_core_internal.ComponentDefsKey, this.constructor);
|
|
1647
|
+
if (!defs) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CustomIdHandlerRouteMissing, [this.constructor.name]);
|
|
1648
|
+
return defs;
|
|
1649
|
+
}
|
|
1650
|
+
decoded;
|
|
1651
|
+
get route() {
|
|
1652
|
+
if (this.decoded) return this.decoded;
|
|
1653
|
+
const decoded = (0, _seedcord_core_internal.decodeFor)(this.registeredDefs, this.event.customId);
|
|
1654
|
+
this.decoded = decoded;
|
|
1655
|
+
return decoded;
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1658
|
+
* The decoded params of the single route this handler is registered for.
|
|
1659
|
+
*
|
|
1660
|
+
* Reading this decodes `this.event.customId` once (cached after the first read) and throws
|
|
1661
|
+
* `StaleCustomId` or `InvalidCustomId` when the wire no longer matches the current shape, which the
|
|
1662
|
+
* controller boundary turns into a reply. On a handler registered for several routes this is
|
|
1663
|
+
* `never`, so use {@link match} instead.
|
|
1664
|
+
*/
|
|
1665
|
+
get params() {
|
|
1666
|
+
return this.route.params;
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Run the arm for whichever route the component was minted from. Use this only when the handler is
|
|
1670
|
+
* registered for several routes. A single-route handler reads `this.params` directly. On a multi-route
|
|
1671
|
+
* handler `this.params` is `never`, so match is the only way to read the decoded params.
|
|
1672
|
+
*
|
|
1673
|
+
* Provide one arm per registered route, keyed by its prefix, and each arm receives that route's own
|
|
1674
|
+
* decoded params. The arms must cover every registered def, a missing prefix or an unknown key is a
|
|
1675
|
+
* compile error. Decoding runs before any arm, so a stale or corrupt wire throws before an arm body executes.
|
|
1676
|
+
*
|
|
1677
|
+
* @param arms - One handler per registered route, keyed by prefix.
|
|
1678
|
+
* @returns The result of the arm that ran.
|
|
1679
|
+
*
|
|
1680
|
+
* @example
|
|
1681
|
+
* ```ts
|
|
1682
|
+
* \@ButtonRoute(Approve, Reject)
|
|
1683
|
+
* class ReviewButtons extends ButtonHandler<[typeof Approve, typeof Reject]> {
|
|
1684
|
+
* async execute() {
|
|
1685
|
+
* await this.match({
|
|
1686
|
+
* approve: ({ userId }) => this.event.reply(`approved <@${userId}>`),
|
|
1687
|
+
* reject: ({ userId }) => this.event.reply(`rejected <@${userId}>`)
|
|
1688
|
+
* });
|
|
1689
|
+
* }
|
|
1690
|
+
* }
|
|
1691
|
+
* ```
|
|
1692
|
+
*/
|
|
1693
|
+
async match(arms) {
|
|
1694
|
+
const { prefix, params } = this.route;
|
|
1695
|
+
const arm = arms[prefix];
|
|
1696
|
+
if (!arm) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CustomIdMatchArmMissing, [prefix]);
|
|
1697
|
+
return await arm(params);
|
|
1698
|
+
}
|
|
1699
|
+
};
|
|
1700
|
+
|
|
1701
|
+
//#endregion
|
|
1702
|
+
//#region src/handlers/interaction/components/ButtonHandler.ts
|
|
1703
|
+
/**
|
|
1704
|
+
* Base class for a button interaction handler.
|
|
1705
|
+
*
|
|
1706
|
+
* Register the customId definitions this handler decodes with `@ButtonRoute`, list the same ones in the
|
|
1707
|
+
* generic, then read `this.params` for a single route or `this.match` for several. Passing different
|
|
1708
|
+
* definitions to the decorator and the generic is a compile error.
|
|
1709
|
+
*
|
|
1710
|
+
* @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ApproveId]`.
|
|
1711
|
+
* @typeParam Cache - The interaction cache state, `'cached'` by default.
|
|
1712
|
+
*
|
|
1713
|
+
* @example
|
|
1714
|
+
* ```ts
|
|
1715
|
+
* \@ButtonRoute(ApproveId)
|
|
1716
|
+
* class ApproveButton extends ButtonHandler<[typeof ApproveId]> {
|
|
1717
|
+
* async execute() {
|
|
1718
|
+
* const { userId } = this.params;
|
|
1719
|
+
* await this.event.reply(`approved <@${userId}>`);
|
|
1720
|
+
* }
|
|
1721
|
+
* }
|
|
1722
|
+
* ```
|
|
1723
|
+
*/
|
|
1724
|
+
var ButtonHandler = class extends ComponentHandler {};
|
|
1725
|
+
|
|
1726
|
+
//#endregion
|
|
1727
|
+
//#region src/handlers/interaction/components/ModalHandler.ts
|
|
1728
|
+
/**
|
|
1729
|
+
* Base class for a modal submit handler.
|
|
1730
|
+
*
|
|
1731
|
+
* Register the customId definitions this handler decodes with `@ModalRoute`, list the same ones in the
|
|
1732
|
+
* generic, then read `this.params` for a single route or `this.match` for several. Read the submitted
|
|
1733
|
+
* inputs from `this.event.fields`.
|
|
1734
|
+
*
|
|
1735
|
+
* @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ConfigId]`.
|
|
1736
|
+
* @typeParam Cache - The interaction cache state, `'cached'` by default.
|
|
1737
|
+
*
|
|
1738
|
+
* @example
|
|
1739
|
+
* ```ts
|
|
1740
|
+
* \@ModalRoute(ConfigId)
|
|
1741
|
+
* class ConfigModal extends ModalHandler<[typeof ConfigId]> {
|
|
1742
|
+
* async execute() {
|
|
1743
|
+
* const { guildId } = this.params;
|
|
1744
|
+
* const name = this.event.fields.getTextInputValue('name');
|
|
1745
|
+
* await this.event.reply(`saved ${name} for ${guildId}`);
|
|
1746
|
+
* }
|
|
1747
|
+
* }
|
|
1748
|
+
* ```
|
|
1749
|
+
*/
|
|
1750
|
+
var ModalHandler = class extends ComponentHandler {};
|
|
1751
|
+
|
|
1752
|
+
//#endregion
|
|
1753
|
+
//#region src/handlers/interaction/components/SelectMenuHandler.ts
|
|
1754
|
+
/**
|
|
1755
|
+
* Base class for a select menu handler.
|
|
1756
|
+
*
|
|
1757
|
+
* Pass the select kind first and the customId definitions second, the same order as `@SelectMenuRoute`,
|
|
1758
|
+
* so `this.event` and `this.event.values` are narrowed to that kind. Read `this.params` for a single
|
|
1759
|
+
* route or `this.match` for several.
|
|
1760
|
+
*
|
|
1761
|
+
* @typeParam Kind - The select kind from {@link SelectMenuKind}, e.g. `SelectMenuKind.User`.
|
|
1762
|
+
* @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof AssignId]`.
|
|
1763
|
+
* @typeParam Cache - The interaction cache state, `'cached'` by default.
|
|
1764
|
+
*
|
|
1765
|
+
* @example
|
|
1766
|
+
* ```ts
|
|
1767
|
+
* \@SelectMenuRoute(SelectMenuKind.User, AssignId)
|
|
1768
|
+
* class AssignSelect extends SelectMenuHandler<SelectMenuKind.User, [typeof AssignId]> {
|
|
1769
|
+
* async execute() {
|
|
1770
|
+
* const { roleId } = this.params;
|
|
1771
|
+
* await this.event.reply(`assigning ${this.event.values.length} member(s) to <@&${roleId}>`);
|
|
1772
|
+
* }
|
|
1773
|
+
* }
|
|
1774
|
+
* ```
|
|
1775
|
+
*/
|
|
1776
|
+
var SelectMenuHandler = class extends ComponentHandler {};
|
|
1777
|
+
|
|
1778
|
+
//#endregion
|
|
1779
|
+
//#region src/handlers/event/EventHandler.ts
|
|
1780
|
+
/**
|
|
1781
|
+
* Base class for a Discord client event handler.
|
|
1782
|
+
*
|
|
1783
|
+
* Pass the event name(s) as the generic, the same one(s) as `@RegisterEvent`. A single-event handler reads
|
|
1784
|
+
* `this.event` (the payload tuple) directly. A handler registered for several events branches with `this.match`,
|
|
1785
|
+
* keyed by event name, since the union of payload tuples is not directly readable.
|
|
1786
|
+
*
|
|
1787
|
+
* @typeParam Names - One or more `ClientEvents` keys, e.g. `Events.MessageCreate` or `Events.MessageCreate | Events.MessageUpdate`.
|
|
1788
|
+
*
|
|
1789
|
+
* @example
|
|
1790
|
+
* ```ts
|
|
1791
|
+
* \@RegisterEvent([Events.MessageCreate], [Events.MessageUpdate])
|
|
1792
|
+
* class PingPong extends EventHandler<Events.MessageCreate | Events.MessageUpdate> {
|
|
1793
|
+
* async execute() {
|
|
1794
|
+
* await this.match({
|
|
1795
|
+
* [Events.MessageCreate]: (message) => message.reply('pong'),
|
|
1796
|
+
* [Events.MessageUpdate]: (_oldMessage, edited) => edited.reply('pong')
|
|
1797
|
+
* });
|
|
1798
|
+
* }
|
|
1799
|
+
* }
|
|
1800
|
+
* ```
|
|
1801
|
+
*/
|
|
1802
|
+
var EventHandler = class extends BaseHandler {
|
|
1803
|
+
firedEvent;
|
|
1804
|
+
constructor(event, core, eventName) {
|
|
1805
|
+
super(event, core);
|
|
1806
|
+
this.firedEvent = eventName;
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Run the arm for whichever event fired. Use this only when the handler is registered for several
|
|
1810
|
+
* events. A single-event handler reads `this.event` directly. On a multi-event handler `this.event` is
|
|
1811
|
+
* `never`, so match is the only way to read the payload.
|
|
1812
|
+
*
|
|
1813
|
+
* Provide one arm per registered event, keyed by its name, and each arm receives that event's own
|
|
1814
|
+
* payload tuple. The arms must cover every event in the generic, a missing event or an unknown key is a
|
|
1815
|
+
* compile error.
|
|
1816
|
+
*
|
|
1817
|
+
* @param arms - One handler per registered event, keyed by event name.
|
|
1818
|
+
* @returns The result of the arm that ran.
|
|
1819
|
+
*
|
|
1820
|
+
* @example
|
|
1821
|
+
* ```ts
|
|
1822
|
+
* \@RegisterEvent([Events.MessageCreate], [Events.MessageUpdate])
|
|
1823
|
+
* class MessageWatcher extends EventHandler<Events.MessageCreate | Events.MessageUpdate> {
|
|
1824
|
+
* async execute() {
|
|
1825
|
+
* await this.match({
|
|
1826
|
+
* [Events.MessageCreate]: (message) => this.scan(message),
|
|
1827
|
+
* [Events.MessageUpdate]: (_old, updated) => this.scan(updated)
|
|
1828
|
+
* });
|
|
1829
|
+
* }
|
|
1830
|
+
* }
|
|
1831
|
+
* ```
|
|
1832
|
+
*/
|
|
1833
|
+
async match(arms) {
|
|
1834
|
+
const name = this.firedEvent;
|
|
1835
|
+
if (name === void 0) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.EventMatchArmMissing, ["<no event>"]);
|
|
1836
|
+
const arm = arms[name];
|
|
1837
|
+
if (typeof arm !== "function") throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.EventMatchArmMissing, [name]);
|
|
1838
|
+
return await arm(...this.getEvent());
|
|
1839
|
+
}
|
|
1840
|
+
};
|
|
1841
|
+
|
|
1842
|
+
//#endregion
|
|
1843
|
+
//#region src/handlers/event/EventMiddleware.ts
|
|
1844
|
+
/**
|
|
1845
|
+
* Base class for Discord event middleware.
|
|
1846
|
+
*
|
|
1847
|
+
* Middleware runs before event handlers and can stop the event by throwing a `Silence`. Unlike `EventHandler`, it
|
|
1848
|
+
* runs the SAME for every event it is registered for, so it has no `match`. Specify a single event in the
|
|
1849
|
+
* generic and `{ events }` to read `this.event` fully typed. Span several events (or omit `{ events }` for a
|
|
1850
|
+
* catchall) and `this.event` narrows to `never`, read `this.eventName` and do work that does not depend on the
|
|
1851
|
+
* payload shape. A middleware that needs each event's payload is written one event per class.
|
|
1852
|
+
*
|
|
1853
|
+
* @typeParam EventName - One or more `ClientEvents` keys. Defaults to every event (catchall).
|
|
1854
|
+
*/
|
|
1855
|
+
var EventMiddleware = class extends BaseHandler {
|
|
1856
|
+
firedEvent;
|
|
1857
|
+
constructor(event, core, eventName) {
|
|
1858
|
+
super(event, core);
|
|
1859
|
+
this.firedEvent = eventName;
|
|
1860
|
+
}
|
|
1861
|
+
/**
|
|
1862
|
+
* The event that fired this middleware. Read it on a catchall or multi-event middleware, where `this.event`
|
|
1863
|
+
* is `never`, to do work that does not depend on the payload shape. A single-event middleware reads
|
|
1864
|
+
* `this.event` directly and does not need this.
|
|
1865
|
+
*/
|
|
1866
|
+
get eventName() {
|
|
1867
|
+
if (this.firedEvent === void 0) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.EventMiddlewareNameUnavailable);
|
|
1868
|
+
return this.firedEvent;
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1871
|
+
|
|
1872
|
+
//#endregion
|
|
1873
|
+
//#region src/pagination/controls.ts
|
|
1874
|
+
const DEFAULT_LABEL = {
|
|
1875
|
+
first: "First",
|
|
1876
|
+
prev: "Prev",
|
|
1877
|
+
indicator: "",
|
|
1878
|
+
next: "Next",
|
|
1879
|
+
last: "Last"
|
|
1880
|
+
};
|
|
1881
|
+
const CONTROL_SLOT = {
|
|
1882
|
+
first: 0,
|
|
1883
|
+
prev: 1,
|
|
1884
|
+
indicator: 2,
|
|
1885
|
+
next: 3,
|
|
1886
|
+
last: 4
|
|
1887
|
+
};
|
|
1888
|
+
const MAX_BUTTONS_PER_ROW = 5;
|
|
1889
|
+
var Controls = class {
|
|
1890
|
+
cursor;
|
|
1891
|
+
view;
|
|
1892
|
+
constructor(cursor, view) {
|
|
1893
|
+
this.cursor = cursor;
|
|
1894
|
+
this.view = view;
|
|
1895
|
+
}
|
|
1896
|
+
button(key, cosmetics) {
|
|
1897
|
+
const { target, disabled } = this.navState(key);
|
|
1898
|
+
const defaultLabel = key === "indicator" ? this.indicatorText() : DEFAULT_LABEL[key];
|
|
1899
|
+
const label = cosmetics?.label ?? (key === "indicator" || !cosmetics?.emoji ? defaultLabel : void 0);
|
|
1900
|
+
const button = new _discordjs_builders.ButtonBuilder().setCustomId(this.cursor.encode({
|
|
1901
|
+
page: Math.min(Math.max(0, target), _seedcord_core_internal.PAGE_MAX),
|
|
1902
|
+
slot: CONTROL_SLOT[key]
|
|
1903
|
+
})).setStyle(cosmetics?.style ?? discord_js.ButtonStyle.Secondary).setDisabled(disabled);
|
|
1904
|
+
if (label !== void 0) button.setLabel(label);
|
|
1905
|
+
if (cosmetics?.emoji) button.setEmoji(cosmetics.emoji);
|
|
1906
|
+
return button;
|
|
1907
|
+
}
|
|
1908
|
+
navState(key) {
|
|
1909
|
+
const { page, totalPages, hasPrev, hasNext } = this.view;
|
|
1910
|
+
const knownTotal = totalPages !== void 0;
|
|
1911
|
+
return {
|
|
1912
|
+
target: {
|
|
1913
|
+
first: 0,
|
|
1914
|
+
prev: page - 1,
|
|
1915
|
+
indicator: page,
|
|
1916
|
+
next: page + 1,
|
|
1917
|
+
last: knownTotal ? totalPages - 1 : page
|
|
1918
|
+
}[key],
|
|
1919
|
+
disabled: {
|
|
1920
|
+
first: !hasPrev,
|
|
1921
|
+
prev: !hasPrev,
|
|
1922
|
+
indicator: true,
|
|
1923
|
+
next: !hasNext || page + 1 > _seedcord_core_internal.PAGE_MAX,
|
|
1924
|
+
last: !hasNext || !knownTotal || totalPages - 1 > _seedcord_core_internal.PAGE_MAX
|
|
1925
|
+
}[key]
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
row(...keys) {
|
|
1929
|
+
if (keys.length === 0) throw new _seedcord_errors_internal.SeedcordRangeError(_seedcord_errors.SeedcordErrorCode.PaginationEmptyControls);
|
|
1930
|
+
if (keys.length > MAX_BUTTONS_PER_ROW) throw new _seedcord_errors_internal.SeedcordRangeError(_seedcord_errors.SeedcordErrorCode.PaginationTooManyControls, [keys.length]);
|
|
1931
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1932
|
+
for (const key of keys) {
|
|
1933
|
+
if (seen.has(key)) throw new _seedcord_errors_internal.SeedcordTypeError(_seedcord_errors.SeedcordErrorCode.PaginationDuplicateControls, [key]);
|
|
1934
|
+
seen.add(key);
|
|
1935
|
+
}
|
|
1936
|
+
return new _discordjs_builders.ActionRowBuilder().addComponents(keys.map((key) => this.button(key)));
|
|
1937
|
+
}
|
|
1938
|
+
indicatorText() {
|
|
1939
|
+
const { page, totalPages } = this.view;
|
|
1940
|
+
return totalPages === void 0 ? `Page ${page + 1}` : `Page ${page + 1} of ${totalPages}`;
|
|
1941
|
+
}
|
|
1942
|
+
};
|
|
1943
|
+
|
|
1944
|
+
//#endregion
|
|
1945
|
+
//#region src/pagination/render.ts
|
|
1946
|
+
var PageContainer = class extends _seedcord_core.BuilderComponent {
|
|
1947
|
+
constructor(view, renderItem, controlRow) {
|
|
1948
|
+
super("container");
|
|
1949
|
+
const base = view.page * view.perPage;
|
|
1950
|
+
let lines = [];
|
|
1951
|
+
const flush = () => {
|
|
1952
|
+
if (lines.length === 0) return;
|
|
1953
|
+
this.instance.addTextDisplayComponents(new _discordjs_builders.TextDisplayBuilder().setContent(lines.join("\n")));
|
|
1954
|
+
lines = [];
|
|
1955
|
+
};
|
|
1956
|
+
view.items.forEach((item, offset) => {
|
|
1957
|
+
const rendered = renderItem(item, base + offset);
|
|
1958
|
+
if (typeof rendered === "string") {
|
|
1959
|
+
lines.push(rendered);
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
flush();
|
|
1963
|
+
this.instance.addSectionComponents(rendered.component);
|
|
1964
|
+
});
|
|
1965
|
+
flush();
|
|
1966
|
+
this.instance.addActionRowComponents(controlRow);
|
|
1967
|
+
}
|
|
1968
|
+
};
|
|
1969
|
+
function toReplyResponse(renderable) {
|
|
1970
|
+
if (typeof renderable === "string") return { components: [new _discordjs_builders.TextDisplayBuilder().setContent(renderable)] };
|
|
1971
|
+
if (Array.isArray(renderable)) return { components: renderable };
|
|
1972
|
+
return renderable;
|
|
1973
|
+
}
|
|
1974
|
+
const ARRAY_KEYS = [
|
|
1975
|
+
"first",
|
|
1976
|
+
"prev",
|
|
1977
|
+
"indicator",
|
|
1978
|
+
"next",
|
|
1979
|
+
"last"
|
|
1980
|
+
];
|
|
1981
|
+
const CURSOR_KEYS = [
|
|
1982
|
+
"prev",
|
|
1983
|
+
"indicator",
|
|
1984
|
+
"next"
|
|
1985
|
+
];
|
|
1986
|
+
/**
|
|
1987
|
+
* Build a page's V2 reply. A `render` override builds the whole tree, otherwise the default container lists
|
|
1988
|
+
* the items and appends the controls (all five for a known total, prev/indicator/next for a cursor source).
|
|
1989
|
+
*/
|
|
1990
|
+
function renderPage(view, cursor, config) {
|
|
1991
|
+
const controls = new Controls(cursor, view);
|
|
1992
|
+
if (config.render) return toReplyResponse(config.render(view, controls));
|
|
1993
|
+
return { components: [new PageContainer(view, config.renderItem ?? String, controls.row(...view.totalPages === void 0 ? CURSOR_KEYS : ARRAY_KEYS)).component] };
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
//#endregion
|
|
1997
|
+
//#region src/pagination/Paginator.ts
|
|
1998
|
+
function contextOf(interaction, core) {
|
|
1999
|
+
return {
|
|
2000
|
+
interaction,
|
|
2001
|
+
user: interaction.user,
|
|
2002
|
+
guild: interaction.guild,
|
|
2003
|
+
...core && { core }
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
2006
|
+
/**
|
|
2007
|
+
* A restart-proof paginator. Each nav button's customId encodes its full target page, so clicks are
|
|
2008
|
+
* idempotent and survive a restart. A persistent `@ButtonRoute` on `Handler` dispatches the clicks.
|
|
2009
|
+
*
|
|
2010
|
+
* @typeParam Item - The item type, inferred from the source.
|
|
2011
|
+
* @typeParam Prefix - The route prefix, inferred from `config.prefix`.
|
|
2012
|
+
*
|
|
2013
|
+
* @example
|
|
2014
|
+
* ```ts
|
|
2015
|
+
* export const Bans = new Paginator({
|
|
2016
|
+
* prefix: 'bans',
|
|
2017
|
+
* source: new ArraySource((ctx) => ctx.guild.bans.fetch().then((b) => [...b.values()]), { perPage: 10 }),
|
|
2018
|
+
* renderItem: (ban) => ban.user.tag
|
|
2019
|
+
* });
|
|
2020
|
+
*
|
|
2021
|
+
* \@ButtonRoute(Bans.cursor)
|
|
2022
|
+
* export class BansNav extends Bans.Handler {}
|
|
2023
|
+
* ```
|
|
2024
|
+
*/
|
|
2025
|
+
var Paginator = class {
|
|
2026
|
+
/** The page cursor, pass it to your `@ButtonRoute`. */
|
|
2027
|
+
cursor;
|
|
2028
|
+
/** The nav handler base. Extend it with an empty body and decorate it, `@ButtonRoute(p.cursor)`. */
|
|
2029
|
+
Handler;
|
|
2030
|
+
config;
|
|
2031
|
+
constructor(config) {
|
|
2032
|
+
this.cursor = (0, _seedcord_core_internal.pageCursor)(config.prefix);
|
|
2033
|
+
this.config = config;
|
|
2034
|
+
const loadPage = (ctx, n) => this.page(ctx, n);
|
|
2035
|
+
this.Handler = class Nav extends ButtonHandler {
|
|
2036
|
+
async execute() {
|
|
2037
|
+
await this.event.deferUpdate();
|
|
2038
|
+
const response = await loadPage(contextOf(this.event, this.core), this.params.page);
|
|
2039
|
+
await new ReplySender(this.event).edit(this.event.message, response);
|
|
2040
|
+
}
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
/** Render page 0 and send it, picking reply or followUp from the interaction's state. */
|
|
2044
|
+
async start(interaction, core) {
|
|
2045
|
+
const response = await this.page(contextOf(interaction, core), 0);
|
|
2046
|
+
return new ReplySender(interaction).send(response, this.config.ephemeral ?? false);
|
|
2047
|
+
}
|
|
2048
|
+
/** Render a page as a {@link ReplyResponse}. To post it elsewhere, add `flags: MessageFlags.IsComponentsV2`. */
|
|
2049
|
+
async page(ctx, n) {
|
|
2050
|
+
return renderPage(await this.config.source.page(ctx, n), this.cursor, this.config);
|
|
2051
|
+
}
|
|
2052
|
+
};
|
|
2053
|
+
|
|
2054
|
+
//#endregion
|
|
2055
|
+
//#region src/pagination/sources.ts
|
|
2056
|
+
const DEFAULT_PER_PAGE = 10;
|
|
2057
|
+
/**
|
|
2058
|
+
* A source for a bounded list you can load whole. It loads the full list on every click, then slices the
|
|
2059
|
+
* page via {@link paginate}, so the real total is known (a real last button, "Page X of Y"). Cache inside
|
|
2060
|
+
* your loader if the load is expensive.
|
|
2061
|
+
*
|
|
2062
|
+
* @typeParam Item - The item type, inferred from the loader's return.
|
|
2063
|
+
*/
|
|
2064
|
+
var ArraySource = class {
|
|
2065
|
+
load;
|
|
2066
|
+
perPage;
|
|
2067
|
+
constructor(load, opts) {
|
|
2068
|
+
this.load = load;
|
|
2069
|
+
this.perPage = opts?.perPage ?? DEFAULT_PER_PAGE;
|
|
2070
|
+
if (!Number.isSafeInteger(this.perPage) || this.perPage <= 0) throw new _seedcord_errors_internal.SeedcordRangeError(_seedcord_errors.SeedcordErrorCode.PaginationInvalidPerPage, [this.perPage]);
|
|
2071
|
+
}
|
|
2072
|
+
async page(ctx, n) {
|
|
2073
|
+
return (0, _seedcord_core.paginate)(await this.load(ctx), n, this.perPage);
|
|
2074
|
+
}
|
|
2075
|
+
};
|
|
2076
|
+
/**
|
|
2077
|
+
* A source for a large or unknown-length set you fetch one page at a time (SQL LIMIT/OFFSET, a paged API).
|
|
2078
|
+
* The fetcher receives the page index and the page size and reports whether a next page exists. A cursor
|
|
2079
|
+
* source has no cheap total, so `totalPages` is undefined, the last button is omitted, and the indicator
|
|
2080
|
+
* reads "Page X".
|
|
2081
|
+
*
|
|
2082
|
+
* @typeParam Item - The item type, inferred from the fetcher's slice.
|
|
2083
|
+
*/
|
|
2084
|
+
var CursorSource = class {
|
|
2085
|
+
fetch;
|
|
2086
|
+
perPage;
|
|
2087
|
+
constructor(fetch, opts) {
|
|
2088
|
+
this.fetch = fetch;
|
|
2089
|
+
this.perPage = opts?.perPage ?? DEFAULT_PER_PAGE;
|
|
2090
|
+
if (!Number.isSafeInteger(this.perPage) || this.perPage <= 0) throw new _seedcord_errors_internal.SeedcordRangeError(_seedcord_errors.SeedcordErrorCode.PaginationInvalidPerPage, [this.perPage]);
|
|
2091
|
+
}
|
|
2092
|
+
async page(ctx, n) {
|
|
2093
|
+
const page = Math.max(0, Math.trunc(n));
|
|
2094
|
+
const { items, hasNext } = await this.fetch(ctx, page, this.perPage);
|
|
2095
|
+
return {
|
|
2096
|
+
items: [...items],
|
|
2097
|
+
page,
|
|
2098
|
+
perPage: this.perPage,
|
|
2099
|
+
hasPrev: page > 0,
|
|
2100
|
+
hasNext
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
};
|
|
2104
|
+
|
|
2105
|
+
//#endregion
|
|
2106
|
+
//#region src/subscribers/Subscriber.ts
|
|
2107
|
+
/**
|
|
2108
|
+
* Abstract base class for handling application subscribers.
|
|
2109
|
+
*
|
|
2110
|
+
* Provides type-safe access to subscribers data and the core framework instance.
|
|
2111
|
+
* Extend this class to create custom subscriber handlers.
|
|
2112
|
+
*
|
|
2113
|
+
* @typeParam KeyOfSubscribers - The specific subscriber type this handler processes
|
|
2114
|
+
*/
|
|
2115
|
+
var Subscriber = class {
|
|
2116
|
+
data;
|
|
2117
|
+
core;
|
|
2118
|
+
logger;
|
|
2119
|
+
/**
|
|
2120
|
+
* Instantiates the subscriber with event data and core.
|
|
2121
|
+
*
|
|
2122
|
+
* @param data - The subscriber event data
|
|
2123
|
+
* @param core - The core framework instance
|
|
2124
|
+
*/
|
|
2125
|
+
constructor(data, core) {
|
|
2126
|
+
this.data = data;
|
|
2127
|
+
this.core = core;
|
|
2128
|
+
this.logger = new _seedcord_services.Logger(this.constructor.name);
|
|
2129
|
+
}
|
|
2130
|
+
};
|
|
2131
|
+
|
|
2132
|
+
//#endregion
|
|
2133
|
+
//#region src/subscribers/decorators/Subscribe.ts
|
|
2134
|
+
/**
|
|
2135
|
+
* Registers a subscriber handler class with a specific subscriber event.
|
|
2136
|
+
*
|
|
2137
|
+
* Associates the decorated class with a subscriber event type for automatic
|
|
2138
|
+
* registration and execution when the subscriber is emitted.
|
|
2139
|
+
*
|
|
2140
|
+
* @typeParam TSubscriber - The subscriber event name to register for
|
|
2141
|
+
* @param subscriber - The subscriber event name to register for
|
|
2142
|
+
* @param options - Options to configure the subscriber handler registration.
|
|
2143
|
+
* @decorator
|
|
2144
|
+
* @example
|
|
2145
|
+
* ```typescript
|
|
2146
|
+
* \@Subscribe('userJoin')
|
|
2147
|
+
* class WelcomeHandler extends Subscriber<'userJoin'> {
|
|
2148
|
+
* async execute() {
|
|
2149
|
+
* // Handle user join event
|
|
2150
|
+
* }
|
|
2151
|
+
* }
|
|
2152
|
+
* ```
|
|
2153
|
+
* @example
|
|
2154
|
+
* ```ts
|
|
2155
|
+
* \@Subscribe('userJoin', { frequency: 'once' })
|
|
2156
|
+
* // or
|
|
2157
|
+
* \@Subscribe('userJoin')
|
|
2158
|
+
* ```
|
|
2159
|
+
*/
|
|
2160
|
+
function Subscribe(subscriber, options) {
|
|
2161
|
+
return function(constructor) {
|
|
2162
|
+
const meta = {
|
|
2163
|
+
subscriber,
|
|
2164
|
+
frequency: options?.frequency
|
|
2165
|
+
};
|
|
2166
|
+
Reflect.defineMetadata(_seedcord_core_internal.SubscribeMetadataKey, meta, constructor);
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
//#endregion
|
|
2171
|
+
//#region src/subscribers/bases/WebhookLog.ts
|
|
2172
|
+
/**
|
|
2173
|
+
* Abstract webhook logging handler for subscriber events.
|
|
2174
|
+
*
|
|
2175
|
+
* Extends Subscriber to provide webhook-based logging capabilities.
|
|
2176
|
+
* Implementations must define the webhook client to send messages to by implementing the webhook property.
|
|
2177
|
+
*
|
|
2178
|
+
* @typeParam KeyOfSubscribers - The specific subscriber type this handler processes
|
|
2179
|
+
*/
|
|
2180
|
+
var WebhookLog = class extends Subscriber {
|
|
2181
|
+
constructor(data, core) {
|
|
2182
|
+
super(data, core);
|
|
2183
|
+
}
|
|
2184
|
+
};
|
|
2185
|
+
|
|
2186
|
+
//#endregion
|
|
2187
|
+
//#region src/hmr/HmrModuleHandler.ts
|
|
2188
|
+
/**
|
|
2189
|
+
* Reloads THandler and optional TMiddleware modules on HMR updates.
|
|
2190
|
+
*
|
|
2191
|
+
* You can either implement a custom `onHmr()` method in your class by extending `HmrAware`, or if it's a plugin, override the existing `onHmr()` with fully custom logic. Or, use this class's `handle()` method inside your `onHmr()` to get standard HMR handling for your modules based on the provided options you give to the constructor.
|
|
2192
|
+
*
|
|
2193
|
+
* This is only useful during development.
|
|
2194
|
+
*/
|
|
2195
|
+
var HmrModuleHandler = class {
|
|
2196
|
+
store;
|
|
2197
|
+
options;
|
|
2198
|
+
constructor(options) {
|
|
2199
|
+
this.options = {
|
|
2200
|
+
...options,
|
|
2201
|
+
logger: options.logger.inChannel("hmr")
|
|
2202
|
+
};
|
|
2203
|
+
this.store = {
|
|
2204
|
+
fileToHandlers: /* @__PURE__ */ new Map(),
|
|
2205
|
+
fileToMiddlewares: /* @__PURE__ */ new Map(),
|
|
2206
|
+
handlerArtifacts: /* @__PURE__ */ new Map()
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
/**
|
|
2210
|
+
* Handles an HMR update event by reloading affected modules based on the event details and config. It receives an {@link HmrUpdateEvent}, then checks the type of update.
|
|
2211
|
+
*
|
|
2212
|
+
* - If it's a deletion, it unloads the module.
|
|
2213
|
+
* - If it's an update/creation, it checks if the file is tracked, unloads it if so, then reloads the file if it is in the handlers or middlewares directory and is a valid handler/middleware.
|
|
2214
|
+
*/
|
|
2215
|
+
async handle(event) {
|
|
2216
|
+
const { file, affectedModules, type } = event;
|
|
2217
|
+
const { logger, handlersDir, middlewaresDir } = this.options;
|
|
2218
|
+
if (type === "delete" || type === "deleteDir") {
|
|
2219
|
+
this.unload(file);
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
if (type === "update" && !(0, node_fs.existsSync)(file)) {
|
|
2223
|
+
if (this.isTracked(file)) {
|
|
2224
|
+
logger.info(`Received update for non-existent file: ${(0, _seedcord_utils.formatFilePath)(file)}, treating as delete`);
|
|
2225
|
+
this.unload(file);
|
|
2226
|
+
}
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
const rollbackEnabled = event.rollback ?? true;
|
|
2230
|
+
const filesToReload = affectedModules && affectedModules.length > 0 ? affectedModules : [file];
|
|
2231
|
+
const absHandlersDir = (0, node_path.resolve)(process.cwd(), handlersDir);
|
|
2232
|
+
const absMiddlewaresDir = middlewaresDir ? (0, node_path.resolve)(process.cwd(), middlewaresDir) : null;
|
|
2233
|
+
for (const fileToReload of filesToReload) {
|
|
2234
|
+
const isHandler = fileToReload.startsWith(absHandlersDir);
|
|
2235
|
+
const isMiddleware = absMiddlewaresDir ? fileToReload.startsWith(absMiddlewaresDir) : false;
|
|
2236
|
+
if (!isHandler && !isMiddleware) continue;
|
|
2237
|
+
await this.reloadWithRollback(fileToReload, rollbackEnabled);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
async reloadWithRollback(file, rollbackEnabled) {
|
|
2241
|
+
const { logger } = this.options;
|
|
2242
|
+
const snapshot = this.snapshotUnits(file);
|
|
2243
|
+
this.unload(file);
|
|
2244
|
+
if (await this.reloadFile(file)) return;
|
|
2245
|
+
this.unload(file);
|
|
2246
|
+
if (!rollbackEnabled) return;
|
|
2247
|
+
this.restoreUnits(file, snapshot);
|
|
2248
|
+
logger.warn(`${chalk.default.yellow.bold("Rolled back")} ${chalk.default.gray((0, _seedcord_utils.formatFilePath)(file))} to the last-good version`);
|
|
2249
|
+
}
|
|
2250
|
+
/**
|
|
2251
|
+
* Tracks a handler module as being loaded from a specific file.
|
|
2252
|
+
* @param file - The file path the handler was loaded from.
|
|
2253
|
+
* @param handler - The handler module instance.
|
|
2254
|
+
*/
|
|
2255
|
+
trackHandler(file, handler) {
|
|
2256
|
+
let set = this.store.fileToHandlers.get(file);
|
|
2257
|
+
if (!set) {
|
|
2258
|
+
set = /* @__PURE__ */ new Set();
|
|
2259
|
+
this.store.fileToHandlers.set(file, set);
|
|
2260
|
+
}
|
|
2261
|
+
set.add(handler);
|
|
2262
|
+
if (this.options.getArtifacts) {
|
|
2263
|
+
const artifacts = this.options.getArtifacts(handler);
|
|
2264
|
+
this.store.handlerArtifacts.set(handler, artifacts);
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
/**
|
|
2268
|
+
* Tracks a middleware module as being loaded from a specific file.
|
|
2269
|
+
* @param file - The file path the middleware was loaded from.
|
|
2270
|
+
* @param middleware - The middleware module instance.
|
|
2271
|
+
*/
|
|
2272
|
+
trackMiddleware(file, middleware) {
|
|
2273
|
+
let set = this.store.fileToMiddlewares.get(file);
|
|
2274
|
+
if (!set) {
|
|
2275
|
+
set = /* @__PURE__ */ new Set();
|
|
2276
|
+
this.store.fileToMiddlewares.set(file, set);
|
|
2277
|
+
}
|
|
2278
|
+
set.add(middleware);
|
|
2279
|
+
}
|
|
2280
|
+
isTracked(file) {
|
|
2281
|
+
return this.store.fileToHandlers.has(file) || this.store.fileToMiddlewares.has(file);
|
|
2282
|
+
}
|
|
2283
|
+
unload(file) {
|
|
2284
|
+
const { logger } = this.options;
|
|
2285
|
+
const handlers = this.store.fileToHandlers.get(file);
|
|
2286
|
+
if (handlers) {
|
|
2287
|
+
for (const handler of handlers) {
|
|
2288
|
+
const artifacts = this.store.handlerArtifacts.get(handler);
|
|
2289
|
+
this.options.unregisterHandler(handler, artifacts);
|
|
2290
|
+
this.store.handlerArtifacts.delete(handler);
|
|
2291
|
+
const name = handler.name ?? "Handler";
|
|
2292
|
+
logger.info(`${chalk.default.red.bold("Unloaded")} ${chalk.default.cyan.bold(name)} from ${chalk.default.gray((0, _seedcord_utils.formatFilePath)(file))}`);
|
|
2293
|
+
}
|
|
2294
|
+
this.store.fileToHandlers.delete(file);
|
|
2295
|
+
}
|
|
2296
|
+
const middlewares = this.store.fileToMiddlewares.get(file);
|
|
2297
|
+
if (middlewares) {
|
|
2298
|
+
for (const middleware of middlewares) {
|
|
2299
|
+
this.options.unregisterMiddleware?.(middleware);
|
|
2300
|
+
const name = middleware.name ?? "Middleware";
|
|
2301
|
+
logger.info(`${chalk.default.red.bold("Unloaded")} ${chalk.default.cyan.bold(name)} from ${chalk.default.gray((0, _seedcord_utils.formatFilePath)(file))}`);
|
|
2302
|
+
}
|
|
2303
|
+
this.store.fileToMiddlewares.delete(file);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
async reloadFile(file) {
|
|
2307
|
+
const { logger, isHandler, isMiddleware, registerHandler, registerMiddleware } = this.options;
|
|
2308
|
+
if (!(0, node_fs.existsSync)(file)) {
|
|
2309
|
+
logger.info(`File does not exist, skipping reload: ${(0, _seedcord_utils.formatFilePath)(file)}`);
|
|
2310
|
+
return false;
|
|
2311
|
+
}
|
|
2312
|
+
try {
|
|
2313
|
+
let fileUrl = (0, node_url.pathToFileURL)(file).href;
|
|
2314
|
+
if (process.env.VITEST === "true") fileUrl += `?update=${Date.now()}`;
|
|
2315
|
+
const imported = await import(fileUrl);
|
|
2316
|
+
for (const val of Object.values(imported)) if (isHandler(val)) {
|
|
2317
|
+
registerHandler(val, file);
|
|
2318
|
+
this.trackHandler(file, val);
|
|
2319
|
+
const name = val.name ?? "Handler";
|
|
2320
|
+
logger.info(`${chalk.default.blue.bold("Reloaded")} ${chalk.default.cyan.bold(name)} from ${chalk.default.gray((0, _seedcord_utils.formatFilePath)(file))}`);
|
|
2321
|
+
} else if (isMiddleware && registerMiddleware && isMiddleware(val)) {
|
|
2322
|
+
registerMiddleware(val, file);
|
|
2323
|
+
this.trackMiddleware(file, val);
|
|
2324
|
+
const name = val.name ?? "Middleware";
|
|
2325
|
+
logger.info(`${chalk.default.blue.bold("Reloaded")} ${chalk.default.cyan.bold(name)} from ${chalk.default.gray((0, _seedcord_utils.formatFilePath)(file))}`);
|
|
2326
|
+
}
|
|
2327
|
+
return true;
|
|
2328
|
+
} catch (error) {
|
|
2329
|
+
logger.error(`Failed to reload file: ${file}`, error);
|
|
2330
|
+
return false;
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
snapshotUnits(file) {
|
|
2334
|
+
return {
|
|
2335
|
+
handlers: [...this.store.fileToHandlers.get(file) ?? []],
|
|
2336
|
+
middlewares: [...this.store.fileToMiddlewares.get(file) ?? []]
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
2339
|
+
restoreUnits(file, snapshot) {
|
|
2340
|
+
for (const handler of snapshot.handlers) {
|
|
2341
|
+
this.options.registerHandler(handler, file);
|
|
2342
|
+
this.trackHandler(file, handler);
|
|
2343
|
+
}
|
|
2344
|
+
if (this.options.registerMiddleware) for (const middleware of snapshot.middlewares) {
|
|
2345
|
+
this.options.registerMiddleware(middleware, file);
|
|
2346
|
+
this.trackMiddleware(file, middleware);
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
};
|
|
2350
|
+
|
|
2351
|
+
//#endregion
|
|
2352
|
+
//#region src/bot/utilities/miscellaneous/contextMenuLeaves.ts
|
|
2353
|
+
/**
|
|
2354
|
+
* Collects the names of every registered context-menu command across a set of command builders, split into a
|
|
2355
|
+
* user set and a message set. The same builder may appear more than once (a guild command is pushed once per
|
|
2356
|
+
* guild), and slash commands carry no context-menu name, so both collapse here. The split mirrors the
|
|
2357
|
+
* controller's two name-keyed maps, so the boot guard checks each name against its own kind.
|
|
2358
|
+
*
|
|
2359
|
+
* @internal
|
|
2360
|
+
*/
|
|
2361
|
+
function contextMenuLeaves(commands) {
|
|
2362
|
+
const user = /* @__PURE__ */ new Set();
|
|
2363
|
+
const message = /* @__PURE__ */ new Set();
|
|
2364
|
+
for (const command of commands) {
|
|
2365
|
+
if (!(command instanceof _discordjs_builders.ContextMenuCommandBuilder)) continue;
|
|
2366
|
+
if (command.type === discord_js.ApplicationCommandType.User) user.add(command.name);
|
|
2367
|
+
else message.add(command.name);
|
|
2368
|
+
}
|
|
2369
|
+
return {
|
|
2370
|
+
user,
|
|
2371
|
+
message
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
//#endregion
|
|
2376
|
+
//#region src/bot/utilities/miscellaneous/slashRouteLeaves.ts
|
|
2377
|
+
/**
|
|
2378
|
+
* Collects the route keys of every executable slash leaf across a set of command builders, deduplicated. The
|
|
2379
|
+
* same builder may appear more than once (a guild command is pushed once per guild), and context-menu
|
|
2380
|
+
* commands carry no slash route, so both collapse here. Route keys come from the same {@link routeLeavesOf}
|
|
2381
|
+
* that `seedcord codegen` reads, so the boot-time check compares against the generated registry's keys.
|
|
2382
|
+
*
|
|
2383
|
+
* @internal
|
|
2384
|
+
*/
|
|
2385
|
+
function slashRouteLeaves(commands) {
|
|
2386
|
+
const leaves = /* @__PURE__ */ new Set();
|
|
2387
|
+
for (const command of commands) {
|
|
2388
|
+
if (!(command instanceof _discordjs_builders.SlashCommandBuilder)) continue;
|
|
2389
|
+
for (const leaf of (0, _seedcord_utils_internal.routeLeavesOf)(command.toJSON())) leaves.add(leaf.route);
|
|
2390
|
+
}
|
|
2391
|
+
return leaves;
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
//#endregion
|
|
2395
|
+
//#region src/bot/controllers/CommandRegistry.ts
|
|
2396
|
+
/**
|
|
2397
|
+
* Manages Discord application command registration and deployment.
|
|
2398
|
+
*
|
|
2399
|
+
* Scans command directories, builds command structures, and registers both global and guild-scoped commands
|
|
2400
|
+
* to Discord's API. Accessed via `core.bot.commands`. Do not construct it directly.
|
|
2401
|
+
*/
|
|
2402
|
+
var CommandRegistry = class {
|
|
2403
|
+
core;
|
|
2404
|
+
onDeployed;
|
|
2405
|
+
name = "Commands";
|
|
2406
|
+
logger = new _seedcord_services.Logger("Commands");
|
|
2407
|
+
isInitialised = false;
|
|
2408
|
+
globalCommands = [];
|
|
2409
|
+
guildCommands = new discord_js.Collection();
|
|
2410
|
+
ctorToCommand = /* @__PURE__ */ new Map();
|
|
2411
|
+
hmrHandler;
|
|
2412
|
+
pendingEvents = /* @__PURE__ */ new Map();
|
|
2413
|
+
constructor(core, onDeployed) {
|
|
2414
|
+
this.core = core;
|
|
2415
|
+
this.onDeployed = onDeployed;
|
|
2416
|
+
const commandsDir = this.core.config.bot.commands.path;
|
|
2417
|
+
if (!commandsDir) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CoreControllerPathMissing, ["CommandRegistry", "commands"]);
|
|
2418
|
+
if (!envapt.Envapter.isDevelopment && !envapt.Envapter.isTest) return;
|
|
2419
|
+
this.hmrHandler = new HmrModuleHandler({
|
|
2420
|
+
handlersDir: commandsDir,
|
|
2421
|
+
isHandler: this.isCommandClass.bind(this),
|
|
2422
|
+
registerHandler: this.registerCommand.bind(this),
|
|
2423
|
+
unregisterHandler: this.unregisterCommand.bind(this),
|
|
2424
|
+
getArtifacts: this.getArtifacts.bind(this),
|
|
2425
|
+
logger: this.logger
|
|
2426
|
+
});
|
|
2427
|
+
}
|
|
2428
|
+
getArtifacts(ctor) {
|
|
2429
|
+
return this.ctorToCommand.get(ctor);
|
|
2430
|
+
}
|
|
2431
|
+
/** @internal */
|
|
2432
|
+
async init() {
|
|
2433
|
+
if (this.isInitialised) return;
|
|
2434
|
+
this.isInitialised = true;
|
|
2435
|
+
const commandsDir = this.core.config.bot.commands.path;
|
|
2436
|
+
if (!commandsDir) return;
|
|
2437
|
+
this.logger.info(chalk.default.bold(commandsDir));
|
|
2438
|
+
await this.loadCommands(commandsDir);
|
|
2439
|
+
this.logger.utils.summary("Loaded commands", {
|
|
2440
|
+
global: this.globalCommands.length,
|
|
2441
|
+
"guild groups": this.guildCommands.size
|
|
2442
|
+
});
|
|
2443
|
+
getDevChannel()?.on("seedcord:refresh-commands", (data) => {
|
|
2444
|
+
this.refresh(data.shouldRefresh);
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
/** @internal */
|
|
2448
|
+
async refresh(shouldRefresh = true) {
|
|
2449
|
+
if (!shouldRefresh) {
|
|
2450
|
+
this.logger.info(chalk.default.italic("Command refresh cancelled."));
|
|
2451
|
+
this.pendingEvents.clear();
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2454
|
+
this.logger.info(chalk.default.italic("Refreshing commands..."));
|
|
2455
|
+
for (const event of this.pendingEvents.values()) await this.hmrHandler?.handle(event);
|
|
2456
|
+
this.pendingEvents.clear();
|
|
2457
|
+
await this.setCommands();
|
|
2458
|
+
}
|
|
2459
|
+
/** @internal */
|
|
2460
|
+
async onHmr(event) {
|
|
2461
|
+
const commandsDir = this.core.config.bot.commands.path;
|
|
2462
|
+
if (commandsDir && event.file.startsWith((0, node_path.resolve)(process.cwd(), commandsDir))) {
|
|
2463
|
+
this.pendingEvents.delete(event.file);
|
|
2464
|
+
this.pendingEvents.set(event.file, event);
|
|
2465
|
+
getDevChannel()?.send("seedcord:commands-update-prompt", { files: [...this.pendingEvents.keys()].map((f) => (0, _seedcord_utils.formatFilePath)(f)) });
|
|
2466
|
+
} else await this.hmrHandler?.handle(event);
|
|
2467
|
+
}
|
|
2468
|
+
async loadCommands(dir) {
|
|
2469
|
+
await (0, _seedcord_utils.traverseDirectory)(dir, (fullPath, rel, mod) => {
|
|
2470
|
+
for (const exported of Object.values(mod)) if (this.isCommandClass(exported)) {
|
|
2471
|
+
this.registerCommand(exported, rel);
|
|
2472
|
+
this.hmrHandler?.trackHandler(fullPath, exported);
|
|
2473
|
+
}
|
|
2474
|
+
}, this.logger);
|
|
2475
|
+
}
|
|
2476
|
+
isCommandClass(obj) {
|
|
2477
|
+
if (typeof obj !== "function") return false;
|
|
2478
|
+
return obj.prototype instanceof _seedcord_core.BuilderComponent && Reflect.hasMetadata(_seedcord_core_internal.CommandMetadataKey, obj);
|
|
2479
|
+
}
|
|
2480
|
+
registerCommand(ctor, rel) {
|
|
2481
|
+
const meta = Reflect.getMetadata(_seedcord_core_internal.CommandMetadataKey, ctor);
|
|
2482
|
+
if (!meta) return;
|
|
2483
|
+
const comp = new ctor().component;
|
|
2484
|
+
const commandType = comp instanceof _discordjs_builders.SlashCommandBuilder ? "slash command" : "context menu command";
|
|
2485
|
+
if (meta.scope === "global") {
|
|
2486
|
+
this.globalCommands.push(comp);
|
|
2487
|
+
this.logger.utils.registration(ctor.name, rel);
|
|
2488
|
+
this.logger.utils.item(`Global ${commandType}: ${chalk.default.bold.cyan(comp.name)}`);
|
|
2489
|
+
} else {
|
|
2490
|
+
for (const g of meta.guilds) {
|
|
2491
|
+
const arr = this.guildCommands.get(g) ?? [];
|
|
2492
|
+
arr.push(comp);
|
|
2493
|
+
this.guildCommands.set(g, arr);
|
|
2494
|
+
}
|
|
2495
|
+
this.logger.utils.registration(ctor.name, rel);
|
|
2496
|
+
this.logger.utils.item(`Guild ${commandType}: ${chalk.default.bold.cyan(comp.name)} for ${chalk.default.magenta.bold(meta.guilds.length)} guild(s)`);
|
|
2497
|
+
}
|
|
2498
|
+
this.ctorToCommand.set(ctor, {
|
|
2499
|
+
name: comp.name,
|
|
2500
|
+
scope: meta.scope,
|
|
2501
|
+
...meta.scope === "guild" && { guilds: meta.guilds }
|
|
2502
|
+
});
|
|
2503
|
+
}
|
|
2504
|
+
unregisterCommand(ctor, artifacts) {
|
|
2505
|
+
const info = artifacts ?? this.ctorToCommand.get(ctor);
|
|
2506
|
+
if (!info) return;
|
|
2507
|
+
if (info.scope === "global") {
|
|
2508
|
+
const idx = this.globalCommands.findIndex((c) => c.name === info.name);
|
|
2509
|
+
if (idx !== -1) this.globalCommands.splice(idx, 1);
|
|
2510
|
+
} else for (const g of info.guilds ?? []) {
|
|
2511
|
+
const arr = this.guildCommands.get(g);
|
|
2512
|
+
if (arr) {
|
|
2513
|
+
const idx = arr.findIndex((c) => c.name === info.name);
|
|
2514
|
+
if (idx !== -1) arr.splice(idx, 1);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
this.ctorToCommand.delete(ctor);
|
|
2518
|
+
}
|
|
2519
|
+
/** @internal */
|
|
2520
|
+
async setCommands() {
|
|
2521
|
+
const result = {
|
|
2522
|
+
global: new discord_js.Collection(),
|
|
2523
|
+
guilds: new discord_js.Collection()
|
|
2524
|
+
};
|
|
2525
|
+
if (this.globalCommands.length > 0) {
|
|
2526
|
+
const deployed = await this.core.bot.client.application?.commands.set(this.globalCommands);
|
|
2527
|
+
if (deployed) result.global = deployed;
|
|
2528
|
+
const tag = this.globalCommands.length === 1 ? "command" : "commands";
|
|
2529
|
+
this.logger.utils.summary("Configured global", { [tag]: this.globalCommands.length });
|
|
2530
|
+
this.logger.utils.item(`${this.globalCommands.map((command) => chalk.default.bold.cyan(command.name)).join(", ")}`);
|
|
2531
|
+
}
|
|
2532
|
+
for (const [guildId, commands] of this.guildCommands.entries()) {
|
|
2533
|
+
const guild = this.core.bot.client.guilds.cache.get(guildId);
|
|
2534
|
+
if (!guild) {
|
|
2535
|
+
this.logger.warn(`Guild with ID ${guildId} not found, skipping command registration.`);
|
|
2536
|
+
continue;
|
|
2537
|
+
}
|
|
2538
|
+
const deployed = await guild.commands.set(commands);
|
|
2539
|
+
result.guilds.set(guildId, deployed);
|
|
2540
|
+
const tag = commands.length === 1 ? "command" : "commands";
|
|
2541
|
+
this.logger.utils.summary(`Configured commands for ${chalk.default.bold.yellow(guild.name)}`, { [tag]: commands.length });
|
|
2542
|
+
this.logger.utils.item(`${commands.map((command) => chalk.default.bold.cyan(command.name)).join(", ")}`);
|
|
2543
|
+
}
|
|
2544
|
+
this.onDeployed?.(result);
|
|
2545
|
+
return result;
|
|
2546
|
+
}
|
|
2547
|
+
/** The deduplicated slash route keys across every global and guild command. @internal */
|
|
2548
|
+
routeLeaves() {
|
|
2549
|
+
return slashRouteLeaves([...this.globalCommands, ...this.guildCommands.values()].flat());
|
|
2550
|
+
}
|
|
2551
|
+
/** The registered context-menu command names, split by kind, across every global and guild command. @internal */
|
|
2552
|
+
contextMenuLeaves() {
|
|
2553
|
+
return contextMenuLeaves([...this.globalCommands, ...this.guildCommands.values()].flat());
|
|
2554
|
+
}
|
|
2555
|
+
};
|
|
2556
|
+
|
|
2557
|
+
//#endregion
|
|
2558
|
+
//#region src/miscellaneous/deriveEventActor.ts
|
|
2559
|
+
function deriveEventActor(args) {
|
|
2560
|
+
const tuple = Array.isArray(args) ? args : [args];
|
|
2561
|
+
for (const arg of tuple) {
|
|
2562
|
+
if (arg instanceof discord_js.Message) return {
|
|
2563
|
+
guild: arg.guild,
|
|
2564
|
+
member: arg.member,
|
|
2565
|
+
user: arg.author,
|
|
2566
|
+
channelId: arg.channelId
|
|
2567
|
+
};
|
|
2568
|
+
if (arg instanceof discord_js.GuildMember) return {
|
|
2569
|
+
guild: arg.guild,
|
|
2570
|
+
member: arg,
|
|
2571
|
+
user: arg.user,
|
|
2572
|
+
channelId: null
|
|
2573
|
+
};
|
|
2574
|
+
if (arg instanceof discord_js.User) return {
|
|
2575
|
+
guild: null,
|
|
2576
|
+
member: null,
|
|
2577
|
+
user: arg,
|
|
2578
|
+
channelId: null
|
|
2579
|
+
};
|
|
2580
|
+
}
|
|
2581
|
+
return {
|
|
2582
|
+
guild: null,
|
|
2583
|
+
member: null,
|
|
2584
|
+
user: null,
|
|
2585
|
+
channelId: null
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
//#endregion
|
|
2590
|
+
//#region src/bot/gates/runGates.ts
|
|
2591
|
+
function interactionGateContext(interaction, core) {
|
|
2592
|
+
const rawMember = interaction.member;
|
|
2593
|
+
const memberRoleIds = rawMember instanceof discord_js.GuildMember ? [...rawMember.roles.cache.keys()].filter((id) => id !== interaction.guildId) : rawMember?.roles ?? [];
|
|
2594
|
+
return {
|
|
2595
|
+
kind: "interaction",
|
|
2596
|
+
interaction,
|
|
2597
|
+
core,
|
|
2598
|
+
user: interaction.user,
|
|
2599
|
+
guild: interaction.guild,
|
|
2600
|
+
member: rawMember instanceof discord_js.GuildMember ? rawMember : null,
|
|
2601
|
+
userId: interaction.user.id,
|
|
2602
|
+
guildId: interaction.guildId,
|
|
2603
|
+
channelId: interaction.channelId,
|
|
2604
|
+
memberRoleIds,
|
|
2605
|
+
memberPermissions: interaction.memberPermissions?.bitfield ?? null
|
|
2606
|
+
};
|
|
2607
|
+
}
|
|
2608
|
+
function eventGateContext(eventName, args, core) {
|
|
2609
|
+
const actor = deriveEventActor(args);
|
|
2610
|
+
return {
|
|
2611
|
+
kind: "event",
|
|
2612
|
+
core,
|
|
2613
|
+
eventName,
|
|
2614
|
+
payload: args,
|
|
2615
|
+
user: actor.user,
|
|
2616
|
+
guild: actor.guild,
|
|
2617
|
+
member: actor.member,
|
|
2618
|
+
userId: actor.user?.id ?? null,
|
|
2619
|
+
guildId: actor.guild?.id ?? null,
|
|
2620
|
+
channelId: actor.channelId,
|
|
2621
|
+
memberRoleIds: actor.member ? [...actor.member.roles.cache.keys()].filter((id) => id !== actor.guild?.id) : [],
|
|
2622
|
+
memberPermissions: actor.member?.permissions.bitfield ?? null
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
//#endregion
|
|
2627
|
+
//#region src/miscellaneous/FaultThrottle.ts
|
|
2628
|
+
/**
|
|
2629
|
+
* Drops duplicate fault reports inside a fixed window so a recurring fault (a database outage on a
|
|
2630
|
+
* hot path) reports once per window instead of flooding the webhook. Keyed on a structural string, so
|
|
2631
|
+
* the map stays small, and a stale entry is evicted on the next read of its key.
|
|
2632
|
+
*
|
|
2633
|
+
* @internal
|
|
2634
|
+
*/
|
|
2635
|
+
var FaultThrottle = class {
|
|
2636
|
+
windowMs;
|
|
2637
|
+
now;
|
|
2638
|
+
lastReportedAt = /* @__PURE__ */ new Map();
|
|
2639
|
+
constructor(windowMs = 6e4, now = () => Date.now()) {
|
|
2640
|
+
this.windowMs = windowMs;
|
|
2641
|
+
this.now = now;
|
|
2642
|
+
}
|
|
2643
|
+
/** Evicts the entry for this key if it has aged out, so the map never holds stale keys forever. */
|
|
2644
|
+
shouldReport(key) {
|
|
2645
|
+
const last = this.lastReportedAt.get(key);
|
|
2646
|
+
if (last === void 0) return true;
|
|
2647
|
+
if (this.now() - last < this.windowMs) return false;
|
|
2648
|
+
this.lastReportedAt.delete(key);
|
|
2649
|
+
return true;
|
|
2650
|
+
}
|
|
2651
|
+
markReported(key) {
|
|
2652
|
+
this.lastReportedAt.set(key, this.now());
|
|
2653
|
+
}
|
|
2654
|
+
clear() {
|
|
2655
|
+
this.lastReportedAt.clear();
|
|
2656
|
+
}
|
|
2657
|
+
};
|
|
2658
|
+
|
|
2659
|
+
//#endregion
|
|
2660
|
+
//#region src/miscellaneous/extractErrorResponse.ts
|
|
2661
|
+
const logger$2 = new _seedcord_services.Logger("ErrorsHandling");
|
|
2662
|
+
/**
|
|
2663
|
+
* The single throttle for every fault publish, so a recurring fault reports once per window across both
|
|
2664
|
+
* the interaction and event paths. Exported so tests reset it between cases.
|
|
2665
|
+
*
|
|
2666
|
+
* @internal
|
|
2667
|
+
*/
|
|
2668
|
+
const faultThrottle = new FaultThrottle();
|
|
2669
|
+
/**
|
|
2670
|
+
* Processes an error into a user-facing reply and routes its report.
|
|
2671
|
+
*
|
|
2672
|
+
* A {@link Notice} always renders its own reply. A reporting denial (`report: true`) additionally
|
|
2673
|
+
* publishes `handledException` on the interaction path. A raw, non-denial throw publishes
|
|
2674
|
+
* `unknownException` and renders the configured `defaultError` (or a generic {@link Fault}). One uuid is
|
|
2675
|
+
* threaded into both the reply and the bus payload.
|
|
2676
|
+
*
|
|
2677
|
+
* @internal
|
|
2678
|
+
*/
|
|
2679
|
+
function extractErrorResponse(error, core, origin) {
|
|
2680
|
+
const uuid = node_crypto.randomUUID();
|
|
2681
|
+
const developerUsername = core.config.notifications?.developerUsername;
|
|
2682
|
+
const ctx = developerUsername === void 0 ? { uuid } : {
|
|
2683
|
+
uuid,
|
|
2684
|
+
developerUsername
|
|
2685
|
+
};
|
|
2686
|
+
if (error instanceof _seedcord_core.Notice) {
|
|
2687
|
+
if (error.report) reportFault(error, core, origin, uuid);
|
|
2688
|
+
return {
|
|
2689
|
+
uuid,
|
|
2690
|
+
response: error.render(ctx)
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
reportRawFault(error, core, origin, uuid);
|
|
2694
|
+
const override = core.config.errors?.defaultError;
|
|
2695
|
+
return {
|
|
2696
|
+
uuid,
|
|
2697
|
+
response: override ? new override(uuid).render(ctx) : new _seedcord_core.Fault().render(ctx)
|
|
2698
|
+
};
|
|
2699
|
+
}
|
|
2700
|
+
function withThrottle(origin, error, publish) {
|
|
2701
|
+
const key = faultKey(origin, error);
|
|
2702
|
+
if (!faultThrottle.shouldReport(key)) {
|
|
2703
|
+
logger$2.info(`throttled duplicate fault ${key}`);
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2706
|
+
publish();
|
|
2707
|
+
faultThrottle.markReported(key);
|
|
2708
|
+
}
|
|
2709
|
+
function reportFault(denial, core, origin, uuid) {
|
|
2710
|
+
withThrottle(origin, denial, () => {
|
|
2711
|
+
logger$2.error(`${denial.name}: ${uuid}`, denial);
|
|
2712
|
+
if (origin.interaction) core.bus.publish("handledException", {
|
|
2713
|
+
denial,
|
|
2714
|
+
uuid,
|
|
2715
|
+
source: buildInteractionSource(origin.interaction)
|
|
2716
|
+
});
|
|
2717
|
+
else if (origin.event) core.bus.publish("handledException", {
|
|
2718
|
+
denial,
|
|
2719
|
+
uuid,
|
|
2720
|
+
source: buildEventSource(origin.event, origin)
|
|
2721
|
+
});
|
|
2722
|
+
else core.bus.publish("unknownException", {
|
|
2723
|
+
uuid,
|
|
2724
|
+
error: denial,
|
|
2725
|
+
guild: origin.guild,
|
|
2726
|
+
user: origin.user,
|
|
2727
|
+
metadata: metadataFor(origin)
|
|
2728
|
+
});
|
|
2729
|
+
});
|
|
2730
|
+
}
|
|
2731
|
+
function reportRawFault(error, core, origin, uuid) {
|
|
2732
|
+
withThrottle(origin, error, () => {
|
|
2733
|
+
if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
|
|
2734
|
+
else logger$2.error(`${uuid} | ${error.message}`);
|
|
2735
|
+
core.bus.publish("unknownException", {
|
|
2736
|
+
uuid,
|
|
2737
|
+
error,
|
|
2738
|
+
guild: origin.guild,
|
|
2739
|
+
user: origin.user,
|
|
2740
|
+
metadata: metadataFor(origin)
|
|
2741
|
+
});
|
|
2742
|
+
});
|
|
2743
|
+
}
|
|
2744
|
+
function metadataFor(origin) {
|
|
2745
|
+
if (origin.event) return {
|
|
2746
|
+
eventName: origin.event.name,
|
|
2747
|
+
handler: origin.event.handler,
|
|
2748
|
+
args: origin.event.args
|
|
2749
|
+
};
|
|
2750
|
+
return origin.metadata;
|
|
2751
|
+
}
|
|
2752
|
+
function buildEventSource(event, origin) {
|
|
2753
|
+
return {
|
|
2754
|
+
kind: "event",
|
|
2755
|
+
eventName: event.name,
|
|
2756
|
+
handler: event.handler,
|
|
2757
|
+
userId: origin.user?.id ?? null,
|
|
2758
|
+
guildId: origin.guild?.id ?? null,
|
|
2759
|
+
channelId: event.channelId,
|
|
2760
|
+
raw: event.args
|
|
2761
|
+
};
|
|
2762
|
+
}
|
|
2763
|
+
function faultKey(origin, error) {
|
|
2764
|
+
const name = error instanceof _seedcord_core.Notice ? error.name : error instanceof discord_js.DiscordAPIError ? String(error.code) : error.constructor.name;
|
|
2765
|
+
if (origin.event) return `${origin.event.name}:${origin.event.handler}:${name}`;
|
|
2766
|
+
if (origin.interaction) return `${interactionRoute(origin.interaction)}:${name}`;
|
|
2767
|
+
if (origin.route) return `autocomplete:${origin.route}:${name}`;
|
|
2768
|
+
return `autocomplete:${name}`;
|
|
2769
|
+
}
|
|
2770
|
+
function interactionRoute(interaction) {
|
|
2771
|
+
if (interaction.isChatInputCommand()) return slashRouteOf(interaction);
|
|
2772
|
+
if (interaction.isContextMenuCommand()) return interaction.commandName;
|
|
2773
|
+
if (interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit()) return (0, _seedcord_core_internal.prefixOf)(interaction.customId) || interaction.customId;
|
|
2774
|
+
return "interaction";
|
|
2775
|
+
}
|
|
2776
|
+
function buildInteractionSource(interaction) {
|
|
2777
|
+
const command = interaction.isChatInputCommand() ? slashRouteOf(interaction) : interaction.isContextMenuCommand() ? interaction.commandName : null;
|
|
2778
|
+
const customId = interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit() ? interaction.customId : null;
|
|
2779
|
+
return {
|
|
2780
|
+
kind: "interaction",
|
|
2781
|
+
interactionKind: interactionKind(interaction),
|
|
2782
|
+
command,
|
|
2783
|
+
customId,
|
|
2784
|
+
userId: interaction.user.id,
|
|
2785
|
+
guildId: interaction.guildId,
|
|
2786
|
+
channelId: interaction.channelId,
|
|
2787
|
+
interactionId: interaction.id,
|
|
2788
|
+
raw: interaction
|
|
2789
|
+
};
|
|
2790
|
+
}
|
|
2791
|
+
function interactionKind(interaction) {
|
|
2792
|
+
if (interaction.isChatInputCommand()) return "slash";
|
|
2793
|
+
if (interaction.isContextMenuCommand()) return "context-menu";
|
|
2794
|
+
if (interaction.isButton()) return "button";
|
|
2795
|
+
if (interaction.isAnySelectMenu()) return "select";
|
|
2796
|
+
return "modal";
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
//#endregion
|
|
2800
|
+
//#region src/bot/handleEventFault.ts
|
|
2801
|
+
const logger$1 = new _seedcord_services.Logger("EventBoundary");
|
|
2802
|
+
/**
|
|
2803
|
+
* The event controller boundary. Reports a throw from an event handler and never replies, since a
|
|
2804
|
+
* generic event has no reply target. A {@link Silence} or a non-reporting {@link Notice} stops quietly, a
|
|
2805
|
+
* reporting denial or a raw error publishes a fault with a best-effort actor derived from the args.
|
|
2806
|
+
*
|
|
2807
|
+
* @internal
|
|
2808
|
+
*/
|
|
2809
|
+
function handleEventFault(caught, eventName, handlerName, args, core) {
|
|
2810
|
+
if (caught instanceof _seedcord_core.Silence) {
|
|
2811
|
+
if (caught.reason !== void 0) logger$1.debug(`Silence: ${caught.reason}`);
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2814
|
+
if (!Error.isError(caught)) throw caught;
|
|
2815
|
+
const ignore = new Set(core.config.errors?.ignoreEventApiCodes ?? []);
|
|
2816
|
+
if (caught instanceof discord_js.DiscordAPIError && ignore.has(caught.code)) {
|
|
2817
|
+
logger$1.debug(`swallowed api code ${caught.code}`);
|
|
2818
|
+
return;
|
|
2819
|
+
}
|
|
2820
|
+
if (caught instanceof _seedcord_core.Notice && !caught.report) return;
|
|
2821
|
+
const actor = deriveEventActor(args);
|
|
2822
|
+
extractErrorResponse(caught, core, {
|
|
2823
|
+
event: {
|
|
2824
|
+
name: eventName,
|
|
2825
|
+
handler: handlerName,
|
|
2826
|
+
args,
|
|
2827
|
+
channelId: actor.channelId
|
|
2828
|
+
},
|
|
2829
|
+
guild: actor.guild,
|
|
2830
|
+
user: actor.user
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
//#endregion
|
|
2835
|
+
//#region src/bot/controllers/EventDispatcher.ts
|
|
2836
|
+
/**
|
|
2837
|
+
* Scans event handler directories, registers handlers with Discord client events,
|
|
2838
|
+
* and coordinates event execution through the handler system. Does not handle interactions.
|
|
2839
|
+
* Multiple handlers can point to one event.
|
|
2840
|
+
*
|
|
2841
|
+
* @internal
|
|
2842
|
+
*/
|
|
2843
|
+
var EventDispatcher = class {
|
|
2844
|
+
core;
|
|
2845
|
+
logger = new _seedcord_services.Logger("Events");
|
|
2846
|
+
isInitialized = false;
|
|
2847
|
+
name = "Events";
|
|
2848
|
+
eventMap = /* @__PURE__ */ new Map();
|
|
2849
|
+
middlewares = [];
|
|
2850
|
+
executedOnceHandlers = /* @__PURE__ */ new Set();
|
|
2851
|
+
attachedEvents = /* @__PURE__ */ new Set();
|
|
2852
|
+
hmrHandler;
|
|
2853
|
+
constructor(core) {
|
|
2854
|
+
this.core = core;
|
|
2855
|
+
const eventsDir = this.core.config.bot.events.path;
|
|
2856
|
+
if (!eventsDir) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CoreControllerPathMissing, ["EventDispatcher", "events"]);
|
|
2857
|
+
if (!envapt.Envapter.isDevelopment && !envapt.Envapter.isTest) return;
|
|
2858
|
+
this.hmrHandler = new HmrModuleHandler({
|
|
2859
|
+
handlersDir: eventsDir,
|
|
2860
|
+
...(0, _seedcord_utils.hasKeys)(this.core.config.bot.events, ["middlewares"]) && { middlewaresDir: this.core.config.bot.events.middlewares },
|
|
2861
|
+
isHandler: this.isEventHandlerClass.bind(this),
|
|
2862
|
+
isMiddleware: this.isMiddlewareClass.bind(this),
|
|
2863
|
+
registerHandler: this.registerHandler.bind(this),
|
|
2864
|
+
registerMiddleware: this.registerMiddleware.bind(this),
|
|
2865
|
+
unregisterHandler: this.unregisterHandler.bind(this),
|
|
2866
|
+
unregisterMiddleware: this.unregisterMiddleware.bind(this),
|
|
2867
|
+
getArtifacts: this.getArtifacts.bind(this),
|
|
2868
|
+
logger: this.logger
|
|
2869
|
+
});
|
|
2870
|
+
}
|
|
2871
|
+
getArtifacts(ctor) {
|
|
2872
|
+
const events = [];
|
|
2873
|
+
for (const [event, handlers] of this.eventMap.entries()) if (handlers.some((h) => h.ctor === ctor)) events.push(event);
|
|
2874
|
+
return events;
|
|
2875
|
+
}
|
|
2876
|
+
async init() {
|
|
2877
|
+
if (this.isInitialized) return;
|
|
2878
|
+
this.isInitialized = true;
|
|
2879
|
+
const handlersDir = this.core.config.bot.events.path;
|
|
2880
|
+
if (!handlersDir) return;
|
|
2881
|
+
this.logger.info(chalk.default.bold(handlersDir));
|
|
2882
|
+
const middlewareDir = (0, _seedcord_utils.hasKeys)(this.core.config.bot.events, ["middlewares"]) ? this.core.config.bot.events.middlewares : void 0;
|
|
2883
|
+
if (middlewareDir) {
|
|
2884
|
+
this.logger.info(`${chalk.default.bold(middlewareDir)} ${chalk.default.gray("(middlewares)")}`);
|
|
2885
|
+
await this.loadMiddlewares(middlewareDir);
|
|
2886
|
+
}
|
|
2887
|
+
await this.loadHandlers(handlersDir);
|
|
2888
|
+
this.attachToClient();
|
|
2889
|
+
this.logger.info(`${chalk.default.bold.green("Loaded event handlers:")}`);
|
|
2890
|
+
const eventList = [`${chalk.default.magenta.bold(this.middlewares.length)} middlewares`];
|
|
2891
|
+
this.eventMap.forEach((handlers, eventName) => {
|
|
2892
|
+
eventList.push(`${chalk.default.magenta.bold(handlers.length)} ${eventName}`);
|
|
2893
|
+
});
|
|
2894
|
+
this.logger.utils.list(eventList);
|
|
2895
|
+
}
|
|
2896
|
+
async loadHandlers(dir) {
|
|
2897
|
+
await (0, _seedcord_utils.traverseDirectory)(dir, (fullPath, relativePath, imported) => {
|
|
2898
|
+
for (const val of Object.values(imported)) {
|
|
2899
|
+
if (!this.isEventHandlerClass(val)) continue;
|
|
2900
|
+
this.registerHandler(val, relativePath);
|
|
2901
|
+
this.hmrHandler?.trackHandler(fullPath, val);
|
|
2902
|
+
}
|
|
2903
|
+
}, this.logger);
|
|
2904
|
+
}
|
|
2905
|
+
async loadMiddlewares(dir) {
|
|
2906
|
+
await (0, _seedcord_utils.traverseDirectory)(dir, (fullPath, relativePath, imported) => {
|
|
2907
|
+
for (const val of Object.values(imported)) {
|
|
2908
|
+
if (!this.isMiddlewareClass(val)) continue;
|
|
2909
|
+
this.registerMiddleware(val, relativePath);
|
|
2910
|
+
this.hmrHandler?.trackMiddleware(fullPath, val);
|
|
2911
|
+
}
|
|
2912
|
+
}, this.logger);
|
|
2913
|
+
}
|
|
2914
|
+
/** @internal For use in dev mode */
|
|
2915
|
+
async onHmr(event) {
|
|
2916
|
+
await this.hmrHandler?.handle(event);
|
|
2917
|
+
}
|
|
2918
|
+
unregisterHandler(handlerClass, artifacts) {
|
|
2919
|
+
const events = artifacts ?? [...this.eventMap.keys()];
|
|
2920
|
+
for (const event of events) {
|
|
2921
|
+
const handlers = this.eventMap.get(event);
|
|
2922
|
+
if (!handlers) continue;
|
|
2923
|
+
const index = handlers.findIndex((h) => h.ctor === handlerClass);
|
|
2924
|
+
if (index !== -1) {
|
|
2925
|
+
handlers.splice(index, 1);
|
|
2926
|
+
if (handlers.length === 0) this.eventMap.delete(event);
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
unregisterMiddleware(middlewareCtor) {
|
|
2931
|
+
const index = this.middlewares.findIndex((entry) => entry.ctor === middlewareCtor);
|
|
2932
|
+
if (index !== -1) this.middlewares.splice(index, 1);
|
|
2933
|
+
}
|
|
2934
|
+
registerMiddleware(middlewareCtor, relativePath) {
|
|
2935
|
+
const metadata = Reflect.getMetadata(_seedcord_core_internal.MiddlewareMetadataKey, middlewareCtor);
|
|
2936
|
+
if (metadata?.type !== "event") return;
|
|
2937
|
+
if (this.middlewares.some((entry) => entry.ctor === middlewareCtor)) return;
|
|
2938
|
+
this.middlewares.push({
|
|
2939
|
+
ctor: middlewareCtor,
|
|
2940
|
+
priority: metadata.priority,
|
|
2941
|
+
...metadata.events && { events: metadata.events }
|
|
2942
|
+
});
|
|
2943
|
+
this.middlewares.sort((a, b) => a.priority - b.priority);
|
|
2944
|
+
this.logger.utils.registration(`${middlewareCtor.name} ${chalk.default.gray(`(${metadata.priority})`)}`, relativePath, "event middleware");
|
|
2945
|
+
}
|
|
2946
|
+
async runMiddlewares(eventName, args) {
|
|
2947
|
+
for (const { ctor, events } of this.middlewares) {
|
|
2948
|
+
if (events && !events.includes(eventName)) continue;
|
|
2949
|
+
try {
|
|
2950
|
+
await new ctor(args, this.core, eventName).execute();
|
|
2951
|
+
} catch (caught) {
|
|
2952
|
+
handleEventFault(caught, String(eventName), ctor.name, args, this.core);
|
|
2953
|
+
return false;
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
return true;
|
|
2957
|
+
}
|
|
2958
|
+
isEventHandlerClass(obj) {
|
|
2959
|
+
if (typeof obj !== "function") return false;
|
|
2960
|
+
return obj.prototype instanceof EventHandler && Reflect.hasMetadata(_seedcord_core_internal.EventMetadataKey, obj);
|
|
2961
|
+
}
|
|
2962
|
+
isMiddlewareClass(obj) {
|
|
2963
|
+
if (typeof obj !== "function") return false;
|
|
2964
|
+
return obj.prototype instanceof EventMiddleware && Reflect.hasMetadata(_seedcord_core_internal.MiddlewareMetadataKey, obj);
|
|
2965
|
+
}
|
|
2966
|
+
registerHandler(handlerClass, relativePath) {
|
|
2967
|
+
const raw = Reflect.getMetadata(_seedcord_core_internal.EventMetadataKey, handlerClass);
|
|
2968
|
+
const register = (key, frequency) => {
|
|
2969
|
+
let handlers = this.eventMap.get(key);
|
|
2970
|
+
if (!handlers) {
|
|
2971
|
+
handlers = [];
|
|
2972
|
+
this.eventMap.set(key, handlers);
|
|
2973
|
+
}
|
|
2974
|
+
handlers.push({
|
|
2975
|
+
ctor: handlerClass,
|
|
2976
|
+
frequency
|
|
2977
|
+
});
|
|
2978
|
+
if (this.isInitialized && !this.attachedEvents.has(key)) this.attachListener(key);
|
|
2979
|
+
};
|
|
2980
|
+
if (Array.isArray(raw)) for (const entry of raw) register(entry.event, entry.frequency);
|
|
2981
|
+
else {
|
|
2982
|
+
const names = areRoutes(raw) ? raw : typeof raw === "string" ? [raw] : [];
|
|
2983
|
+
for (const name of names) register(name, "on");
|
|
2984
|
+
}
|
|
2985
|
+
this.logger.utils.registration(handlerClass.name, relativePath);
|
|
2986
|
+
}
|
|
2987
|
+
attachToClient() {
|
|
2988
|
+
for (const [eventName] of this.eventMap) this.attachListener(eventName);
|
|
2989
|
+
}
|
|
2990
|
+
attachListener(eventName) {
|
|
2991
|
+
if (this.attachedEvents.has(eventName)) return;
|
|
2992
|
+
this.attachedEvents.add(eventName);
|
|
2993
|
+
const handlerEntries = this.eventMap.get(eventName);
|
|
2994
|
+
this.logger.debug(`Attaching ${chalk.default.bold.green(eventName)} to the client with ${chalk.default.gray(handlerEntries?.length ?? 0)} handler(s)`);
|
|
2995
|
+
this.core.bot.client.on(eventName, (...args) => {
|
|
2996
|
+
this.core.bot.emit("any:event", eventName, ...args);
|
|
2997
|
+
(async () => {
|
|
2998
|
+
await this.processEvent(eventName, args).catch((err) => {
|
|
2999
|
+
this.logger.error(`[${chalk.default.bold.red("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
|
|
3000
|
+
this.core.bot.emit("error:unhandled:event", err);
|
|
3001
|
+
});
|
|
3002
|
+
})();
|
|
3003
|
+
});
|
|
3004
|
+
}
|
|
3005
|
+
async processEvent(eventName, args) {
|
|
3006
|
+
const handlerEntries = this.eventMap.get(eventName);
|
|
3007
|
+
if (!handlerEntries || handlerEntries.length === 0) return;
|
|
3008
|
+
const handlersToExecute = handlerEntries.filter((entry) => entry.frequency !== "once" || !this.executedOnceHandlers.has(entry.ctor));
|
|
3009
|
+
if (handlersToExecute.length === 0) return;
|
|
3010
|
+
if (!await this.runMiddlewares(eventName, args)) return;
|
|
3011
|
+
for (const entry of handlersToExecute) {
|
|
3012
|
+
if (entry.frequency === "once") {
|
|
3013
|
+
if (this.executedOnceHandlers.has(entry.ctor)) continue;
|
|
3014
|
+
this.executedOnceHandlers.add(entry.ctor);
|
|
3015
|
+
}
|
|
3016
|
+
await this.processHandler(eventName, entry.ctor, args);
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
async processHandler(eventName, ctor, args) {
|
|
3020
|
+
try {
|
|
3021
|
+
this.logger.debug(`Processing ${chalk.default.bold.green(eventName)} with ${chalk.default.gray(ctor.name)}`);
|
|
3022
|
+
const handler = new ctor(args, this.core, eventName);
|
|
3023
|
+
await (0, _seedcord_core_internal.runHandlerGates)(ctor, eventGateContext(eventName, args, this.core));
|
|
3024
|
+
await handler.execute();
|
|
3025
|
+
} catch (caught) {
|
|
3026
|
+
handleEventFault(caught, String(eventName), ctor.name, args, this.core);
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
};
|
|
3030
|
+
|
|
3031
|
+
//#endregion
|
|
3032
|
+
//#region src/bot/defaults/UnhandledEvent.ts
|
|
3033
|
+
/**
|
|
3034
|
+
* Default handler for unhandled interactions. A reply failure here rides the controller boundary as a
|
|
3035
|
+
* raw fault, so it needs no decorator of its own.
|
|
3036
|
+
*
|
|
3037
|
+
* @internal
|
|
3038
|
+
*/
|
|
3039
|
+
var UnhandledEvent = class extends InteractionHandler {
|
|
3040
|
+
async execute() {
|
|
3041
|
+
await this.event.reply({
|
|
3042
|
+
content: `Feature not implemented yet.`,
|
|
3043
|
+
flags: discord_js.MessageFlags.Ephemeral
|
|
3044
|
+
});
|
|
3045
|
+
}
|
|
3046
|
+
};
|
|
3047
|
+
|
|
3048
|
+
//#endregion
|
|
3049
|
+
//#region src/bot/handleInteractionFault.ts
|
|
3050
|
+
const logger = new _seedcord_services.Logger("InteractionBoundary");
|
|
3051
|
+
/**
|
|
3052
|
+
* The interaction controller boundary. Sorts a throw from any handler-lifecycle phase into the right
|
|
3053
|
+
* user reply plus the right bus event, building a {@link ReplySender} lazily so it reads the live
|
|
3054
|
+
* acknowledgement state at catch time.
|
|
3055
|
+
*
|
|
3056
|
+
* @internal
|
|
3057
|
+
*/
|
|
3058
|
+
async function handleInteractionFault(caught, interaction, core) {
|
|
3059
|
+
if (caught instanceof _seedcord_core.Silence) {
|
|
3060
|
+
if (caught.reason !== void 0) logger.debug(`Silence: ${caught.reason}`);
|
|
3061
|
+
return;
|
|
3062
|
+
}
|
|
3063
|
+
if (!Error.isError(caught)) throw caught;
|
|
3064
|
+
const ignore = new Set(core.config.errors?.ignoreApiCodes ?? []);
|
|
3065
|
+
if (caught instanceof discord_js.DiscordAPIError && ignore.has(caught.code)) {
|
|
3066
|
+
logger.debug(`swallowed api code ${caught.code}`);
|
|
3067
|
+
return;
|
|
3068
|
+
}
|
|
3069
|
+
if (interaction.isAutocomplete()) {
|
|
3070
|
+
extractErrorResponse(caught, core, {
|
|
3071
|
+
guild: interaction.guild,
|
|
3072
|
+
user: interaction.user,
|
|
3073
|
+
metadata: interaction,
|
|
3074
|
+
route: slashRouteOf(interaction)
|
|
3075
|
+
});
|
|
3076
|
+
return;
|
|
3077
|
+
}
|
|
3078
|
+
const { response } = extractErrorResponse(caught, core, {
|
|
3079
|
+
interaction,
|
|
3080
|
+
guild: interaction.guild,
|
|
3081
|
+
user: interaction.user,
|
|
3082
|
+
metadata: interaction
|
|
3083
|
+
});
|
|
3084
|
+
await new ReplySender(interaction).send(response, caught instanceof _seedcord_core.Notice ? caught.ephemeral : true);
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
//#endregion
|
|
3088
|
+
//#region src/bot/controllers/InteractionDispatcher.ts
|
|
3089
|
+
/**
|
|
3090
|
+
* Scans handler directories, routes each interaction to its registered handler, and runs the handler.
|
|
3091
|
+
* One handler per interaction.
|
|
3092
|
+
*
|
|
3093
|
+
* @internal
|
|
3094
|
+
*/
|
|
3095
|
+
var InteractionDispatcher = class {
|
|
3096
|
+
core;
|
|
3097
|
+
logger = new _seedcord_services.Logger("Interactions");
|
|
3098
|
+
isInitialized = false;
|
|
3099
|
+
name = "Interactions";
|
|
3100
|
+
slashMap = /* @__PURE__ */ new Map();
|
|
3101
|
+
buttonMap = /* @__PURE__ */ new Map();
|
|
3102
|
+
modalMap = /* @__PURE__ */ new Map();
|
|
3103
|
+
stringSelectMap = /* @__PURE__ */ new Map();
|
|
3104
|
+
userSelectMap = /* @__PURE__ */ new Map();
|
|
3105
|
+
roleSelectMap = /* @__PURE__ */ new Map();
|
|
3106
|
+
channelSelectMap = /* @__PURE__ */ new Map();
|
|
3107
|
+
mentionableSelectMap = /* @__PURE__ */ new Map();
|
|
3108
|
+
messageContextMenuMap = /* @__PURE__ */ new Map();
|
|
3109
|
+
userContextMenuMap = /* @__PURE__ */ new Map();
|
|
3110
|
+
autocompleteMap = /* @__PURE__ */ new Map();
|
|
3111
|
+
keysToIgnore = /* @__PURE__ */ new Set();
|
|
3112
|
+
middlewares = [];
|
|
3113
|
+
hmrHandler;
|
|
3114
|
+
routeTypes = [
|
|
3115
|
+
[_seedcord_core_internal.InteractionRoutes.Slash, this.slashMap],
|
|
3116
|
+
[_seedcord_core_internal.InteractionRoutes.Button, this.buttonMap],
|
|
3117
|
+
[_seedcord_core_internal.InteractionRoutes.Modal, this.modalMap],
|
|
3118
|
+
[_seedcord_core_internal.InteractionRoutes.StringMenu, this.stringSelectMap],
|
|
3119
|
+
[_seedcord_core_internal.InteractionRoutes.UserMenu, this.userSelectMap],
|
|
3120
|
+
[_seedcord_core_internal.InteractionRoutes.RoleMenu, this.roleSelectMap],
|
|
3121
|
+
[_seedcord_core_internal.InteractionRoutes.ChannelMenu, this.channelSelectMap],
|
|
3122
|
+
[_seedcord_core_internal.InteractionRoutes.MentionableMenu, this.mentionableSelectMap],
|
|
3123
|
+
[_seedcord_core_internal.InteractionRoutes.MessageContextMenu, this.messageContextMenuMap],
|
|
3124
|
+
[_seedcord_core_internal.InteractionRoutes.UserContextMenu, this.userContextMenuMap],
|
|
3125
|
+
[_seedcord_core_internal.InteractionRoutes.Autocomplete, this.autocompleteMap]
|
|
3126
|
+
];
|
|
3127
|
+
constructor(core) {
|
|
3128
|
+
this.core = core;
|
|
3129
|
+
const ignoredKeysFromConfig = (0, _seedcord_utils.hasKeys)(this.core.config.bot.interactions, ["ignoreCustomIds"]) ? this.core.config.bot.interactions.ignoreCustomIds : void 0;
|
|
3130
|
+
if (ignoredKeysFromConfig) for (const ignoredKey of ignoredKeysFromConfig) this.keysToIgnore.add(ignoredKey);
|
|
3131
|
+
this.keysToIgnore.add(CONFIRM_DEF);
|
|
3132
|
+
const interactionsDir = this.core.config.bot.interactions.path;
|
|
3133
|
+
if (!interactionsDir) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CoreControllerPathMissing, ["InteractionDispatcher", "interactions"]);
|
|
3134
|
+
if (!envapt.Envapter.isDevelopment && !envapt.Envapter.isTest) return;
|
|
3135
|
+
this.hmrHandler = new HmrModuleHandler({
|
|
3136
|
+
handlersDir: interactionsDir,
|
|
3137
|
+
...(0, _seedcord_utils.hasKeys)(this.core.config.bot.interactions, ["middlewares"]) && this.core.config.bot.interactions.middlewares && { middlewaresDir: this.core.config.bot.interactions.middlewares },
|
|
3138
|
+
isHandler: this.isHandlerClass.bind(this),
|
|
3139
|
+
isMiddleware: this.isMiddlewareClass.bind(this),
|
|
3140
|
+
registerHandler: this.registerHandler.bind(this),
|
|
3141
|
+
registerMiddleware: this.registerMiddleware.bind(this),
|
|
3142
|
+
unregisterHandler: this.unregisterHandler.bind(this),
|
|
3143
|
+
unregisterMiddleware: this.unregisterMiddleware.bind(this),
|
|
3144
|
+
getArtifacts: this.getArtifacts.bind(this),
|
|
3145
|
+
logger: this.logger
|
|
3146
|
+
});
|
|
3147
|
+
}
|
|
3148
|
+
/**
|
|
3149
|
+
* Warns for each command route with no registered `@SlashRoute` handler, which would fall through to
|
|
3150
|
+
* UnhandledEvent at runtime. A bot may route commands outside the registry, so this does not throw.
|
|
3151
|
+
*
|
|
3152
|
+
* @internal
|
|
3153
|
+
*/
|
|
3154
|
+
warnUnhandledRoutes(commandLeaves) {
|
|
3155
|
+
for (const route of commandLeaves) if (!this.slashMap.has(route)) this.logger.warn(`Slash route ${chalk.default.bold.cyan(route)} has no registered ${chalk.default.bold("@SlashRoute")} handler.`);
|
|
3156
|
+
}
|
|
3157
|
+
/**
|
|
3158
|
+
* Warns for each context-menu command with no matching handler, which would fall through to UnhandledEvent
|
|
3159
|
+
* at runtime. Checked per kind since a user and a message command can share a name.
|
|
3160
|
+
*
|
|
3161
|
+
* @internal
|
|
3162
|
+
*/
|
|
3163
|
+
warnUnhandledContextMenuRoutes(leaves) {
|
|
3164
|
+
for (const name of leaves.user) if (!this.userContextMenuMap.has(name)) this.logger.warn(`User context menu ${chalk.default.bold.cyan(name)} has no registered ${chalk.default.bold("@ContextMenuRoute")} handler.`);
|
|
3165
|
+
for (const name of leaves.message) if (!this.messageContextMenuMap.has(name)) this.logger.warn(`Message context menu ${chalk.default.bold.cyan(name)} has no registered ${chalk.default.bold("@ContextMenuRoute")} handler.`);
|
|
3166
|
+
}
|
|
3167
|
+
getArtifacts(handlerClass) {
|
|
3168
|
+
const artifacts = [];
|
|
3169
|
+
for (const [routeType] of this.routeTypes) {
|
|
3170
|
+
const meta = Reflect.getMetadata(_seedcord_core_internal.InteractionRouteKeys[routeType], handlerClass);
|
|
3171
|
+
if (areRoutes(meta)) artifacts.push({
|
|
3172
|
+
routeType,
|
|
3173
|
+
routes: meta
|
|
3174
|
+
});
|
|
3175
|
+
}
|
|
3176
|
+
return artifacts;
|
|
3177
|
+
}
|
|
3178
|
+
async init() {
|
|
3179
|
+
if (this.isInitialized) return;
|
|
3180
|
+
this.isInitialized = true;
|
|
3181
|
+
const handlersDir = this.core.config.bot.interactions.path;
|
|
3182
|
+
if (!handlersDir) return;
|
|
3183
|
+
this.logger.info(chalk.default.bold(handlersDir));
|
|
3184
|
+
const middlewareDir = (0, _seedcord_utils.hasKeys)(this.core.config.bot.interactions, ["middlewares"]) ? this.core.config.bot.interactions.middlewares : void 0;
|
|
3185
|
+
if (middlewareDir) {
|
|
3186
|
+
this.logger.info(`${chalk.default.bold(middlewareDir)} ${chalk.default.gray("(middlewares)")}`);
|
|
3187
|
+
await this.loadMiddlewares(middlewareDir);
|
|
3188
|
+
}
|
|
3189
|
+
await this.loadHandlers(handlersDir);
|
|
3190
|
+
this.attachToClient();
|
|
3191
|
+
this.logger.info(`${chalk.default.bold.green("Loaded interaction handlers:")}`);
|
|
3192
|
+
this.logger.utils.list([
|
|
3193
|
+
`${chalk.default.magenta.bold(this.middlewares.length)} middlewares`,
|
|
3194
|
+
`${chalk.default.magenta.bold(this.slashMap.size)} slash commands`,
|
|
3195
|
+
`${chalk.default.magenta.bold(this.buttonMap.size)} buttons`,
|
|
3196
|
+
`${chalk.default.magenta.bold(this.modalMap.size)} modals`,
|
|
3197
|
+
`${chalk.default.magenta.bold(this.stringSelectMap.size)} string selects`,
|
|
3198
|
+
`${chalk.default.magenta.bold(this.userSelectMap.size)} user selects`,
|
|
3199
|
+
`${chalk.default.magenta.bold(this.roleSelectMap.size)} role selects`,
|
|
3200
|
+
`${chalk.default.magenta.bold(this.channelSelectMap.size)} channel selects`,
|
|
3201
|
+
`${chalk.default.magenta.bold(this.mentionableSelectMap.size)} mentionable selects`,
|
|
3202
|
+
`${chalk.default.magenta.bold(this.messageContextMenuMap.size)} message context menus`,
|
|
3203
|
+
`${chalk.default.magenta.bold(this.userContextMenuMap.size)} user context menus`,
|
|
3204
|
+
`${chalk.default.magenta.bold(this.autocompleteMap.size)} autocomplete`
|
|
3205
|
+
]);
|
|
3206
|
+
}
|
|
3207
|
+
async loadHandlers(dir) {
|
|
3208
|
+
await (0, _seedcord_utils.traverseDirectory)(dir, (fullPath, relativePath, imported) => {
|
|
3209
|
+
for (const val of Object.values(imported)) {
|
|
3210
|
+
if (!this.isHandlerClass(val)) continue;
|
|
3211
|
+
this.registerHandler(val, relativePath);
|
|
3212
|
+
this.hmrHandler?.trackHandler(fullPath, val);
|
|
3213
|
+
}
|
|
3214
|
+
}, this.logger);
|
|
3215
|
+
}
|
|
3216
|
+
async loadMiddlewares(dir) {
|
|
3217
|
+
await (0, _seedcord_utils.traverseDirectory)(dir, (fullPath, relativePath, imported) => {
|
|
3218
|
+
for (const val of Object.values(imported)) {
|
|
3219
|
+
if (!this.isMiddlewareClass(val)) continue;
|
|
3220
|
+
this.registerMiddleware(val, relativePath);
|
|
3221
|
+
this.hmrHandler?.trackMiddleware(fullPath, val);
|
|
3222
|
+
}
|
|
3223
|
+
}, this.logger);
|
|
3224
|
+
}
|
|
3225
|
+
registerMiddleware(middlewareCtor, relativePath) {
|
|
3226
|
+
const metadata = Reflect.getMetadata(_seedcord_core_internal.MiddlewareMetadataKey, middlewareCtor);
|
|
3227
|
+
if (metadata?.type !== "interaction") return;
|
|
3228
|
+
if (this.middlewares.some((entry) => entry.ctor === middlewareCtor)) return;
|
|
3229
|
+
if (this.middlewares.some((entry) => entry.ctor.name === middlewareCtor.name)) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.InteractionDuplicateMiddleware, [middlewareCtor.name]);
|
|
3230
|
+
this.middlewares.push({
|
|
3231
|
+
ctor: middlewareCtor,
|
|
3232
|
+
priority: metadata.priority
|
|
3233
|
+
});
|
|
3234
|
+
this.middlewares.sort((a, b) => a.priority - b.priority);
|
|
3235
|
+
this.logger.utils.registration(`${middlewareCtor.name} ${chalk.default.gray(`(${metadata.priority})`)}`, relativePath, "middleware");
|
|
3236
|
+
}
|
|
3237
|
+
isHandlerClass(obj) {
|
|
3238
|
+
if (typeof obj !== "function") return false;
|
|
3239
|
+
return obj.prototype instanceof InteractionHandler && Reflect.hasMetadata(_seedcord_core_internal.InteractionMetadataKey, obj) || obj.prototype instanceof AutocompleteHandler && Reflect.hasMetadata(_seedcord_core_internal.InteractionMetadataKey, obj);
|
|
3240
|
+
}
|
|
3241
|
+
isMiddlewareClass(obj) {
|
|
3242
|
+
if (typeof obj !== "function") return false;
|
|
3243
|
+
return obj.prototype instanceof InteractionMiddleware && Reflect.hasMetadata(_seedcord_core_internal.MiddlewareMetadataKey, obj);
|
|
3244
|
+
}
|
|
3245
|
+
registerHandler(handlerClass, relativePath) {
|
|
3246
|
+
const writes = [];
|
|
3247
|
+
for (const [routeType, map] of this.routeTypes) {
|
|
3248
|
+
const meta = Reflect.getMetadata(_seedcord_core_internal.InteractionRouteKeys[routeType], handlerClass);
|
|
3249
|
+
if (!areRoutes(meta)) continue;
|
|
3250
|
+
for (const route of meta) {
|
|
3251
|
+
const existing = map.get(route);
|
|
3252
|
+
if (existing && existing !== handlerClass) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.InteractionDuplicateRoute, [
|
|
3253
|
+
route,
|
|
3254
|
+
existing.name,
|
|
3255
|
+
handlerClass.name
|
|
3256
|
+
]);
|
|
3257
|
+
writes.push([map, route]);
|
|
3258
|
+
}
|
|
3259
|
+
this.logger.utils.registration(handlerClass.name, (0, _seedcord_utils.formatFilePath)(relativePath));
|
|
3260
|
+
}
|
|
3261
|
+
for (const [map, route] of writes) map.set(route, handlerClass);
|
|
3262
|
+
}
|
|
3263
|
+
/** @internal For use in dev mode */
|
|
3264
|
+
async onHmr(event) {
|
|
3265
|
+
await this.hmrHandler?.handle(event);
|
|
3266
|
+
}
|
|
3267
|
+
unregisterHandler(handlerClass, artifacts) {
|
|
3268
|
+
if (artifacts) {
|
|
3269
|
+
for (const { routeType, routes } of artifacts) {
|
|
3270
|
+
const map = this.routeTypes.find(([type]) => type === routeType)?.[1];
|
|
3271
|
+
if (map) routes.forEach((route) => map.delete(route));
|
|
3272
|
+
}
|
|
3273
|
+
return;
|
|
3274
|
+
}
|
|
3275
|
+
for (const [routeType, map] of this.routeTypes) {
|
|
3276
|
+
const meta = Reflect.getMetadata(_seedcord_core_internal.InteractionRouteKeys[routeType], handlerClass);
|
|
3277
|
+
if (!areRoutes(meta)) continue;
|
|
3278
|
+
meta.forEach((route) => map.delete(route));
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
unregisterMiddleware(middlewareCtor) {
|
|
3282
|
+
const index = this.middlewares.findIndex((entry) => entry.ctor === middlewareCtor);
|
|
3283
|
+
if (index !== -1) this.middlewares.splice(index, 1);
|
|
3284
|
+
}
|
|
3285
|
+
attachToClient() {
|
|
3286
|
+
this.core.bot.client.on(discord_js.Events.InteractionCreate, (interaction) => {
|
|
3287
|
+
this.core.bot.emit("any:interaction", interaction);
|
|
3288
|
+
this.handleInteraction(interaction).catch((err) => {
|
|
3289
|
+
this.logger.error(`[${chalk.default.bold.red("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
|
|
3290
|
+
this.core.bot.emit("error:unhandled:interaction", err);
|
|
3291
|
+
});
|
|
3292
|
+
});
|
|
3293
|
+
}
|
|
3294
|
+
async handleCustomIdInteraction(interaction, getMap, interactionType) {
|
|
3295
|
+
if ([...this.keysToIgnore].some((matcher) => matcher.owns(interaction.customId))) return;
|
|
3296
|
+
const prefix = (0, _seedcord_core_internal.prefixOf)(interaction.customId);
|
|
3297
|
+
if (!prefix) return this.logger.warn(`${interactionType} has invalid customId: ${interaction.customId}`);
|
|
3298
|
+
await this.processInteraction(interaction, () => prefix, (key) => getMap().get(key));
|
|
3299
|
+
}
|
|
3300
|
+
async processInteraction(interaction, extractKey, getHandler) {
|
|
3301
|
+
const key = extractKey(interaction);
|
|
3302
|
+
try {
|
|
3303
|
+
if (!interaction.isAutocomplete()) for (const { ctor } of this.middlewares) await new ctor(interaction, this.core).execute();
|
|
3304
|
+
let HandlerCtor = getHandler(key);
|
|
3305
|
+
if (!HandlerCtor) {
|
|
3306
|
+
this.logger.warn(`No handler found for key ${chalk.default.bold.cyan(key)}. Falling back to UnhandledEvent.`);
|
|
3307
|
+
HandlerCtor = UnhandledEvent;
|
|
3308
|
+
}
|
|
3309
|
+
this.logger.debug(`Processing ${chalk.default.bold.green(key)} with ${chalk.default.gray(HandlerCtor.name)}`);
|
|
3310
|
+
const handler = new HandlerCtor(interaction, this.core);
|
|
3311
|
+
if (!interaction.isAutocomplete()) await (0, _seedcord_core_internal.runHandlerGates)(HandlerCtor, interactionGateContext(interaction, this.core));
|
|
3312
|
+
await handler.execute();
|
|
3313
|
+
} catch (caught) {
|
|
3314
|
+
await handleInteractionFault(caught, interaction, this.core);
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
async handleInteraction(interaction) {
|
|
3318
|
+
switch (true) {
|
|
3319
|
+
case interaction.isChatInputCommand():
|
|
3320
|
+
await this.handleSlashCommand(interaction);
|
|
3321
|
+
break;
|
|
3322
|
+
case interaction.isButton():
|
|
3323
|
+
await this.handleButton(interaction);
|
|
3324
|
+
break;
|
|
3325
|
+
case interaction.isModalSubmit():
|
|
3326
|
+
await this.handleModal(interaction);
|
|
3327
|
+
break;
|
|
3328
|
+
case interaction.isStringSelectMenu():
|
|
3329
|
+
await this.handleStringSelectMenu(interaction);
|
|
3330
|
+
break;
|
|
3331
|
+
case interaction.isUserSelectMenu():
|
|
3332
|
+
await this.handleUserSelectMenu(interaction);
|
|
3333
|
+
break;
|
|
3334
|
+
case interaction.isRoleSelectMenu():
|
|
3335
|
+
await this.handleRoleSelectMenu(interaction);
|
|
3336
|
+
break;
|
|
3337
|
+
case interaction.isChannelSelectMenu():
|
|
3338
|
+
await this.handleChannelSelectMenu(interaction);
|
|
3339
|
+
break;
|
|
3340
|
+
case interaction.isMentionableSelectMenu():
|
|
3341
|
+
await this.handleMentionableSelectMenu(interaction);
|
|
3342
|
+
break;
|
|
3343
|
+
case interaction.isMessageContextMenuCommand():
|
|
3344
|
+
await this.handleMessageContextMenu(interaction);
|
|
3345
|
+
break;
|
|
3346
|
+
case interaction.isUserContextMenuCommand():
|
|
3347
|
+
await this.handleUserContextMenu(interaction);
|
|
3348
|
+
break;
|
|
3349
|
+
case interaction.isAutocomplete():
|
|
3350
|
+
await this.handleAutocomplete(interaction);
|
|
3351
|
+
break;
|
|
3352
|
+
default:
|
|
3353
|
+
this.logger.warn(`Unhandled interaction type: ${interaction.type}`);
|
|
3354
|
+
break;
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
async handleSlashCommand(interaction) {
|
|
3358
|
+
const route = slashRouteOf(interaction);
|
|
3359
|
+
await this.processInteraction(interaction, () => route, (key) => this.slashMap.get(key));
|
|
3360
|
+
}
|
|
3361
|
+
async handleButton(interaction) {
|
|
3362
|
+
await this.handleCustomIdInteraction(interaction, () => this.buttonMap, "Button");
|
|
3363
|
+
}
|
|
3364
|
+
async handleModal(interaction) {
|
|
3365
|
+
await this.handleCustomIdInteraction(interaction, () => this.modalMap, "Modal");
|
|
3366
|
+
}
|
|
3367
|
+
async handleStringSelectMenu(interaction) {
|
|
3368
|
+
await this.handleCustomIdInteraction(interaction, () => this.stringSelectMap, "String select menu");
|
|
3369
|
+
}
|
|
3370
|
+
async handleUserSelectMenu(interaction) {
|
|
3371
|
+
await this.handleCustomIdInteraction(interaction, () => this.userSelectMap, "User select menu");
|
|
3372
|
+
}
|
|
3373
|
+
async handleRoleSelectMenu(interaction) {
|
|
3374
|
+
await this.handleCustomIdInteraction(interaction, () => this.roleSelectMap, "Role select menu");
|
|
3375
|
+
}
|
|
3376
|
+
async handleChannelSelectMenu(interaction) {
|
|
3377
|
+
await this.handleCustomIdInteraction(interaction, () => this.channelSelectMap, "Channel select menu");
|
|
3378
|
+
}
|
|
3379
|
+
async handleMentionableSelectMenu(interaction) {
|
|
3380
|
+
await this.handleCustomIdInteraction(interaction, () => this.mentionableSelectMap, "Mentionable select menu");
|
|
3381
|
+
}
|
|
3382
|
+
async handleMessageContextMenu(interaction) {
|
|
3383
|
+
await this.processInteraction(interaction, () => interaction.commandName, (key) => this.messageContextMenuMap.get(key));
|
|
3384
|
+
}
|
|
3385
|
+
async handleUserContextMenu(interaction) {
|
|
3386
|
+
await this.processInteraction(interaction, () => interaction.commandName, (key) => this.userContextMenuMap.get(key));
|
|
3387
|
+
}
|
|
3388
|
+
async handleAutocomplete(interaction) {
|
|
3389
|
+
const route = slashRouteOf(interaction);
|
|
3390
|
+
if (!this.autocompleteMap.has(route)) {
|
|
3391
|
+
this.logger.warn(`No autocomplete handler for ${chalk.default.bold.cyan(route)}.`);
|
|
3392
|
+
await interaction.respond([]);
|
|
3393
|
+
return;
|
|
3394
|
+
}
|
|
3395
|
+
await this.processInteraction(interaction, () => route, (key) => this.autocompleteMap.get(key));
|
|
3396
|
+
}
|
|
3397
|
+
};
|
|
3398
|
+
|
|
3399
|
+
//#endregion
|
|
3400
|
+
//#region \0@oxc-project+runtime@0.135.0/helpers/esm/decorateMetadata.js
|
|
3401
|
+
function __decorateMetadata(k, v) {
|
|
3402
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
3403
|
+
}
|
|
3404
|
+
|
|
3405
|
+
//#endregion
|
|
3406
|
+
//#region \0@oxc-project+runtime@0.135.0/helpers/esm/decorate.js
|
|
3407
|
+
function __decorate(decorators, target, key, desc) {
|
|
3408
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3409
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
3410
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
3411
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3414
|
+
//#endregion
|
|
3415
|
+
//#region src/bot/Bot.ts
|
|
3416
|
+
/**
|
|
3417
|
+
* Discord bot implementation that manages client and controllers
|
|
3418
|
+
*
|
|
3419
|
+
* Don't instantiate this class directly. Use `core.bot` instead.
|
|
3420
|
+
*/
|
|
3421
|
+
var Bot = class extends Plugin {
|
|
3422
|
+
/** @internal */
|
|
3423
|
+
logger = new _seedcord_services.Logger("Bot");
|
|
3424
|
+
isInitialized = false;
|
|
3425
|
+
_client;
|
|
3426
|
+
interactions;
|
|
3427
|
+
events;
|
|
3428
|
+
commands;
|
|
3429
|
+
emojiInjector;
|
|
3430
|
+
emojis = Emojis;
|
|
3431
|
+
mentions = CommandMentions;
|
|
3432
|
+
/** @internal For use in dev mode */
|
|
3433
|
+
async onHmr(event) {
|
|
3434
|
+
if (this.interactions) await this.interactions.onHmr(event);
|
|
3435
|
+
if (this.events) await this.events.onHmr(event);
|
|
3436
|
+
if (this.commands) await this.commands.onHmr(event);
|
|
3437
|
+
}
|
|
3438
|
+
/** @internal */
|
|
3439
|
+
constructor(core) {
|
|
3440
|
+
super(core);
|
|
3441
|
+
this._client = new discord_js.Client(core.config.bot.clientOptions);
|
|
3442
|
+
if (core.config.bot.interactions.path) this.interactions = new InteractionDispatcher(core);
|
|
3443
|
+
if (core.config.bot.events.path) this.events = new EventDispatcher(core);
|
|
3444
|
+
if (core.config.bot.commands.path) {
|
|
3445
|
+
const mentionInjector = new CommandMentionInjector(core);
|
|
3446
|
+
this.commands = new CommandRegistry(core, (result) => {
|
|
3447
|
+
mentionInjector.inject(result);
|
|
3448
|
+
});
|
|
3449
|
+
}
|
|
3450
|
+
this.emojiInjector = new EmojiInjector(core);
|
|
3451
|
+
core.shutdown.addTask(_seedcord_services.ShutdownPhase.DiscordCleanup, "stop-bot", async () => await this.stop(), 2e3);
|
|
3452
|
+
}
|
|
3453
|
+
/**
|
|
3454
|
+
* Initializes Discord client and all controllers
|
|
3455
|
+
* @internal
|
|
3456
|
+
*/
|
|
3457
|
+
async init() {
|
|
3458
|
+
if (this.isInitialized) return;
|
|
3459
|
+
this.isInitialized = true;
|
|
3460
|
+
const token = this.botToken;
|
|
3461
|
+
if (this.interactions) await this.interactions.init();
|
|
3462
|
+
if (this.events) await this.events.init();
|
|
3463
|
+
await this.login(token);
|
|
3464
|
+
await this.emojiInjector.init();
|
|
3465
|
+
if (this.commands) {
|
|
3466
|
+
await this.commands.init();
|
|
3467
|
+
await this.commands.setCommands();
|
|
3468
|
+
this.interactions?.warnUnhandledRoutes(this.commands.routeLeaves());
|
|
3469
|
+
this.interactions?.warnUnhandledContextMenuRoutes(this.commands.contextMenuLeaves());
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
/**
|
|
3473
|
+
* Stops the bot and cleans up connections
|
|
3474
|
+
* @internal
|
|
3475
|
+
*/
|
|
3476
|
+
async stop() {
|
|
3477
|
+
this._client.removeAllListeners();
|
|
3478
|
+
await this.logout();
|
|
3479
|
+
}
|
|
3480
|
+
/**
|
|
3481
|
+
* Logs the bot into Discord using the configured token
|
|
3482
|
+
*/
|
|
3483
|
+
async login(token) {
|
|
3484
|
+
await this._client.login(token);
|
|
3485
|
+
this.logger.info(`Logged in as ${chalk.default.bold.magenta(this._client.user?.username)}!`);
|
|
3486
|
+
return this;
|
|
3487
|
+
}
|
|
3488
|
+
/**
|
|
3489
|
+
* Logs out and destroys the Discord client connection
|
|
3490
|
+
*/
|
|
3491
|
+
async logout() {
|
|
3492
|
+
await this._client.destroy();
|
|
3493
|
+
this.logger.info(chalk.default.bold.red("Logged out of Discord!"));
|
|
3494
|
+
}
|
|
3495
|
+
get client() {
|
|
3496
|
+
return this._client;
|
|
3497
|
+
}
|
|
3498
|
+
emit(event, ...args) {
|
|
3499
|
+
return super.emit(event, ...args);
|
|
3500
|
+
}
|
|
3501
|
+
};
|
|
3502
|
+
__decorate([(0, envapt.Envapt)("DISCORD_BOT_TOKEN", { converter: (raw) => (0, _seedcord_errors_internal.validateDiscordToken)(raw) }), __decorateMetadata("design:type", String)], Bot.prototype, "botToken", void 0);
|
|
3503
|
+
|
|
3504
|
+
//#endregion
|
|
3505
|
+
//#region src/hmr/HmrManager.ts
|
|
3506
|
+
var HmrManager = class {
|
|
3507
|
+
logger = new _seedcord_services.Logger("HMR", { channel: "hmr" });
|
|
3508
|
+
listeners = /* @__PURE__ */ new Set();
|
|
3509
|
+
constructor() {}
|
|
3510
|
+
/** @internal */
|
|
3511
|
+
init() {
|
|
3512
|
+
if (!{}.hot) return;
|
|
3513
|
+
setDevChannel((0, _seedcord_types_internal.wrapHot)({}.hot));
|
|
3514
|
+
if (envapt.Envapter.isDevelopment || envapt.Envapter.isTest) {
|
|
3515
|
+
this.logger.info("Enabled");
|
|
3516
|
+
getDevChannel()?.on("seedcord:hmr", (payload) => {
|
|
3517
|
+
const affected = payload.affectedModules?.length ?? 0;
|
|
3518
|
+
this.logger.info(`${chalk.default.bold("1")} module changed, ${chalk.default.bold(affected)} affected modules`);
|
|
3519
|
+
if (payload.affectedModules) this.logger.utils.list(payload.affectedModules.map((mod) => (0, _seedcord_utils.formatFilePath)(mod)), "Affected modules: ");
|
|
3520
|
+
this.handleUpdate(payload);
|
|
3521
|
+
});
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
/** @internal */
|
|
3525
|
+
register(listener) {
|
|
3526
|
+
this.listeners.add(listener);
|
|
3527
|
+
}
|
|
3528
|
+
/** @internal */
|
|
3529
|
+
unregister(listener) {
|
|
3530
|
+
this.listeners.delete(listener);
|
|
3531
|
+
}
|
|
3532
|
+
async handleUpdate(event) {
|
|
3533
|
+
const promises = [...this.listeners].map(async (listener) => {
|
|
3534
|
+
try {
|
|
3535
|
+
await listener.onHmr(event);
|
|
3536
|
+
} catch (error) {
|
|
3537
|
+
this.logger.error(`Error handling HMR update in ${chalk.default.bold(listener.name)}: `, error);
|
|
3538
|
+
}
|
|
3539
|
+
});
|
|
3540
|
+
await Promise.all(promises);
|
|
3541
|
+
}
|
|
3542
|
+
};
|
|
3543
|
+
|
|
3544
|
+
//#endregion
|
|
3545
|
+
//#region src/subscribers/bases/webhookHelpers.ts
|
|
3546
|
+
const DISCORD_WEBHOOK_REGEX = new RegExp(String.raw`^https?:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w$-]+$`);
|
|
3547
|
+
/**
|
|
3548
|
+
* Whether a string is a well-formed Discord webhook URL.
|
|
3549
|
+
*
|
|
3550
|
+
* @internal
|
|
3551
|
+
*/
|
|
3552
|
+
function isDiscordWebhookUrl(value) {
|
|
3553
|
+
return URL.canParse(value) && DISCORD_WEBHOOK_REGEX.test(value);
|
|
3554
|
+
}
|
|
3555
|
+
/**
|
|
3556
|
+
* Serializes a value to a JSON attachment, made circular-safe first.
|
|
3557
|
+
*
|
|
3558
|
+
* @internal
|
|
3559
|
+
*/
|
|
3560
|
+
function jsonAttachment(name, description, data) {
|
|
3561
|
+
const content = (0, _seedcord_utils.filterCirculars)(data);
|
|
3562
|
+
return new discord_js.AttachmentBuilder(Buffer.from(JSON.stringify(content, void 0, 2), "utf8"), {
|
|
3563
|
+
name,
|
|
3564
|
+
description
|
|
3565
|
+
});
|
|
3566
|
+
}
|
|
3567
|
+
/**
|
|
3568
|
+
* The small divider the default webhook reporters place between sections.
|
|
3569
|
+
*
|
|
3570
|
+
* @internal
|
|
3571
|
+
*/
|
|
3572
|
+
var WebhookSeparator = class extends _seedcord_core.BuilderComponent {
|
|
3573
|
+
constructor() {
|
|
3574
|
+
super("separator");
|
|
3575
|
+
this.instance.setSpacing(discord_js.SeparatorSpacingSize.Small).setDivider(true);
|
|
3576
|
+
}
|
|
3577
|
+
};
|
|
3578
|
+
|
|
3579
|
+
//#endregion
|
|
3580
|
+
//#region src/subscribers/default/HandledException.ts
|
|
3581
|
+
var _HandledException;
|
|
3582
|
+
function webhookUrlValidator$1(raw) {
|
|
3583
|
+
if (raw !== null && typeof raw !== "string") throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigHandledExceptionWebhookInvalid);
|
|
3584
|
+
const value = raw?.trim() ?? "";
|
|
3585
|
+
if (value === "") throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigHandledExceptionWebhookMissing);
|
|
3586
|
+
if (!isDiscordWebhookUrl(value)) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigHandledExceptionWebhookInvalid);
|
|
3587
|
+
return value;
|
|
3588
|
+
}
|
|
3589
|
+
let HandledException = class HandledException extends WebhookLog {
|
|
3590
|
+
static {
|
|
3591
|
+
_HandledException = this;
|
|
3592
|
+
}
|
|
3593
|
+
webhook = new discord_js.WebhookClient({ url: _HandledException.handledExceptionWebhookUrl });
|
|
3594
|
+
async execute() {
|
|
3595
|
+
const { source } = this.data;
|
|
3596
|
+
try {
|
|
3597
|
+
await this.webhook.send({
|
|
3598
|
+
flags: flagsFor(false),
|
|
3599
|
+
withComponents: true,
|
|
3600
|
+
username: "Handled Exception",
|
|
3601
|
+
components: [new HandledExceptionContainer(this.data).component],
|
|
3602
|
+
files: [jsonAttachment("source.json", "The raw source the fault came from", source.raw)]
|
|
3603
|
+
});
|
|
3604
|
+
} catch (error) {
|
|
3605
|
+
this.logger.error("Failed to send handled exception webhook", error);
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
};
|
|
3609
|
+
__decorate([(0, envapt.Envapt)("HANDLED_EXCEPTION_WEBHOOK_URL", { converter: (raw) => webhookUrlValidator$1(raw) }), __decorateMetadata("design:type", String)], HandledException, "handledExceptionWebhookUrl", void 0);
|
|
3610
|
+
HandledException = _HandledException = __decorate([Subscribe("handledException")], HandledException);
|
|
3611
|
+
var HandledExceptionContainer = class extends _seedcord_core.BuilderComponent {
|
|
3612
|
+
constructor(data) {
|
|
3613
|
+
super("container");
|
|
3614
|
+
const { denial, uuid, source } = data;
|
|
3615
|
+
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"));
|
|
3616
|
+
}
|
|
3617
|
+
};
|
|
3618
|
+
/**
|
|
3619
|
+
* The summary block of a fault report, narrowed per source kind so an interaction fault shows its
|
|
3620
|
+
* command and customId while an event fault shows its event name and handler.
|
|
3621
|
+
*
|
|
3622
|
+
* @internal
|
|
3623
|
+
*/
|
|
3624
|
+
function faultSummary(denial, source) {
|
|
3625
|
+
const head = `### A handled fault was reported: \`${denial.name}\`\n**Message:** ${denial.message}\n`;
|
|
3626
|
+
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"}\``;
|
|
3627
|
+
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}\``;
|
|
3628
|
+
}
|
|
3629
|
+
/**
|
|
3630
|
+
* The text shown for a denial's cause in the report. Tolerates a non-serializable cause (a bigint or a
|
|
3631
|
+
* circular object) so building the report never throws.
|
|
3632
|
+
*
|
|
3633
|
+
* @internal
|
|
3634
|
+
*/
|
|
3635
|
+
function causeStack(denial) {
|
|
3636
|
+
const { cause } = denial;
|
|
3637
|
+
if (Error.isError(cause)) return cause.stack ?? cause.message;
|
|
3638
|
+
if (typeof cause === "string") return cause;
|
|
3639
|
+
if (typeof cause === "bigint" || typeof cause === "symbol" || typeof cause === "function") return cause.toString();
|
|
3640
|
+
if (cause !== void 0) try {
|
|
3641
|
+
return JSON.stringify(cause);
|
|
3642
|
+
} catch {
|
|
3643
|
+
return "[unserializable cause]";
|
|
3644
|
+
}
|
|
3645
|
+
return denial.stack ?? "No cause recorded.";
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3648
|
+
//#endregion
|
|
3649
|
+
//#region src/subscribers/default/UnknownException.ts
|
|
3650
|
+
var _UnknownException;
|
|
3651
|
+
function webhookUrlValidator(raw) {
|
|
3652
|
+
if (raw !== null && typeof raw !== "string") throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigUnknownExceptionWebhookInvalid);
|
|
3653
|
+
const value = raw?.trim() ?? "";
|
|
3654
|
+
if (value === "") throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigUnknownExceptionWebhookMissing);
|
|
3655
|
+
if (!isDiscordWebhookUrl(value)) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigUnknownExceptionWebhookInvalid);
|
|
3656
|
+
return value;
|
|
3657
|
+
}
|
|
3658
|
+
let UnknownException = class UnknownException extends WebhookLog {
|
|
3659
|
+
static {
|
|
3660
|
+
_UnknownException = this;
|
|
3661
|
+
}
|
|
3662
|
+
webhook = new discord_js.WebhookClient({ url: _UnknownException.unknownExceptionWebhookUrl });
|
|
3663
|
+
async execute() {
|
|
3664
|
+
const { metadata } = this.data;
|
|
3665
|
+
const metadataFile = metadata ? jsonAttachment("metadata.json", "Metadata associated with the error", metadata) : null;
|
|
3666
|
+
try {
|
|
3667
|
+
await this.webhook.send({
|
|
3668
|
+
flags: flagsFor(false),
|
|
3669
|
+
withComponents: true,
|
|
3670
|
+
username: "Unknown Exception",
|
|
3671
|
+
components: [new UnhandledErrorContainer(this.data).component],
|
|
3672
|
+
files: metadataFile ? [metadataFile] : []
|
|
3673
|
+
});
|
|
3674
|
+
} catch (error) {
|
|
3675
|
+
this.logger.error("Failed to send unknown exception webhook", error);
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
};
|
|
3679
|
+
__decorate([(0, envapt.Envapt)("UNKNOWN_EXCEPTION_WEBHOOK_URL", { converter: (raw) => webhookUrlValidator(raw) }), __decorateMetadata("design:type", String)], UnknownException, "unknownExceptionWebhookUrl", void 0);
|
|
3680
|
+
UnknownException = _UnknownException = __decorate([Subscribe("unknownException")], UnknownException);
|
|
3681
|
+
var UnhandledErrorContainer = class extends _seedcord_core.BuilderComponent {
|
|
3682
|
+
constructor(data) {
|
|
3683
|
+
super("container");
|
|
3684
|
+
const { uuid, error, guild, user, metadata } = data;
|
|
3685
|
+
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.stack}\`\`\``));
|
|
3686
|
+
this.addTimestampsIfAvailable(error);
|
|
3687
|
+
this.addMetadataIfAvailable(metadata);
|
|
3688
|
+
}
|
|
3689
|
+
addTimestampsIfAvailable(error) {
|
|
3690
|
+
if (!(error instanceof discord_js.DiscordAPIError)) return;
|
|
3691
|
+
const now = Date.now();
|
|
3692
|
+
const snowflake = error.url.match(/\/interactions\/(\d+)\//)?.[1];
|
|
3693
|
+
if (!snowflake) return void 0;
|
|
3694
|
+
const interactionTs = Number(discord_js.SnowflakeUtil.deconstruct(snowflake).timestamp);
|
|
3695
|
+
const diff = now - interactionTs;
|
|
3696
|
+
const seconds = Math.floor(diff / 1e3);
|
|
3697
|
+
const millis = diff % 1e3;
|
|
3698
|
+
this.instance.addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### Timestamps\n- **\`Interaction sent\` :** ${new Date(interactionTs).toISOString()} (${interactionTs})\n- **\`Error logged \` :** ${new Date(now).toISOString()} (${now})\n- **\`Offset \` :** ${seconds}s ${millis}ms`));
|
|
3699
|
+
}
|
|
3700
|
+
addMetadataIfAvailable(metadata) {
|
|
3701
|
+
if (!metadata) return;
|
|
3702
|
+
this.instance.addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent("### Metadata")).addFileComponents((file) => file.setURL("attachment://metadata.json"));
|
|
3703
|
+
}
|
|
3704
|
+
};
|
|
3705
|
+
|
|
3706
|
+
//#endregion
|
|
3707
|
+
//#region src/subscribers/Bus.ts
|
|
3708
|
+
/**
|
|
3709
|
+
* Manages application subscribers and event handling
|
|
3710
|
+
*
|
|
3711
|
+
* Provides a centralized system for registering and executing custom subscribers
|
|
3712
|
+
* throughout the application lifecycle. Bus subscribers are loaded from configured directories
|
|
3713
|
+
* and can be triggered programmatically or by framework events. Accessed via `core.bus`. Do not
|
|
3714
|
+
* construct it directly.
|
|
3715
|
+
*/
|
|
3716
|
+
var Bus = class extends Plugin {
|
|
3717
|
+
core;
|
|
3718
|
+
logger = new _seedcord_services.Logger("Subscribers");
|
|
3719
|
+
/** @internal */
|
|
3720
|
+
name = "Subscribers";
|
|
3721
|
+
isInitialized = false;
|
|
3722
|
+
subscribersMap = /* @__PURE__ */ new Map();
|
|
3723
|
+
executedOnceHandlers = /* @__PURE__ */ new Set();
|
|
3724
|
+
hmrHandler;
|
|
3725
|
+
constructor(core) {
|
|
3726
|
+
super(core);
|
|
3727
|
+
this.core = core;
|
|
3728
|
+
if (this.core.config.subscribers.path) {
|
|
3729
|
+
if (!envapt.Envapter.isDevelopment && !envapt.Envapter.isTest) return;
|
|
3730
|
+
this.hmrHandler = new HmrModuleHandler({
|
|
3731
|
+
handlersDir: this.core.config.subscribers.path,
|
|
3732
|
+
isHandler: this.isSubscriber.bind(this),
|
|
3733
|
+
registerHandler: this.registerSubscriber.bind(this),
|
|
3734
|
+
unregisterHandler: this.unregisterSubscriber.bind(this),
|
|
3735
|
+
getArtifacts: this.getArtifacts.bind(this),
|
|
3736
|
+
logger: this.logger
|
|
3737
|
+
});
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
getArtifacts(ctor) {
|
|
3741
|
+
return [Reflect.getMetadata(_seedcord_core_internal.SubscribeMetadataKey, ctor).subscriber];
|
|
3742
|
+
}
|
|
3743
|
+
/** @internal */
|
|
3744
|
+
async init() {
|
|
3745
|
+
if (this.isInitialized) return;
|
|
3746
|
+
this.isInitialized = true;
|
|
3747
|
+
this.registerSubscriber(UnknownException);
|
|
3748
|
+
this.registerSubscriber(HandledException);
|
|
3749
|
+
envapt.Envapter.require("UNKNOWN_EXCEPTION_WEBHOOK_URL", "HANDLED_EXCEPTION_WEBHOOK_URL");
|
|
3750
|
+
const subscribersDir = this.core.config.subscribers.path;
|
|
3751
|
+
if (subscribersDir) {
|
|
3752
|
+
this.logger.info(chalk.default.bold(subscribersDir));
|
|
3753
|
+
await this.loadSubscribers(subscribersDir);
|
|
3754
|
+
const totalSubscribers = [...this.subscribersMap.values()].reduce((acc, handlers) => acc + handlers.length, 0);
|
|
3755
|
+
this.logger.utils.list([`${chalk.default.bold.magenta(totalSubscribers)} subscribers`], chalk.default.bold.green("Loaded"));
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3758
|
+
/** @internal For use in dev mode */
|
|
3759
|
+
async onHmr(event) {
|
|
3760
|
+
if (this.hmrHandler) await this.hmrHandler.handle(event);
|
|
3761
|
+
}
|
|
3762
|
+
async loadSubscribers(dir) {
|
|
3763
|
+
await (0, _seedcord_utils.traverseDirectory)(dir, (fullPath, relativePath, imported) => {
|
|
3764
|
+
for (const exportName of Object.keys(imported)) {
|
|
3765
|
+
const val = imported[exportName];
|
|
3766
|
+
if (this.isSubscriber(val)) {
|
|
3767
|
+
this.registerSubscriber(val);
|
|
3768
|
+
this.hmrHandler?.trackHandler(fullPath, val);
|
|
3769
|
+
this.logger.utils.registration(val.name, relativePath);
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
}, this.logger);
|
|
3773
|
+
}
|
|
3774
|
+
registerSubscriber(handler) {
|
|
3775
|
+
const options = Reflect.getMetadata(_seedcord_core_internal.SubscribeMetadataKey, handler);
|
|
3776
|
+
let handlers = this.subscribersMap.get(options.subscriber);
|
|
3777
|
+
if (!handlers) {
|
|
3778
|
+
handlers = [];
|
|
3779
|
+
this.subscribersMap.set(options.subscriber, handlers);
|
|
3780
|
+
}
|
|
3781
|
+
handlers.push({
|
|
3782
|
+
ctor: handler,
|
|
3783
|
+
frequency: options.frequency ?? "on"
|
|
3784
|
+
});
|
|
3785
|
+
}
|
|
3786
|
+
unregisterSubscriber(handler, artifacts) {
|
|
3787
|
+
const subscribers = artifacts ?? [...this.subscribersMap.keys()];
|
|
3788
|
+
for (const subscriber of subscribers) {
|
|
3789
|
+
const handlers = this.subscribersMap.get(subscriber);
|
|
3790
|
+
if (!handlers) continue;
|
|
3791
|
+
const index = handlers.findIndex((entry) => entry.ctor === handler);
|
|
3792
|
+
if (index !== -1) handlers.splice(index, 1);
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
isSubscriber(obj) {
|
|
3796
|
+
if (typeof obj !== "function") return false;
|
|
3797
|
+
return obj.prototype instanceof Subscriber && Reflect.hasMetadata(_seedcord_core_internal.SubscribeMetadataKey, obj);
|
|
3798
|
+
}
|
|
3799
|
+
/**
|
|
3800
|
+
* Publishes an event to its subscribers and native listeners.
|
|
3801
|
+
*
|
|
3802
|
+
* Fire-and-forget, so subscriber handlers run asynchronously and this returns before they
|
|
3803
|
+
* complete, and callers must not assume side effects have landed. Errors thrown by a subscriber
|
|
3804
|
+
* are caught and logged, never surfaced here. A `'once'` subscriber is marked as executed when
|
|
3805
|
+
* it starts (even if it throws), so it never runs twice.
|
|
3806
|
+
*
|
|
3807
|
+
* @param event - The subscription key to publish
|
|
3808
|
+
* @param data - Payload passed to each subscriber
|
|
3809
|
+
* @returns Whether the underlying emitter had native listeners for the event
|
|
3810
|
+
*/
|
|
3811
|
+
publish(event, data) {
|
|
3812
|
+
this.processSubscriber(event, data);
|
|
3813
|
+
return super.emit(event, ...[data]);
|
|
3814
|
+
}
|
|
3815
|
+
async processSubscriber(subscriberName, data) {
|
|
3816
|
+
const handlers = this.subscribersMap.get(subscriberName);
|
|
3817
|
+
if (!handlers) return;
|
|
3818
|
+
for (const entry of handlers) {
|
|
3819
|
+
if (entry.frequency === "once" && this.executedOnceHandlers.has(entry.ctor)) continue;
|
|
3820
|
+
try {
|
|
3821
|
+
if (entry.frequency === "once") this.executedOnceHandlers.add(entry.ctor);
|
|
3822
|
+
const Ctor = entry.ctor;
|
|
3823
|
+
await new Ctor(data, this.core).execute();
|
|
3824
|
+
} catch (err) {
|
|
3825
|
+
this.logger.error(`Error in subscriber ${String(subscriberName)} handler ${entry.ctor.name}:`, err);
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3829
|
+
};
|
|
3830
|
+
|
|
3831
|
+
//#endregion
|
|
3832
|
+
//#region src/version.ts
|
|
4
3833
|
/** Package version */
|
|
5
|
-
const version = "0.0.
|
|
3834
|
+
const version = "0.1.0-next.0";
|
|
6
3835
|
|
|
7
3836
|
//#endregion
|
|
3837
|
+
//#region src/Seedcord.ts
|
|
3838
|
+
/**
|
|
3839
|
+
* Main Seedcord bot framework class
|
|
3840
|
+
*
|
|
3841
|
+
* Primary entry point for creating Discord bots with Seedcord.
|
|
3842
|
+
* Manages component lifecycle and provides plugin support.
|
|
3843
|
+
*/
|
|
3844
|
+
var Seedcord = class Seedcord extends Pluggable {
|
|
3845
|
+
config;
|
|
3846
|
+
/** @internal */
|
|
3847
|
+
[_seedcord_types_internal.SeedcordBrand] = true;
|
|
3848
|
+
/** @internal */
|
|
3849
|
+
augmentTarget = "@seedcord/gateway";
|
|
3850
|
+
/** @internal */
|
|
3851
|
+
version = version;
|
|
3852
|
+
static isInstantiated = false;
|
|
3853
|
+
/** @see {@link CoordinatedShutdown} */
|
|
3854
|
+
shutdown;
|
|
3855
|
+
/** @see {@link CoordinatedStartup} */
|
|
3856
|
+
startup;
|
|
3857
|
+
/** @see {@link Bus} */
|
|
3858
|
+
bus;
|
|
3859
|
+
/** @see {@link Bot} */
|
|
3860
|
+
bot;
|
|
3861
|
+
/** @see {@link IRateLimiter} */
|
|
3862
|
+
rateLimiter;
|
|
3863
|
+
/** @see {@link HealthCheck} */
|
|
3864
|
+
healthCheck;
|
|
3865
|
+
/** @see {@link HmrManager} */
|
|
3866
|
+
hmrManager;
|
|
3867
|
+
/**
|
|
3868
|
+
* Creates a new Seedcord instance
|
|
3869
|
+
*
|
|
3870
|
+
* @param config - Bot configuration including paths and Discord client options
|
|
3871
|
+
* @throws A **SeedcordError** When attempting to create multiple instances (singleton)
|
|
3872
|
+
*/
|
|
3873
|
+
constructor(config) {
|
|
3874
|
+
if (Seedcord.isInstantiated) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CoreSingletonViolation);
|
|
3875
|
+
Seedcord.isInstantiated = true;
|
|
3876
|
+
const shutdown = new _seedcord_services.CoordinatedShutdown(config.shutdownEnabled);
|
|
3877
|
+
const startup = new _seedcord_services.CoordinatedStartup();
|
|
3878
|
+
super(shutdown, startup);
|
|
3879
|
+
this.config = config;
|
|
3880
|
+
(0, _seedcord_core_internal.setBotColor)(config.botColor);
|
|
3881
|
+
this.shutdown = shutdown;
|
|
3882
|
+
this.startup = startup;
|
|
3883
|
+
this.hmrManager = new HmrManager();
|
|
3884
|
+
this.hmrManager.init();
|
|
3885
|
+
this.bus = new Bus(this);
|
|
3886
|
+
this.bot = new Bot(this);
|
|
3887
|
+
this.rateLimiter = new _seedcord_rate_limiter.MemoryRateLimiter();
|
|
3888
|
+
this.healthCheck = new _seedcord_services.HealthCheck(this.shutdown, config.healthCheck);
|
|
3889
|
+
this.registerStartupTasks();
|
|
3890
|
+
}
|
|
3891
|
+
/** @internal */
|
|
3892
|
+
static reset() {
|
|
3893
|
+
Seedcord.isInstantiated = false;
|
|
3894
|
+
}
|
|
3895
|
+
/**
|
|
3896
|
+
* Registers default startup tasks
|
|
3897
|
+
* @internal
|
|
3898
|
+
*/
|
|
3899
|
+
registerStartupTasks() {
|
|
3900
|
+
if (envapt.Envapter.isDevelopment || envapt.Envapter.isTest) this.registerHmrAwareModules();
|
|
3901
|
+
this.startup.addTask(_seedcord_services.StartupPhase.Configuration, "Bus Initialization", async () => {
|
|
3902
|
+
this.bus.logger.utils.initialization("Subscribers", "start");
|
|
3903
|
+
await this.bus.init();
|
|
3904
|
+
this.bus.logger.utils.initialization("Subscribers", "end");
|
|
3905
|
+
});
|
|
3906
|
+
this.startup.addTask(_seedcord_services.StartupPhase.Instantiation, "Bot Initialization", async () => {
|
|
3907
|
+
this.bot.logger.utils.initialization("Bot", "start");
|
|
3908
|
+
await this.bot.init();
|
|
3909
|
+
this.bot.logger.utils.initialization("Bot", "end");
|
|
3910
|
+
});
|
|
3911
|
+
this.startup.addTask(_seedcord_services.StartupPhase.Ready, "Health Check", async () => {
|
|
3912
|
+
this.healthCheck.logger.utils.initialization("HealthCheck", "start");
|
|
3913
|
+
await this.healthCheck.init();
|
|
3914
|
+
this.healthCheck.logger.utils.initialization("HealthCheck", "end");
|
|
3915
|
+
});
|
|
3916
|
+
}
|
|
3917
|
+
/**
|
|
3918
|
+
* Starts the bot and runs all initialization tasks
|
|
3919
|
+
*
|
|
3920
|
+
* @returns This Seedcord instance when fully initialized
|
|
3921
|
+
*/
|
|
3922
|
+
async start() {
|
|
3923
|
+
await super.init();
|
|
3924
|
+
return this;
|
|
3925
|
+
}
|
|
3926
|
+
registerHmrAwareModules() {
|
|
3927
|
+
this.startup.addTask(_seedcord_services.StartupPhase.Configuration, "HMR Registration", async () => {
|
|
3928
|
+
this.hmrManager.register(this.bot);
|
|
3929
|
+
this.hmrManager.register(this.bus);
|
|
3930
|
+
for (const plugin of this.plugins) this.hmrManager.register(plugin);
|
|
3931
|
+
await Promise.resolve();
|
|
3932
|
+
});
|
|
3933
|
+
}
|
|
3934
|
+
};
|
|
3935
|
+
|
|
3936
|
+
//#endregion
|
|
3937
|
+
exports.ArraySource = ArraySource;
|
|
3938
|
+
exports.AutocompleteHandler = AutocompleteHandler;
|
|
3939
|
+
exports.AutocompleteRoute = AutocompleteRoute;
|
|
3940
|
+
exports.ButtonHandler = ButtonHandler;
|
|
3941
|
+
exports.ButtonRoute = ButtonRoute;
|
|
3942
|
+
exports.CommandMentions = CommandMentions;
|
|
3943
|
+
exports.ContextMenuHandler = ContextMenuHandler;
|
|
3944
|
+
exports.ContextMenuRoute = ContextMenuRoute;
|
|
3945
|
+
exports.CursorSource = CursorSource;
|
|
3946
|
+
exports.Emojis = Emojis;
|
|
3947
|
+
exports.EventHandler = EventHandler;
|
|
3948
|
+
exports.EventMiddleware = EventMiddleware;
|
|
3949
|
+
exports.Gated = Gated;
|
|
3950
|
+
exports.HmrModuleHandler = HmrModuleHandler;
|
|
3951
|
+
exports.IgnoreBots = IgnoreBots;
|
|
3952
|
+
exports.InteractionHandler = InteractionHandler;
|
|
3953
|
+
exports.InteractionMiddleware = InteractionMiddleware;
|
|
3954
|
+
exports.Middleware = Middleware;
|
|
3955
|
+
exports.MiddlewareType = MiddlewareType;
|
|
3956
|
+
exports.ModalHandler = ModalHandler;
|
|
3957
|
+
exports.ModalRoute = ModalRoute;
|
|
3958
|
+
exports.Nsfw = Nsfw;
|
|
3959
|
+
exports.Paginator = Paginator;
|
|
3960
|
+
exports.Pluggable = Pluggable;
|
|
3961
|
+
exports.Plugin = Plugin;
|
|
3962
|
+
Object.defineProperty(exports, 'RegisterCommand', {
|
|
3963
|
+
enumerable: true,
|
|
3964
|
+
get: function () {
|
|
3965
|
+
return _seedcord_core.RegisterCommand;
|
|
3966
|
+
}
|
|
3967
|
+
});
|
|
3968
|
+
exports.RegisterEvent = RegisterEvent;
|
|
3969
|
+
exports.ReplySender = ReplySender;
|
|
3970
|
+
exports.RequireBotPermissions = RequireBotPermissions;
|
|
3971
|
+
exports.RequirePermissions = RequirePermissions;
|
|
3972
|
+
exports.RequireRole = RequireRole;
|
|
3973
|
+
exports.Seedcord = Seedcord;
|
|
3974
|
+
exports.SelectMenuHandler = SelectMenuHandler;
|
|
3975
|
+
exports.SelectMenuKind = SelectMenuKind;
|
|
3976
|
+
exports.SelectMenuRoute = SelectMenuRoute;
|
|
3977
|
+
exports.SlashHandler = SlashHandler;
|
|
3978
|
+
exports.SlashRoute = SlashRoute;
|
|
3979
|
+
exports.Subscribe = Subscribe;
|
|
3980
|
+
exports.Subscriber = Subscriber;
|
|
3981
|
+
exports.WebhookLog = WebhookLog;
|
|
3982
|
+
exports.checkBotPermissions = checkBotPermissions;
|
|
3983
|
+
exports.checkPermissions = checkPermissions;
|
|
3984
|
+
exports.fetchGuildMember = fetchGuildMember;
|
|
3985
|
+
exports.fetchManyGuildMembers = fetchManyGuildMembers;
|
|
3986
|
+
exports.fetchManyUsers = fetchManyUsers;
|
|
3987
|
+
exports.fetchRole = fetchRole;
|
|
3988
|
+
exports.fetchText = fetchText;
|
|
3989
|
+
exports.fetchUser = fetchUser;
|
|
3990
|
+
exports.getBotRole = getBotRole;
|
|
3991
|
+
exports.getConfirmation = getConfirmation;
|
|
3992
|
+
exports.hasPermsToAssign = hasPermsToAssign;
|
|
3993
|
+
exports.updateMemberRoles = updateMemberRoles;
|
|
8
3994
|
exports.version = version;
|
|
3995
|
+
Object.keys(_seedcord_core).forEach(function (k) {
|
|
3996
|
+
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
|
3997
|
+
enumerable: true,
|
|
3998
|
+
get: function () { return _seedcord_core[k]; }
|
|
3999
|
+
});
|
|
4000
|
+
});
|
|
4001
|
+
|
|
4002
|
+
Object.keys(_seedcord_errors).forEach(function (k) {
|
|
4003
|
+
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
|
4004
|
+
enumerable: true,
|
|
4005
|
+
get: function () { return _seedcord_errors[k]; }
|
|
4006
|
+
});
|
|
4007
|
+
});
|
|
4008
|
+
|
|
4009
|
+
Object.keys(_seedcord_rate_limiter).forEach(function (k) {
|
|
4010
|
+
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
|
4011
|
+
enumerable: true,
|
|
4012
|
+
get: function () { return _seedcord_rate_limiter[k]; }
|
|
4013
|
+
});
|
|
4014
|
+
});
|
|
4015
|
+
|
|
4016
|
+
Object.keys(_seedcord_services).forEach(function (k) {
|
|
4017
|
+
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
|
4018
|
+
enumerable: true,
|
|
4019
|
+
get: function () { return _seedcord_services[k]; }
|
|
4020
|
+
});
|
|
4021
|
+
});
|
|
4022
|
+
|
|
4023
|
+
Object.keys(_seedcord_utils).forEach(function (k) {
|
|
4024
|
+
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
|
4025
|
+
enumerable: true,
|
|
4026
|
+
get: function () { return _seedcord_utils[k]; }
|
|
4027
|
+
});
|
|
4028
|
+
});
|
|
4029
|
+
|
|
9
4030
|
//# sourceMappingURL=index.cjs.map
|