@seedcord/gateway 0.1.0-next.4 → 0.1.0-next.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,26 +1,24 @@
1
1
  import "reflect-metadata";
2
2
  import { installNodeDefaults } from "@seedcord/logger/node";
3
- import { BaseReplySender, CommandMetadataKey, EventMetadataKey, GateNotice, GatedMetadataKey, HmrManager, InteractionMetadataKey, InteractionRouteKeys, InteractionRoutes, MiddlewareMetadataKey, MissingPermissions, NoticeCard, PAGE_MAX, PermissionNames, SubscribeMetadataKey, WebhookUrlMetadataKey, areRoutes, decodeComponentRoute, deferFlags, getDevChannel, pageCursor, pickNotice, prefixOf, routeIdOf, runHandlerGates, sendFlags, setBotColor, slowGateMonitor } from "@seedcord/core/internal";
3
+ import { BaseReplySender, CommandMetadataKey, EventMetadataKey, FaultThrottle, GateNotice, GatedMetadataKey, HmrManager, InteractionMetadataKey, InteractionRouteKeys, InteractionRoutes, MiddlewareMetadataKey, MissingPermissions, NoticeCard, PAGE_MAX, PermissionNames, PublishDefault, areRoutes, asError, decodeComponentRoute, deferFlags, dispatchedPayload, getDevChannel, outcomeFor, pageCursor, pickNotice, prefixOf, queuedMsFor, reportedWrite, routeIdOf, runHandlerGates, sendFlags, setBotColor, slowGateMonitor } from "@seedcord/core/internal";
4
4
  import { SeedcordErrorCode } from "@seedcord/errors";
5
5
  import { SeedcordError, SeedcordRangeError, SeedcordTypeError, validateDiscordToken } from "@seedcord/errors/internal";
6
- import { BaseHandler, BuilderComponent, CustomId, DispatchContext, Fault, Notice, RepliableHandler as RepliableHandler$1, RowComponent, Silence, defineGate, paginate } from "@seedcord/core";
6
+ import { BaseHandler, BuilderComponent, Bus, CustomId, DispatchContext, Fault, Notice, RepliableHandler as RepliableHandler$1, RowComponent, Silence, Subscriber as Subscriber$1, WebhookLog as WebhookLog$1, defineGate, paginate } from "@seedcord/core";
7
7
  import { Logger, LoggerChannelRegistry, paint } from "@seedcord/logger";
8
8
  import { ActionRowBuilder, ButtonBuilder, ContextMenuCommandBuilder, SlashCommandBuilder, TextDisplayBuilder } from "@discordjs/builders";
9
9
  import { buildSlashRoute, routeLeavesOf } from "@seedcord/utils/internal";
10
10
  import { ApplicationCommandType, ButtonStyle, Client, Collection, ComponentType, DiscordAPIError, Events, Guild, GuildMember, Message, MessageFlags, PermissionFlagsBits, RESTJSONErrorCodes, Role, TextChannel, User, chatInputApplicationCommandMention } from "discord.js";
11
11
  import * as crypto from "node:crypto";
12
- import chalk from "chalk";
13
- import { Converters, Envapter } from "envapt";
14
- import { MessageFlags as MessageFlags$1, Routes, SeparatorSpacingSize } from "discord-api-types/v10";
15
- import { filterCirculars, formatFilePath, hasKeys, stripAnsi, timestampFromSnowflake } from "@seedcord/utils";
16
- import { DiscordAPIError as DiscordAPIError$1, REST } from "@discordjs/rest";
17
- import { CoordinatedShutdown, CoordinatedStartup, HealthCheck, Pluggable, ShutdownPhase, StartupPhase, settleWithin } from "@seedcord/core/node/internal";
12
+ import { Plugin as Plugin$1 } from "@seedcord/core/plugin";
13
+ import { CoordinatedShutdown, CoordinatedStartup, HealthCheck, Pluggable, ShutdownPhase, StartupPhase, SubscriberLoader, settleWithin } from "@seedcord/core/node/internal";
18
14
  import { MemoryRateLimiter } from "@seedcord/rate-limiter";
19
15
  import { SeedcordBrand } from "@seedcord/types/internal";
20
- import { TypedEventEmitter } from "@seedcord/event-emitter";
16
+ import { Envapter } from "envapt";
17
+ import chalk from "chalk";
21
18
  import { Envapt } from "envapt/legacy";
22
19
  import { resolve } from "node:path";
23
20
  import { HmrModuleHandler } from "@seedcord/core/hmr";
21
+ import { formatFilePath, hasKeys } from "@seedcord/utils";
24
22
  import { traverseDirectory } from "@seedcord/utils/node";
25
23
 
26
24
  export * from "@seedcord/errors"
@@ -29,10 +27,10 @@ export * from "@seedcord/core"
29
27
 
30
28
  export * from "@seedcord/logger"
31
29
 
32
- export * from "@seedcord/utils"
33
-
34
30
  export * from "@seedcord/rate-limiter"
35
31
 
32
+ export * from "@seedcord/utils"
33
+
36
34
  export * from "@seedcord/event-emitter"
37
35
 
38
36
  //#region src/logger-bootstrap.ts
