@seedcord/core 0.1.3 → 0.2.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.
Files changed (60) hide show
  1. package/README.md +35 -5
  2. package/dist/{subscribers-nEpbJj3Z.mjs → Bus-DcuUtOEL.mjs} +401 -31
  3. package/dist/Bus-DcuUtOEL.mjs.map +1 -0
  4. package/dist/{HmrModuleHandler-KxRP8lqJ.mjs → HmrModuleHandler-9AUfKXRl.mjs} +2 -2
  5. package/dist/{HmrModuleHandler-KxRP8lqJ.mjs.map → HmrModuleHandler-9AUfKXRl.mjs.map} +1 -1
  6. package/dist/{Plugin-CDAV7P9l.mjs → Plugin-Bkeqcjy6.mjs} +12 -3
  7. package/dist/Plugin-Bkeqcjy6.mjs.map +1 -0
  8. package/dist/{Plugin-qRc7Ch4g.d.mts → Plugin-Cl5P7-PO.d.mts} +19 -7
  9. package/dist/{cursor-D207VrRo.mjs → cursor-B07ArwDz.mjs} +4 -4
  10. package/dist/cursor-B07ArwDz.mjs.map +1 -0
  11. package/dist/{cursor-C77xH5QT.d.mts → cursor-__0_F4rW.d.mts} +10 -3
  12. package/dist/hmr.index.mjs +1 -1
  13. package/dist/{index-C0k7lRDi.d.mts → index-TYnQ-ztr.d.mts} +15 -4
  14. package/dist/index.d.mts +24 -11
  15. package/dist/index.mjs +26 -6
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/internal.index.d.mts +13 -18
  18. package/dist/internal.index.mjs +16 -48
  19. package/dist/internal.index.mjs.map +1 -1
  20. package/dist/node-internal.index.d.mts +6 -5
  21. package/dist/node-internal.index.mjs +54 -49
  22. package/dist/node-internal.index.mjs.map +1 -1
  23. package/dist/plugin.index.d.mts +1 -1
  24. package/dist/plugin.index.mjs +1 -1
  25. package/dist/subscribers-DuHg-bGT.mjs +3 -0
  26. package/package.json +27 -35
  27. package/dist/HmrModuleHandler-CxGOo1LT.cjs +0 -182
  28. package/dist/HmrModuleHandler-CxGOo1LT.cjs.map +0 -1
  29. package/dist/Plugin-CDAV7P9l.mjs.map +0 -1
  30. package/dist/Plugin-DVNh1kvX.cjs +0 -95
  31. package/dist/Plugin-DVNh1kvX.cjs.map +0 -1
  32. package/dist/cursor-D207VrRo.mjs.map +0 -1
  33. package/dist/cursor-DFgJ70_B.cjs +0 -1339
  34. package/dist/cursor-DFgJ70_B.cjs.map +0 -1
  35. package/dist/devChannel-Cdnmz6hd.cjs +0 -25
  36. package/dist/devChannel-Cdnmz6hd.cjs.map +0 -1
  37. package/dist/hmr.index.cjs +0 -4
  38. package/dist/hmr.index.d.cts +0 -1
  39. package/dist/index.cjs +0 -889
  40. package/dist/index.cjs.map +0 -1
  41. package/dist/index.d.cts +0 -1
  42. package/dist/internal.index.cjs +0 -717
  43. package/dist/internal.index.cjs.map +0 -1
  44. package/dist/internal.index.d.cts +0 -1
  45. package/dist/node-internal.index.cjs +0 -799
  46. package/dist/node-internal.index.cjs.map +0 -1
  47. package/dist/node-internal.index.d.cts +0 -1
  48. package/dist/phases-8qiiLKjS.cjs +0 -42
  49. package/dist/phases-8qiiLKjS.cjs.map +0 -1
  50. package/dist/plugin.index.cjs +0 -4
  51. package/dist/plugin.index.d.cts +0 -1
  52. package/dist/publishDefault-BCrMG9T8.mjs +0 -273
  53. package/dist/publishDefault-BCrMG9T8.mjs.map +0 -1
  54. package/dist/publishDefault-D52iT8mF.cjs +0 -368
  55. package/dist/publishDefault-D52iT8mF.cjs.map +0 -1
  56. package/dist/slashRouteLeaves-HVl6Ymr0.cjs +0 -28
  57. package/dist/slashRouteLeaves-HVl6Ymr0.cjs.map +0 -1
  58. package/dist/subscribers-6x4xw39Z.cjs +0 -531
  59. package/dist/subscribers-6x4xw39Z.cjs.map +0 -1
  60. package/dist/subscribers-nEpbJj3Z.mjs.map +0 -1
