@seedcord/core 0.9.4 → 0.11.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 +28 -10
- package/dist/CoordinatedShutdown-H5YjZn8A.d.mts +78 -0
- package/dist/CoordinatedStartup-CGnJrqcl.mjs +168 -0
- package/dist/CoordinatedStartup-CGnJrqcl.mjs.map +1 -0
- package/dist/{HmrModuleHandler-DOAtah3j.mjs → HmrModuleHandler-8ZC-UuZh.mjs} +9 -22
- package/dist/HmrModuleHandler-8ZC-UuZh.mjs.map +1 -0
- package/dist/PluginHost-B8kxLOeb.mjs +245 -0
- package/dist/PluginHost-B8kxLOeb.mjs.map +1 -0
- package/dist/{Plugin-Dhx_ivqj.d.mts → PluginHost-DB4j8unu.d.mts} +110 -17
- package/dist/PluginLifecycle-AhQ46NIr.mjs +158 -0
- package/dist/PluginLifecycle-AhQ46NIr.mjs.map +1 -0
- package/dist/ServerHost-CvGQr7oN.mjs +191 -0
- package/dist/ServerHost-CvGQr7oN.mjs.map +1 -0
- package/dist/ServerHost-DYeh0KTk.d.mts +40 -0
- package/dist/hmr.index.d.mts +0 -1
- package/dist/hmr.index.mjs +1 -1
- package/dist/{cursor-BTU5bGoo.d.mts → index-Du71Xx69.d.mts} +253 -6
- package/dist/index.d.mts +19 -84
- package/dist/index.mjs +17 -37
- package/dist/index.mjs.map +1 -1
- package/dist/internal.index.d.mts +185 -22
- package/dist/internal.index.mjs +713 -19
- package/dist/internal.index.mjs.map +1 -1
- package/dist/node-internal.index.d.mts +25 -70
- package/dist/node-internal.index.mjs +84 -298
- package/dist/node-internal.index.mjs.map +1 -1
- package/dist/node.index.d.mts +3 -2
- package/dist/node.index.mjs +53 -200
- package/dist/node.index.mjs.map +1 -1
- package/dist/{phases-D_zQBXga.d.mts → phases-FTM69J3a.d.mts} +4 -4
- package/dist/plugin.index.d.mts +2 -2
- package/dist/plugin.index.mjs +2 -2
- package/dist/publishDefault-D3dAyzZ9.mjs +7 -0
- package/dist/publishDefault-D3dAyzZ9.mjs.map +1 -0
- package/dist/subscribers-Ce3SWy23.mjs +1913 -0
- package/dist/subscribers-Ce3SWy23.mjs.map +1 -0
- package/package.json +17 -16
- package/dist/Bus-BxPa-Uvp.mjs +0 -866
- package/dist/Bus-BxPa-Uvp.mjs.map +0 -1
- package/dist/HmrModuleHandler-DOAtah3j.mjs.map +0 -1
- package/dist/Pluggable-CXyUZIvT.mjs +0 -310
- package/dist/Pluggable-CXyUZIvT.mjs.map +0 -1
- package/dist/Pluggable-CwKbLui3.d.mts +0 -162
- package/dist/Plugin-CmNW5n6a.mjs +0 -122
- package/dist/Plugin-CmNW5n6a.mjs.map +0 -1
- package/dist/contextMenuLeaves-COzDYa6c.d.mts +0 -17
- package/dist/cursor-CgNBnJfu.mjs +0 -802
- package/dist/cursor-CgNBnJfu.mjs.map +0 -1
- package/dist/devChannel-CQU7hq7Q.mjs +0 -14
- package/dist/devChannel-CQU7hq7Q.mjs.map +0 -1
- package/dist/index-B_-EjyvS.d.mts +0 -135
- package/dist/publishDefault-BuSZPbjT.mjs +0 -14
- package/dist/publishDefault-BuSZPbjT.mjs.map +0 -1
- package/dist/slashRouteLeaves-k1hJnXDO.mjs +0 -31
- package/dist/slashRouteLeaves-k1hJnXDO.mjs.map +0 -1
- package/dist/subscribers-xGy7a7xR.mjs +0 -167
- package/dist/subscribers-xGy7a7xR.mjs.map +0 -1
- package/dist/types-BG0BimVr.d.mts +0 -13
|
@@ -0,0 +1,1913 @@
|
|
|
1
|
+
import { t as PublishDefault } from "./publishDefault-D3dAyzZ9.mjs";
|
|
2
|
+
import "reflect-metadata";
|
|
3
|
+
import { CustomId, decodeFor } from "@seedcord/custom-id";
|
|
4
|
+
import { ActionRowBuilder, ButtonBuilder, ChannelSelectMenuBuilder, CheckboxBuilder, CheckboxGroupBuilder, CheckboxGroupOptionBuilder, ContainerBuilder, ContextMenuCommandBuilder, EmbedBuilder, FileBuilder, FileUploadBuilder, LabelBuilder, MediaGalleryBuilder, MentionableSelectMenuBuilder, ModalBuilder, RadioGroupBuilder, RadioGroupOptionBuilder, RoleSelectMenuBuilder, SectionBuilder, SeparatorBuilder, SlashCommandBuilder, SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, TextDisplayBuilder, TextInputBuilder, UserSelectMenuBuilder } from "@discordjs/builders";
|
|
5
|
+
import { ApplicationCommandType, InteractionContextType, MessageFlags, PermissionFlagsBits, Routes, SeparatorSpacingSize } from "discord-api-types/v10";
|
|
6
|
+
import { SeedcordErrorCode, paint } from "@seedcord/errors";
|
|
7
|
+
import { SeedcordError, SeedcordRangeError, SeedcordTypeError } from "@seedcord/errors/internal";
|
|
8
|
+
import { Logger } from "@seedcord/logger";
|
|
9
|
+
import { asError, isStringArray, pluralize, routeLeavesOf } from "@seedcord/utils/internal";
|
|
10
|
+
import { filterCirculars, prettify, stripAnsi, timestampFromSnowflake, toEpochSeconds } from "@seedcord/utils";
|
|
11
|
+
import { TypedEventEmitter } from "@seedcord/event-emitter";
|
|
12
|
+
import { Envapter } from "envapt";
|
|
13
|
+
import { DiscordAPIError, REST } from "@discordjs/rest";
|
|
14
|
+
|
|
15
|
+
//#region src/stops/Notice.ts
|
|
16
|
+
/**
|
|
17
|
+
* Base class for a user-facing refusal or a reported fault.
|
|
18
|
+
*
|
|
19
|
+
* Throw a `Notice` to stop a handler and reply to the user. The framework catches it at the controller
|
|
20
|
+
* boundary and renders {@link Notice.render}, which always decides what the user sees. With `report`
|
|
21
|
+
* false that render is all that happens. With `report` true the framework also logs the fault and
|
|
22
|
+
* publishes it to the `handledException` bus. A raw, non-Notice throw shows the generic message.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { Notice, BuilderComponent, type RenderContext, type ReplyResponse } from '@seedcord/gateway';
|
|
27
|
+
* import { TextDisplayBuilder } from '@discordjs/builders';
|
|
28
|
+
*
|
|
29
|
+
* // reading `.component` applies the configured bot color to the container accent
|
|
30
|
+
* class TooPoorCard extends BuilderComponent<'container'> {
|
|
31
|
+
* constructor(balance: number) {
|
|
32
|
+
* super('container');
|
|
33
|
+
* this.instance.addTextDisplayComponents(
|
|
34
|
+
* new TextDisplayBuilder().setContent(`### Insufficient balance\nYou need more than ${balance} coins.`)
|
|
35
|
+
* );
|
|
36
|
+
* }
|
|
37
|
+
* }
|
|
38
|
+
*
|
|
39
|
+
* class TooPoor extends Notice {
|
|
40
|
+
* constructor(private readonly balance: number) {
|
|
41
|
+
* super(`balance ${balance} is below the cost`);
|
|
42
|
+
* }
|
|
43
|
+
*
|
|
44
|
+
* render(_ctx: RenderContext): ReplyResponse {
|
|
45
|
+
* return { components: [new TooPoorCard(this.balance).component] };
|
|
46
|
+
* }
|
|
47
|
+
* }
|
|
48
|
+
*
|
|
49
|
+
* // in a handler, throwing stops the handler and replies with render(ctx)
|
|
50
|
+
* if (wallet.balance < cost) throw new TooPoor(wallet.balance);
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
var Notice = class extends Error {
|
|
54
|
+
/**
|
|
55
|
+
* Whether this denial is a reported fault. True also logs it and publishes it to the `handledException`
|
|
56
|
+
* bus. The user always sees {@link Notice.render} either way.
|
|
57
|
+
*
|
|
58
|
+
* @defaultValue `false`
|
|
59
|
+
*/
|
|
60
|
+
report = false;
|
|
61
|
+
/**
|
|
62
|
+
* Whether the reply is ephemeral, seen only by the invoking user. Set it false for a refusal the
|
|
63
|
+
* whole channel should see.
|
|
64
|
+
*
|
|
65
|
+
* @defaultValue `true`
|
|
66
|
+
*/
|
|
67
|
+
ephemeral = true;
|
|
68
|
+
/**
|
|
69
|
+
* A short one-line reason. When every arm of an {@link or} gate refuses and each one sets this, `or`
|
|
70
|
+
* throws a refusal listing them under `You need to meet at least one of these:`. Write a verb phrase that
|
|
71
|
+
* finishes that line, for example, "be the bot owner".
|
|
72
|
+
*/
|
|
73
|
+
summary;
|
|
74
|
+
constructor(message, options) {
|
|
75
|
+
super(message, options);
|
|
76
|
+
this.name = new.target.name;
|
|
77
|
+
Error.captureStackTrace(this, this.constructor);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/components/botColorHolder.ts
|
|
83
|
+
const DEFAULT_COLOR = "Default";
|
|
84
|
+
let readBotColor = () => void 0;
|
|
85
|
+
function bindBotColor(read) {
|
|
86
|
+
readBotColor = read;
|
|
87
|
+
}
|
|
88
|
+
function getBotColor() {
|
|
89
|
+
return readBotColor() ?? DEFAULT_COLOR;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/components/builderTypes.ts
|
|
94
|
+
/**
|
|
95
|
+
* Available Discord.js builder classes for use with BuilderComponent for commands, embeds, modals, etc.
|
|
96
|
+
*
|
|
97
|
+
* @internal
|
|
98
|
+
*/
|
|
99
|
+
const BuilderTypes = {
|
|
100
|
+
command: SlashCommandBuilder,
|
|
101
|
+
context_menu: ContextMenuCommandBuilder,
|
|
102
|
+
subcommand: SlashCommandSubcommandBuilder,
|
|
103
|
+
group: SlashCommandSubcommandGroupBuilder,
|
|
104
|
+
embed: EmbedBuilder,
|
|
105
|
+
modal: ModalBuilder,
|
|
106
|
+
label: LabelBuilder,
|
|
107
|
+
text_input: TextInputBuilder,
|
|
108
|
+
file_upload: FileUploadBuilder,
|
|
109
|
+
checkbox: CheckboxBuilder,
|
|
110
|
+
checkbox_group: CheckboxGroupBuilder,
|
|
111
|
+
checkbox_group_option: CheckboxGroupOptionBuilder,
|
|
112
|
+
radio_group: RadioGroupBuilder,
|
|
113
|
+
radio_group_option: RadioGroupOptionBuilder,
|
|
114
|
+
button: ButtonBuilder,
|
|
115
|
+
menu_string: StringSelectMenuBuilder,
|
|
116
|
+
menu_option_string: StringSelectMenuOptionBuilder,
|
|
117
|
+
menu_user: UserSelectMenuBuilder,
|
|
118
|
+
menu_channel: ChannelSelectMenuBuilder,
|
|
119
|
+
menu_mentionable: MentionableSelectMenuBuilder,
|
|
120
|
+
menu_role: RoleSelectMenuBuilder,
|
|
121
|
+
container: ContainerBuilder,
|
|
122
|
+
text_display: TextDisplayBuilder,
|
|
123
|
+
file: FileBuilder,
|
|
124
|
+
media: MediaGalleryBuilder,
|
|
125
|
+
section: SectionBuilder,
|
|
126
|
+
separator: SeparatorBuilder
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Available Discord.js action row classes for use with RowComponent for Select Menus and Buttons
|
|
130
|
+
*
|
|
131
|
+
* @internal
|
|
132
|
+
*/
|
|
133
|
+
const RowTypes = {
|
|
134
|
+
button: ActionRowBuilder,
|
|
135
|
+
menu_string: ActionRowBuilder,
|
|
136
|
+
menu_user: ActionRowBuilder,
|
|
137
|
+
menu_channel: ActionRowBuilder,
|
|
138
|
+
menu_mentionable: ActionRowBuilder,
|
|
139
|
+
menu_role: ActionRowBuilder
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/components/colors.ts
|
|
144
|
+
const Colors = {
|
|
145
|
+
Aqua: 1752220,
|
|
146
|
+
Blue: 3447003,
|
|
147
|
+
Blurple: 5793266,
|
|
148
|
+
DarkAqua: 1146986,
|
|
149
|
+
DarkBlue: 2123412,
|
|
150
|
+
DarkButNotBlack: 2895667,
|
|
151
|
+
DarkerGrey: 8359053,
|
|
152
|
+
DarkGold: 12745742,
|
|
153
|
+
DarkGreen: 2067276,
|
|
154
|
+
DarkGrey: 9936031,
|
|
155
|
+
DarkNavy: 2899536,
|
|
156
|
+
DarkOrange: 11027200,
|
|
157
|
+
DarkPurple: 7419530,
|
|
158
|
+
DarkRed: 10038562,
|
|
159
|
+
DarkVividPink: 11342935,
|
|
160
|
+
Default: 0,
|
|
161
|
+
Fuchsia: 15418782,
|
|
162
|
+
Gold: 15844367,
|
|
163
|
+
Green: 5763719,
|
|
164
|
+
Grey: 9807270,
|
|
165
|
+
Greyple: 10070709,
|
|
166
|
+
LightGrey: 12370112,
|
|
167
|
+
LuminousVividPink: 15277667,
|
|
168
|
+
Navy: 3426654,
|
|
169
|
+
NotQuiteBlack: 2303786,
|
|
170
|
+
Orange: 15105570,
|
|
171
|
+
Purple: 10181046,
|
|
172
|
+
Red: 15548997,
|
|
173
|
+
White: 16777215,
|
|
174
|
+
Yellow: 16705372
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region src/components/resolveColor.ts
|
|
179
|
+
const MAX_COLOR = 16777215;
|
|
180
|
+
const RED_SHIFT = 16;
|
|
181
|
+
const GREEN_SHIFT = 8;
|
|
182
|
+
const palette = Colors;
|
|
183
|
+
/**
|
|
184
|
+
* Resolves a {@link BotColor} to the integer Discord expects.
|
|
185
|
+
*
|
|
186
|
+
* Ported from discord.js (Apache-2.0), `resolveColor` in src/util/Util.js.
|
|
187
|
+
*/
|
|
188
|
+
function resolveColor(color) {
|
|
189
|
+
let resolved;
|
|
190
|
+
if (typeof color === "string") {
|
|
191
|
+
if (color === "Random") return Math.floor(Math.random() * 16777216);
|
|
192
|
+
if (color === "Default") return 0;
|
|
193
|
+
if (/^#?[\da-f]{6}$/i.test(color)) return Number.parseInt(color.replace("#", ""), 16);
|
|
194
|
+
resolved = palette[color];
|
|
195
|
+
} else if (typeof color === "number") resolved = color;
|
|
196
|
+
else resolved = (color[0] << RED_SHIFT) + (color[1] << GREEN_SHIFT) + color[2];
|
|
197
|
+
if (resolved === void 0 || !Number.isSafeInteger(resolved)) throw new SeedcordTypeError(SeedcordErrorCode.ColorUnresolvable, [JSON.stringify(color)]);
|
|
198
|
+
if (resolved < 0 || resolved > MAX_COLOR) throw new SeedcordRangeError(SeedcordErrorCode.ColorOutOfRange, [resolved]);
|
|
199
|
+
return resolved;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
//#endregion
|
|
203
|
+
//#region src/components/Component.ts
|
|
204
|
+
/** @internal */
|
|
205
|
+
var BaseComponent = class {
|
|
206
|
+
_component;
|
|
207
|
+
constructor(ComponentClass) {
|
|
208
|
+
this._component = new ComponentClass();
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* The wrapped builder, for calling Discord.js methods like setTitle() and setDescription() inside a subclass.
|
|
212
|
+
*
|
|
213
|
+
* @example this.instance.setTitle('My Modal')
|
|
214
|
+
*/
|
|
215
|
+
get instance() {
|
|
216
|
+
return this._component;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
/**
|
|
220
|
+
* Base class for Discord.js builder components
|
|
221
|
+
*
|
|
222
|
+
* Wraps Discord.js builders (SlashCommandBuilder, EmbedBuilder, etc.) with
|
|
223
|
+
* Seedcord-specific defaults and helper methods.
|
|
224
|
+
*
|
|
225
|
+
* @typeParam BuilderKey - The type of Discord.js builder being wrapped
|
|
226
|
+
*/
|
|
227
|
+
var BuilderComponent = class extends BaseComponent {
|
|
228
|
+
type;
|
|
229
|
+
colorApplied = false;
|
|
230
|
+
constructor(type) {
|
|
231
|
+
const ComponentClass = BuilderTypes[type];
|
|
232
|
+
super(ComponentClass);
|
|
233
|
+
this.type = type;
|
|
234
|
+
if (this.instance instanceof SlashCommandBuilder || this.instance instanceof ContextMenuCommandBuilder) this.instance.setContexts(InteractionContextType.Guild);
|
|
235
|
+
}
|
|
236
|
+
get component() {
|
|
237
|
+
this.applyBotColor();
|
|
238
|
+
return this.instance;
|
|
239
|
+
}
|
|
240
|
+
applyBotColor() {
|
|
241
|
+
if (this.colorApplied) return;
|
|
242
|
+
this.colorApplied = true;
|
|
243
|
+
const color = getBotColor();
|
|
244
|
+
if (this.instance instanceof EmbedBuilder) {
|
|
245
|
+
if (this.instance.data.color === void 0) this.instance.setColor(resolveColor(color));
|
|
246
|
+
} else if (this.instance instanceof ContainerBuilder) {
|
|
247
|
+
const accent = this.instance.data.accent_color;
|
|
248
|
+
if (accent === null || accent === void 0) this.instance.setAccentColor(color === "Default" ? void 0 : resolveColor(color));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
const BUILDER_COMPONENT = Symbol.for("seedcord:core:builder-component");
|
|
253
|
+
Object.defineProperty(BuilderComponent.prototype, BUILDER_COMPONENT, { value: true });
|
|
254
|
+
/** @internal */
|
|
255
|
+
function isBuilderComponentClass(value) {
|
|
256
|
+
if (typeof value !== "function") return false;
|
|
257
|
+
const proto = value.prototype;
|
|
258
|
+
return typeof proto === "object" && proto !== null && BUILDER_COMPONENT in proto;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Base class for Discord action row components
|
|
262
|
+
*
|
|
263
|
+
* Wraps Discord.js action row builder with Seedcord-specific defaults and helper methods.
|
|
264
|
+
*
|
|
265
|
+
* @typeParam RowKey - The Discord.js action row type being wrapped
|
|
266
|
+
*/
|
|
267
|
+
var RowComponent = class extends BaseComponent {
|
|
268
|
+
type;
|
|
269
|
+
constructor(type) {
|
|
270
|
+
const ComponentClass = RowTypes[type];
|
|
271
|
+
super(ComponentClass);
|
|
272
|
+
this.type = type;
|
|
273
|
+
}
|
|
274
|
+
get component() {
|
|
275
|
+
return this.instance;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
//#endregion
|
|
280
|
+
//#region src/stops/NoticeCard.ts
|
|
281
|
+
var NoticeCard = class extends BuilderComponent {
|
|
282
|
+
constructor(description, title = "Cannot Proceed") {
|
|
283
|
+
super("container");
|
|
284
|
+
this.instance.addTextDisplayComponents(new TextDisplayBuilder().setContent(`### ${title}\n${description}`));
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
//#endregion
|
|
289
|
+
//#region src/commands/contextMenuLeaves.ts
|
|
290
|
+
function contextMenuLeaves(commands) {
|
|
291
|
+
const user = /* @__PURE__ */ new Set();
|
|
292
|
+
const message = /* @__PURE__ */ new Set();
|
|
293
|
+
for (const command of commands) if (command.type === ApplicationCommandType.User) user.add(command.name);
|
|
294
|
+
else if (command.type === ApplicationCommandType.Message) message.add(command.name);
|
|
295
|
+
return {
|
|
296
|
+
user,
|
|
297
|
+
message
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
//#endregion
|
|
302
|
+
//#region src/commands/scopes.ts
|
|
303
|
+
const GLOBAL_ID = "global";
|
|
304
|
+
const allOf$1 = new Intl.ListFormat("en", { type: "conjunction" });
|
|
305
|
+
function commandsRoute(applicationId, scope) {
|
|
306
|
+
return scope === "global" ? Routes.applicationCommands(applicationId) : Routes.applicationGuildCommands(applicationId, scope);
|
|
307
|
+
}
|
|
308
|
+
function globalFirst(scopes) {
|
|
309
|
+
return [...scopes].toSorted(([a], [b]) => Number(b === GLOBAL_ID) - Number(a === GLOBAL_ID));
|
|
310
|
+
}
|
|
311
|
+
function scopeIdsOf(target) {
|
|
312
|
+
return target.scope === "global" ? [GLOBAL_ID] : target.guilds;
|
|
313
|
+
}
|
|
314
|
+
function skippedNote(skipped, guildLabel) {
|
|
315
|
+
const guilds = skipped.filter((scope) => scope !== GLOBAL_ID).map(guildLabel);
|
|
316
|
+
const parts = [...skipped.includes("global") ? ["the global commands"] : [], ...guilds.length > 0 ? [`the commands in ${allOf$1.format(guilds)}`] : []];
|
|
317
|
+
return `Skipped ${allOf$1.format(parts)} because they match the last deploy.`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
//#endregion
|
|
321
|
+
//#region src/miscellaneous/guarded.ts
|
|
322
|
+
function accessorStore() {
|
|
323
|
+
return Object.create(null);
|
|
324
|
+
}
|
|
325
|
+
function clearStore(store) {
|
|
326
|
+
for (const key of Object.keys(store)) Reflect.deleteProperty(store, key);
|
|
327
|
+
}
|
|
328
|
+
function guardedAccessor(accessor, storage) {
|
|
329
|
+
return new Proxy(storage, { get(target, key, receiver) {
|
|
330
|
+
if (typeof key !== "string" || Object.hasOwn(target, key)) return Reflect.get(target, key, receiver);
|
|
331
|
+
throw new SeedcordError(SeedcordErrorCode.CoreAccessorUnresolved, [accessor, key]);
|
|
332
|
+
} });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
//#endregion
|
|
336
|
+
//#region src/commands/linkCommands.ts
|
|
337
|
+
const MULTI_GUILD = Symbol("seedcord:commands:multi-guild");
|
|
338
|
+
function commandRefOf({ name, type }) {
|
|
339
|
+
return {
|
|
340
|
+
name,
|
|
341
|
+
type: type ?? ApplicationCommandType.ChatInput
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function commandKey(command) {
|
|
345
|
+
const { type, name } = commandRefOf(command);
|
|
346
|
+
return `${type}:${name}`;
|
|
347
|
+
}
|
|
348
|
+
function indexById(commands) {
|
|
349
|
+
return new Map(commands.map((command) => [command.id, command]));
|
|
350
|
+
}
|
|
351
|
+
function isSlash(json) {
|
|
352
|
+
return commandRefOf(json).type === ApplicationCommandType.ChatInput;
|
|
353
|
+
}
|
|
354
|
+
function uniqueRefs(refs) {
|
|
355
|
+
return [...new Map(refs.map((ref) => [commandKey(ref), ref])).values()];
|
|
356
|
+
}
|
|
357
|
+
function deployedIds(deploy) {
|
|
358
|
+
const ids = /* @__PURE__ */ new Map();
|
|
359
|
+
for (const [scope, collection] of deploy) {
|
|
360
|
+
if (scope === "global") continue;
|
|
361
|
+
for (const command of collection.values()) {
|
|
362
|
+
const key = commandKey(command);
|
|
363
|
+
const existing = ids.get(key);
|
|
364
|
+
if (existing === void 0) ids.set(key, command.id);
|
|
365
|
+
else if (existing !== command.id) ids.set(key, MULTI_GUILD);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
for (const command of deploy.get("global")?.values() ?? []) ids.set(commandKey(command), command.id);
|
|
369
|
+
return ids;
|
|
370
|
+
}
|
|
371
|
+
function allDeployed(deploy) {
|
|
372
|
+
return [...deploy.values()].flatMap((collection) => [...collection.values()]);
|
|
373
|
+
}
|
|
374
|
+
function mentionOf(route, id) {
|
|
375
|
+
const spaced = route.replaceAll("/", " ");
|
|
376
|
+
return id ? `</${spaced}:${id}>` : `/${spaced}`;
|
|
377
|
+
}
|
|
378
|
+
var CommandLinker = class {
|
|
379
|
+
deploy;
|
|
380
|
+
ids;
|
|
381
|
+
constructor(deploy) {
|
|
382
|
+
this.deploy = deploy;
|
|
383
|
+
this.ids = deployedIds(deploy);
|
|
384
|
+
}
|
|
385
|
+
link(all) {
|
|
386
|
+
const slash = all.filter(isSlash);
|
|
387
|
+
const leaves = contextMenuLeaves(all);
|
|
388
|
+
const inCode = uniqueRefs([
|
|
389
|
+
...slash.map(commandRefOf),
|
|
390
|
+
...[...leaves.user].map((name) => ({
|
|
391
|
+
name,
|
|
392
|
+
type: ApplicationCommandType.User
|
|
393
|
+
})),
|
|
394
|
+
...[...leaves.message].map((name) => ({
|
|
395
|
+
name,
|
|
396
|
+
type: ApplicationCommandType.Message
|
|
397
|
+
}))
|
|
398
|
+
]);
|
|
399
|
+
return {
|
|
400
|
+
commands: this.slashRoutes(slash),
|
|
401
|
+
contextMenus: {
|
|
402
|
+
user: this.menus(leaves.user, ApplicationCommandType.User),
|
|
403
|
+
message: this.menus(leaves.message, ApplicationCommandType.Message)
|
|
404
|
+
},
|
|
405
|
+
multiGuild: inCode.filter((ref) => this.ids.get(commandKey(ref)) === MULTI_GUILD),
|
|
406
|
+
notInCode: this.notInCode(inCode)
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
slashRoutes(slash) {
|
|
410
|
+
const commands = accessorStore();
|
|
411
|
+
for (const command of slash) {
|
|
412
|
+
const id = this.idOf(commandRefOf(command));
|
|
413
|
+
for (const { route, description } of routeLeavesOf(command)) commands[route] = {
|
|
414
|
+
mention: mentionOf(route, id),
|
|
415
|
+
...id && { id },
|
|
416
|
+
route,
|
|
417
|
+
description
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
return commands;
|
|
421
|
+
}
|
|
422
|
+
menus(names, kind) {
|
|
423
|
+
const menus = accessorStore();
|
|
424
|
+
for (const name of names) {
|
|
425
|
+
const id = this.idOf({
|
|
426
|
+
name,
|
|
427
|
+
type: kind
|
|
428
|
+
});
|
|
429
|
+
menus[name] = {
|
|
430
|
+
...id && { id },
|
|
431
|
+
name,
|
|
432
|
+
kind
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
return menus;
|
|
436
|
+
}
|
|
437
|
+
idOf(ref) {
|
|
438
|
+
const id = this.ids.get(commandKey(ref));
|
|
439
|
+
return id === MULTI_GUILD ? void 0 : id;
|
|
440
|
+
}
|
|
441
|
+
notInCode(inCode) {
|
|
442
|
+
const inCodeKeys = new Set(inCode.map(commandKey));
|
|
443
|
+
const extra = /* @__PURE__ */ new Map();
|
|
444
|
+
for (const command of allDeployed(this.deploy)) {
|
|
445
|
+
const key = commandKey(command);
|
|
446
|
+
if (!inCodeKeys.has(key)) extra.set(key, commandRefOf(command));
|
|
447
|
+
}
|
|
448
|
+
return [...extra.values()];
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
function linkCommands(deploy, all) {
|
|
452
|
+
return new CommandLinker(deploy).link(all);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
//#endregion
|
|
456
|
+
//#region src/commands/CommandInjector.ts
|
|
457
|
+
const commandStorage = accessorStore();
|
|
458
|
+
const userMenuStorage = accessorStore();
|
|
459
|
+
const messageMenuStorage = accessorStore();
|
|
460
|
+
/**
|
|
461
|
+
* The bot's deployed slash commands, keyed by {@link SlashRegistry}. A server bot fills these after
|
|
462
|
+
* each deploy. An edge bot fills them on its first request, from the ids `seedcord build` wrote into
|
|
463
|
+
* the worker.
|
|
464
|
+
*
|
|
465
|
+
* @throws A **SeedcordError** when you read a command before the bot fills it.
|
|
466
|
+
*
|
|
467
|
+
* @example
|
|
468
|
+
* ```ts
|
|
469
|
+
* import { Commands } from '@seedcord/gateway';
|
|
470
|
+
*
|
|
471
|
+
* await this.reply(`Try ${Commands['config/set'].mention}`);
|
|
472
|
+
*
|
|
473
|
+
* // a help listing
|
|
474
|
+
* const lines = Object.values(Commands).map((c) => `${c.mention} ${c.description}`);
|
|
475
|
+
* ```
|
|
476
|
+
*/
|
|
477
|
+
const Commands = guardedAccessor("Commands", commandStorage);
|
|
478
|
+
/**
|
|
479
|
+
* The bot's deployed context-menu commands. A server bot fills these after each deploy. An edge bot
|
|
480
|
+
* fills them on its first request, from the ids `seedcord build` wrote into the worker.
|
|
481
|
+
*
|
|
482
|
+
* @throws A **SeedcordError** when you read a command before the bot fills it.
|
|
483
|
+
*
|
|
484
|
+
* @example
|
|
485
|
+
* ```ts
|
|
486
|
+
* import { ContextMenus } from '@seedcord/gateway';
|
|
487
|
+
*
|
|
488
|
+
* const id = ContextMenus.user['View Profile'].id;
|
|
489
|
+
* ```
|
|
490
|
+
*/
|
|
491
|
+
const ContextMenus = {
|
|
492
|
+
user: guardedAccessor("ContextMenus.user", userMenuStorage),
|
|
493
|
+
message: guardedAccessor("ContextMenus.message", messageMenuStorage)
|
|
494
|
+
};
|
|
495
|
+
function installLinks({ commands, contextMenus }) {
|
|
496
|
+
const pairs = [
|
|
497
|
+
[commandStorage, commands],
|
|
498
|
+
[userMenuStorage, contextMenus.user],
|
|
499
|
+
[messageMenuStorage, contextMenus.message]
|
|
500
|
+
];
|
|
501
|
+
for (const [store, values] of pairs) {
|
|
502
|
+
clearStore(store);
|
|
503
|
+
Object.assign(store, values);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const KIND_LABELS = {
|
|
507
|
+
[ApplicationCommandType.ChatInput]: "slash command",
|
|
508
|
+
[ApplicationCommandType.User]: "user menu",
|
|
509
|
+
[ApplicationCommandType.Message]: "message menu",
|
|
510
|
+
[ApplicationCommandType.PrimaryEntryPoint]: "entry point command"
|
|
511
|
+
};
|
|
512
|
+
function multiGuildWarning({ name, type }) {
|
|
513
|
+
const consequence = type === ApplicationCommandType.ChatInput ? "falling back to plain text (not clickable)" : "leaving its id unset";
|
|
514
|
+
return `${paint.sky.bold(name)} is deployed to multiple guilds, ${consequence}.`;
|
|
515
|
+
}
|
|
516
|
+
function warnLinks(logger, { multiGuild, notInCode }) {
|
|
517
|
+
for (const ref of multiGuild) logger.warn(multiGuildWarning(ref));
|
|
518
|
+
for (const { name, type } of notInCode) logger.warn(`The ${KIND_LABELS[type]} ${paint.sky.bold(name)} is deployed on Discord with no command for it in your code.`);
|
|
519
|
+
}
|
|
520
|
+
/** @internal */
|
|
521
|
+
var CommandInjector = class {
|
|
522
|
+
logger = new Logger("Commands", { channel: "bot" });
|
|
523
|
+
inject(deploy, all) {
|
|
524
|
+
const linked = linkCommands(deploy, all.map((command) => command.toJSON()));
|
|
525
|
+
installLinks(linked);
|
|
526
|
+
warnLinks(this.logger, linked);
|
|
527
|
+
const routes = Object.values(linked.commands);
|
|
528
|
+
const clickable = routes.filter((command) => command.id !== void 0).length;
|
|
529
|
+
this.logger.utils.summary("Linked commands", {
|
|
530
|
+
clickable,
|
|
531
|
+
plain: routes.length - clickable,
|
|
532
|
+
"context menus": Object.keys(linked.contextMenus.user).length + Object.keys(linked.contextMenus.message).length
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
//#endregion
|
|
538
|
+
//#region src/metadataKeys.ts
|
|
539
|
+
/**
|
|
540
|
+
* Every interaction kind seedcord routes.
|
|
541
|
+
*/
|
|
542
|
+
let InteractionKind = /* @__PURE__ */ function(InteractionKind) {
|
|
543
|
+
InteractionKind["Slash"] = "slash";
|
|
544
|
+
InteractionKind["Button"] = "button";
|
|
545
|
+
InteractionKind["Modal"] = "modal";
|
|
546
|
+
InteractionKind["StringMenu"] = "stringMenu";
|
|
547
|
+
InteractionKind["UserMenu"] = "userMenu";
|
|
548
|
+
InteractionKind["RoleMenu"] = "roleMenu";
|
|
549
|
+
InteractionKind["ChannelMenu"] = "channelMenu";
|
|
550
|
+
InteractionKind["MentionableMenu"] = "mentionableMenu";
|
|
551
|
+
InteractionKind["MessageContextMenu"] = "messageContextMenu";
|
|
552
|
+
InteractionKind["UserContextMenu"] = "userContextMenu";
|
|
553
|
+
InteractionKind["Autocomplete"] = "autocomplete";
|
|
554
|
+
return InteractionKind;
|
|
555
|
+
}({});
|
|
556
|
+
const CommandMetadataKey = Symbol.for("seedcord:command:metadata");
|
|
557
|
+
const InteractionMetadataKey = Symbol.for("seedcord:interaction:metadata");
|
|
558
|
+
const InteractionMiddlewareMetadataKey = Symbol.for("seedcord:middleware:interaction:metadata");
|
|
559
|
+
const EventMiddlewareMetadataKey = Symbol.for("seedcord:middleware:event:metadata");
|
|
560
|
+
const SubscribeMetadataKey = Symbol.for("seedcord:subscribe:metadata");
|
|
561
|
+
const EventMetadataKey = Symbol.for("seedcord:event:metadata");
|
|
562
|
+
const GatedMetadataKey = Symbol.for("seedcord:gated:metadata");
|
|
563
|
+
const WebhookUrlMetadataKey = Symbol.for("seedcord:webhook-url:metadata");
|
|
564
|
+
const InteractionRouteKeys = {
|
|
565
|
+
["slash"]: Symbol.for("seedcord:interaction:slash"),
|
|
566
|
+
["button"]: Symbol.for("seedcord:interaction:button"),
|
|
567
|
+
["modal"]: Symbol.for("seedcord:interaction:modal"),
|
|
568
|
+
["stringMenu"]: Symbol.for("seedcord:interaction:string-menu"),
|
|
569
|
+
["userMenu"]: Symbol.for("seedcord:interaction:user-menu"),
|
|
570
|
+
["roleMenu"]: Symbol.for("seedcord:interaction:role-menu"),
|
|
571
|
+
["channelMenu"]: Symbol.for("seedcord:interaction:channel-menu"),
|
|
572
|
+
["mentionableMenu"]: Symbol.for("seedcord:interaction:mentionable-menu"),
|
|
573
|
+
["messageContextMenu"]: Symbol.for("seedcord:interaction:message-context-menu"),
|
|
574
|
+
["userContextMenu"]: Symbol.for("seedcord:interaction:user-context-menu"),
|
|
575
|
+
["autocomplete"]: Symbol.for("seedcord:interaction:autocomplete")
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
//#endregion
|
|
579
|
+
//#region src/decorators/Command.ts
|
|
580
|
+
const ConfigGuilds = "config";
|
|
581
|
+
function RegisterCommand(scope = "config", guilds = []) {
|
|
582
|
+
return (ctor) => {
|
|
583
|
+
const existingMeta = Reflect.getOwnMetadata(CommandMetadataKey, ctor);
|
|
584
|
+
if (existingMeta) throw new SeedcordTypeError(SeedcordErrorCode.DecoratorCommandAlreadyRegistered, [
|
|
585
|
+
ctor.name,
|
|
586
|
+
decoratorCall(existingMeta.scope),
|
|
587
|
+
decoratorCall(scope)
|
|
588
|
+
]);
|
|
589
|
+
if (scope === "global" && guilds.length > 0) throw new SeedcordTypeError(SeedcordErrorCode.DecoratorCommandGlobalWithGuilds, [ctor.name]);
|
|
590
|
+
if (scope === "guild" && (!Array.isArray(guilds) || guilds.length === 0)) throw new SeedcordTypeError(SeedcordErrorCode.DecoratorCommandGuildWithoutGuilds, [ctor.name]);
|
|
591
|
+
const meta = scope === "guild" ? {
|
|
592
|
+
scope,
|
|
593
|
+
guilds
|
|
594
|
+
} : { scope };
|
|
595
|
+
Reflect.defineMetadata(CommandMetadataKey, meta, ctor);
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
function decoratorCall(scope) {
|
|
599
|
+
if (scope === "global") return "@RegisterCommand('global')";
|
|
600
|
+
if (scope === "guild") return "@RegisterCommand('guild', [...])";
|
|
601
|
+
return "@RegisterCommand()";
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
//#endregion
|
|
605
|
+
//#region src/customId/routing.ts
|
|
606
|
+
/**
|
|
607
|
+
* route decorators store a handler's customId defs here, so the component base can decode
|
|
608
|
+
* against them at runtime.
|
|
609
|
+
*
|
|
610
|
+
* @internal
|
|
611
|
+
*/
|
|
612
|
+
const ComponentDefsKey = Symbol.for("seedcord:custom-id:component-defs");
|
|
613
|
+
/**
|
|
614
|
+
* throws `CustomIdHandlerRouteMissing` when the class has no stored defs. a drifted or corrupt wire
|
|
615
|
+
* throws `StaleCustomId` or `InvalidCustomId`.
|
|
616
|
+
*
|
|
617
|
+
* @internal
|
|
618
|
+
*/
|
|
619
|
+
function decodeComponentRoute(handlerClass, wire) {
|
|
620
|
+
const defs = Reflect.getMetadata(ComponentDefsKey, handlerClass);
|
|
621
|
+
if (!defs) throw new SeedcordTypeError(SeedcordErrorCode.CustomIdHandlerRouteMissing, [handlerClass.name]);
|
|
622
|
+
return decodeFor(defs, wire);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
//#endregion
|
|
626
|
+
//#region src/decorators/interactionRoutes.ts
|
|
627
|
+
function interactionRoutesOf(constructor) {
|
|
628
|
+
const pairs = [];
|
|
629
|
+
for (const route of Object.values(InteractionKind)) {
|
|
630
|
+
const meta = Reflect.getMetadata(InteractionRouteKeys[route], constructor);
|
|
631
|
+
if (isStringArray(meta)) pairs.push([route, meta]);
|
|
632
|
+
}
|
|
633
|
+
return pairs;
|
|
634
|
+
}
|
|
635
|
+
function storeInteractionRoute(route, routes, constructor) {
|
|
636
|
+
const key = InteractionRouteKeys[route];
|
|
637
|
+
const saved = Reflect.getMetadata(key, constructor);
|
|
638
|
+
const existing = isStringArray(saved) ? saved : [];
|
|
639
|
+
const toStore = typeof routes === "string" ? [routes] : routes;
|
|
640
|
+
Reflect.defineMetadata(key, [...existing, ...toStore], constructor);
|
|
641
|
+
Reflect.defineMetadata(InteractionMetadataKey, true, constructor);
|
|
642
|
+
}
|
|
643
|
+
function storeComponentRoute(route, defs, constructor) {
|
|
644
|
+
storeInteractionRoute(route, defs.map((def) => def.prefix), constructor);
|
|
645
|
+
Reflect.defineMetadata(ComponentDefsKey, defs, constructor);
|
|
646
|
+
}
|
|
647
|
+
function contextMenuRouteOf(kind) {
|
|
648
|
+
return kind === ApplicationCommandType.User ? "userContextMenu" : "messageContextMenu";
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
//#endregion
|
|
652
|
+
//#region src/decorators/middleware.ts
|
|
653
|
+
/**
|
|
654
|
+
* Registers an interaction middleware. It runs after the dispatcher builds the handler and before its
|
|
655
|
+
* gates, over that handler's reply surface.
|
|
656
|
+
*
|
|
657
|
+
* @param options - The kinds this middleware runs on and its ordering.
|
|
658
|
+
* @decorator
|
|
659
|
+
*
|
|
660
|
+
* @example
|
|
661
|
+
* ```ts
|
|
662
|
+
* \@RegisterInteractionMiddleware({ kinds: [InteractionKind.Button], priority: 10 })
|
|
663
|
+
* class Audit extends InteractionMiddleware<InteractionKind.Button> {
|
|
664
|
+
* async execute() {
|
|
665
|
+
* this.logger.info(this.dispatch.routeId);
|
|
666
|
+
* }
|
|
667
|
+
* }
|
|
668
|
+
* ```
|
|
669
|
+
*
|
|
670
|
+
* @throws A **SeedcordTypeError** when `priority` is not a finite number.
|
|
671
|
+
*/
|
|
672
|
+
function RegisterInteractionMiddleware(options = {}) {
|
|
673
|
+
return function(ctor) {
|
|
674
|
+
const priority = Number(options.priority ?? 0);
|
|
675
|
+
if (!Number.isFinite(priority)) throw new SeedcordTypeError(SeedcordErrorCode.DecoratorInvalidMiddlewarePriority, [ctor.name, String(options.priority)]);
|
|
676
|
+
if (options.kinds?.length === 0) throw new SeedcordTypeError(SeedcordErrorCode.DecoratorEmptyMiddlewareFilter, [ctor.name, "kinds"]);
|
|
677
|
+
const metadata = {
|
|
678
|
+
priority,
|
|
679
|
+
...options.kinds && { kinds: options.kinds }
|
|
680
|
+
};
|
|
681
|
+
Reflect.defineMetadata(InteractionMiddlewareMetadataKey, metadata, ctor);
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
/** @internal */
|
|
685
|
+
function interactionMiddlewareMetaOf(constructor) {
|
|
686
|
+
return Reflect.getMetadata(InteractionMiddlewareMetadataKey, constructor);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
//#endregion
|
|
690
|
+
//#region src/reply/ackLegality.ts
|
|
691
|
+
const showModalReason = {
|
|
692
|
+
reason: "this interaction was already acknowledged",
|
|
693
|
+
alternative: "showModal() must be the initial response."
|
|
694
|
+
};
|
|
695
|
+
function deferReason(what) {
|
|
696
|
+
return {
|
|
697
|
+
reason: `this interaction was already acknowledged with ${what}`,
|
|
698
|
+
alternative: "A deferral can only be the initial response."
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
const ILLEGAL = {
|
|
702
|
+
reply: {
|
|
703
|
+
"deferred-reply": {
|
|
704
|
+
reason: "this interaction was already deferred",
|
|
705
|
+
alternative: "Use edit() to fill the deferred reply or followUp() for a new message."
|
|
706
|
+
},
|
|
707
|
+
"deferred-update": {
|
|
708
|
+
reason: "this interaction was already acknowledged with a deferred update",
|
|
709
|
+
alternative: "Use followUp() for a new message."
|
|
710
|
+
},
|
|
711
|
+
replied: {
|
|
712
|
+
reason: "this interaction was already replied to",
|
|
713
|
+
alternative: "Use followUp() for a new message or edit() to rewrite the reply."
|
|
714
|
+
}
|
|
715
|
+
},
|
|
716
|
+
defer: {
|
|
717
|
+
"deferred-reply": deferReason("a deferred reply"),
|
|
718
|
+
"deferred-update": deferReason("a deferred update"),
|
|
719
|
+
replied: deferReason("a reply")
|
|
720
|
+
},
|
|
721
|
+
deferUpdate: {
|
|
722
|
+
"deferred-reply": deferReason("a deferred reply"),
|
|
723
|
+
"deferred-update": deferReason("a deferred update"),
|
|
724
|
+
replied: deferReason("a reply")
|
|
725
|
+
},
|
|
726
|
+
update: {
|
|
727
|
+
"deferred-reply": {
|
|
728
|
+
reason: "this interaction was deferred as a reply",
|
|
729
|
+
alternative: "The placeholder is yours via edit(). update() rewrites a source message."
|
|
730
|
+
},
|
|
731
|
+
replied: {
|
|
732
|
+
reason: "this interaction was already replied to",
|
|
733
|
+
alternative: "update() rewrites a source message. Use edit() or followUp() instead."
|
|
734
|
+
}
|
|
735
|
+
},
|
|
736
|
+
followUp: { unacked: {
|
|
737
|
+
reason: "this interaction has not been acknowledged yet",
|
|
738
|
+
alternative: "Ack first with reply() or defer()."
|
|
739
|
+
} },
|
|
740
|
+
edit: { unacked: {
|
|
741
|
+
reason: "nothing has been sent yet",
|
|
742
|
+
alternative: "Send with reply() or defer() first, then edit() rewrites it."
|
|
743
|
+
} },
|
|
744
|
+
delete: { unacked: {
|
|
745
|
+
reason: "nothing has been sent yet",
|
|
746
|
+
alternative: "Acknowledge first with reply() or defer(), then delete() removes it."
|
|
747
|
+
} },
|
|
748
|
+
showModal: {
|
|
749
|
+
"deferred-reply": showModalReason,
|
|
750
|
+
"deferred-update": showModalReason,
|
|
751
|
+
replied: showModalReason
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
const SEND_TARGET = {
|
|
755
|
+
unacked: "reply",
|
|
756
|
+
"deferred-reply": "edit",
|
|
757
|
+
"deferred-update": "followUp",
|
|
758
|
+
replied: "followUp"
|
|
759
|
+
};
|
|
760
|
+
function checkAckLegality(method, state, routeId, ackedBy) {
|
|
761
|
+
if (method === "send") return;
|
|
762
|
+
const illegal = ILLEGAL[method][state];
|
|
763
|
+
if (!illegal) return;
|
|
764
|
+
throw new SeedcordError(SeedcordErrorCode.ReplyIllegalAckState, [
|
|
765
|
+
method,
|
|
766
|
+
illegal.reason,
|
|
767
|
+
illegal.alternative,
|
|
768
|
+
routeId
|
|
769
|
+
], { cause: ackedBy });
|
|
770
|
+
}
|
|
771
|
+
function sendTarget(state) {
|
|
772
|
+
return SEND_TARGET[state];
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
//#endregion
|
|
776
|
+
//#region src/reply/AckTrace.ts
|
|
777
|
+
var AckTrace = class extends Error {
|
|
778
|
+
constructor(method) {
|
|
779
|
+
super(`${method}() acknowledged this interaction`);
|
|
780
|
+
this.name = "AckTrace";
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
//#endregion
|
|
785
|
+
//#region src/reply/responseReport.ts
|
|
786
|
+
let replyLogger;
|
|
787
|
+
function logger() {
|
|
788
|
+
replyLogger ??= new Logger("Reply", { channel: "interactions" });
|
|
789
|
+
return replyLogger;
|
|
790
|
+
}
|
|
791
|
+
/** @internal */
|
|
792
|
+
function publishResponse(telemetry, report) {
|
|
793
|
+
const durationMs = performance.now() - report.startedAt;
|
|
794
|
+
const { routeId } = telemetry.dispatch;
|
|
795
|
+
const write = {
|
|
796
|
+
dispatchId: telemetry.dispatch.id,
|
|
797
|
+
routeId,
|
|
798
|
+
interactionId: telemetry.interactionId,
|
|
799
|
+
method: report.method,
|
|
800
|
+
durationMs
|
|
801
|
+
};
|
|
802
|
+
telemetry.bus[PublishDefault]("responseAttempted", report.outcome === "failed" ? {
|
|
803
|
+
...write,
|
|
804
|
+
outcome: "failed",
|
|
805
|
+
messageId: null,
|
|
806
|
+
error: report.error
|
|
807
|
+
} : {
|
|
808
|
+
...write,
|
|
809
|
+
outcome: "sent",
|
|
810
|
+
messageId: report.messageId
|
|
811
|
+
});
|
|
812
|
+
logger().trace(`${paint.sky.bold(routeId)} ${report.method} ${report.outcome} ${paint.mute("in")} ${Math.round(durationMs)}ms`);
|
|
813
|
+
}
|
|
814
|
+
async function reportedWrite(telemetry, method, write) {
|
|
815
|
+
const startedAt = performance.now();
|
|
816
|
+
const result = await attemptWrite(telemetry, method, startedAt, write);
|
|
817
|
+
publishResponse(telemetry, {
|
|
818
|
+
method,
|
|
819
|
+
startedAt,
|
|
820
|
+
outcome: "sent",
|
|
821
|
+
messageId: null
|
|
822
|
+
});
|
|
823
|
+
return result;
|
|
824
|
+
}
|
|
825
|
+
async function attemptWrite(telemetry, method, startedAt, write) {
|
|
826
|
+
try {
|
|
827
|
+
return await write();
|
|
828
|
+
} catch (caught) {
|
|
829
|
+
publishResponse(telemetry, {
|
|
830
|
+
method,
|
|
831
|
+
startedAt,
|
|
832
|
+
outcome: "failed",
|
|
833
|
+
error: asError(caught)
|
|
834
|
+
});
|
|
835
|
+
throw caught;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
//#endregion
|
|
840
|
+
//#region src/reply/translateSerialization.ts
|
|
841
|
+
const allOf = new Intl.ListFormat("en", { type: "conjunction" });
|
|
842
|
+
const MAX_DEPTH = 8;
|
|
843
|
+
const OPERATOR_WORD = {
|
|
844
|
+
"<=": "at most",
|
|
845
|
+
">=": "at least",
|
|
846
|
+
"<": "fewer than",
|
|
847
|
+
">": "more than"
|
|
848
|
+
};
|
|
849
|
+
const KIND_WORD = {
|
|
850
|
+
string: "a string",
|
|
851
|
+
number: "a number",
|
|
852
|
+
boolean: "a boolean",
|
|
853
|
+
bigint: "a bigint",
|
|
854
|
+
array: "a list",
|
|
855
|
+
tuple: "a list",
|
|
856
|
+
date: "a date",
|
|
857
|
+
object: "an object"
|
|
858
|
+
};
|
|
859
|
+
function asRecord(value) {
|
|
860
|
+
return typeof value === "object" && value !== null ? value : null;
|
|
861
|
+
}
|
|
862
|
+
function firstLine(message) {
|
|
863
|
+
if (typeof message !== "string") return "unknown validation failure";
|
|
864
|
+
return message.split("\n")[0] ?? message;
|
|
865
|
+
}
|
|
866
|
+
const GIVEN_PREVIEW_LIMIT = 40;
|
|
867
|
+
function gotPhrase(given) {
|
|
868
|
+
if (given === void 0) return "nothing";
|
|
869
|
+
if (given === null) return "null";
|
|
870
|
+
if (typeof given === "string") {
|
|
871
|
+
if (given.length === 0) return "an empty string";
|
|
872
|
+
if (given.length > GIVEN_PREVIEW_LIMIT) return `${given.length} characters`;
|
|
873
|
+
return JSON.stringify(given);
|
|
874
|
+
}
|
|
875
|
+
if (Array.isArray(given)) return given.length === 0 ? "an empty list" : `${given.length} items`;
|
|
876
|
+
if (typeof given === "number" || typeof given === "bigint" || typeof given === "boolean") return String(given);
|
|
877
|
+
if (typeof given === "object") return given.constructor?.name ?? "an object";
|
|
878
|
+
return typeof given;
|
|
879
|
+
}
|
|
880
|
+
function needPhrase(expected, given) {
|
|
881
|
+
const bounds = [...expected.matchAll(/expected(\.length)?\s*(<=|>=|<|>)\s*(\d+)/g)];
|
|
882
|
+
if (bounds.length === 0) return null;
|
|
883
|
+
const phrases = bounds.map(([, lengthPart, operator, limit]) => {
|
|
884
|
+
const word = OPERATOR_WORD[operator ?? ""] ?? operator ?? "";
|
|
885
|
+
if (!lengthPart) return `${word} ${limit}`;
|
|
886
|
+
const unit = typeof given === "string" ? "character" : "item";
|
|
887
|
+
return `${word} ${limit} ${pluralize(Number(limit), unit)}`;
|
|
888
|
+
});
|
|
889
|
+
return allOf.format(phrases);
|
|
890
|
+
}
|
|
891
|
+
function instanceName(expected) {
|
|
892
|
+
if (typeof expected === "function" && expected.name) return expected.name;
|
|
893
|
+
return "specific class instance";
|
|
894
|
+
}
|
|
895
|
+
function describeLeaf(record) {
|
|
896
|
+
if (typeof record.constraint === "string") {
|
|
897
|
+
const need = typeof record.expected === "string" ? needPhrase(record.expected, record.given) : null;
|
|
898
|
+
if (need) return `need ${need}, got ${gotPhrase(record.given)}`;
|
|
899
|
+
return `${firstLine(record.message)}${"given" in record ? ` (got ${gotPhrase(record.given)})` : ""}`;
|
|
900
|
+
}
|
|
901
|
+
if (typeof record.expected === "function") return `need a ${instanceName(record.expected)}, got ${gotPhrase(record.given)}`;
|
|
902
|
+
if (typeof record.validator === "string") {
|
|
903
|
+
const kind = KIND_WORD[record.validator.replace(/^s\./, "").replace(/\(.*$/, "")];
|
|
904
|
+
if (kind) return `need ${kind}, got ${gotPhrase(record.given)}`;
|
|
905
|
+
return `${firstLine(record.message)}, got ${gotPhrase(record.given)}`;
|
|
906
|
+
}
|
|
907
|
+
return firstLine(record.message);
|
|
908
|
+
}
|
|
909
|
+
function describeUnion(branches, depth) {
|
|
910
|
+
const records = [];
|
|
911
|
+
for (const branch of branches) {
|
|
912
|
+
const record = asRecord(branch);
|
|
913
|
+
if (record) records.push(record);
|
|
914
|
+
}
|
|
915
|
+
const bounded = records.find((record) => typeof record.constraint === "string");
|
|
916
|
+
if (bounded) return describeLeaf(bounded);
|
|
917
|
+
const names = [];
|
|
918
|
+
let allInstances = records.length > 0;
|
|
919
|
+
for (const record of records) if (typeof record.expected === "function") names.push(instanceName(record.expected));
|
|
920
|
+
else allInstances = false;
|
|
921
|
+
if (allInstances) return `need a ${[...new Set(names)].join(" or ")}, got ${gotPhrase(records[0]?.given)}`;
|
|
922
|
+
return detailOf(branches[0], depth + 1);
|
|
923
|
+
}
|
|
924
|
+
function detailOf(value, depth) {
|
|
925
|
+
const record = asRecord(value);
|
|
926
|
+
if (!record || depth >= MAX_DEPTH) return firstLine(record?.message);
|
|
927
|
+
if (Array.isArray(record.errors) && record.errors.length > 0) {
|
|
928
|
+
const errors = record.errors;
|
|
929
|
+
const [first] = errors;
|
|
930
|
+
if (Array.isArray(first)) {
|
|
931
|
+
const [key, inner] = first;
|
|
932
|
+
return `${String(key)}: ${detailOf(inner, depth + 1)}`;
|
|
933
|
+
}
|
|
934
|
+
return describeUnion(errors, depth);
|
|
935
|
+
}
|
|
936
|
+
return describeLeaf(record);
|
|
937
|
+
}
|
|
938
|
+
function translateSerializationError(error, componentClass, index, routeId) {
|
|
939
|
+
const detail = detailOf(error, 0);
|
|
940
|
+
return new SeedcordError(SeedcordErrorCode.ReplyComponentSerialization, [
|
|
941
|
+
componentClass,
|
|
942
|
+
index,
|
|
943
|
+
detail,
|
|
944
|
+
routeId
|
|
945
|
+
], { cause: error });
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
//#endregion
|
|
949
|
+
//#region src/reply/serializeReply.ts
|
|
950
|
+
function serializeReply(response, routeId) {
|
|
951
|
+
if (typeof response === "string") try {
|
|
952
|
+
return { components: [new TextDisplayBuilder().setContent(response).toJSON()] };
|
|
953
|
+
} catch (error) {
|
|
954
|
+
throw translateSerializationError(error, "TextDisplayBuilder", 0, routeId);
|
|
955
|
+
}
|
|
956
|
+
return {
|
|
957
|
+
components: response.components.map((component, index) => {
|
|
958
|
+
try {
|
|
959
|
+
return component.toJSON();
|
|
960
|
+
} catch (error) {
|
|
961
|
+
const name = component.constructor?.name;
|
|
962
|
+
throw translateSerializationError(error, !name || name === "Object" ? "component" : name, index, routeId);
|
|
963
|
+
}
|
|
964
|
+
}),
|
|
965
|
+
...response.allowedMentions && { allowedMentions: response.allowedMentions },
|
|
966
|
+
...response.files && { files: response.files }
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
//#endregion
|
|
971
|
+
//#region src/gates/Gate.ts
|
|
972
|
+
/**
|
|
973
|
+
* Builds a {@link Gate} from a check. The check refuses by throwing a Notice or a Silence and passes
|
|
974
|
+
* by returning. The required context is inferred from `ctx`, so annotate it as narrowly as the fields
|
|
975
|
+
* the check reads. No annotation defaults to {@link GateContextBase}, an agnostic gate that fits every
|
|
976
|
+
* handler.
|
|
977
|
+
*
|
|
978
|
+
* @typeParam Name - The gate's name, captured as a literal for mismatch errors.
|
|
979
|
+
* @typeParam Ctx - The context the check reads, inferred from the `ctx` annotation.
|
|
980
|
+
*
|
|
981
|
+
* @param name - The gate's name, used in mismatch errors.
|
|
982
|
+
* @param fn - The check, which refuses by throwing a Notice or a Silence and passes by returning.
|
|
983
|
+
*
|
|
984
|
+
* @see {@link defineEffectGate}
|
|
985
|
+
* @see the `@Gated` decorator from your transport package
|
|
986
|
+
*
|
|
987
|
+
* @example
|
|
988
|
+
* ```ts
|
|
989
|
+
* // a gate factory that takes its own option and closes over it
|
|
990
|
+
* function MinRoles(min: number): Gate<GateContextBase, 'MinRoles'> {
|
|
991
|
+
* return defineGate('MinRoles', (ctx) => {
|
|
992
|
+
* if (ctx.memberRoleIds.length < min) throw new NotEnoughRoles(min);
|
|
993
|
+
* });
|
|
994
|
+
* }
|
|
995
|
+
* ```
|
|
996
|
+
*
|
|
997
|
+
* @example
|
|
998
|
+
* ```ts
|
|
999
|
+
* // agnostic gate (no ctx annotation): fits any handler
|
|
1000
|
+
* const Owner = defineGate('owner', (ctx) => {
|
|
1001
|
+
* if (!ctx.userId) throw new NotOwner();
|
|
1002
|
+
* });
|
|
1003
|
+
* ```
|
|
1004
|
+
*/
|
|
1005
|
+
function defineGate(name, fn) {
|
|
1006
|
+
return {
|
|
1007
|
+
name,
|
|
1008
|
+
check: async (ctx) => fn(ctx)
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Builds an {@link EffectGate} from a `check` and a `commit`. `check` peeks and refuses by throwing, and
|
|
1013
|
+
* `commit` applies the side effect, running only once the whole gate set passes so a later refusal never
|
|
1014
|
+
* commits. In an `or`, a refusing arm's queued commit is rolled back. This is how {@link Cooldown} peeks
|
|
1015
|
+
* in `check` and charges the slot in `commit`.
|
|
1016
|
+
*
|
|
1017
|
+
* @typeParam Name - The gate's literal name, used to name a mismatch error.
|
|
1018
|
+
* @typeParam Ctx - The context both `check` and `commit` read, inferred from the `ctx` annotation.
|
|
1019
|
+
*
|
|
1020
|
+
* @param name - The gate's name, used in mismatch errors.
|
|
1021
|
+
* @param check - Peeks and refuses by throwing, without applying the side effect.
|
|
1022
|
+
* @param commit - Applies the side effect, running only after the whole gate set passes.
|
|
1023
|
+
*
|
|
1024
|
+
* @see {@link defineGate}
|
|
1025
|
+
* @see the `@Gated` decorator from your transport package
|
|
1026
|
+
* @see {@link Cooldown}
|
|
1027
|
+
*
|
|
1028
|
+
* @example
|
|
1029
|
+
* ```ts
|
|
1030
|
+
* // an effect-gate factory that takes its own limit and closes over it
|
|
1031
|
+
* function UsesPerDay(max: number): EffectGate<GateContextBase, 'UsesPerDay'> {
|
|
1032
|
+
* return defineEffectGate(
|
|
1033
|
+
* 'UsesPerDay',
|
|
1034
|
+
* (ctx) => {
|
|
1035
|
+
* // peek and refuse by throwing. do not mutate yet
|
|
1036
|
+
* if (usedToday(ctx.userId) >= max) throw new OutOfUsesNotice(max);
|
|
1037
|
+
* },
|
|
1038
|
+
* (ctx) => {
|
|
1039
|
+
* // runs only after the whole gate set passes
|
|
1040
|
+
* recordUse(ctx.userId);
|
|
1041
|
+
* }
|
|
1042
|
+
* );
|
|
1043
|
+
* }
|
|
1044
|
+
* ```
|
|
1045
|
+
*/
|
|
1046
|
+
function defineEffectGate(name, check, commit) {
|
|
1047
|
+
return {
|
|
1048
|
+
name,
|
|
1049
|
+
check: async (ctx) => check(ctx),
|
|
1050
|
+
commit: async (ctx) => commit(ctx)
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
//#endregion
|
|
1055
|
+
//#region src/notices/index.ts
|
|
1056
|
+
var GateNotice = class extends Notice {
|
|
1057
|
+
render() {
|
|
1058
|
+
return { components: [new NoticeCard(this.message).component] };
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1061
|
+
var NotOwner = class extends GateNotice {
|
|
1062
|
+
constructor(message = "Only the bot owner can use this.") {
|
|
1063
|
+
super(message);
|
|
1064
|
+
this.summary = "be the bot owner";
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
var NotInGuild = class extends GateNotice {
|
|
1068
|
+
constructor(message = "This can only be used in a server.") {
|
|
1069
|
+
super(message);
|
|
1070
|
+
this.summary = "be in a server";
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
var NotInDm = class extends GateNotice {
|
|
1074
|
+
constructor(message = "This can only be used in a direct message.") {
|
|
1075
|
+
super(message);
|
|
1076
|
+
this.summary = "be in a direct message";
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
function retryAt(resetAt) {
|
|
1080
|
+
return `<t:${toEpochSeconds(resetAt)}:R>`;
|
|
1081
|
+
}
|
|
1082
|
+
var OnCooldown = class extends GateNotice {
|
|
1083
|
+
resetAt;
|
|
1084
|
+
constructor(resetAt, message) {
|
|
1085
|
+
super(message ?? `You are doing that too fast. Try again ${retryAt(resetAt)}.`);
|
|
1086
|
+
this.resetAt = resetAt;
|
|
1087
|
+
this.summary = `try again ${retryAt(resetAt)}`;
|
|
1088
|
+
}
|
|
1089
|
+
};
|
|
1090
|
+
var NotAllowed = class extends Notice {
|
|
1091
|
+
constructor() {
|
|
1092
|
+
super("not allowed");
|
|
1093
|
+
}
|
|
1094
|
+
render() {
|
|
1095
|
+
return { components: [new NoticeCard("You are not allowed to use this command.").component] };
|
|
1096
|
+
}
|
|
1097
|
+
};
|
|
1098
|
+
var NeedsAny = class extends Notice {
|
|
1099
|
+
summaries;
|
|
1100
|
+
constructor(summaries) {
|
|
1101
|
+
super("not allowed");
|
|
1102
|
+
this.summaries = [...new Set(summaries)];
|
|
1103
|
+
}
|
|
1104
|
+
render() {
|
|
1105
|
+
const bullets = this.summaries.map((summary) => `• ${summary}`).join("\n");
|
|
1106
|
+
return { components: [new NoticeCard(`You need to meet at least one of these:\n${bullets}`).component] };
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
var MissingRole = class extends GateNotice {
|
|
1110
|
+
constructor(message, roleId) {
|
|
1111
|
+
super(message ?? (roleId ? `You need the <@&${roleId}> role to use this.` : "You do not have the required role."));
|
|
1112
|
+
this.summary = roleId ? `hold the <@&${roleId}> role` : "hold the required role";
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
var MissingPermissions = class extends Notice {
|
|
1116
|
+
subject;
|
|
1117
|
+
missingPerms;
|
|
1118
|
+
customLead;
|
|
1119
|
+
constructor(message, subject, missingPerms) {
|
|
1120
|
+
super(message ?? "A required permission is missing.");
|
|
1121
|
+
this.subject = subject;
|
|
1122
|
+
this.missingPerms = missingPerms;
|
|
1123
|
+
this.customLead = message;
|
|
1124
|
+
const names = missingPerms.join(", ");
|
|
1125
|
+
this.summary = subject === null ? `hold ${names}` : `${subject} needs ${names}`;
|
|
1126
|
+
}
|
|
1127
|
+
render() {
|
|
1128
|
+
const bullets = this.missingPerms.map((perm) => `• ${perm}`).join("\n");
|
|
1129
|
+
const lead = this.customLead ?? `${this.subject === null ? "You are" : `${this.subject} is`} missing the following permission entries:`;
|
|
1130
|
+
return { components: [new NoticeCard(`${lead}\n${bullets}`).component] };
|
|
1131
|
+
}
|
|
1132
|
+
};
|
|
1133
|
+
var HasDangerousPermissions = class extends Notice {
|
|
1134
|
+
subject;
|
|
1135
|
+
dangerousPerms;
|
|
1136
|
+
customLead;
|
|
1137
|
+
constructor(message, subject, dangerousPerms) {
|
|
1138
|
+
super(message ?? "A dangerous permission is enabled.");
|
|
1139
|
+
this.subject = subject;
|
|
1140
|
+
this.dangerousPerms = dangerousPerms;
|
|
1141
|
+
this.customLead = message;
|
|
1142
|
+
this.summary = `${subject} must not hold ${dangerousPerms.join(", ")}`;
|
|
1143
|
+
}
|
|
1144
|
+
render() {
|
|
1145
|
+
const bullets = this.dangerousPerms.map((perm) => `• ${perm}`).join("\n");
|
|
1146
|
+
const lead = this.customLead ?? `${this.subject} has the following permission entries that must not be enabled:`;
|
|
1147
|
+
return { components: [new NoticeCard(`${lead}\n${bullets}`).component] };
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
|
|
1151
|
+
//#endregion
|
|
1152
|
+
//#region src/gates/effects.ts
|
|
1153
|
+
const commitQueues = /* @__PURE__ */ new WeakMap();
|
|
1154
|
+
function isEffectGate(gate) {
|
|
1155
|
+
return typeof gate.commit === "function";
|
|
1156
|
+
}
|
|
1157
|
+
async function runCheck(gate, ctx) {
|
|
1158
|
+
await gate.check(ctx);
|
|
1159
|
+
if (!isEffectGate(gate)) return;
|
|
1160
|
+
let queue = commitQueues.get(ctx);
|
|
1161
|
+
if (!queue) {
|
|
1162
|
+
queue = [];
|
|
1163
|
+
commitQueues.set(ctx, queue);
|
|
1164
|
+
}
|
|
1165
|
+
queue.push(() => gate.commit(ctx));
|
|
1166
|
+
}
|
|
1167
|
+
async function runCommits(ctx) {
|
|
1168
|
+
const queue = commitQueues.get(ctx);
|
|
1169
|
+
if (!queue) return;
|
|
1170
|
+
for (const commit of queue) await commit();
|
|
1171
|
+
}
|
|
1172
|
+
function discardCommits(ctx) {
|
|
1173
|
+
commitQueues.delete(ctx);
|
|
1174
|
+
}
|
|
1175
|
+
function markCommits(ctx) {
|
|
1176
|
+
return commitQueues.get(ctx)?.length ?? 0;
|
|
1177
|
+
}
|
|
1178
|
+
function rollbackCommits(ctx, mark) {
|
|
1179
|
+
const queue = commitQueues.get(ctx);
|
|
1180
|
+
if (queue && queue.length > mark) queue.length = mark;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
//#endregion
|
|
1184
|
+
//#region src/gates/catalog/options.ts
|
|
1185
|
+
function pickNotice(options, makeDefault) {
|
|
1186
|
+
return options?.notice ?? makeDefault(options?.message);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
//#endregion
|
|
1190
|
+
//#region src/permissions/bits.ts
|
|
1191
|
+
const PermissionNames = new Map(Object.entries(PermissionFlagsBits).map(([key, bit]) => [bit, prettify(key)]));
|
|
1192
|
+
const ADMINISTRATOR = PermissionFlagsBits.Administrator;
|
|
1193
|
+
function nameOf$1(bit) {
|
|
1194
|
+
return PermissionNames.get(bit) ?? String(bit);
|
|
1195
|
+
}
|
|
1196
|
+
function holds(held, bit) {
|
|
1197
|
+
return (held & ADMINISTRATOR) === ADMINISTRATOR || (held & bit) === bit;
|
|
1198
|
+
}
|
|
1199
|
+
/** @internal */
|
|
1200
|
+
function hasAll(held, scope) {
|
|
1201
|
+
return scope.every((bit) => holds(held, bit));
|
|
1202
|
+
}
|
|
1203
|
+
/** @internal only reached once hasAll failed, so the administrator bit is absent */
|
|
1204
|
+
function missingNames(held, scope) {
|
|
1205
|
+
return scope.reduce((names, bit) => {
|
|
1206
|
+
if ((held & bit) !== bit) names.push(nameOf$1(bit));
|
|
1207
|
+
return names;
|
|
1208
|
+
}, []);
|
|
1209
|
+
}
|
|
1210
|
+
/** @internal */
|
|
1211
|
+
function presentNames(held, scope) {
|
|
1212
|
+
return scope.reduce((names, bit) => {
|
|
1213
|
+
if (holds(held, bit)) names.push(nameOf$1(bit));
|
|
1214
|
+
return names;
|
|
1215
|
+
}, []);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
//#endregion
|
|
1219
|
+
//#region src/gates/catalog/permissions.ts
|
|
1220
|
+
const MEMBER_CHECK = {
|
|
1221
|
+
unresolved: "Your server member data could not be resolved. Try again.",
|
|
1222
|
+
subject: null
|
|
1223
|
+
};
|
|
1224
|
+
const BOT_CHECK = {
|
|
1225
|
+
unresolved: "The bot's permissions could not be resolved. Try again.",
|
|
1226
|
+
subject: "The bot"
|
|
1227
|
+
};
|
|
1228
|
+
function refusePermissions(held, scope, options, guildId, kind) {
|
|
1229
|
+
if (held === null) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message ?? (guildId ? kind.unresolved : void 0)));
|
|
1230
|
+
if (hasAll(held, scope)) return;
|
|
1231
|
+
throw pickNotice(options?.missing, (message) => new MissingPermissions(message, kind.subject, missingNames(held, scope)));
|
|
1232
|
+
}
|
|
1233
|
+
function RequirePermissions(scope, options) {
|
|
1234
|
+
if (options?.in === "guild") return defineGate("RequirePermissions", (ctx) => {
|
|
1235
|
+
refusePermissions(ctx.memberGuildPermissions, scope, options, ctx.guildId, MEMBER_CHECK);
|
|
1236
|
+
});
|
|
1237
|
+
return defineGate("RequirePermissions", (ctx) => {
|
|
1238
|
+
refusePermissions(ctx.memberPermissions, scope, options, ctx.guildId, MEMBER_CHECK);
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
function RequireBotPermissions(scope, options) {
|
|
1242
|
+
if (options?.in === "guild") return defineGate("RequireBotPermissions", (ctx) => {
|
|
1243
|
+
refusePermissions(ctx.appGuildPermissions, scope, options, ctx.guildId, BOT_CHECK);
|
|
1244
|
+
});
|
|
1245
|
+
return defineGate("RequireBotPermissions", (ctx) => {
|
|
1246
|
+
refusePermissions(ctx.appPermissions, scope, options, ctx.guildId, BOT_CHECK);
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Requires the caller to hold `roleId`, read from the member's role ids. Refuses outside a guild first,
|
|
1251
|
+
* then when the role is missing. Roles are guild-global, so there is no scope dimension.
|
|
1252
|
+
*
|
|
1253
|
+
* @param roleId - The role snowflake the caller must hold.
|
|
1254
|
+
* @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-role one with `missingRole`.
|
|
1255
|
+
*
|
|
1256
|
+
* @see the `@Gated` decorator from your transport package
|
|
1257
|
+
*
|
|
1258
|
+
* @example
|
|
1259
|
+
* ```ts
|
|
1260
|
+
* \@Gated(RequireRole('123456789012345678'))
|
|
1261
|
+
* \@SlashRoute('vip')
|
|
1262
|
+
* class VipHandler extends SlashHandler<'vip'> {
|
|
1263
|
+
* async execute() {
|
|
1264
|
+
* // ...
|
|
1265
|
+
* }
|
|
1266
|
+
* }
|
|
1267
|
+
* ```
|
|
1268
|
+
*/
|
|
1269
|
+
function RequireRole(roleId, options) {
|
|
1270
|
+
return defineGate("RequireRole", (ctx) => {
|
|
1271
|
+
if (!ctx.guildId) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message));
|
|
1272
|
+
if (ctx.memberRoleIds.includes(roleId)) return;
|
|
1273
|
+
throw pickNotice(options?.missingRole, (message) => new MissingRole(message, roleId));
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
//#endregion
|
|
1278
|
+
//#region src/stops/Silence.ts
|
|
1279
|
+
/**
|
|
1280
|
+
* Throw to stop a handler with no reply and no report.
|
|
1281
|
+
*
|
|
1282
|
+
* The framework boundary catches `Silence` before {@link Notice}, makes zero Discord calls, and stops.
|
|
1283
|
+
* Ideally you'd only throw this in `EventHandlers` (or `Gates` for those), because it doesn't make sense to
|
|
1284
|
+
* leave the user with no reply for an interaction.
|
|
1285
|
+
*
|
|
1286
|
+
* @example
|
|
1287
|
+
* ```ts
|
|
1288
|
+
* import { Silence } from '@seedcord/gateway';
|
|
1289
|
+
*
|
|
1290
|
+
* // a helper under a guildMemberAdd handler, stopping when this guild configured nothing to send
|
|
1291
|
+
* const template = await settings.welcome(member.guild.id);
|
|
1292
|
+
* if (!template) throw new Silence('no welcome message set');
|
|
1293
|
+
* ```
|
|
1294
|
+
*/
|
|
1295
|
+
var Silence = class extends Error {
|
|
1296
|
+
reason;
|
|
1297
|
+
/**
|
|
1298
|
+
* @param reason - Optional note written only to a debug log, never shown to the user or reported.
|
|
1299
|
+
*/
|
|
1300
|
+
constructor(reason) {
|
|
1301
|
+
super(reason ?? "Silence");
|
|
1302
|
+
this.reason = reason;
|
|
1303
|
+
Error.captureStackTrace(this, this.constructor);
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
//#endregion
|
|
1308
|
+
//#region src/miscellaneous/ResolvedEmoji.ts
|
|
1309
|
+
/**
|
|
1310
|
+
* A resolved custom emoji. Renders as `<:name:id>` in message content, or `<a:name:id>` when animated,
|
|
1311
|
+
* and passes straight to a builder's `setEmoji`.
|
|
1312
|
+
*/
|
|
1313
|
+
var ResolvedEmoji = class {
|
|
1314
|
+
name;
|
|
1315
|
+
id;
|
|
1316
|
+
animated;
|
|
1317
|
+
constructor(name, id, animated = false) {
|
|
1318
|
+
this.name = name;
|
|
1319
|
+
this.id = id;
|
|
1320
|
+
this.animated = animated;
|
|
1321
|
+
}
|
|
1322
|
+
toString() {
|
|
1323
|
+
return `<${this.animated ? "a" : ""}:${this.name}:${this.id}>`;
|
|
1324
|
+
}
|
|
1325
|
+
};
|
|
1326
|
+
|
|
1327
|
+
//#endregion
|
|
1328
|
+
//#region src/pagination/cursor.ts
|
|
1329
|
+
const PAGE_MAX = 1e6;
|
|
1330
|
+
const SLOT_MAX = 4;
|
|
1331
|
+
/** Build the page cursor for a paginator. */
|
|
1332
|
+
function pageCursor(prefix) {
|
|
1333
|
+
return new CustomId(prefix).int("page", 0, PAGE_MAX).int("slot", 0, SLOT_MAX);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
//#endregion
|
|
1337
|
+
//#region src/reply/flags.ts
|
|
1338
|
+
function sendFlags(opts) {
|
|
1339
|
+
let flags = MessageFlags.IsComponentsV2;
|
|
1340
|
+
if (opts?.ephemeral ?? true) flags |= MessageFlags.Ephemeral;
|
|
1341
|
+
if (opts?.silent) flags |= MessageFlags.SuppressNotifications;
|
|
1342
|
+
return flags;
|
|
1343
|
+
}
|
|
1344
|
+
function deferFlags(opts) {
|
|
1345
|
+
return opts?.ephemeral ?? true ? MessageFlags.Ephemeral : 0;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
//#endregion
|
|
1349
|
+
//#region src/subscribers/ReportThrottle.ts
|
|
1350
|
+
var ReportThrottle = class ReportThrottle {
|
|
1351
|
+
windowMs;
|
|
1352
|
+
now;
|
|
1353
|
+
static perCore = /* @__PURE__ */ new WeakMap();
|
|
1354
|
+
static for(core) {
|
|
1355
|
+
let throttle = ReportThrottle.perCore.get(core);
|
|
1356
|
+
if (!throttle) {
|
|
1357
|
+
throttle = new ReportThrottle();
|
|
1358
|
+
ReportThrottle.perCore.set(core, throttle);
|
|
1359
|
+
}
|
|
1360
|
+
return throttle;
|
|
1361
|
+
}
|
|
1362
|
+
windows = /* @__PURE__ */ new Map();
|
|
1363
|
+
constructor(windowMs = 6e4, now = () => Date.now()) {
|
|
1364
|
+
this.windowMs = windowMs;
|
|
1365
|
+
this.now = now;
|
|
1366
|
+
}
|
|
1367
|
+
take(key) {
|
|
1368
|
+
const window = this.windows.get(key);
|
|
1369
|
+
if (window && this.now() - window.lastSentAt < this.windowMs) {
|
|
1370
|
+
window.suppressedSinceSend += 1;
|
|
1371
|
+
return null;
|
|
1372
|
+
}
|
|
1373
|
+
const suppressed = window?.suppressedSinceSend ?? 0;
|
|
1374
|
+
this.windows.set(key, {
|
|
1375
|
+
lastSentAt: this.now(),
|
|
1376
|
+
suppressedSinceSend: 0
|
|
1377
|
+
});
|
|
1378
|
+
return suppressed;
|
|
1379
|
+
}
|
|
1380
|
+
restore(key, suppressed) {
|
|
1381
|
+
const window = this.windows.get(key);
|
|
1382
|
+
if (window) window.suppressedSinceSend += suppressed + 1;
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1386
|
+
//#endregion
|
|
1387
|
+
//#region src/subscribers/Subscriber.ts
|
|
1388
|
+
/**
|
|
1389
|
+
* Abstract base class for handling application subscribers.
|
|
1390
|
+
*
|
|
1391
|
+
* Provides type-safe access to subscribers data and the core framework instance.
|
|
1392
|
+
* Extend this class to create custom subscriber handlers.
|
|
1393
|
+
*
|
|
1394
|
+
* @typeParam KeyOfSubscribers - The specific subscriber type this handler processes
|
|
1395
|
+
* @typeParam TCore - The transport's Core
|
|
1396
|
+
*/
|
|
1397
|
+
var Subscriber = class {
|
|
1398
|
+
data;
|
|
1399
|
+
core;
|
|
1400
|
+
logger;
|
|
1401
|
+
/**
|
|
1402
|
+
* Instantiates the subscriber with event data and core.
|
|
1403
|
+
*
|
|
1404
|
+
* @param data - The subscriber event data
|
|
1405
|
+
* @param core - The core framework instance
|
|
1406
|
+
*/
|
|
1407
|
+
constructor(data, core) {
|
|
1408
|
+
this.data = data;
|
|
1409
|
+
this.core = core;
|
|
1410
|
+
this.logger = new Logger(this.constructor.name, { channel: "subscribers" });
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
|
|
1414
|
+
//#endregion
|
|
1415
|
+
//#region src/subscribers/bases/webhookHelpers.ts
|
|
1416
|
+
const DISCORD_WEBHOOK_REGEX = new RegExp(String.raw`^https?:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w$-]+$`);
|
|
1417
|
+
function isDiscordWebhookUrl(value) {
|
|
1418
|
+
return URL.canParse(value) && DISCORD_WEBHOOK_REGEX.test(value);
|
|
1419
|
+
}
|
|
1420
|
+
function jsonAttachment(name, description, data) {
|
|
1421
|
+
const content = filterCirculars(data);
|
|
1422
|
+
return {
|
|
1423
|
+
name,
|
|
1424
|
+
description,
|
|
1425
|
+
data: new TextEncoder().encode(JSON.stringify(content, void 0, 2))
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
function breakBackticks(text) {
|
|
1429
|
+
return text.replaceAll("```", "```");
|
|
1430
|
+
}
|
|
1431
|
+
const REPORT_BUDGET = 1800;
|
|
1432
|
+
function body(value) {
|
|
1433
|
+
const raw = Error.isError(value) ? value.stack ?? `${value.name}: ${value.message}` : String(value);
|
|
1434
|
+
return breakBackticks(stripAnsi(raw));
|
|
1435
|
+
}
|
|
1436
|
+
function clamp(text, budget) {
|
|
1437
|
+
if (text.length <= budget) return text;
|
|
1438
|
+
return budget > 0 ? `${text.slice(0, budget - 1)}…` : "";
|
|
1439
|
+
}
|
|
1440
|
+
function omittedLine(count) {
|
|
1441
|
+
return `\n\nand ${count} more ${pluralize(count, "failure")}`;
|
|
1442
|
+
}
|
|
1443
|
+
function memberReport(members, budget) {
|
|
1444
|
+
const room = Math.max(budget - omittedLine(members.length).length, 0);
|
|
1445
|
+
const share = Math.floor(room / members.length);
|
|
1446
|
+
let report = "";
|
|
1447
|
+
let shown = 0;
|
|
1448
|
+
for (const [index, member] of members.entries()) {
|
|
1449
|
+
const heading = `\n\nFailure ${index + 1} of ${members.length}:\n`;
|
|
1450
|
+
const entry = heading + clamp(body(member), Math.max(share - heading.length, 0));
|
|
1451
|
+
if (report.length + entry.length > room) break;
|
|
1452
|
+
report += entry;
|
|
1453
|
+
shown++;
|
|
1454
|
+
}
|
|
1455
|
+
const hidden = members.length - shown;
|
|
1456
|
+
return hidden > 0 ? report + omittedLine(hidden) : report;
|
|
1457
|
+
}
|
|
1458
|
+
function errorReport(error) {
|
|
1459
|
+
let head = body(error);
|
|
1460
|
+
if (Error.isError(error.cause)) head += `\n\nCaused by:\n${body(error.cause)}`;
|
|
1461
|
+
const members = error instanceof AggregateError ? error.errors : [];
|
|
1462
|
+
if (members.length === 0) return clamp(head, REPORT_BUDGET);
|
|
1463
|
+
const report = clamp(head, REPORT_BUDGET - omittedLine(members.length).length);
|
|
1464
|
+
return report + memberReport(members, REPORT_BUDGET - report.length);
|
|
1465
|
+
}
|
|
1466
|
+
var WebhookSeparator = class extends BuilderComponent {
|
|
1467
|
+
constructor() {
|
|
1468
|
+
super("separator");
|
|
1469
|
+
this.instance.setSpacing(SeparatorSpacingSize.Small).setDivider(true);
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
|
|
1473
|
+
//#endregion
|
|
1474
|
+
//#region src/subscribers/bases/WebhookSender.ts
|
|
1475
|
+
const HTTP_NOT_FOUND = 404;
|
|
1476
|
+
const HTTP_UNAUTHORIZED = 401;
|
|
1477
|
+
var WebhookSender = class {
|
|
1478
|
+
rest = new REST();
|
|
1479
|
+
route;
|
|
1480
|
+
constructor(url) {
|
|
1481
|
+
const match = /\/webhooks\/(\d+)\/([\w$-]+)$/.exec(url);
|
|
1482
|
+
if (!match?.[1] || !match[2]) throw new SeedcordTypeError(SeedcordErrorCode.CoreWebhookUrlInvalid, [new.target.name]);
|
|
1483
|
+
this.route = Routes.webhook(match[1], match[2]);
|
|
1484
|
+
}
|
|
1485
|
+
async verify() {
|
|
1486
|
+
try {
|
|
1487
|
+
await this.rest.get(this.route, { auth: false });
|
|
1488
|
+
return "ok";
|
|
1489
|
+
} catch (error) {
|
|
1490
|
+
return error instanceof DiscordAPIError && (error.status === HTTP_NOT_FOUND || error.status === HTTP_UNAUTHORIZED) ? "missing" : "unreachable";
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
async send(options) {
|
|
1494
|
+
const { flags, username, avatarUrl, components, files } = options;
|
|
1495
|
+
await this.rest.post(this.route, {
|
|
1496
|
+
auth: false,
|
|
1497
|
+
query: new URLSearchParams({
|
|
1498
|
+
with_components: "true",
|
|
1499
|
+
wait: "true"
|
|
1500
|
+
}),
|
|
1501
|
+
body: {
|
|
1502
|
+
flags,
|
|
1503
|
+
username,
|
|
1504
|
+
...avatarUrl && { avatar_url: avatarUrl },
|
|
1505
|
+
components: components.map((component) => component.toJSON()),
|
|
1506
|
+
...files?.length && { attachments: files.map((file, id) => ({
|
|
1507
|
+
id,
|
|
1508
|
+
filename: file.name,
|
|
1509
|
+
description: file.description
|
|
1510
|
+
})) }
|
|
1511
|
+
},
|
|
1512
|
+
...files?.length && { files: files.map((file) => ({
|
|
1513
|
+
name: file.name,
|
|
1514
|
+
data: file.data
|
|
1515
|
+
})) }
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
};
|
|
1519
|
+
|
|
1520
|
+
//#endregion
|
|
1521
|
+
//#region src/subscribers/bases/WebhookLog.ts
|
|
1522
|
+
/**
|
|
1523
|
+
* Base class for subscribers that deliver their event to a Discord webhook.
|
|
1524
|
+
*
|
|
1525
|
+
* Declare the url's environment variable with `@WebhookUrl` and implement {@link report}. The base
|
|
1526
|
+
* resolves the url, validates it, reuses one sender per url, and sends. Registration skips a reporter
|
|
1527
|
+
* whose variable is unset and warns at boot.
|
|
1528
|
+
*
|
|
1529
|
+
* @typeParam KeyOfSubscribers - The subscription key this reporter receives
|
|
1530
|
+
* @typeParam TCore - The transport's Core
|
|
1531
|
+
*/
|
|
1532
|
+
var WebhookLog = class WebhookLog extends Subscriber {
|
|
1533
|
+
static senders = /* @__PURE__ */ new Map();
|
|
1534
|
+
/**
|
|
1535
|
+
* Events sharing this key collapse into one card per minute. Return `null` to send every event.
|
|
1536
|
+
*/
|
|
1537
|
+
throttleKey() {
|
|
1538
|
+
return null;
|
|
1539
|
+
}
|
|
1540
|
+
async execute() {
|
|
1541
|
+
const envKey = WebhookLog.envKeyOf(this.constructor);
|
|
1542
|
+
const url = WebhookLog.urlOf(envKey);
|
|
1543
|
+
if (url === null) throw new SeedcordError(SeedcordErrorCode.ConfigMissingEnv, [envKey]);
|
|
1544
|
+
const key = this.throttleKey();
|
|
1545
|
+
const throttle = ReportThrottle.for(this.core);
|
|
1546
|
+
const suppressed = key === null ? 0 : throttle.take(key);
|
|
1547
|
+
if (suppressed === null) {
|
|
1548
|
+
this.logger.debug(`counted a repeat into the next ${paint.sky.bold(this.constructor.name)} card`);
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
try {
|
|
1552
|
+
const { username, avatarUrl, components, files } = await this.report(suppressed);
|
|
1553
|
+
await WebhookLog.senderFor(url).send({
|
|
1554
|
+
flags: sendFlags({ ephemeral: false }),
|
|
1555
|
+
username: username ?? this.constructor.name,
|
|
1556
|
+
avatarUrl,
|
|
1557
|
+
components,
|
|
1558
|
+
files
|
|
1559
|
+
});
|
|
1560
|
+
} catch (error) {
|
|
1561
|
+
if (key !== null) throttle.restore(key, suppressed);
|
|
1562
|
+
this.logger.error("Webhook report failed", error);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
/** @internal */
|
|
1566
|
+
static senderFor(url) {
|
|
1567
|
+
let sender = WebhookLog.senders.get(url);
|
|
1568
|
+
if (!sender) {
|
|
1569
|
+
sender = new WebhookSender(url);
|
|
1570
|
+
WebhookLog.senders.set(url, sender);
|
|
1571
|
+
}
|
|
1572
|
+
return sender;
|
|
1573
|
+
}
|
|
1574
|
+
/** @internal */
|
|
1575
|
+
static envKeyOf(ctor) {
|
|
1576
|
+
const envKey = Reflect.getMetadata(WebhookUrlMetadataKey, ctor);
|
|
1577
|
+
if (!envKey) throw new SeedcordTypeError(SeedcordErrorCode.DecoratorWebhookUrlMissing, [ctor.name]);
|
|
1578
|
+
return envKey;
|
|
1579
|
+
}
|
|
1580
|
+
/** @internal Returns null when the env var is unset, throws when set but malformed. */
|
|
1581
|
+
static urlOf(envKey) {
|
|
1582
|
+
const raw = Envapter.get(envKey)?.trim();
|
|
1583
|
+
if (!raw) return null;
|
|
1584
|
+
if (!isDiscordWebhookUrl(raw)) throw new SeedcordError(SeedcordErrorCode.ConfigInvalidEnv, [envKey, "the webhook URL from the channel's Integrations settings"]);
|
|
1585
|
+
return raw;
|
|
1586
|
+
}
|
|
1587
|
+
};
|
|
1588
|
+
|
|
1589
|
+
//#endregion
|
|
1590
|
+
//#region src/subscribers/decorators/Subscribe.ts
|
|
1591
|
+
/**
|
|
1592
|
+
* Registers a subscriber handler class with a specific subscriber event.
|
|
1593
|
+
*
|
|
1594
|
+
* Associates the decorated class with a subscriber event type for automatic
|
|
1595
|
+
* registration and execution when the subscriber is emitted.
|
|
1596
|
+
*
|
|
1597
|
+
* @typeParam TSubscriber - The subscriber event name to register for
|
|
1598
|
+
* @param subscriber - The subscriber event name to register for
|
|
1599
|
+
* @param options - Options to configure the subscriber handler registration.
|
|
1600
|
+
* @decorator
|
|
1601
|
+
* @example
|
|
1602
|
+
* ```typescript
|
|
1603
|
+
* \@Subscribe('userJoin')
|
|
1604
|
+
* class WelcomeHandler extends Subscriber<'userJoin'> {
|
|
1605
|
+
* async execute() {
|
|
1606
|
+
* }
|
|
1607
|
+
* }
|
|
1608
|
+
* ```
|
|
1609
|
+
* @example
|
|
1610
|
+
* ```ts
|
|
1611
|
+
* \@Subscribe('userJoin', { frequency: 'once' })
|
|
1612
|
+
* // or
|
|
1613
|
+
* \@Subscribe('userJoin')
|
|
1614
|
+
* ```
|
|
1615
|
+
*/
|
|
1616
|
+
function Subscribe(subscriber, options) {
|
|
1617
|
+
return function(constructor) {
|
|
1618
|
+
const meta = {
|
|
1619
|
+
subscriber,
|
|
1620
|
+
frequency: options?.frequency
|
|
1621
|
+
};
|
|
1622
|
+
Reflect.defineMetadata(SubscribeMetadataKey, meta, constructor);
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
//#endregion
|
|
1627
|
+
//#region src/subscribers/decorators/WebhookUrl.ts
|
|
1628
|
+
/**
|
|
1629
|
+
* Declares the environment variable a {@link WebhookLog} reporter reads its webhook url from.
|
|
1630
|
+
*
|
|
1631
|
+
* An unset variable disables the reporter with a boot warning. A set but malformed value throws
|
|
1632
|
+
* at boot.
|
|
1633
|
+
*
|
|
1634
|
+
* @param envKey - The environment variable name holding the webhook url
|
|
1635
|
+
* @decorator
|
|
1636
|
+
* @example
|
|
1637
|
+
* ```ts
|
|
1638
|
+
* \@Subscribe('memberBanned')
|
|
1639
|
+
* \@WebhookUrl('BAN_LOG_WEBHOOK_URL')
|
|
1640
|
+
* class BanLog extends WebhookLog<'memberBanned'> {
|
|
1641
|
+
* report() {
|
|
1642
|
+
* return { components: [new BanCard(this.data).component] };
|
|
1643
|
+
* }
|
|
1644
|
+
* }
|
|
1645
|
+
* ```
|
|
1646
|
+
*/
|
|
1647
|
+
function WebhookUrl(envKey) {
|
|
1648
|
+
return function(constructor) {
|
|
1649
|
+
Reflect.defineMetadata(WebhookUrlMetadataKey, envKey, constructor);
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
//#endregion
|
|
1654
|
+
//#region \0@oxc-project+runtime@0.152.0/helpers/esm/decorate.js
|
|
1655
|
+
function __decorate(decorators, target, key, desc) {
|
|
1656
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1657
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
1658
|
+
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;
|
|
1659
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
//#endregion
|
|
1663
|
+
//#region src/subscribers/default/HandledException.ts
|
|
1664
|
+
let HandledException = class HandledException extends WebhookLog {
|
|
1665
|
+
throttleKey() {
|
|
1666
|
+
return `handledException:${this.data.origin}:${this.data.denial.name}`;
|
|
1667
|
+
}
|
|
1668
|
+
report(suppressed) {
|
|
1669
|
+
return {
|
|
1670
|
+
username: "Handled Exception",
|
|
1671
|
+
components: [new HandledExceptionContainer(this.data, suppressed).component],
|
|
1672
|
+
files: [jsonAttachment("source.json", "The raw source the fault came from", this.data.source.raw)]
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
};
|
|
1676
|
+
HandledException = __decorate([Subscribe("handledException"), WebhookUrl("HANDLED_EXCEPTION_WEBHOOK_URL")], HandledException);
|
|
1677
|
+
var HandledExceptionContainer = class extends BuilderComponent {
|
|
1678
|
+
constructor(data, suppressed) {
|
|
1679
|
+
super("container");
|
|
1680
|
+
const { denial, uuid, source } = data;
|
|
1681
|
+
this.instance.addTextDisplayComponents((text) => text.setContent(faultSummary(denial, source))).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### UUID \`${uuid}\`\n\`\`\`${breakBackticks(causeStack(denial))}\`\`\``)).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent("### Source")).addFileComponents((file) => file.setURL("attachment://source.json"));
|
|
1682
|
+
this.addSuppressedCount(suppressed);
|
|
1683
|
+
}
|
|
1684
|
+
addSuppressedCount(suppressed) {
|
|
1685
|
+
if (suppressed === 0) return;
|
|
1686
|
+
this.instance.addTextDisplayComponents((text) => text.setContent(`-# ${suppressed} more of these since the last report`));
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
function faultSummary(denial, source) {
|
|
1690
|
+
const head = stripAnsi(`### A handled fault was reported: \`${denial.name}\`\n**Message:** ${denial.message}\n`);
|
|
1691
|
+
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"}\``;
|
|
1692
|
+
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}\``;
|
|
1693
|
+
}
|
|
1694
|
+
function causeStack(denial) {
|
|
1695
|
+
const { cause } = denial;
|
|
1696
|
+
if (Error.isError(cause)) return stripAnsi(cause.stack ?? cause.message);
|
|
1697
|
+
if (typeof cause === "string") return stripAnsi(cause);
|
|
1698
|
+
if (typeof cause === "bigint" || typeof cause === "symbol" || typeof cause === "function") return cause.toString();
|
|
1699
|
+
if (cause !== void 0) try {
|
|
1700
|
+
return JSON.stringify(cause);
|
|
1701
|
+
} catch {
|
|
1702
|
+
return "[unserializable cause]";
|
|
1703
|
+
}
|
|
1704
|
+
return stripAnsi(denial.stack ?? "No cause recorded.");
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
//#endregion
|
|
1708
|
+
//#region src/subscribers/default/UnknownException.ts
|
|
1709
|
+
let UnknownException = class UnknownException extends WebhookLog {
|
|
1710
|
+
throttleKey() {
|
|
1711
|
+
return `unknownException:${this.data.origin}:${nameOf(this.data.error)}`;
|
|
1712
|
+
}
|
|
1713
|
+
report(suppressed) {
|
|
1714
|
+
const { metadata } = this.data;
|
|
1715
|
+
return {
|
|
1716
|
+
username: "Unknown Exception",
|
|
1717
|
+
components: [new UnhandledErrorContainer(this.data, suppressed).component],
|
|
1718
|
+
files: metadata ? [jsonAttachment("metadata.json", "Metadata associated with the error", metadata)] : void 0
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
};
|
|
1722
|
+
UnknownException = __decorate([Subscribe("unknownException"), WebhookUrl("UNKNOWN_EXCEPTION_WEBHOOK_URL")], UnknownException);
|
|
1723
|
+
function nameOf(error) {
|
|
1724
|
+
return error.name === "Error" ? error.constructor.name : error.name;
|
|
1725
|
+
}
|
|
1726
|
+
var UnhandledErrorContainer = class extends BuilderComponent {
|
|
1727
|
+
constructor(data, suppressed) {
|
|
1728
|
+
super("container");
|
|
1729
|
+
const { uuid, error, guild, user, metadata } = data;
|
|
1730
|
+
this.instance.addTextDisplayComponents((text) => text.setContent(`### An unknown exception was thrown\n**Guild ID:** \`${guild?.id ?? "Not used in a guild"}\`\n**Guild Name:** ${guild?.name ?? "Not used in a guild"}\n**User ID:** \`${user?.id ?? "Missing user info"}\`\n**Username:** ${user?.username ?? "Missing user info"}\n`)).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### UUID \`${uuid}\`\n\`\`\`${errorReport(error)}\`\`\``));
|
|
1731
|
+
this.addTimestampsIfAvailable(error);
|
|
1732
|
+
this.addMetadataIfAvailable(metadata);
|
|
1733
|
+
this.addSuppressedCount(suppressed);
|
|
1734
|
+
}
|
|
1735
|
+
addSuppressedCount(suppressed) {
|
|
1736
|
+
if (suppressed === 0) return;
|
|
1737
|
+
this.instance.addTextDisplayComponents((text) => text.setContent(`-# ${suppressed} more of these since the last report`));
|
|
1738
|
+
}
|
|
1739
|
+
addTimestampsIfAvailable(error) {
|
|
1740
|
+
if (!(error instanceof DiscordAPIError)) return;
|
|
1741
|
+
const now = Date.now();
|
|
1742
|
+
const snowflake = error.url.match(/\/interactions\/(\d+)\//)?.[1];
|
|
1743
|
+
if (!snowflake) return void 0;
|
|
1744
|
+
const interactionTs = timestampFromSnowflake(snowflake);
|
|
1745
|
+
const diff = now - interactionTs;
|
|
1746
|
+
const seconds = Math.floor(diff / 1e3);
|
|
1747
|
+
const millis = diff % 1e3;
|
|
1748
|
+
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`));
|
|
1749
|
+
}
|
|
1750
|
+
addMetadataIfAvailable(metadata) {
|
|
1751
|
+
if (!metadata) return;
|
|
1752
|
+
this.instance.addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent("### Metadata")).addFileComponents((file) => file.setURL("attachment://metadata.json"));
|
|
1753
|
+
}
|
|
1754
|
+
};
|
|
1755
|
+
|
|
1756
|
+
//#endregion
|
|
1757
|
+
//#region src/subscribers/slots.ts
|
|
1758
|
+
const RegisterDefaults = Symbol("seedcord:bus:register-defaults");
|
|
1759
|
+
const VerifyWebhooks = Symbol("seedcord:bus:verify-webhooks");
|
|
1760
|
+
const RegisteredCount = Symbol("seedcord:bus:registered-count");
|
|
1761
|
+
const RegisterSubscriber = Symbol("seedcord:bus:register");
|
|
1762
|
+
const UnregisterSubscriber = Symbol("seedcord:bus:unregister");
|
|
1763
|
+
|
|
1764
|
+
//#endregion
|
|
1765
|
+
//#region src/subscribers/Bus.ts
|
|
1766
|
+
const loggerSlot = Symbol("seedcord:bus:logger");
|
|
1767
|
+
/**
|
|
1768
|
+
* Registers and dispatches subscribers. Subscribers run on a programmatic or framework publish.
|
|
1769
|
+
* Accessed via `core.bus`. Do not construct it directly.
|
|
1770
|
+
*/
|
|
1771
|
+
var Bus = class extends TypedEventEmitter {
|
|
1772
|
+
core;
|
|
1773
|
+
logger = new Logger("Subscribers", { channel: "subscribers" });
|
|
1774
|
+
/** @internal */
|
|
1775
|
+
[loggerSlot] = this.logger;
|
|
1776
|
+
subscribersMap = /* @__PURE__ */ new Map();
|
|
1777
|
+
executedOnce = /* @__PURE__ */ new Set();
|
|
1778
|
+
webhookProbes = /* @__PURE__ */ new Map();
|
|
1779
|
+
constructor(core) {
|
|
1780
|
+
super();
|
|
1781
|
+
this.core = core;
|
|
1782
|
+
}
|
|
1783
|
+
/** @internal keep this inside start(). a bad webhook url thrown outside it never resets the singleton guard */
|
|
1784
|
+
[RegisterDefaults]() {
|
|
1785
|
+
this[RegisterSubscriber](registrationFor(UnknownException));
|
|
1786
|
+
this[RegisterSubscriber](registrationFor(HandledException));
|
|
1787
|
+
}
|
|
1788
|
+
/** @internal */
|
|
1789
|
+
[RegisterSubscriber](registration) {
|
|
1790
|
+
if (!this.probeWebhook(registration.ctor)) return;
|
|
1791
|
+
for (const key of registration.keys) {
|
|
1792
|
+
let registrations = this.subscribersMap.get(key);
|
|
1793
|
+
if (!registrations) {
|
|
1794
|
+
registrations = [];
|
|
1795
|
+
this.subscribersMap.set(key, registrations);
|
|
1796
|
+
}
|
|
1797
|
+
registrations.push(registration);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
/** @internal */
|
|
1801
|
+
[UnregisterSubscriber](ctor, keys) {
|
|
1802
|
+
for (const key of keys ?? [...this.subscribersMap.keys()]) {
|
|
1803
|
+
const registrations = this.subscribersMap.get(key);
|
|
1804
|
+
if (!registrations) continue;
|
|
1805
|
+
const index = registrations.findIndex((entry) => entry.ctor === ctor);
|
|
1806
|
+
if (index !== -1) registrations.splice(index, 1);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
/** @internal */
|
|
1810
|
+
get [RegisteredCount]() {
|
|
1811
|
+
return [...this.subscribersMap.values()].reduce((total, entries) => total + entries.length, 0);
|
|
1812
|
+
}
|
|
1813
|
+
/** @internal registration already dropped every reporter with no url set */
|
|
1814
|
+
async [VerifyWebhooks]() {
|
|
1815
|
+
const missing = [];
|
|
1816
|
+
await Promise.all([...this.webhookProbes].map(async ([url, envKeys]) => {
|
|
1817
|
+
const result = await WebhookLog.senderFor(url).verify();
|
|
1818
|
+
if (result === "missing") missing.push(...envKeys);
|
|
1819
|
+
else if (result === "unreachable") this.logger.warn(`could not verify webhook at ${envKeys.map((key) => paint.amber.bold(key)).join(", ")}`);
|
|
1820
|
+
}));
|
|
1821
|
+
if (missing.length > 0) throw new SeedcordError(SeedcordErrorCode.ConfigWebhookNotFound, [missing.join(", ")]);
|
|
1822
|
+
}
|
|
1823
|
+
probeWebhook(ctor) {
|
|
1824
|
+
if (!(ctor.prototype instanceof WebhookLog)) return true;
|
|
1825
|
+
const envKey = WebhookLog.envKeyOf(ctor);
|
|
1826
|
+
const url = WebhookLog.urlOf(envKey);
|
|
1827
|
+
if (url === null) {
|
|
1828
|
+
this.logger.warn(`${paint.sky.bold(ctor.name)} disabled, ${paint.amber.bold(envKey)} is not set`);
|
|
1829
|
+
return false;
|
|
1830
|
+
}
|
|
1831
|
+
const envKeys = this.webhookProbes.get(url) ?? [];
|
|
1832
|
+
if (!envKeys.includes(envKey)) envKeys.push(envKey);
|
|
1833
|
+
this.webhookProbes.set(url, envKeys);
|
|
1834
|
+
return true;
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Publishes an event to its subscribers and native listeners.
|
|
1838
|
+
*
|
|
1839
|
+
* Fire-and-forget. Subscriber handlers run asynchronously and this returns before they finish.
|
|
1840
|
+
* Errors thrown by a subscriber or by an `on()` listener are caught and logged, never surfaced
|
|
1841
|
+
* here. One throwing listener does not stop the others. Subscribers on one key run concurrently
|
|
1842
|
+
* and carry no ordering guarantee. A `'once'` subscriber is marked as executed when it starts,
|
|
1843
|
+
* even if it throws. It never runs twice.
|
|
1844
|
+
*
|
|
1845
|
+
* The framework's own keys are excluded. Subscribe to those and listen with `on`. The framework
|
|
1846
|
+
* is their only publisher.
|
|
1847
|
+
*
|
|
1848
|
+
* @param event - The subscription key to publish
|
|
1849
|
+
* @param data - Payload passed to each subscriber
|
|
1850
|
+
* @returns Whether the underlying emitter had native listeners for the event
|
|
1851
|
+
*/
|
|
1852
|
+
publish(event, data) {
|
|
1853
|
+
return this.dispatchKey(event, data);
|
|
1854
|
+
}
|
|
1855
|
+
/**
|
|
1856
|
+
* @deprecated Call {@link Bus.publish} to reach subscribers and listeners together.
|
|
1857
|
+
* @throws A **SeedcordTypeError** every time, with `CoreBusEmitUnavailable`. `emit` reaches only the `on()` listeners.
|
|
1858
|
+
*/
|
|
1859
|
+
emit(event, ..._args) {
|
|
1860
|
+
throw new SeedcordTypeError(SeedcordErrorCode.CoreBusEmitUnavailable, [String(event)]);
|
|
1861
|
+
}
|
|
1862
|
+
/** @internal the framework's own publish path, keyed by a symbol no public entry exports */
|
|
1863
|
+
[PublishDefault](event, data) {
|
|
1864
|
+
return this.dispatchKey(event, data);
|
|
1865
|
+
}
|
|
1866
|
+
dispatchKey(event, data) {
|
|
1867
|
+
this.processSubscriber(event, data);
|
|
1868
|
+
return this.emitSafe(event, ...[data]);
|
|
1869
|
+
}
|
|
1870
|
+
onListenerError(error, event) {
|
|
1871
|
+
this.logger.error(`listener for ${String(event)} threw`, error);
|
|
1872
|
+
}
|
|
1873
|
+
async processSubscriber(subscriberName, data) {
|
|
1874
|
+
const registrations = this.subscribersMap.get(subscriberName);
|
|
1875
|
+
if (!registrations) return;
|
|
1876
|
+
const pending = registrations.reduce((acc, entry) => {
|
|
1877
|
+
if (entry.frequency === "once") {
|
|
1878
|
+
if (this.executedOnce.has(entry)) return acc;
|
|
1879
|
+
this.executedOnce.add(entry);
|
|
1880
|
+
}
|
|
1881
|
+
acc.push(this.runSubscriber(entry, subscriberName, data));
|
|
1882
|
+
return acc;
|
|
1883
|
+
}, []);
|
|
1884
|
+
await Promise.allSettled(pending);
|
|
1885
|
+
}
|
|
1886
|
+
async runSubscriber(entry, subscriberName, data) {
|
|
1887
|
+
let name = "<unresolved>";
|
|
1888
|
+
try {
|
|
1889
|
+
const Ctor = entry.ctor;
|
|
1890
|
+
name = Ctor.name;
|
|
1891
|
+
await new Ctor(data, this.core).execute();
|
|
1892
|
+
} catch (err) {
|
|
1893
|
+
this.logger.error(`Error in subscriber ${paint.mute(String(subscriberName))} handler ${paint.sky.bold(name)}:`, err);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
};
|
|
1897
|
+
/** @internal */
|
|
1898
|
+
function registrationFor(ctor) {
|
|
1899
|
+
const meta = Reflect.getMetadata(SubscribeMetadataKey, ctor);
|
|
1900
|
+
return {
|
|
1901
|
+
keys: [meta.subscriber],
|
|
1902
|
+
frequency: meta.frequency ?? "on",
|
|
1903
|
+
ctor
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
/** @internal */
|
|
1907
|
+
function busLoggerOf(bus) {
|
|
1908
|
+
return bus[loggerSlot];
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
//#endregion
|
|
1912
|
+
export { RegisterInteractionMiddleware as $, rollbackCommits as A, guardedAccessor as At, NotInGuild as B, isBuilderComponentClass as Bt, PermissionNames as C, warnLinks as Ct, pickNotice as D, linkCommands as Dt, presentNames as E, indexById as Et, MissingPermissions as F, skippedNote as Ft, serializeReply as G, OnCooldown as H, Notice as Ht, MissingRole as I, contextMenuLeaves as It, publishResponse as J, translateSerializationError as K, NeedsAny as L, NoticeCard as Lt, runCommits as M, commandsRoute as Mt, GateNotice as N, globalFirst as Nt, discardCommits as O, accessorStore as Ot, HasDangerousPermissions as P, scopeIdsOf as Pt, sendTarget as Q, NotAllowed as R, BuilderComponent as Rt, RequireRole as S, installLinks as St, missingNames as T, commandRefOf as Tt, defineEffectGate as U, NotOwner as V, bindBotColor as Vt, defineGate as W, AckTrace as X, reportedWrite as Y, checkAckLegality as Z, pageCursor as _, SubscribeMetadataKey as _t, RegisterSubscriber as a, ComponentDefsKey as at, RequireBotPermissions as b, Commands as bt, VerifyWebhooks as c, RegisterCommand as ct, WebhookLog as d, EventMiddlewareMetadataKey as dt, interactionMiddlewareMetaOf as et, Subscriber as f, GatedMetadataKey as ft, PAGE_MAX as g, InteractionRouteKeys as gt, sendFlags as h, InteractionMiddlewareMetadataKey as ht, RegisterDefaults as i, storeInteractionRoute as it, runCheck as j, GLOBAL_ID as jt, markCommits as k, clearStore as kt, WebhookUrl as l, CommandMetadataKey as lt, deferFlags as m, InteractionMetadataKey as mt, busLoggerOf as n, interactionRoutesOf as nt, RegisteredCount as o, decodeComponentRoute as ot, ReportThrottle as p, InteractionKind as pt, attemptWrite as q, registrationFor as r, storeComponentRoute as rt, UnregisterSubscriber as s, ConfigGuilds as st, Bus as t, contextMenuRouteOf as tt, Subscribe as u, EventMetadataKey as ut, ResolvedEmoji as v, WebhookUrlMetadataKey as vt, hasAll as w, commandKey as wt, RequirePermissions as x, ContextMenus as xt, Silence as y, CommandInjector as yt, NotInDm as z, RowComponent as zt };
|
|
1913
|
+
//# sourceMappingURL=subscribers-Ce3SWy23.mjs.map
|