@@ -147,7 +145,7 @@ let MiddlewareType = /* @__PURE__ */ function(MiddlewareType) {
147
145
  * Interaction middleware cannot specify event filters.
148
146
  *
149
147
  * @param type - Middleware kind from {@link MiddlewareType}
150
- * @param priority - Ordering value where lower runs earlier. {@default `0`}
148
+ * @param priority - Ordering value where lower runs earlier. {@default `0` }
151
149
  * @param options - Additional registration options
152
150
  *
153
151
  * @decorator
@@ -335,7 +333,7 @@ const Emojis = emojiStorage;
335
333
  */
336
334
  var EmojiInjector = class {
337
335
  core;
338
- logger = new Logger("Emojis");
336
+ logger = new Logger("Emojis", { channel: "bot" });
339
337
  constructor(core) {
340
338
  this.core = core;
341
339
  }
@@ -410,7 +408,7 @@ const MULTI_GUILD = Symbol("seedcord:commandmentions:multi-guild");
410
408
  */
411
409
  var CommandMentionInjector = class {
412
410
  core;
413
- logger = new Logger("CommandMentions");
411
+ logger = new Logger("CommandMentions", { channel: "bot" });
414
412
  constructor(core) {
415
413
  this.core = core;
416
414
  }
@@ -568,7 +566,7 @@ function checkBotPermissions(target, scope, inverse = false, errors) {
568
566
  }
569
567
  const me = target.guild.members.me;
570
568
  if (!me) {
571
- new Logger("checkBotPermissions").warn(`Bot member is unavailable in guild ${target.guild.id} while checking permissions in channel ${target.id}`);
569
+ new Logger("checkBotPermissions", { channel: "errors" }).warn(`Bot member is unavailable in guild ${target.guild.id} while checking permissions in channel ${target.id}`);
572
570
  const names = scope.map((bit) => PermissionNames.get(bit) ?? String(bit));
573
571
  throw new ((errors?.missingNotice) ?? MissingPermissions)(void 0, "The bot", names);
574
572
  }
@@ -767,8 +765,11 @@ function seedState(interaction) {
767
765
  */
768
766
  var ReplySender = class extends BaseReplySender {
769
767
  interaction;
770
- constructor(interaction, routeId) {
771
- super(routeId, seedState(interaction));
768
+ constructor(interaction, routeId, bus) {
769
+ super(routeId, seedState(interaction), bus && {
770
+ bus,
771
+ interactionId: interaction.id
772
+ });
772
773
  this.interaction = interaction;
773
774
  }
774
775
  async writeReply(response, opts) {
@@ -875,43 +876,9 @@ function slashRouteOf(interaction) {
875
876
  return buildSlashRoute(interaction.commandName, subcommand, group);
876
877
  }
877
878
 
878
- //#endregion
879
- //#region src/miscellaneous/FaultThrottle.ts
880
- /**
881
- * Drops duplicate fault reports inside a fixed window so a recurring fault (a database outage on a
882
- * hot path) reports once per window instead of flooding the webhook. Keyed on a structural string, so
883
- * the map stays small, and a stale entry is evicted on the next read of its key.
884
- *
885
- * @internal
886
- */
887
- var FaultThrottle = class {
888
- windowMs;
889
- now;
890
- lastReportedAt = /* @__PURE__ */ new Map();
891
- constructor(windowMs = 6e4, now = () => Date.now()) {
892
- this.windowMs = windowMs;
893
- this.now = now;
894
- }
895
- /** Evicts the entry for this key if it has aged out, so the map never holds stale keys forever. */
896
- shouldReport(key) {
897
- const last = this.lastReportedAt.get(key);
898
- if (last === void 0) return true;
899
- if (this.now() - last < this.windowMs) return false;
900
- this.lastReportedAt.delete(key);
901
- return true;
902
- }
903
- markReported(key) {
904
- this.lastReportedAt.set(key, this.now());
905
- }
906
- clear() {
907
- this.lastReportedAt.clear();
908
- }
909
- };
910
-
911
879
  //#endregion
912
880
  //#region src/miscellaneous/extractErrorResponse.ts
913
- const logger$2 = new Logger("ErrorsHandling");
914
- const faultThrottle = new FaultThrottle();
881
+ const logger$2 = new Logger("ErrorsHandling", { channel: "errors" });
915
882
  /**
916
883
  * Processes an error into a user-facing reply and routes its report.
917
884
  *
@@ -941,29 +908,30 @@ function extractErrorResponse(error, core, origin) {
941
908
  response: override ? new override(uuid).render(ctx) : new Fault().render(ctx)
942
909
  };
943
910
  }
944
- function withThrottle(origin, error, publish) {
911
+ function withThrottle(core, origin, error, publish) {
912
+ const throttle = FaultThrottle.for(core);
945
913
  const key = faultKey(origin, error);
946
- if (!faultThrottle.shouldReport(key)) {
947
- logger$2.info(`throttled duplicate fault ${key}`);
914
+ if (!throttle.shouldReport(key)) {
915
+ logger$2.debug(`throttled duplicate fault ${key}`);
948
916
  return;
949
917
  }
950
918
  publish();
951
- faultThrottle.markReported(key);
919
+ throttle.markReported(key);
952
920
  }
953
921
  function reportFault(denial, core, origin, uuid) {
954
- withThrottle(origin, denial, () => {
955
- logger$2.error(`${denial.name}: ${uuid}`, denial);
956
- if (origin.interaction) core.bus.publish("handledException", {
922
+ logger$2.error(`${denial.name}: ${uuid}`, denial);
923
+ withThrottle(core, origin, denial, () => {
924
+ if (origin.interaction) core.bus[PublishDefault]("handledException", {
957
925
  denial,
958
926
  uuid,
959
927
  source: buildInteractionSource(origin.interaction)
960
928
  });
961
- else if (origin.event) core.bus.publish("handledException", {
929
+ else if (origin.event) core.bus[PublishDefault]("handledException", {
962
930
  denial,
963
931
  uuid,
964
932
  source: buildEventSource(origin.event, origin)
965
933
  });
966
- else core.bus.publish("unknownException", {
934
+ else core.bus[PublishDefault]("unknownException", {
967
935
  uuid,
968
936
  error: denial,
969
937
  ...scalarActors(origin),
@@ -978,10 +946,10 @@ function causeLine(error) {
978
946
  return `\ncaused by ${cause.name}: ${first ?? cause.message}`;
979
947
  }
980
948
  function reportRawFault(error, core, origin, uuid) {
981
- withThrottle(origin, error, () => {
982
- if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
983
- else logger$2.error(`${uuid} | ${error.message}${causeLine(error)}`);
984
- core.bus.publish("unknownException", {
949
+ if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
950
+ else logger$2.error(`${uuid} | ${error.message}${causeLine(error)}`);
951
+ withThrottle(core, origin, error, () => {
952
+ core.bus[PublishDefault]("unknownException", {
985
953
  uuid,
986
954
  error,
987
955
  ...scalarActors(origin),
@@ -1149,7 +1117,7 @@ var BaseHandler$1 = class extends BaseHandler {
1149
1117
  */
1150
1118
  var InteractionMiddleware = class extends BaseHandler$1 {
1151
1119
  constructor(event, core) {
1152
- super(event, core);
1120
+ super(event, core, void 0, "interactions");
1153
1121
  }
1154
1122
  };
1155
1123
 
@@ -1169,8 +1137,8 @@ var RepliableHandler = class extends RepliableHandler$1 {
1169
1137
  getEvent() {
1170
1138
  return this.event;
1171
1139
  }
1172
- buildSender(event, _core, routeId) {
1173
- return new ReplySender(event, routeId);
1140
+ buildSender(event, core, routeId) {
1141
+ return new ReplySender(event, routeId, core.bus);
1174
1142
  }
1175
1143
  };
1176
1144
 
@@ -1325,7 +1293,7 @@ var ContextMenuHandler = class extends InteractionHandler {
1325
1293
  */
1326
1294
  var AutocompleteHandler = class extends BaseHandler$1 {
1327
1295
  constructor(event, core, dispatch) {
1328
- super(event, core, dispatch);
1296
+ super(event, core, dispatch, "interactions");
1329
1297
  }
1330
1298
  decodedFocused;
1331
1299
  /**
@@ -1376,7 +1344,11 @@ var AutocompleteHandler = class extends BaseHandler$1 {
1376
1344
  }
1377
1345
  /** Send autocomplete suggestions, callback type 8. Prefer {@link match}, which restricts each field's choices to its declared type. */
1378
1346
  async respond(choices) {
1379
- await this.event.respond(choices);
1347
+ const routeId = this.dispatch?.routeId ?? "autocomplete";
1348
+ await reportedWrite({
1349
+ bus: this.core.bus,
1350
+ interactionId: this.event.id
1351
+ }, routeId, "respond", () => this.event.respond(choices));
1380
1352
  }
1381
1353
  /** The firing command route, for a field whose completion differs per registered command. */
1382
1354
  get route() {
@@ -1569,7 +1541,7 @@ var SelectMenuHandler = class extends ComponentHandler {};
1569
1541
  var EventHandler = class extends BaseHandler$1 {
1570
1542
  firedEvent;
1571
1543
  constructor(event, core, eventName) {
1572
- super(event, core);
1544
+ super(event, core, void 0, "events");
1573
1545
  this.firedEvent = eventName;
1574
1546
  }
1575
1547
  /**
@@ -1622,7 +1594,7 @@ var EventHandler = class extends BaseHandler$1 {
1622
1594
  var EventMiddleware = class extends BaseHandler$1 {
1623
1595
  firedEvent;
1624
1596
  constructor(event, core, eventName) {
1625
- super(event, core);
1597
+ super(event, core, void 0, "events");
1626
1598
  this.firedEvent = eventName;
1627
1599
  }
1628
1600
  /**
@@ -1807,10 +1779,15 @@ var Paginator = class {
1807
1779
  }
1808
1780
  };
1809
1781
  }
1810
- /** Render page 0 and send it, picking reply or followUp from the interaction's state. */
1811
- async start(interaction, core) {
1812
- const response = await this.page(contextOf(interaction, core), 0);
1813
- return new ReplySender(interaction, interactionRoute(interaction)).send(response, { ephemeral: this.config.ephemeral ?? false });
1782
+ /**
1783
+ * Render page 0 and send it through the handler's sender, picking reply or followUp from its ack state.
1784
+ *
1785
+ * @param handler - The handler starting the paginator, normally `this`.
1786
+ */
1787
+ async start(handler) {
1788
+ const interaction = handler.getEvent();
1789
+ const response = await this.page(contextOf(interaction, handler.core), 0);
1790
+ return handler.getSender().send(response, { ephemeral: this.config.ephemeral ?? false });
1814
1791
  }
1815
1792
  /** Render a page as a {@link ReplyResponse}. To post it elsewhere, add `flags: MessageFlags.IsComponentsV2`. */
1816
1793
  async page(ctx, n) {
@@ -1870,250 +1847,32 @@ var CursorSource = class {
1870
1847
  };
1871
1848
 
1872
1849
  //#endregion
1873
- //#region src/subscribers/Subscriber.ts
1850
+ //#region src/subscribers/index.ts
1874
1851
  /**
1875
- * Abstract base class for handling application subscribers.
1876
- *
1877
- * Provides type-safe access to subscribers data and the core framework instance.
1878
- * Extend this class to create custom subscriber handlers.
1852
+ * Base class for a gateway subscriber, binding `this.core` to the gateway {@link Core}.
1879
1853
  *
1880
- * @typeParam KeyOfSubscribers - The specific subscriber type this handler processes
1854
+ * @typeParam KeyOfSubscribers - The subscription key this handler receives
1881
1855
  */
1882
- var Subscriber = class {
1883
- data;
1884
- core;
1885
- logger;
1886
- /**
1887
- * Instantiates the subscriber with event data and core.
1888
- *
1889
- * @param data - The subscriber event data
1890
- * @param core - The core framework instance
1891
- */
1892
- constructor(data, core) {
1893
- this.data = data;
1894
- this.core = core;
1895
- this.logger = new Logger(this.constructor.name);
1896
- }
1897
- };
1898
-
1899
- //#endregion
1900
- //#region src/subscribers/decorators/Subscribe.ts
1901
- /**
1902
- * Registers a subscriber handler class with a specific subscriber event.
1903
- *
1904
- * Associates the decorated class with a subscriber event type for automatic
1905
- * registration and execution when the subscriber is emitted.
1906
- *
1907
- * @typeParam TSubscriber - The subscriber event name to register for
1908
- * @param subscriber - The subscriber event name to register for
1909
- * @param options - Options to configure the subscriber handler registration.
1910
- * @decorator
1911
- * @example
1912
- * ```typescript
1913
- * \@Subscribe('userJoin')
1914
- * class WelcomeHandler extends Subscriber<'userJoin'> {
1915
- * async execute() {
1916
- * // Handle user join event
1917
- * }
1918
- * }
1919
- * ```
1920
- * @example
1921
- * ```ts
1922
- * \@Subscribe('userJoin', { frequency: 'once' })
1923
- * // or
1924
- * \@Subscribe('userJoin')
1925
- * ```
1926
- */
1927
- function Subscribe(subscriber, options) {
1928
- return function(constructor) {
1929
- const meta = {
1930
- subscriber,
1931
- frequency: options?.frequency
1932
- };
1933
- Reflect.defineMetadata(SubscribeMetadataKey, meta, constructor);
1934
- };
1935
- }
1936
-
1937
- //#endregion
1938
- //#region src/subscribers/decorators/WebhookUrl.ts
1856
+ var Subscriber = class extends Subscriber$1 {};
1939
1857
  /**
1940
- * Declares the environment variable a {@link WebhookLog} reporter reads its webhook url from.
1941
- *
1942
- * An unset variable disables the reporter with a boot warning. A set but malformed value throws
1943
- * at boot.
1858
+ * Base class for a gateway subscriber that delivers its event to a Discord webhook, binding
1859
+ * `this.core` to the gateway {@link Core}.
1944
1860
  *
1945
- * @param envKey - The environment variable name holding the webhook url
1946
- * @decorator
1947
- * @example
1948
- * ```ts
1949
- * \@Subscribe('memberBanned')
1950
- * \@WebhookUrl('BAN_LOG_WEBHOOK_URL')
1951
- * class BanLog extends WebhookLog<'memberBanned'> {
1952
- * report() {
1953
- * return { components: [new BanCard(this.data).component] };
1954
- * }
1955
- * }
1956
- * ```
1957
- */
1958
- function WebhookUrl(envKey) {
1959
- return function(constructor) {
1960
- Reflect.defineMetadata(WebhookUrlMetadataKey, envKey, constructor);
1961
- };
1962
- }
1963
-
1964
- //#endregion
1965
- //#region src/miscellaneous/flagsFor.ts
1966
- /** @internal */
1967
- function flagsFor(ephemeral) {
1968
- let flags = MessageFlags$1.IsComponentsV2;
1969
- if (ephemeral) flags |= MessageFlags$1.Ephemeral;
1970
- return flags;
1971
- }
1972
-
1973
- //#endregion
1974
- //#region src/subscribers/bases/webhookHelpers.ts
1975
- const DISCORD_WEBHOOK_REGEX = new RegExp(String.raw`^https?:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w$-]+$`);
1976
- function isDiscordWebhookUrl(value) {
1977
- return URL.canParse(value) && DISCORD_WEBHOOK_REGEX.test(value);
1978
- }
1979
- function jsonAttachment(name, description, data) {
1980
- const content = filterCirculars(data);
1981
- return {
1982
- name,
1983
- description,
1984
- data: Buffer.from(JSON.stringify(content, void 0, 2), "utf8")
1985
- };
1986
- }
1987
- function neutralizeFences(text) {
1988
- return text.replaceAll("```", "`​`​`");
1989
- }
1990
- /** An error's stack with its direct cause appended (one level), ANSI-stripped for the webhook payload. */
1991
- function errorReport(error) {
1992
- let report = error.stack ?? `${error.name}: ${error.message}`;
1993
- if (Error.isError(error.cause)) {
1994
- const cause = error.cause.stack ?? `${error.cause.name}: ${error.cause.message}`;
1995
- report += `\n\nCaused by:\n${cause}`;
1996
- }
1997
- const stripped = neutralizeFences(stripAnsi(report));
1998
- return stripped.length > 1800 ? `${stripped.slice(0, 1799)}…` : stripped;
1999
- }
2000
- var WebhookSeparator = class extends BuilderComponent {
2001
- constructor() {
2002
- super("separator");
2003
- this.instance.setSpacing(SeparatorSpacingSize.Small).setDivider(true);
2004
- }
2005
- };
2006
-
2007
- //#endregion
2008
- //#region src/subscribers/bases/WebhookSender.ts
2009
- const HTTP_NOT_FOUND = 404;
2010
- const HTTP_UNAUTHORIZED = 401;
2011
- /**
2012
- * Sends component messages to a Discord webhook through `@discordjs/rest`.
2013
- *
2014
- * @throws A `SeedcordError` when the url does not carry a webhook id and token.
1861
+ * @typeParam KeyOfSubscribers - The subscription key this reporter receives
2015
1862
  */
2016
- var WebhookSender = class {
2017
- rest = new REST();
2018
- route;
2019
- constructor(url) {
2020
- const match = /\/webhooks\/(\d+)\/([\w$-]+)$/.exec(url);
2021
- if (!match?.[1] || !match[2]) throw new SeedcordError(SeedcordErrorCode.ConfigWebhookUrlInvalid, ["The url"]);
2022
- this.route = Routes.webhook(match[1], match[2]);
2023
- }
2024
- /**
2025
- * Fetches the webhook object, sending no message. Returns `missing` on a 404 or 401 from
2026
- * Discord and `unreachable` on any other failure.
2027
- */
2028
- async verify() {
2029
- try {
2030
- await this.rest.get(this.route, { auth: false });
2031
- return "ok";
2032
- } catch (error) {
2033
- return error instanceof DiscordAPIError$1 && (error.status === HTTP_NOT_FOUND || error.status === HTTP_UNAUTHORIZED) ? "missing" : "unreachable";
2034
- }
2035
- }
2036
- async send(options) {
2037
- const { flags, username, components, files } = options;
2038
- await this.rest.post(this.route, {
2039
- auth: false,
2040
- query: new URLSearchParams({
2041
- with_components: "true",
2042
- wait: "true"
2043
- }),
2044
- body: {
2045
- flags,
2046
- username,
2047
- components: components.map((component) => component.toJSON()),
2048
- ...files?.length && { attachments: files.map((file, id) => ({
2049
- id,
2050
- filename: file.name,
2051
- description: file.description
2052
- })) }
2053
- },
2054
- ...files?.length && { files: files.map((file) => ({
2055
- name: file.name,
2056
- data: file.data
2057
- })) }
2058
- });
2059
- }
2060
- };
1863
+ var WebhookLog = class extends WebhookLog$1 {};
2061
1864
 
2062
1865
  //#endregion
2063
- //#region src/subscribers/bases/WebhookLog.ts
1866
+ //#region src/plugin.ts
2064
1867
  /**
2065
- * Base class for subscribers that deliver their event to a Discord webhook.
1868
+ * Base class for a gateway plugin, binding `this.core` to the gateway {@link Core}.
2066
1869
  *
2067
- * Declare the url's environment variable with `@WebhookUrl` and implement {@link report}. Url
2068
- * resolution, validation, sender reuse, and sending run in the base. When the variable is unset
2069
- * the reporter is skipped at registration with a boot warning.
1870
+ * A plugin that runs on either transport extends the base from `@seedcord/core/plugin`, whose
1871
+ * `this.core` carries the shared members only.
2070
1872
  *
2071
- * @typeParam KeyOfSubscribers - The subscription key this reporter receives
1873
+ * @typeParam Opts - The transport, runtime, and capabilities the plugin declares.
2072
1874
  */
2073
- var WebhookLog = class WebhookLog extends Subscriber {
2074
- static senders = /* @__PURE__ */ new Map();
2075
- async execute() {
2076
- const url = WebhookLog.urlOf(WebhookLog.envKeyOf(this.constructor));
2077
- if (url === null) {
2078
- this.logger.warn(`${chalk.bold(this.constructor.name)} has no webhook url set, this reporter is disabled`);
2079
- return;
2080
- }
2081
- const { username, components, files } = await this.report();
2082
- try {
2083
- await WebhookLog.senderFor(url).send({
2084
- flags: flagsFor(false),
2085
- username: username ?? this.constructor.name,
2086
- components,
2087
- files
2088
- });
2089
- } catch (error) {
2090
- this.logger.error("Failed to send webhook report", error);
2091
- }
2092
- }
2093
- /** @internal */
2094
- static senderFor(url) {
2095
- let sender = WebhookLog.senders.get(url);
2096
- if (!sender) {
2097
- sender = new WebhookSender(url);
2098
- WebhookLog.senders.set(url, sender);
2099
- }
2100
- return sender;
2101
- }
2102
- /** @internal */
2103
- static envKeyOf(ctor) {
2104
- const envKey = Reflect.getMetadata(WebhookUrlMetadataKey, ctor);
2105
- if (!envKey) throw new SeedcordError(SeedcordErrorCode.DecoratorWebhookUrlMissing, [ctor.name]);
2106
- return envKey;
2107
- }
2108
- /** @internal Returns null when the env var is unset, throws when set but malformed. */
2109
- static urlOf(envKey) {
2110
- if (!Envapter.has(envKey)) return null;
2111
- const raw = Envapter.getRequired(envKey, Converters.String).trim();
2112
- if (raw === "") return null;
2113
- if (!isDiscordWebhookUrl(raw)) throw new SeedcordError(SeedcordErrorCode.ConfigWebhookUrlInvalid, [envKey]);
2114
- return raw;
2115
- }
2116
- };
1875
+ var Plugin = class extends Plugin$1 {};
2117
1876
 
2118
1877
  //#endregion
2119
1878
  //#region src/bot/utilities/miscellaneous/contextMenuLeaves.ts
@@ -2169,7 +1928,7 @@ function slashRouteLeaves(commands) {
2169
1928
  var CommandRegistry = class {
2170
1929
  core;
2171
1930
  onDeployed;
2172
- logger = new Logger("Commands");
1931
+ logger = new Logger("Commands", { channel: "commands" });
2173
1932
  isInitialised = false;
2174
1933
  globalCommands = [];
2175
1934
  guildCommands = new Collection();
@@ -2256,7 +2015,7 @@ var CommandRegistry = class {
2256
2015
  this.registerCommand(exported, rel);
2257
2016
  this.hmrHandler?.trackHandler(fullPath, exported);
2258
2017
  }
2259
- }, this.logger);
2018
+ });
2260
2019
  }
2261
2020
  isCommandClass(obj) {
2262
2021
  if (typeof obj !== "function") return false;
@@ -2418,7 +2177,7 @@ function eventGateContext(eventName, args, core) {
2418
2177
 
2419
2178
  //#endregion
2420
2179
  //#region src/bot/handleEventFault.ts
2421
- const logger$1 = new Logger("EventBoundary");
2180
+ const logger$1 = new Logger("EventBoundary", { channel: "errors" });
2422
2181
  /**
2423
2182
  * The event controller boundary. Reports a throw from an event handler and never replies, since a
2424
2183
  * generic event has no reply target. A {@link Silence} or a non-reporting {@link Notice} stops quietly, a
@@ -2431,15 +2190,15 @@ function handleEventFault(caught, eventName, handlerName, args, core) {
2431
2190
  if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger$1.debug(`Silence: ${caught.reason}`);
2432
2191
  return;
2433
2192
  }
2434
- if (!Error.isError(caught)) throw caught;
2193
+ const error = asError(caught);
2435
2194
  const ignore = new Set(core.config.errors?.ignoreEventApiCodes ?? []);
2436
- if (caught instanceof DiscordAPIError && ignore.has(caught.code)) {
2437
- logger$1.debug(`swallowed api code ${caught.code}`);
2195
+ if (error instanceof DiscordAPIError && ignore.has(error.code)) {
2196
+ logger$1.debug(`swallowed api code ${error.code}`);
2438
2197
  return;
2439
2198
  }
2440
- if (caught instanceof Notice && !caught.report) return;
2199
+ if (error instanceof Notice && !error.report) return;
2441
2200
  const actor = deriveEventActor(args);
2442
- extractErrorResponse(caught, core, {
2201
+ extractErrorResponse(error, core, {
2443
2202
  event: {
2444
2203
  name: eventName,
2445
2204
  handler: handlerName,
@@ -2454,15 +2213,15 @@ function handleEventFault(caught, eventName, handlerName, args, core) {
2454
2213
  //#endregion
2455
2214
  //#region src/bot/controllers/EventDispatcher.ts
2456
2215
  /**
2457
- * Scans event handler directories, registers handlers with Discord client events,
2458
- * and coordinates event execution through the handler system. Does not handle interactions.
2459
- * Multiple handlers can point to one event.
2216
+ * Scans event handler directories, attaches one client listener per registered event, and runs the
2217
+ * handlers for each fire. Interactions route through InteractionDispatcher. Multiple handlers can
2218
+ * point to one event.
2460
2219
  *
2461
2220
  * @internal
2462
2221
  */
2463
2222
  var EventDispatcher = class {
2464
2223
  core;
2465
- logger = new Logger("Events");
2224
+ logger = new Logger("Events", { channel: "events" });
2466
2225
  isInitialized = false;
2467
2226
  eventMap = /* @__PURE__ */ new Map();
2468
2227
  middlewares = [];
@@ -2533,7 +2292,7 @@ var EventDispatcher = class {
2533
2292
  this.registerHandler(val, relativePath);
2534
2293
  this.hmrHandler?.trackHandler(fullPath, val);
2535
2294
  }
2536
- }, this.logger);
2295
+ });
2537
2296
  }
2538
2297
  async loadMiddlewares(dir) {
2539
2298
  await traverseDirectory(dir, (fullPath, relativePath, imported) => {
@@ -2542,7 +2301,7 @@ var EventDispatcher = class {
2542
2301
  this.registerMiddleware(val, relativePath);
2543
2302
  this.hmrHandler?.trackMiddleware(fullPath, val);
2544
2303
  }
2545
- }, this.logger);
2304
+ });
2546
2305
  }
2547
2306
  /** @internal For use in dev mode */
2548
2307
  async onHmr(event) {
@@ -2639,10 +2398,14 @@ var EventDispatcher = class {
2639
2398
  this.logger.debug(`Attaching ${paint.mint.bold(eventName)} to the client with ${paint.mute(handlerEntries?.length ?? 0)} handler(s)`);
2640
2399
  this.core.bot.client.on(eventName, (...args) => {
2641
2400
  if (this.draining) return;
2642
- this.core.bot.emitSafe("any:event", eventName, ...args);
2643
- const run = this.processEvent(eventName, args).catch((err) => {
2644
- this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
2645
- this.core.bot.emit("error:unhandled:event", err);
2401
+ this.core.bus[PublishDefault]("anyEvent", {
2402
+ name: eventName,
2403
+ args
2404
+ });
2405
+ const run = this.processEvent(eventName, args).catch((caught) => {
2406
+ const error = asError(caught);
2407
+ this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${error.name}`, error.stack);
2408
+ this.core.bus[PublishDefault]("unhandledEventError", { error });
2646
2409
  });
2647
2410
  this.inFlight.add(run);
2648
2411
  run.finally(() => this.inFlight.delete(run));
@@ -2718,7 +2481,7 @@ const HARMLESS_API_CODES = new Set([
2718
2481
 
2719
2482
  //#endregion
2720
2483
  //#region src/bot/handleInteractionFault.ts
2721
- const logger = new Logger("InteractionBoundary");
2484
+ const logger = new Logger("InteractionBoundary", { channel: "errors" });
2722
2485
  /**
2723
2486
  * The interaction controller boundary. Sorts a throw from any handler-lifecycle phase into the right
2724
2487
  * user reply plus the right bus event. When a handler is built its sender is passed in, so the boundary
@@ -2727,33 +2490,45 @@ const logger = new Logger("InteractionBoundary");
2727
2490
  *
2728
2491
  * @internal
2729
2492
  */
2730
- async function handleInteractionFault(caught, interaction, core, sender) {
2493
+ async function handleInteractionFault(caught, interaction, core, routeId, sender) {
2731
2494
  if (caught instanceof Silence) {
2732
2495
  if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger.debug(`Silence: ${caught.reason}`);
2733
2496
  return;
2734
2497
  }
2735
- if (!Error.isError(caught)) throw caught;
2498
+ const error = asError(caught);
2736
2499
  const ignore = new Set(core.config.errors?.ignoreApiCodes ?? []);
2737
- if (caught instanceof DiscordAPIError && ignore.has(caught.code)) {
2738
- logger.debug(`swallowed api code ${caught.code}`);
2500
+ if (error instanceof DiscordAPIError && ignore.has(error.code)) {
2501
+ logger.debug(`swallowed api code ${error.code}`);
2739
2502
  return;
2740
2503
  }
2741
2504
  if (interaction.isAutocomplete()) {
2742
- extractErrorResponse(caught, core, {
2505
+ extractErrorResponse(error, core, {
2743
2506
  guild: interaction.guild,
2744
2507
  user: interaction.user,
2745
2508
  metadata: interaction,
2746
2509
  route: slashRouteOf(interaction)
2747
2510
  });
2511
+ await sendEmptyChoices(interaction, core, routeId);
2748
2512
  return;
2749
2513
  }
2750
- const { response } = extractErrorResponse(caught, core, {
2514
+ const { response } = extractErrorResponse(error, core, {
2751
2515
  interaction,
2752
2516
  guild: interaction.guild,
2753
2517
  user: interaction.user,
2754
2518
  metadata: interaction
2755
2519
  });
2756
- await sendGuarded(sender ?? new ReplySender(interaction, interactionRoute(interaction)), response, caught instanceof Notice ? caught.ephemeral : true);
2520
+ await sendGuarded(sender ?? new ReplySender(interaction, routeId, core.bus), response, error instanceof Notice ? error.ephemeral : true);
2521
+ }
2522
+ async function sendEmptyChoices(interaction, core, routeId) {
2523
+ const telemetry = {
2524
+ bus: core.bus,
2525
+ interactionId: interaction.id
2526
+ };
2527
+ try {
2528
+ await reportedWrite(telemetry, routeId, "respond", () => interaction.respond([]));
2529
+ } catch (error) {
2530
+ logger.debug(`autocomplete empty-choices send failed: ${String(error)}`);
2531
+ }
2757
2532
  }
2758
2533
  async function sendGuarded(sender, response, ephemeral) {
2759
2534
  try {
@@ -2777,7 +2552,7 @@ async function sendGuarded(sender, response, ephemeral) {
2777
2552
  */
2778
2553
  var InteractionDispatcher = class {
2779
2554
  core;
2780
- logger = new Logger("Interactions");
2555
+ logger = new Logger("Interactions", { channel: "interactions" });
2781
2556
  isInitialized = false;
2782
2557
  slashMap = /* @__PURE__ */ new Map();
2783
2558
  buttonMap = /* @__PURE__ */ new Map();
@@ -2790,6 +2565,7 @@ var InteractionDispatcher = class {
2790
2565
  messageContextMenuMap = /* @__PURE__ */ new Map();
2791
2566
  userContextMenuMap = /* @__PURE__ */ new Map();
2792
2567
  autocompleteMap = /* @__PURE__ */ new Map();
2568
+ handlerFiles = /* @__PURE__ */ new Map();
2793
2569
  keysToIgnore = /* @__PURE__ */ new Set();
2794
2570
  middlewares = [];
2795
2571
  inFlight = /* @__PURE__ */ new Set();
@@ -2908,7 +2684,7 @@ var InteractionDispatcher = class {
2908
2684
  this.registerHandler(val, relativePath);
2909
2685
  this.hmrHandler?.trackHandler(fullPath, val);
2910
2686
  }
2911
- }, this.logger);
2687
+ });
2912
2688
  }
2913
2689
  async loadMiddlewares(dir) {
2914
2690
  await traverseDirectory(dir, (fullPath, relativePath, imported) => {
@@ -2917,7 +2693,7 @@ var InteractionDispatcher = class {
2917
2693
  this.registerMiddleware(val, relativePath);
2918
2694
  this.hmrHandler?.trackMiddleware(fullPath, val);
2919
2695
  }
2920
- }, this.logger);
2696
+ });
2921
2697
  }
2922
2698
  registerMiddleware(middlewareCtor, relativePath) {
2923
2699
  const metadata = Reflect.getMetadata(MiddlewareMetadataKey, middlewareCtor);
@@ -2947,6 +2723,7 @@ var InteractionDispatcher = class {
2947
2723
  return obj.prototype instanceof InteractionMiddleware && Reflect.hasMetadata(MiddlewareMetadataKey, obj);
2948
2724
  }
2949
2725
  registerHandler(handlerClass, relativePath) {
2726
+ const from = formatFilePath(relativePath);
2950
2727
  const writes = [];
2951
2728
  for (const [routeType, map] of this.routeTypes) {
2952
2729
  const meta = Reflect.getMetadata(InteractionRouteKeys[routeType], handlerClass);
@@ -2954,16 +2731,16 @@ var InteractionDispatcher = class {
2954
2731
  for (const route of meta) {
2955
2732
  const existing = map.get(route);
2956
2733
  if (existing && existing !== handlerClass) throw new SeedcordError(SeedcordErrorCode.InteractionDuplicateRoute, [
2957
- route,
2958
- existing.name,
2959
- handlerClass.name
2734
+ `${routeType}:${route}`,
2735
+ `${existing.name} (${this.handlerFiles.get(existing) ?? "unknown file"})`,
2736
+ `${handlerClass.name} (${from})`
2960
2737
  ]);
2961
2738
  writes.push([map, route]);
2962
2739
  }
2963
2740
  }
2964
2741
  if (writes.length === 0) return;
2965
2742
  for (const [map, route] of writes) map.set(route, handlerClass);
2966
- const from = formatFilePath(relativePath);
2743
+ this.handlerFiles.set(handlerClass, from);
2967
2744
  if (this.loading) this.loadedHandlers.push({
2968
2745
  name: handlerClass.name,
2969
2746
  from
@@ -2975,6 +2752,7 @@ var InteractionDispatcher = class {
2975
2752
  await this.hmrHandler?.handle(event);
2976
2753
  }
2977
2754
  unregisterHandler(handlerClass, artifacts) {
2755
+ this.handlerFiles.delete(handlerClass);
2978
2756
  if (artifacts) {
2979
2757
  for (const { routeType, routes } of artifacts) {
2980
2758
  const map = this.routeTypes.find(([type]) => type === routeType)?.[1];
@@ -2995,10 +2773,11 @@ var InteractionDispatcher = class {
2995
2773
  attachToClient() {
2996
2774
  this.core.bot.client.on(Events.InteractionCreate, (interaction) => {
2997
2775
  if (this.draining) return;
2998
- this.core.bot.emitSafe("any:interaction", interaction);
2999
- const run = this.handleInteraction(interaction).catch((err) => {
3000
- this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
3001
- this.core.bot.emit("error:unhandled:interaction", err);
2776
+ this.core.bus[PublishDefault]("anyInteraction", { interaction });
2777
+ const run = this.handleInteraction(interaction).catch((caught) => {
2778
+ const error = asError(caught);
2779
+ this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${error.name}`, error.stack);
2780
+ this.core.bus[PublishDefault]("unhandledInteractionError", { error });
3002
2781
  });
3003
2782
  this.inFlight.add(run);
3004
2783
  run.finally(() => this.inFlight.delete(run));
@@ -3010,37 +2789,75 @@ var InteractionDispatcher = class {
3010
2789
  drain(timeoutMs) {
3011
2790
  return settleWithin(Promise.allSettled(this.inFlight), timeoutMs);
3012
2791
  }
3013
- async handleCustomIdInteraction(interaction, getMap, interactionType) {
2792
+ async handleCustomIdInteraction(interaction, kind, getMap) {
3014
2793
  if ([...this.keysToIgnore].some((matcher) => matcher.owns(interaction.customId))) return;
3015
2794
  const prefix = prefixOf(interaction.customId);
3016
- if (!prefix) return this.logger.warn(`${interactionType} has invalid customId: ${interaction.customId}`);
3017
- await this.processInteraction(interaction, () => prefix, (key) => getMap().get(key));
2795
+ if (!prefix) this.logger.warn(`${paint.sky.bold(kind)} has invalid customId: ${paint.mute(interaction.customId)}`);
2796
+ await this.processInteraction(interaction, kind, () => prefix, (key) => getMap().get(key));
3018
2797
  }
3019
- async processInteraction(interaction, extractKey, getHandler, fallback = UnhandledRepliable) {
2798
+ async processInteraction(interaction, kind, extractKey, getHandler, fallback = UnhandledRepliable) {
3020
2799
  const key = extractKey(interaction);
2800
+ const startedAt = performance.now();
2801
+ const queuedMs = queuedMsFor(interaction.id);
2802
+ const matched = getHandler(key);
2803
+ let routeId = `${kind}:${key || "unrouted"}`;
2804
+ let reported = false;
2805
+ const report = (outcome) => {
2806
+ if (reported) return;
2807
+ reported = true;
2808
+ this.core.bus[PublishDefault]("interactionDispatched", dispatchedPayload({
2809
+ routeId,
2810
+ interactionId: interaction.id,
2811
+ kind,
2812
+ outcome,
2813
+ startedAt,
2814
+ fallback: !matched,
2815
+ queuedMs
2816
+ }));
2817
+ };
3021
2818
  let sender;
3022
2819
  try {
3023
- if (!interaction.isAutocomplete()) for (const { ctor } of this.middlewares) await new ctor(interaction, this.core).execute();
3024
- let HandlerCtor = getHandler(key);
3025
- if (!HandlerCtor) {
3026
- this.logger.warn(`No handler found for key ${paint.sky.bold(key)}. Falling back to ${fallback.name}.`);
3027
- HandlerCtor = fallback;
3028
- }
3029
- this.logger.debug(`Processing ${paint.mint.bold(key)} with ${paint.mute(HandlerCtor.name)}`);
3030
- const dispatch = new DispatchContext(routeIdOf(HandlerCtor));
3031
- const handler = new HandlerCtor(interaction, this.core, dispatch);
2820
+ if (!interaction.isAutocomplete()) await this.runMiddlewares(interaction);
2821
+ const HandlerCtor = matched ?? fallback;
2822
+ const dispatch = new DispatchContext(routeIdOf(HandlerCtor) ?? routeId);
2823
+ routeId = dispatch.routeId ?? routeId;
2824
+ const handler = this.buildHandler(HandlerCtor, interaction, dispatch, key, !matched);
3032
2825
  if (handler instanceof RepliableHandler) sender = handler.getSender();
3033
- if (!interaction.isAutocomplete()) {
3034
- const monitor = slowGateMonitor();
3035
- try {
3036
- await runHandlerGates(HandlerCtor, interactionGateContext(interaction, this.core), dispatch.routeId ?? void 0, monitor?.observe);
3037
- } finally {
3038
- monitor?.report(dispatch.routeId);
3039
- }
2826
+ const refusal = interaction.isAutocomplete() ? null : await this.gateRefusal(HandlerCtor, interaction, dispatch.routeId);
2827
+ if (refusal) {
2828
+ await this.answer(refusal.caught, interaction, routeId, sender, report);
2829
+ return;
3040
2830
  }
3041
2831
  await handler.execute();
2832
+ report("handled");
3042
2833
  } catch (caught) {
3043
- await handleInteractionFault(caught, interaction, this.core, sender);
2834
+ await this.answer(caught, interaction, routeId, sender, report);
2835
+ }
2836
+ }
2837
+ async answer(caught, interaction, routeId, sender, report) {
2838
+ try {
2839
+ await handleInteractionFault(caught, interaction, this.core, routeId, sender);
2840
+ } finally {
2841
+ report(outcomeFor(caught));
2842
+ }
2843
+ }
2844
+ buildHandler(HandlerCtor, interaction, dispatch, key, isFallback) {
2845
+ if (isFallback) this.logger.warn(`No handler found for key ${paint.sky.bold(key)}. Falling back to ${HandlerCtor.name}.`);
2846
+ this.logger.debug(`Processing ${paint.mint.bold(key)} with ${paint.mute(HandlerCtor.name)}`);
2847
+ return new HandlerCtor(interaction, this.core, dispatch);
2848
+ }
2849
+ async runMiddlewares(interaction) {
2850
+ for (const { ctor } of this.middlewares) await new ctor(interaction, this.core).execute();
2851
+ }
2852
+ async gateRefusal(HandlerCtor, interaction, routeId) {
2853
+ const monitor = slowGateMonitor();
2854
+ try {
2855
+ await runHandlerGates(HandlerCtor, interactionGateContext(interaction, this.core), routeId ?? void 0, monitor?.observe);
2856
+ return null;
2857
+ } catch (caught) {
2858
+ return { caught };
2859
+ } finally {
2860
+ monitor?.report(routeId);
3044
2861
  }
3045
2862
  }
3046
2863
  async handleInteraction(interaction) {
@@ -3085,47 +2902,41 @@ var InteractionDispatcher = class {
3085
2902
  }
3086
2903
  async handleSlashCommand(interaction) {
3087
2904
  const route = slashRouteOf(interaction);
3088
- await this.processInteraction(interaction, () => route, (key) => this.slashMap.get(key));
2905
+ await this.processInteraction(interaction, InteractionRoutes.Slash, () => route, (key) => this.slashMap.get(key));
3089
2906
  }
3090
2907
  async handleButton(interaction) {
3091
- await this.handleCustomIdInteraction(interaction, () => this.buttonMap, "Button");
2908
+ await this.handleCustomIdInteraction(interaction, InteractionRoutes.Button, () => this.buttonMap);
3092
2909
  }
3093
2910
  async handleModal(interaction) {
3094
- await this.handleCustomIdInteraction(interaction, () => this.modalMap, "Modal");
2911
+ await this.handleCustomIdInteraction(interaction, InteractionRoutes.Modal, () => this.modalMap);
3095
2912
  }
3096
2913
  async handleStringSelectMenu(interaction) {
3097
- await this.handleCustomIdInteraction(interaction, () => this.stringSelectMap, "String select menu");
2914
+ await this.handleCustomIdInteraction(interaction, InteractionRoutes.StringMenu, () => this.stringSelectMap);
3098
2915
  }
3099
2916
  async handleUserSelectMenu(interaction) {
3100
- await this.handleCustomIdInteraction(interaction, () => this.userSelectMap, "User select menu");
2917
+ await this.handleCustomIdInteraction(interaction, InteractionRoutes.UserMenu, () => this.userSelectMap);
3101
2918
  }
3102
2919
  async handleRoleSelectMenu(interaction) {
3103
- await this.handleCustomIdInteraction(interaction, () => this.roleSelectMap, "Role select menu");
2920
+ await this.handleCustomIdInteraction(interaction, InteractionRoutes.RoleMenu, () => this.roleSelectMap);
3104
2921
  }
3105
2922
  async handleChannelSelectMenu(interaction) {
3106
- await this.handleCustomIdInteraction(interaction, () => this.channelSelectMap, "Channel select menu");
2923
+ await this.handleCustomIdInteraction(interaction, InteractionRoutes.ChannelMenu, () => this.channelSelectMap);
3107
2924
  }
3108
2925
  async handleMentionableSelectMenu(interaction) {
3109
- await this.handleCustomIdInteraction(interaction, () => this.mentionableSelectMap, "Mentionable select menu");
2926
+ await this.handleCustomIdInteraction(interaction, InteractionRoutes.MentionableMenu, () => this.mentionableSelectMap);
3110
2927
  }
3111
2928
  async handleMessageContextMenu(interaction) {
3112
- await this.processInteraction(interaction, () => interaction.commandName, (key) => this.messageContextMenuMap.get(key));
2929
+ await this.processInteraction(interaction, InteractionRoutes.MessageContextMenu, () => interaction.commandName, (key) => this.messageContextMenuMap.get(key));
3113
2930
  }
3114
2931
  async handleUserContextMenu(interaction) {
3115
- await this.processInteraction(interaction, () => interaction.commandName, (key) => this.userContextMenuMap.get(key));
2932
+ await this.processInteraction(interaction, InteractionRoutes.UserContextMenu, () => interaction.commandName, (key) => this.userContextMenuMap.get(key));
3116
2933
  }
3117
2934
  async handleAutocomplete(interaction) {
3118
2935
  const route = slashRouteOf(interaction);
3119
- await this.processInteraction(interaction, () => route, (key) => this.autocompleteMap.get(key), UnhandledAutocomplete);
2936
+ await this.processInteraction(interaction, InteractionRoutes.Autocomplete, () => route, (key) => this.autocompleteMap.get(key), UnhandledAutocomplete);
3120
2937
  }
3121
2938
  };
3122
2939
 
3123
- //#endregion
3124
- //#region \0@oxc-project+runtime@0.135.0/helpers/esm/decorateMetadata.js
3125
- function __decorateMetadata(k, v) {
3126
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
3127
- }
3128
-
3129
2940
  //#endregion
3130
2941
  //#region \0@oxc-project+runtime@0.135.0/helpers/esm/decorate.js
3131
2942
  function __decorate(decorators, target, key, desc) {
@@ -3144,9 +2955,9 @@ const LOGOUT_TIMEOUT_MS = 2e3;
3144
2955
  /**
3145
2956
  * The Discord client and its controllers. Access it through `core.bot`.
3146
2957
  */
3147
- var Bot = class extends TypedEventEmitter {
2958
+ var Bot = class {
3148
2959
  /** @internal */
3149
- logger = new Logger("Bot");
2960
+ logger = new Logger("Bot", { channel: "bot" });
3150
2961
  isInitialized = false;
3151
2962
  _client;
3152
2963
  interactions;
@@ -3163,7 +2974,6 @@ var Bot = class extends TypedEventEmitter {
3163
2974
  }
3164
2975
  /** @internal */
3165
2976
  constructor(core) {
3166
- super();
3167
2977
  this._client = new Client(core.config.bot.clientOptions);
3168
2978
  if (core.config.bot.interactions.path) this.interactions = new InteractionDispatcher(core);
3169
2979
  if (core.config.bot.events.path) this.events = new EventDispatcher(core);
@@ -3213,9 +3023,10 @@ var Bot = class extends TypedEventEmitter {
3213
3023
  await this.logout();
3214
3024
  }
3215
3025
  async login(token) {
3216
- this._client.once(Events.ClientReady, () => this.emit("ready"));
3026
+ const ready = Promise.withResolvers();
3027
+ this._client.once(Events.ClientReady, () => ready.resolve());
3217
3028
  this._client.login(token);
3218
- await this.waitFor("ready");
3029
+ await ready.promise;
3219
3030
  this.logger.info(`Logged in as ${chalk.bold.magenta(this._client.user?.username)}!`);
3220
3031
  return this;
3221
3032
  }
@@ -3226,244 +3037,13 @@ var Bot = class extends TypedEventEmitter {
3226
3037
  get client() {
3227
3038
  return this._client;
3228
3039
  }
3229
- /**
3230
- * The overloads enforce the key/args correlation at compile time only. At runtime this forwards
3231
- * straight to the underlying TypedEventEmitter.
3232
- *
3233
- * @internal
3234
- */
3235
- emit(event, ...args) {
3236
- return super.emit(event, ...args);
3237
- }
3238
- emitSafe(event, ...args) {
3239
- return super.emitSafe(event, ...args);
3240
- }
3241
- onListenerError(error, event) {
3242
- this.logger.error(`listener for ${String(event)} threw`, error);
3243
- }
3244
- };
3245
- __decorate([Envapt("DISCORD_BOT_TOKEN", { converter: (raw) => validateDiscordToken(raw) }), __decorateMetadata("design:type", String)], Bot.prototype, "botToken", void 0);
3246
-
3247
- //#endregion
3248
- //#region src/subscribers/default/HandledException.ts
3249
- let HandledException = class HandledException extends WebhookLog {
3250
- report() {
3251
- return {
3252
- username: "Handled Exception",
3253
- components: [new HandledExceptionContainer(this.data).component],
3254
- files: [jsonAttachment("source.json", "The raw source the fault came from", this.data.source.raw)]
3255
- };
3256
- }
3257
- };
3258
- HandledException = __decorate([Subscribe("handledException"), WebhookUrl("HANDLED_EXCEPTION_WEBHOOK_URL")], HandledException);
3259
- var HandledExceptionContainer = class extends BuilderComponent {
3260
- constructor(data) {
3261
- super("container");
3262
- const { denial, uuid, source } = data;
3263
- 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"));
3264
- }
3265
- };
3266
- function faultSummary(denial, source) {
3267
- const head = stripAnsi(`### A handled fault was reported: \`${denial.name}\`\n**Message:** ${denial.message}\n`);
3268
- 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"}\``;
3269
- 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}\``;
3270
- }
3271
- function causeStack(denial) {
3272
- const { cause } = denial;
3273
- if (Error.isError(cause)) return stripAnsi(cause.stack ?? cause.message);
3274
- if (typeof cause === "string") return stripAnsi(cause);
3275
- if (typeof cause === "bigint" || typeof cause === "symbol" || typeof cause === "function") return cause.toString();
3276
- if (cause !== void 0) try {
3277
- return JSON.stringify(cause);
3278
- } catch {
3279
- return "[unserializable cause]";
3280
- }
3281
- return stripAnsi(denial.stack ?? "No cause recorded.");
3282
- }
3283
-
3284
- //#endregion
3285
- //#region src/subscribers/default/UnknownException.ts
3286
- let UnknownException = class UnknownException extends WebhookLog {
3287
- report() {
3288
- const { metadata } = this.data;
3289
- return {
3290
- username: "Unknown Exception",
3291
- components: [new UnhandledErrorContainer(this.data).component],
3292
- files: metadata ? [jsonAttachment("metadata.json", "Metadata associated with the error", metadata)] : void 0
3293
- };
3294
- }
3295
- };
3296
- UnknownException = __decorate([Subscribe("unknownException"), WebhookUrl("UNKNOWN_EXCEPTION_WEBHOOK_URL")], UnknownException);
3297
- var UnhandledErrorContainer = class extends BuilderComponent {
3298
- constructor(data) {
3299
- super("container");
3300
- const { uuid, error, guild, user, metadata } = data;
3301
- 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)}\`\`\``));
3302
- this.addTimestampsIfAvailable(error);
3303
- this.addMetadataIfAvailable(metadata);
3304
- }
3305
- addTimestampsIfAvailable(error) {
3306
- if (!(error instanceof DiscordAPIError$1)) return;
3307
- const now = Date.now();
3308
- const snowflake = error.url.match(/\/interactions\/(\d+)\//)?.[1];
3309
- if (!snowflake) return void 0;
3310
- const interactionTs = timestampFromSnowflake(snowflake);
3311
- const diff = now - interactionTs;
3312
- const seconds = Math.floor(diff / 1e3);
3313
- const millis = diff % 1e3;
3314
- 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`));
3315
- }
3316
- addMetadataIfAvailable(metadata) {
3317
- if (!metadata) return;
3318
- this.instance.addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent("### Metadata")).addFileComponents((file) => file.setURL("attachment://metadata.json"));
3319
- }
3320
- };
3321
-
3322
- //#endregion
3323
- //#region src/subscribers/Bus.ts
3324
- /**
3325
- * Registers and dispatches subscribers. Subscribers load from the configured directories and run
3326
- * on a programmatic or framework-event publish. Accessed via `core.bus`. Do not construct it directly.
3327
- */
3328
- var Bus = class extends TypedEventEmitter {
3329
- core;
3330
- logger = new Logger("Subscribers");
3331
- isInitialized = false;
3332
- subscribersMap = /* @__PURE__ */ new Map();
3333
- executedOnceHandlers = /* @__PURE__ */ new Set();
3334
- webhookProbes = /* @__PURE__ */ new Map();
3335
- hmrHandler;
3336
- constructor(core) {
3337
- super();
3338
- this.core = core;
3339
- if (this.core.config.subscribers.path) {
3340
- if (!Envapter.isDevelopment && !Envapter.isTest) return;
3341
- this.hmrHandler = new HmrModuleHandler({
3342
- handlersDir: this.core.config.subscribers.path,
3343
- isHandler: this.isSubscriber.bind(this),
3344
- registerHandler: this.registerSubscriber.bind(this),
3345
- unregisterHandler: this.unregisterSubscriber.bind(this),
3346
- getArtifacts: this.getArtifacts.bind(this),
3347
- logger: this.logger
3348
- });
3349
- }
3350
- }
3351
- getArtifacts(ctor) {
3352
- return [Reflect.getMetadata(SubscribeMetadataKey, ctor).subscriber];
3353
- }
3354
- /** @internal */
3355
- async init() {
3356
- if (this.isInitialized) return;
3357
- this.isInitialized = true;
3358
- this.registerSubscriber(UnknownException);
3359
- this.registerSubscriber(HandledException);
3360
- const subscribersDir = this.core.config.subscribers.path;
3361
- if (subscribersDir) {
3362
- this.logger.info(chalk.bold(subscribersDir));
3363
- await this.loadSubscribers(subscribersDir);
3364
- const totalSubscribers = [...this.subscribersMap.values()].reduce((acc, handlers) => acc + handlers.length, 0);
3365
- this.logger.utils.list([`${chalk.bold.magenta(totalSubscribers)} subscribers`], chalk.bold.green("Loaded"));
3366
- }
3367
- if (!Envapter.isTest) await this.verifyWebhooks();
3368
- }
3369
- /** @internal Exposed for tests. */
3370
- async verifyWebhooks() {
3371
- const missing = [];
3372
- await Promise.all([...this.webhookProbes].map(async ([url, envKeys]) => {
3373
- const result = await WebhookLog.senderFor(url).verify();
3374
- if (result === "missing") missing.push(...envKeys);
3375
- else if (result === "unreachable") this.logger.warn(`could not verify webhook at ${paint.sky.bold(envKeys.join(", "))}`);
3376
- }));
3377
- if (missing.length > 0) throw new SeedcordError(SeedcordErrorCode.ConfigWebhookNotFound, [missing.join(", ")]);
3378
- }
3379
- /** @internal */
3380
- async onHmr(event) {
3381
- if (this.hmrHandler) await this.hmrHandler.handle(event);
3382
- }
3383
- async loadSubscribers(dir) {
3384
- await traverseDirectory(dir, (fullPath, relativePath, imported) => {
3385
- for (const exportName of Object.keys(imported)) {
3386
- const val = imported[exportName];
3387
- if (this.isSubscriber(val)) {
3388
- this.registerSubscriber(val);
3389
- this.hmrHandler?.trackHandler(fullPath, val);
3390
- this.logger.utils.registration(val.name, relativePath);
3391
- }
3392
- }
3393
- }, this.logger);
3394
- }
3395
- registerSubscriber(handler) {
3396
- if (handler.prototype instanceof WebhookLog) {
3397
- const envKey = WebhookLog.envKeyOf(handler);
3398
- const url = WebhookLog.urlOf(envKey);
3399
- if (url === null) {
3400
- this.logger.warn(`${chalk.bold(handler.name)} disabled, ${paint.sky.bold(envKey)} is not set`);
3401
- return;
3402
- }
3403
- const envKeys = this.webhookProbes.get(url) ?? [];
3404
- if (!envKeys.includes(envKey)) envKeys.push(envKey);
3405
- this.webhookProbes.set(url, envKeys);
3406
- }
3407
- const options = Reflect.getMetadata(SubscribeMetadataKey, handler);
3408
- let handlers = this.subscribersMap.get(options.subscriber);
3409
- if (!handlers) {
3410
- handlers = [];
3411
- this.subscribersMap.set(options.subscriber, handlers);
3412
- }
3413
- handlers.push({
3414
- ctor: handler,
3415
- frequency: options.frequency ?? "on"
3416
- });
3417
- }
3418
- unregisterSubscriber(handler, artifacts) {
3419
- const subscribers = artifacts ?? [...this.subscribersMap.keys()];
3420
- for (const subscriber of subscribers) {
3421
- const handlers = this.subscribersMap.get(subscriber);
3422
- if (!handlers) continue;
3423
- const index = handlers.findIndex((entry) => entry.ctor === handler);
3424
- if (index !== -1) handlers.splice(index, 1);
3425
- }
3426
- }
3427
- isSubscriber(obj) {
3428
- if (typeof obj !== "function") return false;
3429
- return obj.prototype instanceof Subscriber && Reflect.hasMetadata(SubscribeMetadataKey, obj);
3430
- }
3431
- /**
3432
- * Publishes an event to its subscribers and native listeners.
3433
- *
3434
- * Fire-and-forget, so subscriber handlers run asynchronously and this returns before they
3435
- * complete, and callers must not assume side effects have landed. Errors thrown by a subscriber
3436
- * are caught and logged, never surfaced here. A `'once'` subscriber is marked as executed when
3437
- * it starts (even if it throws), so it never runs twice.
3438
- *
3439
- * @param event - The subscription key to publish
3440
- * @param data - Payload passed to each subscriber
3441
- * @returns Whether the underlying emitter had native listeners for the event
3442
- */
3443
- publish(event, data) {
3444
- this.processSubscriber(event, data);
3445
- return super.emit(event, ...[data]);
3446
- }
3447
- async processSubscriber(subscriberName, data) {
3448
- const handlers = this.subscribersMap.get(subscriberName);
3449
- if (!handlers) return;
3450
- for (const entry of handlers) {
3451
- if (entry.frequency === "once" && this.executedOnceHandlers.has(entry.ctor)) continue;
3452
- try {
3453
- if (entry.frequency === "once") this.executedOnceHandlers.add(entry.ctor);
3454
- const Ctor = entry.ctor;
3455
- await new Ctor(data, this.core).execute();
3456
- } catch (err) {
3457
- this.logger.error(`Error in subscriber ${String(subscriberName)} handler ${entry.ctor.name}:`, err);
3458
- }
3459
- }
3460
- }
3461
3040
  };
3041
+ __decorate([Envapt("DISCORD_BOT_TOKEN", { converter: (raw) => validateDiscordToken(raw) })], Bot.prototype, "botToken", void 0);
3462
3042
 
3463
3043
  //#endregion
3464
3044
  //#region src/version.ts
3465
3045
  /** Package version */
3466
- const version = "0.1.0-next.4";
3046
+ const version = "0.1.0-next.5";
3467
3047
 
3468
3048
  //#endregion
3469
3049
  //#region src/Seedcord.ts
@@ -3481,12 +3061,9 @@ var Seedcord = class Seedcord extends Pluggable {
3481
3061
  version = version;
3482
3062
  healthCheck;
3483
3063
  hmrManager;
3484
- /** @see {@link CoordinatedShutdown} */
3485
- shutdown;
3486
- /** @see {@link CoordinatedStartup} */
3487
- startup;
3488
3064
  /** @see {@link Bus} */
3489
3065
  bus;
3066
+ subscribers;
3490
3067
  /** @see {@link Bot} */
3491
3068
  bot;
3492
3069
  /** @see {@link IRateLimiter} */
@@ -3496,17 +3073,14 @@ var Seedcord = class Seedcord extends Pluggable {
3496
3073
  * @throws A **SeedcordError** When attempting to create multiple instances (singleton)
3497
3074
  */
3498
3075
  constructor(config) {
3499
- const shutdown = new CoordinatedShutdown();
3500
- const startup = new CoordinatedStartup();
3501
- super(shutdown, startup);
3076
+ super(new CoordinatedShutdown(), new CoordinatedStartup());
3502
3077
  this.config = config;
3503
3078
  installNodeDefaults(config.logger);
3504
3079
  setBotColor(config.botColor);
3505
- this.shutdown = shutdown;
3506
- this.startup = startup;
3507
3080
  this.hmrManager = new HmrManager();
3508
3081
  this.hmrManager.init();
3509
3082
  this.bus = new Bus(this);
3083
+ this.subscribers = new SubscriberLoader(this.bus, config.subscribers.path);
3510
3084
  this.bot = new Bot(this);
3511
3085
  this.rateLimiter = config.store ?? new MemoryRateLimiter();
3512
3086
  this.healthCheck = HealthCheck.fromOption(this.shutdown, config.healthCheck);
@@ -3529,18 +3103,18 @@ var Seedcord = class Seedcord extends Pluggable {
3529
3103
  }
3530
3104
  registerStartupTasks() {
3531
3105
  if (Envapter.isDevelopment || Envapter.isTest) this.registerHmrAwareModules();
3532
- this.startup.addTask(StartupPhase.Configuration, "Bus Initialization", async () => {
3106
+ this.startup.addTask(StartupPhase.Configuration, "bus-initialization", async () => {
3533
3107
  this.bus.logger.utils.initialization("Subscribers", "start");
3534
- await this.bus.init();
3108
+ await this.subscribers.init();
3535
3109
  this.bus.logger.utils.initialization("Subscribers", "end");
3536
3110
  });
3537
- this.startup.addTask(StartupPhase.Login, "Bot Initialization", async () => {
3111
+ this.startup.addTask(StartupPhase.Login, "bot-initialization", async () => {
3538
3112
  this.bot.logger.utils.initialization("Bot", "start");
3539
3113
  await this.bot.init();
3540
3114
  this.bot.logger.utils.initialization("Bot", "end");
3541
3115
  });
3542
3116
  const { healthCheck } = this;
3543
- if (healthCheck) this.startup.addTask(StartupPhase.Ready, "Health Check", async () => {
3117
+ if (healthCheck) this.startup.addTask(StartupPhase.Ready, "health-check", async () => {
3544
3118
  healthCheck.logger.utils.initialization("HealthCheck", "start");
3545
3119
  await healthCheck.init();
3546
3120
  healthCheck.logger.utils.initialization("HealthCheck", "end");
@@ -3562,9 +3136,9 @@ var Seedcord = class Seedcord extends Pluggable {
3562
3136
  return this;
3563
3137
  }
3564
3138
  registerHmrAwareModules() {
3565
- this.startup.addTask(StartupPhase.Configuration, "HMR Registration", async () => {
3139
+ this.startup.addTask(StartupPhase.Configuration, "hmr-registration", async () => {
3566
3140
  this.hmrManager.register(this.bot);
3567
- this.hmrManager.register(this.bus);
3141
+ this.hmrManager.register(this.subscribers);
3568
3142
  for (const plugin of this.plugins) this.hmrManager.register(plugin);
3569
3143
  await Promise.resolve();
3570
3144
  });
@@ -3572,5 +3146,5 @@ var Seedcord = class Seedcord extends Pluggable {
3572
3146
  };
3573
3147
 
3574
3148
  //#endregion
3575
- export { ArraySource, AutocompleteHandler, ButtonHandler, CommandMentions, ContextMenuHandler, CursorSource, Emojis, EventHandler, EventMiddleware, Gated, IgnoreBots, InteractionHandler, InteractionMiddleware, Middleware, MiddlewareType, ModalHandler, Nsfw, Paginator, RegisterEvent, RepliableHandler, Seedcord, SelectMenuHandler, SlashHandler, Subscribe, Subscriber, WebhookLog, WebhookUrl, checkBotPermissions, checkPermissions, fetchGuildMember, fetchManyGuildMembers, fetchManyUsers, fetchRole, fetchText, fetchUser, getBotRole, getConfirmation, hasPermsToAssign, updateMemberRoles, version };
3149
+ export { ArraySource, AutocompleteHandler, ButtonHandler, CommandMentions, ContextMenuHandler, CursorSource, Emojis, EventHandler, EventMiddleware, Gated, IgnoreBots, InteractionHandler, InteractionMiddleware, Middleware, MiddlewareType, ModalHandler, Nsfw, Paginator, Plugin, RegisterEvent, RepliableHandler, Seedcord, SelectMenuHandler, SlashHandler, Subscriber, WebhookLog, checkBotPermissions, checkPermissions, fetchGuildMember, fetchManyGuildMembers, fetchManyUsers, fetchRole, fetchText, fetchUser, getBotRole, getConfirmation, hasPermsToAssign, updateMemberRoles, version };
3576
3150
  //# sourceMappingURL=index.mjs.map