package/README.md CHANGED
@@ -1,12 +1,42 @@
1
- <p align="center">
1
+ <div align="center">
2
2
  <picture>
3
3
  <source media="(prefers-color-scheme: dark)" srcset="https://cdn.seedcord.org/assets/wordmark-dark.webp" />
4
4
  <img src="https://cdn.seedcord.org/assets/wordmark-light.webp" alt="seedcord" width="440" />
5
5
  </picture>
6
- </p>
6
+ </div>
7
7
 
8
- # @seedcord/core
8
+ <div align="center">
9
+ <h3>The whole Discord bot, wired and typed</h3>
10
+ <a href="https://seedcord.org">Website</a> ·
11
+ <a href="https://guide.seedcord.org">Guide</a> ·
12
+ <a href="https://docs.seedcord.org">Reference</a> ·
13
+ <a href="https://discord.gg/DzFxY58WXf">Discord</a>
14
+ </div>
9
15
 
10
- Transport-agnostic core for Seedcord bots
16
+ <br />
11
17
 
12
- Part of the [seedcord](https://github.com/seedcord/seedcord) framework. Until a major v1.0.0 release, expect breaking changes in minor versions.
18
+ <div align="center">
19
+
20
+ [![npm](https://img.shields.io/npm/v/@seedcord/core?style=flat-square&logo=npm&logoColor=c8341f&label=&labelColor=1f1f1f&color=c8341f)](https://www.npmjs.com/package/@seedcord/core) [![node](https://img.shields.io/node/v/@seedcord/core?style=flat-square&label=node&labelColor=1f1f1f&color=4d7d33)](https://nodejs.org) [![license](https://img.shields.io/npm/l/@seedcord/core?style=flat-square&label=license&labelColor=1f1f1f&color=f8f6e8)](LICENSE)
21
+
22
+ </div>
23
+
24
+ ## About
25
+
26
+ `@seedcord/core` is the foundational code both seedcord transports share. Command and context-menu injection, the interaction route decorators, the handler bases, gates, the startup and shutdown phases, and the plugin base all live here.
27
+
28
+ Nothing in it opens a connection to Discord. `@seedcord/gateway` and `@seedcord/http` each bind their own transport into these pieces.
29
+
30
+ Until v1.0.0, minor versions can break.
31
+
32
+ ## Installation
33
+
34
+ Your transport already re-exports all of this.
35
+
36
+ Install it directly when you are writing a plugin against the framework:
37
+
38
+ ```sh
39
+ pnpm add @seedcord/core
40
+ ```
41
+
42
+ `@seedcord/core/plugin` holds the base a plugin extends.
@@ -1,15 +1,313 @@
1
- import { h as WebhookUrlMetadataKey, i as BuilderComponent, m as SubscribeMetadataKey, r as sendFlags, t as PublishDefault } from "./publishDefault-BCrMG9T8.mjs";
2
1
  import "reflect-metadata";
3
- import { Logger, paint } from "@seedcord/logger";
4
- import { Routes, SeparatorSpacingSize } from "discord-api-types/v10";
5
- import { SeedcordErrorCode } from "@seedcord/errors";
6
- import { SeedcordError } from "@seedcord/errors/internal";
2
+ 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";
3
+ import { SeedcordErrorCode, paint } from "@seedcord/errors";
4
+ import { Logger } from "@seedcord/logger";
5
+ import { ApplicationCommandType, InteractionContextType, MessageFlags, Routes, SeparatorSpacingSize } from "discord-api-types/v10";
6
+ import { SeedcordError, SeedcordRangeError, SeedcordTypeError } from "@seedcord/errors/internal";
7
7
  import { filterCirculars, stripAnsi, timestampFromSnowflake } from "@seedcord/utils";
8
8
  import { TypedEventEmitter } from "@seedcord/event-emitter";
9
- import chalk from "chalk";
10
9
  import { Converters, Envapter } from "envapt";
11
10
  import { DiscordAPIError, REST } from "@discordjs/rest";
12
11
 
12
+ //#region src/commands/contextMenuLeaves.ts
13
+ /**
14
+ * dedupes names across builders, since the same builder repeats once per guild.
15
+ * a user and a message command can share a name.
16
+ *
17
+ * @internal
18
+ */
19
+ function contextMenuLeaves(commands) {
20
+ const user = /* @__PURE__ */ new Set();
21
+ const message = /* @__PURE__ */ new Set();
22
+ for (const command of commands) {
23
+ if (!(command instanceof ContextMenuCommandBuilder)) continue;
24
+ if (command.type === ApplicationCommandType.User) user.add(command.name);
25
+ else message.add(command.name);
26
+ }
27
+ return {
28
+ user,
29
+ message
30
+ };
31
+ }
32
+
33
+ //#endregion
34
+ //#region src/metadataKeys.ts
35
+ let InteractionRoutes = /* @__PURE__ */ function(InteractionRoutes) {
36
+ InteractionRoutes["Slash"] = "slash";
37
+ InteractionRoutes["Button"] = "button";
38
+ InteractionRoutes["Modal"] = "modal";
39
+ InteractionRoutes["StringMenu"] = "stringMenu";
40
+ InteractionRoutes["UserMenu"] = "userMenu";
41
+ InteractionRoutes["RoleMenu"] = "roleMenu";
42
+ InteractionRoutes["ChannelMenu"] = "channelMenu";
43
+ InteractionRoutes["MentionableMenu"] = "mentionableMenu";
44
+ InteractionRoutes["MessageContextMenu"] = "messageContextMenu";
45
+ InteractionRoutes["UserContextMenu"] = "userContextMenu";
46
+ InteractionRoutes["Autocomplete"] = "autocomplete";
47
+ return InteractionRoutes;
48
+ }({});
49
+ const InteractionMetadataKey = Symbol("seedcord:interaction:metadata");
50
+ const InteractionRouteKeys = {
51
+ ["slash"]: Symbol("seedcord:interaction:slash"),
52
+ ["button"]: Symbol("seedcord:interaction:button"),
53
+ ["modal"]: Symbol("seedcord:interaction:modal"),
54
+ ["stringMenu"]: Symbol("seedcord:interaction:stringMenu"),
55
+ ["userMenu"]: Symbol("seedcord:interaction:userMenu"),
56
+ ["roleMenu"]: Symbol("seedcord:interaction:roleMenu"),
57
+ ["channelMenu"]: Symbol("seedcord:interaction:channelMenu"),
58
+ ["mentionableMenu"]: Symbol("seedcord:interaction:mentionableMenu"),
59
+ ["messageContextMenu"]: Symbol("seedcord:interaction:messageContextMenu"),
60
+ ["userContextMenu"]: Symbol("seedcord:interaction:userContextMenu"),
61
+ ["autocomplete"]: Symbol("seedcord:interaction:autocomplete")
62
+ };
63
+ const MiddlewareMetadataKey = Symbol("seedcord:middleware:metadata");
64
+ const EventMetadataKey = Symbol("seedcord:event:metadata");
65
+ const CommandMetadataKey = Symbol("seedcord:command:metadata");
66
+ const GatedMetadataKey = Symbol("seedcord:gated:metadata");
67
+ const SubscribeMetadataKey = Symbol("seedcord:subscribe:metadata");
68
+ const WebhookUrlMetadataKey = Symbol("seedcord:webhookUrl:metadata");
69
+
70
+ //#endregion
71
+ //#region src/components/botColorHolder.ts
72
+ const DEFAULT_COLOR = "Default";
73
+ let current = DEFAULT_COLOR;
74
+ function setBotColor(color) {
75
+ current = color ?? DEFAULT_COLOR;
76
+ }
77
+ function getBotColor() {
78
+ return current;
79
+ }
80
+
81
+ //#endregion
82
+ //#region src/components/builderTypes.ts
83
+ /**
84
+ * Available Discord.js builder classes for use with BuilderComponent for commands, embeds, modals, etc.
85
+ *
86
+ * @internal
87
+ */
88
+ const BuilderTypes = {
89
+ command: SlashCommandBuilder,
90
+ context_menu: ContextMenuCommandBuilder,
91
+ subcommand: SlashCommandSubcommandBuilder,
92
+ group: SlashCommandSubcommandGroupBuilder,
93
+ embed: EmbedBuilder,
94
+ modal: ModalBuilder,
95
+ label: LabelBuilder,
96
+ text_input: TextInputBuilder,
97
+ file_upload: FileUploadBuilder,
98
+ checkbox: CheckboxBuilder,
99
+ checkbox_group: CheckboxGroupBuilder,
100
+ checkbox_group_option: CheckboxGroupOptionBuilder,
101
+ radio_group: RadioGroupBuilder,
102
+ radio_group_option: RadioGroupOptionBuilder,
103
+ button: ButtonBuilder,
104
+ menu_string: StringSelectMenuBuilder,
105
+ menu_option_string: StringSelectMenuOptionBuilder,
106
+ menu_user: UserSelectMenuBuilder,
107
+ menu_channel: ChannelSelectMenuBuilder,
108
+ menu_mentionable: MentionableSelectMenuBuilder,
109
+ menu_role: RoleSelectMenuBuilder,
110
+ container: ContainerBuilder,
111
+ text_display: TextDisplayBuilder,
112
+ file: FileBuilder,
113
+ media: MediaGalleryBuilder,
114
+ section: SectionBuilder,
115
+ separator: SeparatorBuilder
116
+ };
117
+ /**
118
+ * Available Discord.js action row classes for use with RowComponent for Select Menus and Buttons
119
+ *
120
+ * @internal
121
+ */
122
+ const RowTypes = {
123
+ button: ActionRowBuilder,
124
+ menu_string: ActionRowBuilder,
125
+ menu_user: ActionRowBuilder,
126
+ menu_channel: ActionRowBuilder,
127
+ menu_mentionable: ActionRowBuilder,
128
+ menu_role: ActionRowBuilder
129
+ };
130
+
131
+ //#endregion
132
+ //#region src/components/colors.ts
133
+ const Colors = {
134
+ Aqua: 1752220,
135
+ Blue: 3447003,
136
+ Blurple: 5793266,
137
+ DarkAqua: 1146986,
138
+ DarkBlue: 2123412,
139
+ DarkButNotBlack: 2895667,
140
+ DarkerGrey: 8359053,
141
+ DarkGold: 12745742,
142
+ DarkGreen: 2067276,
143
+ DarkGrey: 9936031,
144
+ DarkNavy: 2899536,
145
+ DarkOrange: 11027200,
146
+ DarkPurple: 7419530,
147
+ DarkRed: 10038562,
148
+ DarkVividPink: 11342935,
149
+ Default: 0,
150
+ Fuchsia: 15418782,
151
+ Gold: 15844367,
152
+ Green: 5763719,
153
+ Grey: 9807270,
154
+ Greyple: 10070709,
155
+ LightGrey: 12370112,
156
+ LuminousVividPink: 15277667,
157
+ Navy: 3426654,
158
+ NotQuiteBlack: 2303786,
159
+ Orange: 15105570,
160
+ Purple: 10181046,
161
+ Red: 15548997,
162
+ White: 16777215,
163
+ Yellow: 16705372
164
+ };
165
+
166
+ //#endregion
167
+ //#region src/components/resolveColor.ts
168
+ const MAX_COLOR = 16777215;
169
+ const RED_SHIFT = 16;
170
+ const GREEN_SHIFT = 8;
171
+ const palette = Colors;
172
+ /**
173
+ * Resolves a {@link BotColor} to the integer Discord expects.
174
+ *
175
+ * Ported from discord.js (Apache-2.0), `resolveColor` in src/util/Util.js.
176
+ */
177
+ function resolveColor(color) {
178
+ let resolved;
179
+ if (typeof color === "string") {
180
+ if (color === "Random") return Math.floor(Math.random() * 16777216);
181
+ if (color === "Default") return 0;
182
+ if (/^#?[\da-f]{6}$/i.test(color)) return Number.parseInt(color.replace("#", ""), 16);
183
+ resolved = palette[color];
184
+ } else if (typeof color === "number") resolved = color;
185
+ else resolved = (color[0] << RED_SHIFT) + (color[1] << GREEN_SHIFT) + color[2];
186
+ if (resolved === void 0 || !Number.isSafeInteger(resolved)) throw new SeedcordTypeError(SeedcordErrorCode.ColorUnresolvable, [JSON.stringify(color)]);
187
+ if (resolved < 0 || resolved > MAX_COLOR) throw new SeedcordRangeError(SeedcordErrorCode.ColorOutOfRange);
188
+ return resolved;
189
+ }
190
+
191
+ //#endregion
192
+ //#region src/components/Component.ts
193
+ /** @internal */
194
+ var BaseComponent = class {
195
+ _component;
196
+ constructor(ComponentClass) {
197
+ this._component = new ComponentClass();
198
+ }
199
+ /**
200
+ * The wrapped builder, for calling Discord.js methods like setTitle() and setDescription() inside a subclass.
201
+ *
202
+ * @example this.instance.setTitle('My Modal')
203
+ */
204
+ get instance() {
205
+ return this._component;
206
+ }
207
+ };
208
+ /**
209
+ * Base class for Discord.js builder components
210
+ *
211
+ * Wraps Discord.js builders (SlashCommandBuilder, EmbedBuilder, etc.) with
212
+ * Seedcord-specific defaults and helper methods.
213
+ *
214
+ * @typeParam BuilderKey - The type of Discord.js builder being wrapped
215
+ */
216
+ var BuilderComponent = class extends BaseComponent {
217
+ type;
218
+ colorApplied = false;
219
+ constructor(type) {
220
+ const ComponentClass = BuilderTypes[type];
221
+ super(ComponentClass);
222
+ this.type = type;
223
+ if (this.instance instanceof SlashCommandBuilder || this.instance instanceof ContextMenuCommandBuilder) this.instance.setContexts(InteractionContextType.Guild);
224
+ }
225
+ get component() {
226
+ this.applyBotColor();
227
+ return this.instance;
228
+ }
229
+ applyBotColor() {
230
+ if (this.colorApplied) return;
231
+ this.colorApplied = true;
232
+ const color = getBotColor();
233
+ if (this.instance instanceof EmbedBuilder) {
234
+ if (this.instance.data.color === void 0) this.instance.setColor(resolveColor(color));
235
+ } else if (this.instance instanceof ContainerBuilder) {
236
+ const accent = this.instance.data.accent_color;
237
+ if (accent === null || accent === void 0) this.instance.setAccentColor(color === "Default" ? void 0 : resolveColor(color));
238
+ }
239
+ }
240
+ };
241
+ /**
242
+ * Base class for Discord action row components
243
+ *
244
+ * Wraps Discord.js action row builder with Seedcord-specific defaults and helper methods.
245
+ *
246
+ * @typeParam RowKey - The Discord.js action row type being wrapped
247
+ */
248
+ var RowComponent = class extends BaseComponent {
249
+ type;
250
+ constructor(type) {
251
+ const ComponentClass = RowTypes[type];
252
+ super(ComponentClass);
253
+ this.type = type;
254
+ }
255
+ get component() {
256
+ return this.instance;
257
+ }
258
+ };
259
+
260
+ //#endregion
261
+ //#region src/reply/flags.ts
262
+ function sendFlags(opts) {
263
+ let flags = MessageFlags.IsComponentsV2;
264
+ if (opts?.ephemeral ?? true) flags |= MessageFlags.Ephemeral;
265
+ if (opts?.silent) flags |= MessageFlags.SuppressNotifications;
266
+ return flags;
267
+ }
268
+ function deferFlags(opts) {
269
+ return opts?.ephemeral ?? true ? MessageFlags.Ephemeral : 0;
270
+ }
271
+
272
+ //#endregion
273
+ //#region src/subscribers/ReportThrottle.ts
274
+ var ReportThrottle = class ReportThrottle {
275
+ windowMs;
276
+ now;
277
+ static perCore = /* @__PURE__ */ new WeakMap();
278
+ static for(core) {
279
+ let throttle = ReportThrottle.perCore.get(core);
280
+ if (!throttle) {
281
+ throttle = new ReportThrottle();
282
+ ReportThrottle.perCore.set(core, throttle);
283
+ }
284
+ return throttle;
285
+ }
286
+ windows = /* @__PURE__ */ new Map();
287
+ constructor(windowMs = 6e4, now = () => Date.now()) {
288
+ this.windowMs = windowMs;
289
+ this.now = now;
290
+ }
291
+ take(key) {
292
+ const window = this.windows.get(key);
293
+ if (window && this.now() - window.lastSentAt < this.windowMs) {
294
+ window.suppressedSinceSend += 1;
295
+ return null;
296
+ }
297
+ const suppressed = window?.suppressedSinceSend ?? 0;
298
+ this.windows.set(key, {
299
+ lastSentAt: this.now(),
300
+ suppressedSinceSend: 0
301
+ });
302
+ return suppressed;
303
+ }
304
+ restore(key, suppressed) {
305
+ const window = this.windows.get(key);
306
+ if (window) window.suppressedSinceSend += suppressed + 1;
307
+ }
308
+ };
309
+
310
+ //#endregion
13
311
  //#region src/subscribers/Subscriber.ts
14
312
  /**
15
313
  * Abstract base class for handling application subscribers.
@@ -51,17 +349,43 @@ function jsonAttachment(name, description, data) {
51
349
  data: new TextEncoder().encode(JSON.stringify(content, void 0, 2))
52
350
  };
53
351
  }
54
- function neutralizeFences(text) {
352
+ function breakBackticks(text) {
55
353
  return text.replaceAll("```", "`​`​`");
56
354
  }
57
- function errorReport(error) {
58
- let report = error.stack ?? `${error.name}: ${error.message}`;
59
- if (Error.isError(error.cause)) {
60
- const cause = error.cause.stack ?? `${error.cause.name}: ${error.cause.message}`;
61
- report += `\n\nCaused by:\n${cause}`;
355
+ const REPORT_BUDGET = 1800;
356
+ function body(value) {
357
+ const raw = Error.isError(value) ? value.stack ?? `${value.name}: ${value.message}` : String(value);
358
+ return breakBackticks(stripAnsi(raw));
359
+ }
360
+ function clamp(text, budget) {
361
+ if (text.length <= budget) return text;
362
+ return budget > 0 ? `${text.slice(0, budget - 1)}…` : "";
363
+ }
364
+ function omittedLine(count) {
365
+ return `\n\nand ${count} more failure${count === 1 ? "" : "s"}`;
366
+ }
367
+ function memberReport(members, budget) {
368
+ const room = Math.max(budget - omittedLine(members.length).length, 0);
369
+ const share = Math.floor(room / members.length);
370
+ let report = "";
371
+ let shown = 0;
372
+ for (const [index, member] of members.entries()) {
373
+ const heading = `\n\nFailure ${index + 1} of ${members.length}:\n`;
374
+ const entry = heading + clamp(body(member), Math.max(share - heading.length, 0));
375
+ if (report.length + entry.length > room) break;
376
+ report += entry;
377
+ shown++;
62
378
  }
63
- const stripped = neutralizeFences(stripAnsi(report));
64
- return stripped.length > 1800 ? `${stripped.slice(0, 1799)}…` : stripped;
379
+ const hidden = members.length - shown;
380
+ return hidden > 0 ? report + omittedLine(hidden) : report;
381
+ }
382
+ function errorReport(error) {
383
+ let head = body(error);
384
+ if (Error.isError(error.cause)) head += `\n\nCaused by:\n${body(error.cause)}`;
385
+ const members = error instanceof AggregateError ? error.errors : [];
386
+ if (members.length === 0) return clamp(head, REPORT_BUDGET);
387
+ const report = clamp(head, REPORT_BUDGET - omittedLine(members.length).length);
388
+ return report + memberReport(members, REPORT_BUDGET - report.length);
65
389
  }
66
390
  var WebhookSeparator = class extends BuilderComponent {
67
391
  constructor() {
@@ -91,7 +415,7 @@ var WebhookSender = class {
91
415
  }
92
416
  }
93
417
  async send(options) {
94
- const { flags, username, components, files } = options;
418
+ const { flags, username, avatarUrl, components, files } = options;
95
419
  await this.rest.post(this.route, {
96
420
  auth: false,
97
421
  query: new URLSearchParams({
@@ -101,6 +425,7 @@ var WebhookSender = class {
101
425
  body: {
102
426
  flags,
103
427
  username,
428
+ ...avatarUrl && { avatar_url: avatarUrl },
104
429
  components: components.map((component) => component.toJSON()),
105
430
  ...files?.length && { attachments: files.map((file, id) => ({
106
431
  id,
@@ -130,22 +455,37 @@ var WebhookSender = class {
130
455
  */
131
456
  var WebhookLog = class WebhookLog extends Subscriber {
132
457
  static senders = /* @__PURE__ */ new Map();
458
+ /**
459
+ * Events sharing this key collapse into one card per minute. Return `null` to send every event.
460
+ */
461
+ throttleKey() {
462
+ return null;
463
+ }
133
464
  async execute() {
134
465
  const url = WebhookLog.urlOf(WebhookLog.envKeyOf(this.constructor));
135
466
  if (url === null) {
136
- this.logger.warn(`${chalk.bold(this.constructor.name)} has no webhook url set, this reporter is disabled`);
467
+ this.logger.warn(`${paint.sky.bold(this.constructor.name)} has no webhook url set, this reporter is disabled`);
468
+ return;
469
+ }
470
+ const key = this.throttleKey();
471
+ const throttle = ReportThrottle.for(this.core);
472
+ const suppressed = key === null ? 0 : throttle.take(key);
473
+ if (suppressed === null) {
474
+ this.logger.debug(`counted a repeat into the next ${paint.sky.bold(this.constructor.name)} card`);
137
475
  return;
138
476
  }
139
- const { username, components, files } = await this.report();
140
477
  try {
478
+ const { username, avatarUrl, components, files } = await this.report(suppressed);
141
479
  await WebhookLog.senderFor(url).send({
142
480
  flags: sendFlags({ ephemeral: false }),
143
481
  username: username ?? this.constructor.name,
482
+ avatarUrl,
144
483
  components,
145
484
  files
146
485
  });
147
486
  } catch (error) {
148
- this.logger.error("Failed to send webhook report", error);
487
+ if (key !== null) throttle.restore(key, suppressed);
488
+ this.logger.error("Webhook report failed", error);
149
489
  }
150
490
  }
151
491
  /** @internal */
@@ -249,20 +589,28 @@ function __decorate(decorators, target, key, desc) {
249
589
  //#endregion
250
590
  //#region src/subscribers/default/HandledException.ts
251
591
  let HandledException = class HandledException extends WebhookLog {
252
- report() {
592
+ throttleKey() {
593
+ return `handledException:${this.data.routeId}:${this.data.denial.name}`;
594
+ }
595
+ report(suppressed) {
253
596
  return {
254
597
  username: "Handled Exception",
255
- components: [new HandledExceptionContainer(this.data).component],
598
+ components: [new HandledExceptionContainer(this.data, suppressed).component],
256
599
  files: [jsonAttachment("source.json", "The raw source the fault came from", this.data.source.raw)]
257
600
  };
258
601
  }
259
602
  };
260
603
  HandledException = __decorate([Subscribe("handledException"), WebhookUrl("HANDLED_EXCEPTION_WEBHOOK_URL")], HandledException);
261
604
  var HandledExceptionContainer = class extends BuilderComponent {
262
- constructor(data) {
605
+ constructor(data, suppressed) {
263
606
  super("container");
264
607
  const { denial, uuid, source } = data;
265
- this.instance.addTextDisplayComponents((text) => text.setContent(faultSummary(denial, source))).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### UUID \`${uuid}\`\n\`\`\`${neutralizeFences(causeStack(denial))}\`\`\``)).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent("### Source")).addFileComponents((file) => file.setURL("attachment://source.json"));
608
+ 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"));
609
+ this.addSuppressedCount(suppressed);
610
+ }
611
+ addSuppressedCount(suppressed) {
612
+ if (suppressed === 0) return;
613
+ this.instance.addTextDisplayComponents((text) => text.setContent(`-# ${suppressed} more of these since the last report`));
266
614
  }
267
615
  };
268
616
  function faultSummary(denial, source) {
@@ -286,23 +634,34 @@ function causeStack(denial) {
286
634
  //#endregion
287
635
  //#region src/subscribers/default/UnknownException.ts
288
636
  let UnknownException = class UnknownException extends WebhookLog {
289
- report() {
637
+ throttleKey() {
638
+ return `unknownException:${this.data.routeId}:${nameOf(this.data.error)}`;
639
+ }
640
+ report(suppressed) {
290
641
  const { metadata } = this.data;
291
642
  return {
292
643
  username: "Unknown Exception",
293
- components: [new UnhandledErrorContainer(this.data).component],
644
+ components: [new UnhandledErrorContainer(this.data, suppressed).component],
294
645
  files: metadata ? [jsonAttachment("metadata.json", "Metadata associated with the error", metadata)] : void 0
295
646
  };
296
647
  }
297
648
  };
298
649
  UnknownException = __decorate([Subscribe("unknownException"), WebhookUrl("UNKNOWN_EXCEPTION_WEBHOOK_URL")], UnknownException);
650
+ function nameOf(error) {
651
+ return error.name === "Error" ? error.constructor.name : error.name;
652
+ }
299
653
  var UnhandledErrorContainer = class extends BuilderComponent {
300
- constructor(data) {
654
+ constructor(data, suppressed) {
301
655
  super("container");
302
656
  const { uuid, error, guild, user, metadata } = data;
303
657
  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)}\`\`\``));
304
658
  this.addTimestampsIfAvailable(error);
305
659
  this.addMetadataIfAvailable(metadata);
660
+ this.addSuppressedCount(suppressed);
661
+ }
662
+ addSuppressedCount(suppressed) {
663
+ if (suppressed === 0) return;
664
+ this.instance.addTextDisplayComponents((text) => text.setContent(`-# ${suppressed} more of these since the last report`));
306
665
  }
307
666
  addTimestampsIfAvailable(error) {
308
667
  if (!(error instanceof DiscordAPIError)) return;
@@ -321,16 +680,23 @@ var UnhandledErrorContainer = class extends BuilderComponent {
321
680
  }
322
681
  };
323
682
 
683
+ //#endregion
684
+ //#region src/subscribers/publishDefault.ts
685
+ /** @internal PublishableKey excludes this key so publish() never emits it externally. */
686
+ const PublishDefault = Symbol("seedcord.publishDefault");
687
+
324
688
  //#endregion
325
689
  //#region src/subscribers/Bus.ts
690
+ const loggerSlot = Symbol("seedcord:bus:logger");
326
691
  /**
327
692
  * Registers and dispatches subscribers. Subscribers run on a programmatic or framework publish.
328
693
  * Accessed via `core.bus`. Do not construct it directly.
329
694
  */
330
695
  var Bus = class extends TypedEventEmitter {
331
696
  core;
332
- /** @internal */
333
697
  logger = new Logger("Subscribers", { channel: "subscribers" });
698
+ /** @internal */
699
+ [loggerSlot] = this.logger;
334
700
  subscribersMap = /* @__PURE__ */ new Map();
335
701
  executedOnce = /* @__PURE__ */ new Set();
336
702
  webhookProbes = /* @__PURE__ */ new Map();
@@ -374,7 +740,7 @@ var Bus = class extends TypedEventEmitter {
374
740
  await Promise.all([...this.webhookProbes].map(async ([url, envKeys]) => {
375
741
  const result = await WebhookLog.senderFor(url).verify();
376
742
  if (result === "missing") missing.push(...envKeys);
377
- else if (result === "unreachable") this.logger.warn(`could not verify webhook at ${paint.sky.bold(envKeys.join(", "))}`);
743
+ else if (result === "unreachable") this.logger.warn(`could not verify webhook at ${envKeys.map((key) => paint.amber.bold(key)).join(", ")}`);
378
744
  }));
379
745
  if (missing.length > 0) throw new SeedcordError(SeedcordErrorCode.ConfigWebhookNotFound, [missing.join(", ")]);
380
746
  }
@@ -383,7 +749,7 @@ var Bus = class extends TypedEventEmitter {
383
749
  const envKey = WebhookLog.envKeyOf(ctor);
384
750
  const url = WebhookLog.urlOf(envKey);
385
751
  if (url === null) {
386
- this.logger.warn(`${chalk.bold(ctor.name)} disabled, ${paint.sky.bold(envKey)} is not set`);
752
+ this.logger.warn(`${paint.sky.bold(ctor.name)} disabled, ${paint.amber.bold(envKey)} is not set`);
387
753
  return false;
388
754
  }
389
755
  const envKeys = this.webhookProbes.get(url) ?? [];
@@ -442,7 +808,7 @@ var Bus = class extends TypedEventEmitter {
442
808
  name = Ctor.name;
443
809
  await new Ctor(data, this.core).execute();
444
810
  } catch (err) {
445
- this.logger.error(`Error in subscriber ${String(subscriberName)} handler ${paint.sky.bold(name)}:`, err);
811
+ this.logger.error(`Error in subscriber ${paint.mute(String(subscriberName))} handler ${paint.sky.bold(name)}:`, err);
446
812
  }
447
813
  }
448
814
  };
@@ -456,7 +822,11 @@ function registrationFor(ctor) {
456
822
  ctor
457
823
  };
458
824
  }
825
+ /** @internal */
826
+ function busLoggerOf(bus) {
827
+ return bus[loggerSlot];
828
+ }
459
829
 
460
830
  //#endregion
461
- export { WebhookLog as a, Subscribe as i, registrationFor as n, Subscriber as o, WebhookUrl as r, Bus as t };
462
- //# sourceMappingURL=subscribers-nEpbJj3Z.mjs.map
831
+ export { WebhookUrlMetadataKey as C, SubscribeMetadataKey as S, GatedMetadataKey as _, WebhookUrl as a, InteractionRoutes as b, Subscriber as c, sendFlags as d, BuilderComponent as f, EventMetadataKey as g, CommandMetadataKey as h, PublishDefault as i, ReportThrottle as l, setBotColor as m, busLoggerOf as n, Subscribe as o, RowComponent as p, registrationFor as r, WebhookLog as s, Bus as t, deferFlags as u, InteractionMetadataKey as v, contextMenuLeaves as w, MiddlewareMetadataKey as x, InteractionRouteKeys as y };
832
+ //# sourceMappingURL=Bus-DcuUtOEL.mjs.map