@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.cjs CHANGED
@@ -38,19 +38,17 @@ let _seedcord_utils_internal = require("@seedcord/utils/internal");
38
38
  let discord_js = require("discord.js");
39
39
  let node_crypto = require("node:crypto");
40
40
  node_crypto = __toESM(node_crypto, 1);
41
- let chalk = require("chalk");
42
- chalk = __toESM(chalk, 1);
43
- let envapt = require("envapt");
44
- let discord_api_types_v10 = require("discord-api-types/v10");
45
- let _seedcord_utils = require("@seedcord/utils");
46
- let _discordjs_rest = require("@discordjs/rest");
41
+ let _seedcord_core_plugin = require("@seedcord/core/plugin");
47
42
  let _seedcord_core_node_internal = require("@seedcord/core/node/internal");
48
43
  let _seedcord_rate_limiter = require("@seedcord/rate-limiter");
49
44
  let _seedcord_types_internal = require("@seedcord/types/internal");
50
- let _seedcord_event_emitter = require("@seedcord/event-emitter");
45
+ let envapt = require("envapt");
46
+ let chalk = require("chalk");
47
+ chalk = __toESM(chalk, 1);
51
48
  let envapt_legacy = require("envapt/legacy");
52
49
  let node_path = require("node:path");
53
50
  let _seedcord_core_hmr = require("@seedcord/core/hmr");
51
+ let _seedcord_utils = require("@seedcord/utils");
54
52
  let _seedcord_utils_node = require("@seedcord/utils/node");
55
53
 
56
54
  //#region src/logger-bootstrap.ts
@@ -165,7 +163,7 @@ let MiddlewareType = /* @__PURE__ */ function(MiddlewareType) {
165
163
  * Interaction middleware cannot specify event filters.
166
164
  *
167
165
  * @param type - Middleware kind from {@link MiddlewareType}
168
- * @param priority - Ordering value where lower runs earlier. {@default `0`}
166
+ * @param priority - Ordering value where lower runs earlier. {@default `0` }
169
167
  * @param options - Additional registration options
170
168
  *
171
169
  * @decorator
@@ -353,7 +351,7 @@ const Emojis = emojiStorage;
353
351
  */
354
352
  var EmojiInjector = class {
355
353
  core;
356
- logger = new _seedcord_logger.Logger("Emojis");
354
+ logger = new _seedcord_logger.Logger("Emojis", { channel: "bot" });
357
355
  constructor(core) {
358
356
  this.core = core;
359
357
  }
@@ -428,7 +426,7 @@ const MULTI_GUILD = Symbol("seedcord:commandmentions:multi-guild");
428
426
  */
429
427
  var CommandMentionInjector = class {
430
428
  core;
431
- logger = new _seedcord_logger.Logger("CommandMentions");
429
+ logger = new _seedcord_logger.Logger("CommandMentions", { channel: "bot" });
432
430
  constructor(core) {
433
431
  this.core = core;
434
432
  }
@@ -586,7 +584,7 @@ function checkBotPermissions(target, scope, inverse = false, errors) {
586
584
  }
587
585
  const me = target.guild.members.me;
588
586
  if (!me) {
589
- new _seedcord_logger.Logger("checkBotPermissions").warn(`Bot member is unavailable in guild ${target.guild.id} while checking permissions in channel ${target.id}`);
587
+ new _seedcord_logger.Logger("checkBotPermissions", { channel: "errors" }).warn(`Bot member is unavailable in guild ${target.guild.id} while checking permissions in channel ${target.id}`);
590
588
  const names = scope.map((bit) => _seedcord_core_internal.PermissionNames.get(bit) ?? String(bit));
591
589
  throw new ((errors?.missingNotice) ?? _seedcord_core_internal.MissingPermissions)(void 0, "The bot", names);
592
590
  }
@@ -785,8 +783,11 @@ function seedState(interaction) {
785
783
  */
786
784
  var ReplySender = class extends _seedcord_core_internal.BaseReplySender {
787
785
  interaction;
788
- constructor(interaction, routeId) {
789
- super(routeId, seedState(interaction));
786
+ constructor(interaction, routeId, bus) {
787
+ super(routeId, seedState(interaction), bus && {
788
+ bus,
789
+ interactionId: interaction.id
790
+ });
790
791
  this.interaction = interaction;
791
792
  }
792
793
  async writeReply(response, opts) {
@@ -893,43 +894,9 @@ function slashRouteOf(interaction) {
893
894
  return (0, _seedcord_utils_internal.buildSlashRoute)(interaction.commandName, subcommand, group);
894
895
  }
895
896
 
896
- //#endregion
897
- //#region src/miscellaneous/FaultThrottle.ts
898
- /**
899
- * Drops duplicate fault reports inside a fixed window so a recurring fault (a database outage on a
900
- * hot path) reports once per window instead of flooding the webhook. Keyed on a structural string, so
901
- * the map stays small, and a stale entry is evicted on the next read of its key.
902
- *
903
- * @internal
904
- */
905
- var FaultThrottle = class {
906
- windowMs;
907
- now;
908
- lastReportedAt = /* @__PURE__ */ new Map();
909
- constructor(windowMs = 6e4, now = () => Date.now()) {
910
- this.windowMs = windowMs;
911
- this.now = now;
912
- }
913
- /** Evicts the entry for this key if it has aged out, so the map never holds stale keys forever. */
914
- shouldReport(key) {
915
- const last = this.lastReportedAt.get(key);
916
- if (last === void 0) return true;
917
- if (this.now() - last < this.windowMs) return false;
918
- this.lastReportedAt.delete(key);
919
- return true;
920
- }
921
- markReported(key) {
922
- this.lastReportedAt.set(key, this.now());
923
- }
924
- clear() {
925
- this.lastReportedAt.clear();
926
- }
927
- };
928
-
929
897
  //#endregion
930
898
  //#region src/miscellaneous/extractErrorResponse.ts
931
- const logger$2 = new _seedcord_logger.Logger("ErrorsHandling");
932
- const faultThrottle = new FaultThrottle();
899
+ const logger$2 = new _seedcord_logger.Logger("ErrorsHandling", { channel: "errors" });
933
900
  /**
934
901
  * Processes an error into a user-facing reply and routes its report.
935
902
  *
@@ -959,29 +926,30 @@ function extractErrorResponse(error, core, origin) {
959
926
  response: override ? new override(uuid).render(ctx) : new _seedcord_core.Fault().render(ctx)
960
927
  };
961
928
  }
962
- function withThrottle(origin, error, publish) {
929
+ function withThrottle(core, origin, error, publish) {
930
+ const throttle = _seedcord_core_internal.FaultThrottle.for(core);
963
931
  const key = faultKey(origin, error);
964
- if (!faultThrottle.shouldReport(key)) {
965
- logger$2.info(`throttled duplicate fault ${key}`);
932
+ if (!throttle.shouldReport(key)) {
933
+ logger$2.debug(`throttled duplicate fault ${key}`);
966
934
  return;
967
935
  }
968
936
  publish();
969
- faultThrottle.markReported(key);
937
+ throttle.markReported(key);
970
938
  }
971
939
  function reportFault(denial, core, origin, uuid) {
972
- withThrottle(origin, denial, () => {
973
- logger$2.error(`${denial.name}: ${uuid}`, denial);
974
- if (origin.interaction) core.bus.publish("handledException", {
940
+ logger$2.error(`${denial.name}: ${uuid}`, denial);
941
+ withThrottle(core, origin, denial, () => {
942
+ if (origin.interaction) core.bus[_seedcord_core_internal.PublishDefault]("handledException", {
975
943
  denial,
976
944
  uuid,
977
945
  source: buildInteractionSource(origin.interaction)
978
946
  });
979
- else if (origin.event) core.bus.publish("handledException", {
947
+ else if (origin.event) core.bus[_seedcord_core_internal.PublishDefault]("handledException", {
980
948
  denial,
981
949
  uuid,
982
950
  source: buildEventSource(origin.event, origin)
983
951
  });
984
- else core.bus.publish("unknownException", {
952
+ else core.bus[_seedcord_core_internal.PublishDefault]("unknownException", {
985
953
  uuid,
986
954
  error: denial,
987
955
  ...scalarActors(origin),
@@ -996,10 +964,10 @@ function causeLine(error) {
996
964
  return `\ncaused by ${cause.name}: ${first ?? cause.message}`;
997
965
  }
998
966
  function reportRawFault(error, core, origin, uuid) {
999
- withThrottle(origin, error, () => {
1000
- if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
1001
- else logger$2.error(`${uuid} | ${error.message}${causeLine(error)}`);
1002
- core.bus.publish("unknownException", {
967
+ if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
968
+ else logger$2.error(`${uuid} | ${error.message}${causeLine(error)}`);
969
+ withThrottle(core, origin, error, () => {
970
+ core.bus[_seedcord_core_internal.PublishDefault]("unknownException", {
1003
971
  uuid,
1004
972
  error,
1005
973
  ...scalarActors(origin),
@@ -1167,7 +1135,7 @@ var BaseHandler = class extends _seedcord_core.BaseHandler {
1167
1135
  */
1168
1136
  var InteractionMiddleware = class extends BaseHandler {
1169
1137
  constructor(event, core) {
1170
- super(event, core);
1138
+ super(event, core, void 0, "interactions");
1171
1139
  }
1172
1140
  };
1173
1141
 
@@ -1187,8 +1155,8 @@ var RepliableHandler = class extends _seedcord_core.RepliableHandler {
1187
1155
  getEvent() {
1188
1156
  return this.event;
1189
1157
  }
1190
- buildSender(event, _core, routeId) {
1191
- return new ReplySender(event, routeId);
1158
+ buildSender(event, core, routeId) {
1159
+ return new ReplySender(event, routeId, core.bus);
1192
1160
  }
1193
1161
  };
1194
1162
 
@@ -1343,7 +1311,7 @@ var ContextMenuHandler = class extends InteractionHandler {
1343
1311
  */
1344
1312
  var AutocompleteHandler = class extends BaseHandler {
1345
1313
  constructor(event, core, dispatch) {
1346
- super(event, core, dispatch);
1314
+ super(event, core, dispatch, "interactions");
1347
1315
  }
1348
1316
  decodedFocused;
1349
1317
  /**
@@ -1394,7 +1362,11 @@ var AutocompleteHandler = class extends BaseHandler {
1394
1362
  }
1395
1363
  /** Send autocomplete suggestions, callback type 8. Prefer {@link match}, which restricts each field's choices to its declared type. */
1396
1364
  async respond(choices) {
1397
- await this.event.respond(choices);
1365
+ const routeId = this.dispatch?.routeId ?? "autocomplete";
1366
+ await (0, _seedcord_core_internal.reportedWrite)({
1367
+ bus: this.core.bus,
1368
+ interactionId: this.event.id
1369
+ }, routeId, "respond", () => this.event.respond(choices));
1398
1370
  }
1399
1371
  /** The firing command route, for a field whose completion differs per registered command. */
1400
1372
  get route() {
@@ -1587,7 +1559,7 @@ var SelectMenuHandler = class extends ComponentHandler {};
1587
1559
  var EventHandler = class extends BaseHandler {
1588
1560
  firedEvent;
1589
1561
  constructor(event, core, eventName) {
1590
- super(event, core);
1562
+ super(event, core, void 0, "events");
1591
1563
  this.firedEvent = eventName;
1592
1564
  }
1593
1565
  /**
@@ -1640,7 +1612,7 @@ var EventHandler = class extends BaseHandler {
1640
1612
  var EventMiddleware = class extends BaseHandler {
1641
1613
  firedEvent;
1642
1614
  constructor(event, core, eventName) {
1643
- super(event, core);
1615
+ super(event, core, void 0, "events");
1644
1616
  this.firedEvent = eventName;
1645
1617
  }
1646
1618
  /**
@@ -1825,10 +1797,15 @@ var Paginator = class {
1825
1797
  }
1826
1798
  };
1827
1799
  }
1828
- /** Render page 0 and send it, picking reply or followUp from the interaction's state. */
1829
- async start(interaction, core) {
1830
- const response = await this.page(contextOf(interaction, core), 0);
1831
- return new ReplySender(interaction, interactionRoute(interaction)).send(response, { ephemeral: this.config.ephemeral ?? false });
1800
+ /**
1801
+ * Render page 0 and send it through the handler's sender, picking reply or followUp from its ack state.
1802
+ *
1803
+ * @param handler - The handler starting the paginator, normally `this`.
1804
+ */
1805
+ async start(handler) {
1806
+ const interaction = handler.getEvent();
1807
+ const response = await this.page(contextOf(interaction, handler.core), 0);
1808
+ return handler.getSender().send(response, { ephemeral: this.config.ephemeral ?? false });
1832
1809
  }
1833
1810
  /** Render a page as a {@link ReplyResponse}. To post it elsewhere, add `flags: MessageFlags.IsComponentsV2`. */
1834
1811
  async page(ctx, n) {
@@ -1888,250 +1865,32 @@ var CursorSource = class {
1888
1865
  };
1889
1866
 
1890
1867
  //#endregion
1891
- //#region src/subscribers/Subscriber.ts
1892
- /**
1893
- * Abstract base class for handling application subscribers.
1894
- *
1895
- * Provides type-safe access to subscribers data and the core framework instance.
1896
- * Extend this class to create custom subscriber handlers.
1897
- *
1898
- * @typeParam KeyOfSubscribers - The specific subscriber type this handler processes
1899
- */
1900
- var Subscriber = class {
1901
- data;
1902
- core;
1903
- logger;
1904
- /**
1905
- * Instantiates the subscriber with event data and core.
1906
- *
1907
- * @param data - The subscriber event data
1908
- * @param core - The core framework instance
1909
- */
1910
- constructor(data, core) {
1911
- this.data = data;
1912
- this.core = core;
1913
- this.logger = new _seedcord_logger.Logger(this.constructor.name);
1914
- }
1915
- };
1916
-
1917
- //#endregion
1918
- //#region src/subscribers/decorators/Subscribe.ts
1919
- /**
1920
- * Registers a subscriber handler class with a specific subscriber event.
1921
- *
1922
- * Associates the decorated class with a subscriber event type for automatic
1923
- * registration and execution when the subscriber is emitted.
1924
- *
1925
- * @typeParam TSubscriber - The subscriber event name to register for
1926
- * @param subscriber - The subscriber event name to register for
1927
- * @param options - Options to configure the subscriber handler registration.
1928
- * @decorator
1929
- * @example
1930
- * ```typescript
1931
- * \@Subscribe('userJoin')
1932
- * class WelcomeHandler extends Subscriber<'userJoin'> {
1933
- * async execute() {
1934
- * // Handle user join event
1935
- * }
1936
- * }
1937
- * ```
1938
- * @example
1939
- * ```ts
1940
- * \@Subscribe('userJoin', { frequency: 'once' })
1941
- * // or
1942
- * \@Subscribe('userJoin')
1943
- * ```
1944
- */
1945
- function Subscribe(subscriber, options) {
1946
- return function(constructor) {
1947
- const meta = {
1948
- subscriber,
1949
- frequency: options?.frequency
1950
- };
1951
- Reflect.defineMetadata(_seedcord_core_internal.SubscribeMetadataKey, meta, constructor);
1952
- };
1953
- }
1954
-
1955
- //#endregion
1956
- //#region src/subscribers/decorators/WebhookUrl.ts
1868
+ //#region src/subscribers/index.ts
1957
1869
  /**
1958
- * Declares the environment variable a {@link WebhookLog} reporter reads its webhook url from.
1870
+ * Base class for a gateway subscriber, binding `this.core` to the gateway {@link Core}.
1959
1871
  *
1960
- * An unset variable disables the reporter with a boot warning. A set but malformed value throws
1961
- * at boot.
1962
- *
1963
- * @param envKey - The environment variable name holding the webhook url
1964
- * @decorator
1965
- * @example
1966
- * ```ts
1967
- * \@Subscribe('memberBanned')
1968
- * \@WebhookUrl('BAN_LOG_WEBHOOK_URL')
1969
- * class BanLog extends WebhookLog<'memberBanned'> {
1970
- * report() {
1971
- * return { components: [new BanCard(this.data).component] };
1972
- * }
1973
- * }
1974
- * ```
1872
+ * @typeParam KeyOfSubscribers - The subscription key this handler receives
1975
1873
  */
1976
- function WebhookUrl(envKey) {
1977
- return function(constructor) {
1978
- Reflect.defineMetadata(_seedcord_core_internal.WebhookUrlMetadataKey, envKey, constructor);
1979
- };
1980
- }
1981
-
1982
- //#endregion
1983
- //#region src/miscellaneous/flagsFor.ts
1984
- /** @internal */
1985
- function flagsFor(ephemeral) {
1986
- let flags = discord_api_types_v10.MessageFlags.IsComponentsV2;
1987
- if (ephemeral) flags |= discord_api_types_v10.MessageFlags.Ephemeral;
1988
- return flags;
1989
- }
1990
-
1991
- //#endregion
1992
- //#region src/subscribers/bases/webhookHelpers.ts
1993
- const DISCORD_WEBHOOK_REGEX = new RegExp(String.raw`^https?:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w$-]+$`);
1994
- function isDiscordWebhookUrl(value) {
1995
- return URL.canParse(value) && DISCORD_WEBHOOK_REGEX.test(value);
1996
- }
1997
- function jsonAttachment(name, description, data) {
1998
- const content = (0, _seedcord_utils.filterCirculars)(data);
1999
- return {
2000
- name,
2001
- description,
2002
- data: Buffer.from(JSON.stringify(content, void 0, 2), "utf8")
2003
- };
2004
- }
2005
- function neutralizeFences(text) {
2006
- return text.replaceAll("```", "`​`​`");
2007
- }
2008
- /** An error's stack with its direct cause appended (one level), ANSI-stripped for the webhook payload. */
2009
- function errorReport(error) {
2010
- let report = error.stack ?? `${error.name}: ${error.message}`;
2011
- if (Error.isError(error.cause)) {
2012
- const cause = error.cause.stack ?? `${error.cause.name}: ${error.cause.message}`;
2013
- report += `\n\nCaused by:\n${cause}`;
2014
- }
2015
- const stripped = neutralizeFences((0, _seedcord_utils.stripAnsi)(report));
2016
- return stripped.length > 1800 ? `${stripped.slice(0, 1799)}…` : stripped;
2017
- }
2018
- var WebhookSeparator = class extends _seedcord_core.BuilderComponent {
2019
- constructor() {
2020
- super("separator");
2021
- this.instance.setSpacing(discord_api_types_v10.SeparatorSpacingSize.Small).setDivider(true);
2022
- }
2023
- };
2024
-
2025
- //#endregion
2026
- //#region src/subscribers/bases/WebhookSender.ts
2027
- const HTTP_NOT_FOUND = 404;
2028
- const HTTP_UNAUTHORIZED = 401;
1874
+ var Subscriber = class extends _seedcord_core.Subscriber {};
2029
1875
  /**
2030
- * Sends component messages to a Discord webhook through `@discordjs/rest`.
1876
+ * Base class for a gateway subscriber that delivers its event to a Discord webhook, binding
1877
+ * `this.core` to the gateway {@link Core}.
2031
1878
  *
2032
- * @throws A `SeedcordError` when the url does not carry a webhook id and token.
1879
+ * @typeParam KeyOfSubscribers - The subscription key this reporter receives
2033
1880
  */
2034
- var WebhookSender = class {
2035
- rest = new _discordjs_rest.REST();
2036
- route;
2037
- constructor(url) {
2038
- const match = /\/webhooks\/(\d+)\/([\w$-]+)$/.exec(url);
2039
- if (!match?.[1] || !match[2]) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigWebhookUrlInvalid, ["The url"]);
2040
- this.route = discord_api_types_v10.Routes.webhook(match[1], match[2]);
2041
- }
2042
- /**
2043
- * Fetches the webhook object, sending no message. Returns `missing` on a 404 or 401 from
2044
- * Discord and `unreachable` on any other failure.
2045
- */
2046
- async verify() {
2047
- try {
2048
- await this.rest.get(this.route, { auth: false });
2049
- return "ok";
2050
- } catch (error) {
2051
- return error instanceof _discordjs_rest.DiscordAPIError && (error.status === HTTP_NOT_FOUND || error.status === HTTP_UNAUTHORIZED) ? "missing" : "unreachable";
2052
- }
2053
- }
2054
- async send(options) {
2055
- const { flags, username, components, files } = options;
2056
- await this.rest.post(this.route, {
2057
- auth: false,
2058
- query: new URLSearchParams({
2059
- with_components: "true",
2060
- wait: "true"
2061
- }),
2062
- body: {
2063
- flags,
2064
- username,
2065
- components: components.map((component) => component.toJSON()),
2066
- ...files?.length && { attachments: files.map((file, id) => ({
2067
- id,
2068
- filename: file.name,
2069
- description: file.description
2070
- })) }
2071
- },
2072
- ...files?.length && { files: files.map((file) => ({
2073
- name: file.name,
2074
- data: file.data
2075
- })) }
2076
- });
2077
- }
2078
- };
1881
+ var WebhookLog = class extends _seedcord_core.WebhookLog {};
2079
1882
 
2080
1883
  //#endregion
2081
- //#region src/subscribers/bases/WebhookLog.ts
1884
+ //#region src/plugin.ts
2082
1885
  /**
2083
- * Base class for subscribers that deliver their event to a Discord webhook.
1886
+ * Base class for a gateway plugin, binding `this.core` to the gateway {@link Core}.
2084
1887
  *
2085
- * Declare the url's environment variable with `@WebhookUrl` and implement {@link report}. Url
2086
- * resolution, validation, sender reuse, and sending run in the base. When the variable is unset
2087
- * the reporter is skipped at registration with a boot warning.
1888
+ * A plugin that runs on either transport extends the base from `@seedcord/core/plugin`, whose
1889
+ * `this.core` carries the shared members only.
2088
1890
  *
2089
- * @typeParam KeyOfSubscribers - The subscription key this reporter receives
1891
+ * @typeParam Opts - The transport, runtime, and capabilities the plugin declares.
2090
1892
  */
2091
- var WebhookLog = class WebhookLog extends Subscriber {
2092
- static senders = /* @__PURE__ */ new Map();
2093
- async execute() {
2094
- const url = WebhookLog.urlOf(WebhookLog.envKeyOf(this.constructor));
2095
- if (url === null) {
2096
- this.logger.warn(`${chalk.default.bold(this.constructor.name)} has no webhook url set, this reporter is disabled`);
2097
- return;
2098
- }
2099
- const { username, components, files } = await this.report();
2100
- try {
2101
- await WebhookLog.senderFor(url).send({
2102
- flags: flagsFor(false),
2103
- username: username ?? this.constructor.name,
2104
- components,
2105
- files
2106
- });
2107
- } catch (error) {
2108
- this.logger.error("Failed to send webhook report", error);
2109
- }
2110
- }
2111
- /** @internal */
2112
- static senderFor(url) {
2113
- let sender = WebhookLog.senders.get(url);
2114
- if (!sender) {
2115
- sender = new WebhookSender(url);
2116
- WebhookLog.senders.set(url, sender);
2117
- }
2118
- return sender;
2119
- }
2120
- /** @internal */
2121
- static envKeyOf(ctor) {
2122
- const envKey = Reflect.getMetadata(_seedcord_core_internal.WebhookUrlMetadataKey, ctor);
2123
- if (!envKey) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.DecoratorWebhookUrlMissing, [ctor.name]);
2124
- return envKey;
2125
- }
2126
- /** @internal Returns null when the env var is unset, throws when set but malformed. */
2127
- static urlOf(envKey) {
2128
- if (!envapt.Envapter.has(envKey)) return null;
2129
- const raw = envapt.Envapter.getRequired(envKey, envapt.Converters.String).trim();
2130
- if (raw === "") return null;
2131
- if (!isDiscordWebhookUrl(raw)) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigWebhookUrlInvalid, [envKey]);
2132
- return raw;
2133
- }
2134
- };
1893
+ var Plugin = class extends _seedcord_core_plugin.Plugin {};
2135
1894
 
2136
1895
  //#endregion
2137
1896
  //#region src/bot/utilities/miscellaneous/contextMenuLeaves.ts
@@ -2187,7 +1946,7 @@ function slashRouteLeaves(commands) {
2187
1946
  var CommandRegistry = class {
2188
1947
  core;
2189
1948
  onDeployed;
2190
- logger = new _seedcord_logger.Logger("Commands");
1949
+ logger = new _seedcord_logger.Logger("Commands", { channel: "commands" });
2191
1950
  isInitialised = false;
2192
1951
  globalCommands = [];
2193
1952
  guildCommands = new discord_js.Collection();
@@ -2274,7 +2033,7 @@ var CommandRegistry = class {
2274
2033
  this.registerCommand(exported, rel);
2275
2034
  this.hmrHandler?.trackHandler(fullPath, exported);
2276
2035
  }
2277
- }, this.logger);
2036
+ });
2278
2037
  }
2279
2038
  isCommandClass(obj) {
2280
2039
  if (typeof obj !== "function") return false;
@@ -2436,7 +2195,7 @@ function eventGateContext(eventName, args, core) {
2436
2195
 
2437
2196
  //#endregion
2438
2197
  //#region src/bot/handleEventFault.ts
2439
- const logger$1 = new _seedcord_logger.Logger("EventBoundary");
2198
+ const logger$1 = new _seedcord_logger.Logger("EventBoundary", { channel: "errors" });
2440
2199
  /**
2441
2200
  * The event controller boundary. Reports a throw from an event handler and never replies, since a
2442
2201
  * generic event has no reply target. A {@link Silence} or a non-reporting {@link Notice} stops quietly, a
@@ -2449,15 +2208,15 @@ function handleEventFault(caught, eventName, handlerName, args, core) {
2449
2208
  if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger$1.debug(`Silence: ${caught.reason}`);
2450
2209
  return;
2451
2210
  }
2452
- if (!Error.isError(caught)) throw caught;
2211
+ const error = (0, _seedcord_core_internal.asError)(caught);
2453
2212
  const ignore = new Set(core.config.errors?.ignoreEventApiCodes ?? []);
2454
- if (caught instanceof discord_js.DiscordAPIError && ignore.has(caught.code)) {
2455
- logger$1.debug(`swallowed api code ${caught.code}`);
2213
+ if (error instanceof discord_js.DiscordAPIError && ignore.has(error.code)) {
2214
+ logger$1.debug(`swallowed api code ${error.code}`);
2456
2215
  return;
2457
2216
  }
2458
- if (caught instanceof _seedcord_core.Notice && !caught.report) return;
2217
+ if (error instanceof _seedcord_core.Notice && !error.report) return;
2459
2218
  const actor = deriveEventActor(args);
2460
- extractErrorResponse(caught, core, {
2219
+ extractErrorResponse(error, core, {
2461
2220
  event: {
2462
2221
  name: eventName,
2463
2222
  handler: handlerName,
@@ -2472,15 +2231,15 @@ function handleEventFault(caught, eventName, handlerName, args, core) {
2472
2231
  //#endregion
2473
2232
  //#region src/bot/controllers/EventDispatcher.ts
2474
2233
  /**
2475
- * Scans event handler directories, registers handlers with Discord client events,
2476
- * and coordinates event execution through the handler system. Does not handle interactions.
2477
- * Multiple handlers can point to one event.
2234
+ * Scans event handler directories, attaches one client listener per registered event, and runs the
2235
+ * handlers for each fire. Interactions route through InteractionDispatcher. Multiple handlers can
2236
+ * point to one event.
2478
2237
  *
2479
2238
  * @internal
2480
2239
  */
2481
2240
  var EventDispatcher = class {
2482
2241
  core;
2483
- logger = new _seedcord_logger.Logger("Events");
2242
+ logger = new _seedcord_logger.Logger("Events", { channel: "events" });
2484
2243
  isInitialized = false;
2485
2244
  eventMap = /* @__PURE__ */ new Map();
2486
2245
  middlewares = [];
@@ -2551,7 +2310,7 @@ var EventDispatcher = class {
2551
2310
  this.registerHandler(val, relativePath);
2552
2311
  this.hmrHandler?.trackHandler(fullPath, val);
2553
2312
  }
2554
- }, this.logger);
2313
+ });
2555
2314
  }
2556
2315
  async loadMiddlewares(dir) {
2557
2316
  await (0, _seedcord_utils_node.traverseDirectory)(dir, (fullPath, relativePath, imported) => {
@@ -2560,7 +2319,7 @@ var EventDispatcher = class {
2560
2319
  this.registerMiddleware(val, relativePath);
2561
2320
  this.hmrHandler?.trackMiddleware(fullPath, val);
2562
2321
  }
2563
- }, this.logger);
2322
+ });
2564
2323
  }
2565
2324
  /** @internal For use in dev mode */
2566
2325
  async onHmr(event) {
@@ -2657,10 +2416,14 @@ var EventDispatcher = class {
2657
2416
  this.logger.debug(`Attaching ${_seedcord_logger.paint.mint.bold(eventName)} to the client with ${_seedcord_logger.paint.mute(handlerEntries?.length ?? 0)} handler(s)`);
2658
2417
  this.core.bot.client.on(eventName, (...args) => {
2659
2418
  if (this.draining) return;
2660
- this.core.bot.emitSafe("any:event", eventName, ...args);
2661
- const run = this.processEvent(eventName, args).catch((err) => {
2662
- this.logger.error(`[${_seedcord_logger.paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
2663
- this.core.bot.emit("error:unhandled:event", err);
2419
+ this.core.bus[_seedcord_core_internal.PublishDefault]("anyEvent", {
2420
+ name: eventName,
2421
+ args
2422
+ });
2423
+ const run = this.processEvent(eventName, args).catch((caught) => {
2424
+ const error = (0, _seedcord_core_internal.asError)(caught);
2425
+ this.logger.error(`[${_seedcord_logger.paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${error.name}`, error.stack);
2426
+ this.core.bus[_seedcord_core_internal.PublishDefault]("unhandledEventError", { error });
2664
2427
  });
2665
2428
  this.inFlight.add(run);
2666
2429
  run.finally(() => this.inFlight.delete(run));
@@ -2736,7 +2499,7 @@ const HARMLESS_API_CODES = new Set([
2736
2499
 
2737
2500
  //#endregion
2738
2501
  //#region src/bot/handleInteractionFault.ts
2739
- const logger = new _seedcord_logger.Logger("InteractionBoundary");
2502
+ const logger = new _seedcord_logger.Logger("InteractionBoundary", { channel: "errors" });
2740
2503
  /**
2741
2504
  * The interaction controller boundary. Sorts a throw from any handler-lifecycle phase into the right
2742
2505
  * user reply plus the right bus event. When a handler is built its sender is passed in, so the boundary
@@ -2745,33 +2508,45 @@ const logger = new _seedcord_logger.Logger("InteractionBoundary");
2745
2508
  *
2746
2509
  * @internal
2747
2510
  */
2748
- async function handleInteractionFault(caught, interaction, core, sender) {
2511
+ async function handleInteractionFault(caught, interaction, core, routeId, sender) {
2749
2512
  if (caught instanceof _seedcord_core.Silence) {
2750
2513
  if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger.debug(`Silence: ${caught.reason}`);
2751
2514
  return;
2752
2515
  }
2753
- if (!Error.isError(caught)) throw caught;
2516
+ const error = (0, _seedcord_core_internal.asError)(caught);
2754
2517
  const ignore = new Set(core.config.errors?.ignoreApiCodes ?? []);
2755
- if (caught instanceof discord_js.DiscordAPIError && ignore.has(caught.code)) {
2756
- logger.debug(`swallowed api code ${caught.code}`);
2518
+ if (error instanceof discord_js.DiscordAPIError && ignore.has(error.code)) {
2519
+ logger.debug(`swallowed api code ${error.code}`);
2757
2520
  return;
2758
2521
  }
2759
2522
  if (interaction.isAutocomplete()) {
2760
- extractErrorResponse(caught, core, {
2523
+ extractErrorResponse(error, core, {
2761
2524
  guild: interaction.guild,
2762
2525
  user: interaction.user,
2763
2526
  metadata: interaction,
2764
2527
  route: slashRouteOf(interaction)
2765
2528
  });
2529
+ await sendEmptyChoices(interaction, core, routeId);
2766
2530
  return;
2767
2531
  }
2768
- const { response } = extractErrorResponse(caught, core, {
2532
+ const { response } = extractErrorResponse(error, core, {
2769
2533
  interaction,
2770
2534
  guild: interaction.guild,
2771
2535
  user: interaction.user,
2772
2536
  metadata: interaction
2773
2537
  });
2774
- await sendGuarded(sender ?? new ReplySender(interaction, interactionRoute(interaction)), response, caught instanceof _seedcord_core.Notice ? caught.ephemeral : true);
2538
+ await sendGuarded(sender ?? new ReplySender(interaction, routeId, core.bus), response, error instanceof _seedcord_core.Notice ? error.ephemeral : true);
2539
+ }
2540
+ async function sendEmptyChoices(interaction, core, routeId) {
2541
+ const telemetry = {
2542
+ bus: core.bus,
2543
+ interactionId: interaction.id
2544
+ };
2545
+ try {
2546
+ await (0, _seedcord_core_internal.reportedWrite)(telemetry, routeId, "respond", () => interaction.respond([]));
2547
+ } catch (error) {
2548
+ logger.debug(`autocomplete empty-choices send failed: ${String(error)}`);
2549
+ }
2775
2550
  }
2776
2551
  async function sendGuarded(sender, response, ephemeral) {
2777
2552
  try {
@@ -2795,7 +2570,7 @@ async function sendGuarded(sender, response, ephemeral) {
2795
2570
  */
2796
2571
  var InteractionDispatcher = class {
2797
2572
  core;
2798
- logger = new _seedcord_logger.Logger("Interactions");
2573
+ logger = new _seedcord_logger.Logger("Interactions", { channel: "interactions" });
2799
2574
  isInitialized = false;
2800
2575
  slashMap = /* @__PURE__ */ new Map();
2801
2576
  buttonMap = /* @__PURE__ */ new Map();
@@ -2808,6 +2583,7 @@ var InteractionDispatcher = class {
2808
2583
  messageContextMenuMap = /* @__PURE__ */ new Map();
2809
2584
  userContextMenuMap = /* @__PURE__ */ new Map();
2810
2585
  autocompleteMap = /* @__PURE__ */ new Map();
2586
+ handlerFiles = /* @__PURE__ */ new Map();
2811
2587
  keysToIgnore = /* @__PURE__ */ new Set();
2812
2588
  middlewares = [];
2813
2589
  inFlight = /* @__PURE__ */ new Set();
@@ -2926,7 +2702,7 @@ var InteractionDispatcher = class {
2926
2702
  this.registerHandler(val, relativePath);
2927
2703
  this.hmrHandler?.trackHandler(fullPath, val);
2928
2704
  }
2929
- }, this.logger);
2705
+ });
2930
2706
  }
2931
2707
  async loadMiddlewares(dir) {
2932
2708
  await (0, _seedcord_utils_node.traverseDirectory)(dir, (fullPath, relativePath, imported) => {
@@ -2935,7 +2711,7 @@ var InteractionDispatcher = class {
2935
2711
  this.registerMiddleware(val, relativePath);
2936
2712
  this.hmrHandler?.trackMiddleware(fullPath, val);
2937
2713
  }
2938
- }, this.logger);
2714
+ });
2939
2715
  }
2940
2716
  registerMiddleware(middlewareCtor, relativePath) {
2941
2717
  const metadata = Reflect.getMetadata(_seedcord_core_internal.MiddlewareMetadataKey, middlewareCtor);
@@ -2965,6 +2741,7 @@ var InteractionDispatcher = class {
2965
2741
  return obj.prototype instanceof InteractionMiddleware && Reflect.hasMetadata(_seedcord_core_internal.MiddlewareMetadataKey, obj);
2966
2742
  }
2967
2743
  registerHandler(handlerClass, relativePath) {
2744
+ const from = (0, _seedcord_utils.formatFilePath)(relativePath);
2968
2745
  const writes = [];
2969
2746
  for (const [routeType, map] of this.routeTypes) {
2970
2747
  const meta = Reflect.getMetadata(_seedcord_core_internal.InteractionRouteKeys[routeType], handlerClass);
@@ -2972,16 +2749,16 @@ var InteractionDispatcher = class {
2972
2749
  for (const route of meta) {
2973
2750
  const existing = map.get(route);
2974
2751
  if (existing && existing !== handlerClass) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.InteractionDuplicateRoute, [
2975
- route,
2976
- existing.name,
2977
- handlerClass.name
2752
+ `${routeType}:${route}`,
2753
+ `${existing.name} (${this.handlerFiles.get(existing) ?? "unknown file"})`,
2754
+ `${handlerClass.name} (${from})`
2978
2755
  ]);
2979
2756
  writes.push([map, route]);
2980
2757
  }
2981
2758
  }
2982
2759
  if (writes.length === 0) return;
2983
2760
  for (const [map, route] of writes) map.set(route, handlerClass);
2984
- const from = (0, _seedcord_utils.formatFilePath)(relativePath);
2761
+ this.handlerFiles.set(handlerClass, from);
2985
2762
  if (this.loading) this.loadedHandlers.push({
2986
2763
  name: handlerClass.name,
2987
2764
  from
@@ -2993,6 +2770,7 @@ var InteractionDispatcher = class {
2993
2770
  await this.hmrHandler?.handle(event);
2994
2771
  }
2995
2772
  unregisterHandler(handlerClass, artifacts) {
2773
+ this.handlerFiles.delete(handlerClass);
2996
2774
  if (artifacts) {
2997
2775
  for (const { routeType, routes } of artifacts) {
2998
2776
  const map = this.routeTypes.find(([type]) => type === routeType)?.[1];
@@ -3013,10 +2791,11 @@ var InteractionDispatcher = class {
3013
2791
  attachToClient() {
3014
2792
  this.core.bot.client.on(discord_js.Events.InteractionCreate, (interaction) => {
3015
2793
  if (this.draining) return;
3016
- this.core.bot.emitSafe("any:interaction", interaction);
3017
- const run = this.handleInteraction(interaction).catch((err) => {
3018
- this.logger.error(`[${_seedcord_logger.paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
3019
- this.core.bot.emit("error:unhandled:interaction", err);
2794
+ this.core.bus[_seedcord_core_internal.PublishDefault]("anyInteraction", { interaction });
2795
+ const run = this.handleInteraction(interaction).catch((caught) => {
2796
+ const error = (0, _seedcord_core_internal.asError)(caught);
2797
+ this.logger.error(`[${_seedcord_logger.paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${error.name}`, error.stack);
2798
+ this.core.bus[_seedcord_core_internal.PublishDefault]("unhandledInteractionError", { error });
3020
2799
  });
3021
2800
  this.inFlight.add(run);
3022
2801
  run.finally(() => this.inFlight.delete(run));
@@ -3028,37 +2807,75 @@ var InteractionDispatcher = class {
3028
2807
  drain(timeoutMs) {
3029
2808
  return (0, _seedcord_core_node_internal.settleWithin)(Promise.allSettled(this.inFlight), timeoutMs);
3030
2809
  }
3031
- async handleCustomIdInteraction(interaction, getMap, interactionType) {
2810
+ async handleCustomIdInteraction(interaction, kind, getMap) {
3032
2811
  if ([...this.keysToIgnore].some((matcher) => matcher.owns(interaction.customId))) return;
3033
2812
  const prefix = (0, _seedcord_core_internal.prefixOf)(interaction.customId);
3034
- if (!prefix) return this.logger.warn(`${interactionType} has invalid customId: ${interaction.customId}`);
3035
- await this.processInteraction(interaction, () => prefix, (key) => getMap().get(key));
2813
+ if (!prefix) this.logger.warn(`${_seedcord_logger.paint.sky.bold(kind)} has invalid customId: ${_seedcord_logger.paint.mute(interaction.customId)}`);
2814
+ await this.processInteraction(interaction, kind, () => prefix, (key) => getMap().get(key));
3036
2815
  }
3037
- async processInteraction(interaction, extractKey, getHandler, fallback = UnhandledRepliable) {
2816
+ async processInteraction(interaction, kind, extractKey, getHandler, fallback = UnhandledRepliable) {
3038
2817
  const key = extractKey(interaction);
2818
+ const startedAt = performance.now();
2819
+ const queuedMs = (0, _seedcord_core_internal.queuedMsFor)(interaction.id);
2820
+ const matched = getHandler(key);
2821
+ let routeId = `${kind}:${key || "unrouted"}`;
2822
+ let reported = false;
2823
+ const report = (outcome) => {
2824
+ if (reported) return;
2825
+ reported = true;
2826
+ this.core.bus[_seedcord_core_internal.PublishDefault]("interactionDispatched", (0, _seedcord_core_internal.dispatchedPayload)({
2827
+ routeId,
2828
+ interactionId: interaction.id,
2829
+ kind,
2830
+ outcome,
2831
+ startedAt,
2832
+ fallback: !matched,
2833
+ queuedMs
2834
+ }));
2835
+ };
3039
2836
  let sender;
3040
2837
  try {
3041
- if (!interaction.isAutocomplete()) for (const { ctor } of this.middlewares) await new ctor(interaction, this.core).execute();
3042
- let HandlerCtor = getHandler(key);
3043
- if (!HandlerCtor) {
3044
- this.logger.warn(`No handler found for key ${_seedcord_logger.paint.sky.bold(key)}. Falling back to ${fallback.name}.`);
3045
- HandlerCtor = fallback;
3046
- }
3047
- this.logger.debug(`Processing ${_seedcord_logger.paint.mint.bold(key)} with ${_seedcord_logger.paint.mute(HandlerCtor.name)}`);
3048
- const dispatch = new _seedcord_core.DispatchContext((0, _seedcord_core_internal.routeIdOf)(HandlerCtor));
3049
- const handler = new HandlerCtor(interaction, this.core, dispatch);
2838
+ if (!interaction.isAutocomplete()) await this.runMiddlewares(interaction);
2839
+ const HandlerCtor = matched ?? fallback;
2840
+ const dispatch = new _seedcord_core.DispatchContext((0, _seedcord_core_internal.routeIdOf)(HandlerCtor) ?? routeId);
2841
+ routeId = dispatch.routeId ?? routeId;
2842
+ const handler = this.buildHandler(HandlerCtor, interaction, dispatch, key, !matched);
3050
2843
  if (handler instanceof RepliableHandler) sender = handler.getSender();
3051
- if (!interaction.isAutocomplete()) {
3052
- const monitor = (0, _seedcord_core_internal.slowGateMonitor)();
3053
- try {
3054
- await (0, _seedcord_core_internal.runHandlerGates)(HandlerCtor, interactionGateContext(interaction, this.core), dispatch.routeId ?? void 0, monitor?.observe);
3055
- } finally {
3056
- monitor?.report(dispatch.routeId);
3057
- }
2844
+ const refusal = interaction.isAutocomplete() ? null : await this.gateRefusal(HandlerCtor, interaction, dispatch.routeId);
2845
+ if (refusal) {
2846
+ await this.answer(refusal.caught, interaction, routeId, sender, report);
2847
+ return;
3058
2848
  }
3059
2849
  await handler.execute();
2850
+ report("handled");
3060
2851
  } catch (caught) {
3061
- await handleInteractionFault(caught, interaction, this.core, sender);
2852
+ await this.answer(caught, interaction, routeId, sender, report);
2853
+ }
2854
+ }
2855
+ async answer(caught, interaction, routeId, sender, report) {
2856
+ try {
2857
+ await handleInteractionFault(caught, interaction, this.core, routeId, sender);
2858
+ } finally {
2859
+ report((0, _seedcord_core_internal.outcomeFor)(caught));
2860
+ }
2861
+ }
2862
+ buildHandler(HandlerCtor, interaction, dispatch, key, isFallback) {
2863
+ if (isFallback) this.logger.warn(`No handler found for key ${_seedcord_logger.paint.sky.bold(key)}. Falling back to ${HandlerCtor.name}.`);
2864
+ this.logger.debug(`Processing ${_seedcord_logger.paint.mint.bold(key)} with ${_seedcord_logger.paint.mute(HandlerCtor.name)}`);
2865
+ return new HandlerCtor(interaction, this.core, dispatch);
2866
+ }
2867
+ async runMiddlewares(interaction) {
2868
+ for (const { ctor } of this.middlewares) await new ctor(interaction, this.core).execute();
2869
+ }
2870
+ async gateRefusal(HandlerCtor, interaction, routeId) {
2871
+ const monitor = (0, _seedcord_core_internal.slowGateMonitor)();
2872
+ try {
2873
+ await (0, _seedcord_core_internal.runHandlerGates)(HandlerCtor, interactionGateContext(interaction, this.core), routeId ?? void 0, monitor?.observe);
2874
+ return null;
2875
+ } catch (caught) {
2876
+ return { caught };
2877
+ } finally {
2878
+ monitor?.report(routeId);
3062
2879
  }
3063
2880
  }
3064
2881
  async handleInteraction(interaction) {
@@ -3103,47 +2920,41 @@ var InteractionDispatcher = class {
3103
2920
  }
3104
2921
  async handleSlashCommand(interaction) {
3105
2922
  const route = slashRouteOf(interaction);
3106
- await this.processInteraction(interaction, () => route, (key) => this.slashMap.get(key));
2923
+ await this.processInteraction(interaction, _seedcord_core_internal.InteractionRoutes.Slash, () => route, (key) => this.slashMap.get(key));
3107
2924
  }
3108
2925
  async handleButton(interaction) {
3109
- await this.handleCustomIdInteraction(interaction, () => this.buttonMap, "Button");
2926
+ await this.handleCustomIdInteraction(interaction, _seedcord_core_internal.InteractionRoutes.Button, () => this.buttonMap);
3110
2927
  }
3111
2928
  async handleModal(interaction) {
3112
- await this.handleCustomIdInteraction(interaction, () => this.modalMap, "Modal");
2929
+ await this.handleCustomIdInteraction(interaction, _seedcord_core_internal.InteractionRoutes.Modal, () => this.modalMap);
3113
2930
  }
3114
2931
  async handleStringSelectMenu(interaction) {
3115
- await this.handleCustomIdInteraction(interaction, () => this.stringSelectMap, "String select menu");
2932
+ await this.handleCustomIdInteraction(interaction, _seedcord_core_internal.InteractionRoutes.StringMenu, () => this.stringSelectMap);
3116
2933
  }
3117
2934
  async handleUserSelectMenu(interaction) {
3118
- await this.handleCustomIdInteraction(interaction, () => this.userSelectMap, "User select menu");
2935
+ await this.handleCustomIdInteraction(interaction, _seedcord_core_internal.InteractionRoutes.UserMenu, () => this.userSelectMap);
3119
2936
  }
3120
2937
  async handleRoleSelectMenu(interaction) {
3121
- await this.handleCustomIdInteraction(interaction, () => this.roleSelectMap, "Role select menu");
2938
+ await this.handleCustomIdInteraction(interaction, _seedcord_core_internal.InteractionRoutes.RoleMenu, () => this.roleSelectMap);
3122
2939
  }
3123
2940
  async handleChannelSelectMenu(interaction) {
3124
- await this.handleCustomIdInteraction(interaction, () => this.channelSelectMap, "Channel select menu");
2941
+ await this.handleCustomIdInteraction(interaction, _seedcord_core_internal.InteractionRoutes.ChannelMenu, () => this.channelSelectMap);
3125
2942
  }
3126
2943
  async handleMentionableSelectMenu(interaction) {
3127
- await this.handleCustomIdInteraction(interaction, () => this.mentionableSelectMap, "Mentionable select menu");
2944
+ await this.handleCustomIdInteraction(interaction, _seedcord_core_internal.InteractionRoutes.MentionableMenu, () => this.mentionableSelectMap);
3128
2945
  }
3129
2946
  async handleMessageContextMenu(interaction) {
3130
- await this.processInteraction(interaction, () => interaction.commandName, (key) => this.messageContextMenuMap.get(key));
2947
+ await this.processInteraction(interaction, _seedcord_core_internal.InteractionRoutes.MessageContextMenu, () => interaction.commandName, (key) => this.messageContextMenuMap.get(key));
3131
2948
  }
3132
2949
  async handleUserContextMenu(interaction) {
3133
- await this.processInteraction(interaction, () => interaction.commandName, (key) => this.userContextMenuMap.get(key));
2950
+ await this.processInteraction(interaction, _seedcord_core_internal.InteractionRoutes.UserContextMenu, () => interaction.commandName, (key) => this.userContextMenuMap.get(key));
3134
2951
  }
3135
2952
  async handleAutocomplete(interaction) {
3136
2953
  const route = slashRouteOf(interaction);
3137
- await this.processInteraction(interaction, () => route, (key) => this.autocompleteMap.get(key), UnhandledAutocomplete);
2954
+ await this.processInteraction(interaction, _seedcord_core_internal.InteractionRoutes.Autocomplete, () => route, (key) => this.autocompleteMap.get(key), UnhandledAutocomplete);
3138
2955
  }
3139
2956
  };
3140
2957
 
3141
- //#endregion
3142
- //#region \0@oxc-project+runtime@0.135.0/helpers/esm/decorateMetadata.js
3143
- function __decorateMetadata(k, v) {
3144
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
3145
- }
3146
-
3147
2958
  //#endregion
3148
2959
  //#region \0@oxc-project+runtime@0.135.0/helpers/esm/decorate.js
3149
2960
  function __decorate(decorators, target, key, desc) {
@@ -3162,9 +2973,9 @@ const LOGOUT_TIMEOUT_MS = 2e3;
3162
2973
  /**
3163
2974
  * The Discord client and its controllers. Access it through `core.bot`.
3164
2975
  */
3165
- var Bot = class extends _seedcord_event_emitter.TypedEventEmitter {
2976
+ var Bot = class {
3166
2977
  /** @internal */
3167
- logger = new _seedcord_logger.Logger("Bot");
2978
+ logger = new _seedcord_logger.Logger("Bot", { channel: "bot" });
3168
2979
  isInitialized = false;
3169
2980
  _client;
3170
2981
  interactions;
@@ -3181,7 +2992,6 @@ var Bot = class extends _seedcord_event_emitter.TypedEventEmitter {
3181
2992
  }
3182
2993
  /** @internal */
3183
2994
  constructor(core) {
3184
- super();
3185
2995
  this._client = new discord_js.Client(core.config.bot.clientOptions);
3186
2996
  if (core.config.bot.interactions.path) this.interactions = new InteractionDispatcher(core);
3187
2997
  if (core.config.bot.events.path) this.events = new EventDispatcher(core);
@@ -3231,9 +3041,10 @@ var Bot = class extends _seedcord_event_emitter.TypedEventEmitter {
3231
3041
  await this.logout();
3232
3042
  }
3233
3043
  async login(token) {
3234
- this._client.once(discord_js.Events.ClientReady, () => this.emit("ready"));
3044
+ const ready = Promise.withResolvers();
3045
+ this._client.once(discord_js.Events.ClientReady, () => ready.resolve());
3235
3046
  this._client.login(token);
3236
- await this.waitFor("ready");
3047
+ await ready.promise;
3237
3048
  this.logger.info(`Logged in as ${chalk.default.bold.magenta(this._client.user?.username)}!`);
3238
3049
  return this;
3239
3050
  }
@@ -3244,244 +3055,13 @@ var Bot = class extends _seedcord_event_emitter.TypedEventEmitter {
3244
3055
  get client() {
3245
3056
  return this._client;
3246
3057
  }
3247
- /**
3248
- * The overloads enforce the key/args correlation at compile time only. At runtime this forwards
3249
- * straight to the underlying TypedEventEmitter.
3250
- *
3251
- * @internal
3252
- */
3253
- emit(event, ...args) {
3254
- return super.emit(event, ...args);
3255
- }
3256
- emitSafe(event, ...args) {
3257
- return super.emitSafe(event, ...args);
3258
- }
3259
- onListenerError(error, event) {
3260
- this.logger.error(`listener for ${String(event)} threw`, error);
3261
- }
3262
- };
3263
- __decorate([(0, envapt_legacy.Envapt)("DISCORD_BOT_TOKEN", { converter: (raw) => (0, _seedcord_errors_internal.validateDiscordToken)(raw) }), __decorateMetadata("design:type", String)], Bot.prototype, "botToken", void 0);
3264
-
3265
- //#endregion
3266
- //#region src/subscribers/default/HandledException.ts
3267
- let HandledException = class HandledException extends WebhookLog {
3268
- report() {
3269
- return {
3270
- username: "Handled Exception",
3271
- components: [new HandledExceptionContainer(this.data).component],
3272
- files: [jsonAttachment("source.json", "The raw source the fault came from", this.data.source.raw)]
3273
- };
3274
- }
3275
- };
3276
- HandledException = __decorate([Subscribe("handledException"), WebhookUrl("HANDLED_EXCEPTION_WEBHOOK_URL")], HandledException);
3277
- var HandledExceptionContainer = class extends _seedcord_core.BuilderComponent {
3278
- constructor(data) {
3279
- super("container");
3280
- const { denial, uuid, source } = data;
3281
- 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"));
3282
- }
3283
- };
3284
- function faultSummary(denial, source) {
3285
- const head = (0, _seedcord_utils.stripAnsi)(`### A handled fault was reported: \`${denial.name}\`\n**Message:** ${denial.message}\n`);
3286
- 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"}\``;
3287
- 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}\``;
3288
- }
3289
- function causeStack(denial) {
3290
- const { cause } = denial;
3291
- if (Error.isError(cause)) return (0, _seedcord_utils.stripAnsi)(cause.stack ?? cause.message);
3292
- if (typeof cause === "string") return (0, _seedcord_utils.stripAnsi)(cause);
3293
- if (typeof cause === "bigint" || typeof cause === "symbol" || typeof cause === "function") return cause.toString();
3294
- if (cause !== void 0) try {
3295
- return JSON.stringify(cause);
3296
- } catch {
3297
- return "[unserializable cause]";
3298
- }
3299
- return (0, _seedcord_utils.stripAnsi)(denial.stack ?? "No cause recorded.");
3300
- }
3301
-
3302
- //#endregion
3303
- //#region src/subscribers/default/UnknownException.ts
3304
- let UnknownException = class UnknownException extends WebhookLog {
3305
- report() {
3306
- const { metadata } = this.data;
3307
- return {
3308
- username: "Unknown Exception",
3309
- components: [new UnhandledErrorContainer(this.data).component],
3310
- files: metadata ? [jsonAttachment("metadata.json", "Metadata associated with the error", metadata)] : void 0
3311
- };
3312
- }
3313
- };
3314
- UnknownException = __decorate([Subscribe("unknownException"), WebhookUrl("UNKNOWN_EXCEPTION_WEBHOOK_URL")], UnknownException);
3315
- var UnhandledErrorContainer = class extends _seedcord_core.BuilderComponent {
3316
- constructor(data) {
3317
- super("container");
3318
- const { uuid, error, guild, user, metadata } = data;
3319
- 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)}\`\`\``));
3320
- this.addTimestampsIfAvailable(error);
3321
- this.addMetadataIfAvailable(metadata);
3322
- }
3323
- addTimestampsIfAvailable(error) {
3324
- if (!(error instanceof _discordjs_rest.DiscordAPIError)) return;
3325
- const now = Date.now();
3326
- const snowflake = error.url.match(/\/interactions\/(\d+)\//)?.[1];
3327
- if (!snowflake) return void 0;
3328
- const interactionTs = (0, _seedcord_utils.timestampFromSnowflake)(snowflake);
3329
- const diff = now - interactionTs;
3330
- const seconds = Math.floor(diff / 1e3);
3331
- const millis = diff % 1e3;
3332
- 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`));
3333
- }
3334
- addMetadataIfAvailable(metadata) {
3335
- if (!metadata) return;
3336
- this.instance.addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent("### Metadata")).addFileComponents((file) => file.setURL("attachment://metadata.json"));
3337
- }
3338
- };
3339
-
3340
- //#endregion
3341
- //#region src/subscribers/Bus.ts
3342
- /**
3343
- * Registers and dispatches subscribers. Subscribers load from the configured directories and run
3344
- * on a programmatic or framework-event publish. Accessed via `core.bus`. Do not construct it directly.
3345
- */
3346
- var Bus = class extends _seedcord_event_emitter.TypedEventEmitter {
3347
- core;
3348
- logger = new _seedcord_logger.Logger("Subscribers");
3349
- isInitialized = false;
3350
- subscribersMap = /* @__PURE__ */ new Map();
3351
- executedOnceHandlers = /* @__PURE__ */ new Set();
3352
- webhookProbes = /* @__PURE__ */ new Map();
3353
- hmrHandler;
3354
- constructor(core) {
3355
- super();
3356
- this.core = core;
3357
- if (this.core.config.subscribers.path) {
3358
- if (!envapt.Envapter.isDevelopment && !envapt.Envapter.isTest) return;
3359
- this.hmrHandler = new _seedcord_core_hmr.HmrModuleHandler({
3360
- handlersDir: this.core.config.subscribers.path,
3361
- isHandler: this.isSubscriber.bind(this),
3362
- registerHandler: this.registerSubscriber.bind(this),
3363
- unregisterHandler: this.unregisterSubscriber.bind(this),
3364
- getArtifacts: this.getArtifacts.bind(this),
3365
- logger: this.logger
3366
- });
3367
- }
3368
- }
3369
- getArtifacts(ctor) {
3370
- return [Reflect.getMetadata(_seedcord_core_internal.SubscribeMetadataKey, ctor).subscriber];
3371
- }
3372
- /** @internal */
3373
- async init() {
3374
- if (this.isInitialized) return;
3375
- this.isInitialized = true;
3376
- this.registerSubscriber(UnknownException);
3377
- this.registerSubscriber(HandledException);
3378
- const subscribersDir = this.core.config.subscribers.path;
3379
- if (subscribersDir) {
3380
- this.logger.info(chalk.default.bold(subscribersDir));
3381
- await this.loadSubscribers(subscribersDir);
3382
- const totalSubscribers = [...this.subscribersMap.values()].reduce((acc, handlers) => acc + handlers.length, 0);
3383
- this.logger.utils.list([`${chalk.default.bold.magenta(totalSubscribers)} subscribers`], chalk.default.bold.green("Loaded"));
3384
- }
3385
- if (!envapt.Envapter.isTest) await this.verifyWebhooks();
3386
- }
3387
- /** @internal Exposed for tests. */
3388
- async verifyWebhooks() {
3389
- const missing = [];
3390
- await Promise.all([...this.webhookProbes].map(async ([url, envKeys]) => {
3391
- const result = await WebhookLog.senderFor(url).verify();
3392
- if (result === "missing") missing.push(...envKeys);
3393
- else if (result === "unreachable") this.logger.warn(`could not verify webhook at ${_seedcord_logger.paint.sky.bold(envKeys.join(", "))}`);
3394
- }));
3395
- if (missing.length > 0) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigWebhookNotFound, [missing.join(", ")]);
3396
- }
3397
- /** @internal */
3398
- async onHmr(event) {
3399
- if (this.hmrHandler) await this.hmrHandler.handle(event);
3400
- }
3401
- async loadSubscribers(dir) {
3402
- await (0, _seedcord_utils_node.traverseDirectory)(dir, (fullPath, relativePath, imported) => {
3403
- for (const exportName of Object.keys(imported)) {
3404
- const val = imported[exportName];
3405
- if (this.isSubscriber(val)) {
3406
- this.registerSubscriber(val);
3407
- this.hmrHandler?.trackHandler(fullPath, val);
3408
- this.logger.utils.registration(val.name, relativePath);
3409
- }
3410
- }
3411
- }, this.logger);
3412
- }
3413
- registerSubscriber(handler) {
3414
- if (handler.prototype instanceof WebhookLog) {
3415
- const envKey = WebhookLog.envKeyOf(handler);
3416
- const url = WebhookLog.urlOf(envKey);
3417
- if (url === null) {
3418
- this.logger.warn(`${chalk.default.bold(handler.name)} disabled, ${_seedcord_logger.paint.sky.bold(envKey)} is not set`);
3419
- return;
3420
- }
3421
- const envKeys = this.webhookProbes.get(url) ?? [];
3422
- if (!envKeys.includes(envKey)) envKeys.push(envKey);
3423
- this.webhookProbes.set(url, envKeys);
3424
- }
3425
- const options = Reflect.getMetadata(_seedcord_core_internal.SubscribeMetadataKey, handler);
3426
- let handlers = this.subscribersMap.get(options.subscriber);
3427
- if (!handlers) {
3428
- handlers = [];
3429
- this.subscribersMap.set(options.subscriber, handlers);
3430
- }
3431
- handlers.push({
3432
- ctor: handler,
3433
- frequency: options.frequency ?? "on"
3434
- });
3435
- }
3436
- unregisterSubscriber(handler, artifacts) {
3437
- const subscribers = artifacts ?? [...this.subscribersMap.keys()];
3438
- for (const subscriber of subscribers) {
3439
- const handlers = this.subscribersMap.get(subscriber);
3440
- if (!handlers) continue;
3441
- const index = handlers.findIndex((entry) => entry.ctor === handler);
3442
- if (index !== -1) handlers.splice(index, 1);
3443
- }
3444
- }
3445
- isSubscriber(obj) {
3446
- if (typeof obj !== "function") return false;
3447
- return obj.prototype instanceof Subscriber && Reflect.hasMetadata(_seedcord_core_internal.SubscribeMetadataKey, obj);
3448
- }
3449
- /**
3450
- * Publishes an event to its subscribers and native listeners.
3451
- *
3452
- * Fire-and-forget, so subscriber handlers run asynchronously and this returns before they
3453
- * complete, and callers must not assume side effects have landed. Errors thrown by a subscriber
3454
- * are caught and logged, never surfaced here. A `'once'` subscriber is marked as executed when
3455
- * it starts (even if it throws), so it never runs twice.
3456
- *
3457
- * @param event - The subscription key to publish
3458
- * @param data - Payload passed to each subscriber
3459
- * @returns Whether the underlying emitter had native listeners for the event
3460
- */
3461
- publish(event, data) {
3462
- this.processSubscriber(event, data);
3463
- return super.emit(event, ...[data]);
3464
- }
3465
- async processSubscriber(subscriberName, data) {
3466
- const handlers = this.subscribersMap.get(subscriberName);
3467
- if (!handlers) return;
3468
- for (const entry of handlers) {
3469
- if (entry.frequency === "once" && this.executedOnceHandlers.has(entry.ctor)) continue;
3470
- try {
3471
- if (entry.frequency === "once") this.executedOnceHandlers.add(entry.ctor);
3472
- const Ctor = entry.ctor;
3473
- await new Ctor(data, this.core).execute();
3474
- } catch (err) {
3475
- this.logger.error(`Error in subscriber ${String(subscriberName)} handler ${entry.ctor.name}:`, err);
3476
- }
3477
- }
3478
- }
3479
3058
  };
3059
+ __decorate([(0, envapt_legacy.Envapt)("DISCORD_BOT_TOKEN", { converter: (raw) => (0, _seedcord_errors_internal.validateDiscordToken)(raw) })], Bot.prototype, "botToken", void 0);
3480
3060
 
3481
3061
  //#endregion
3482
3062
  //#region src/version.ts
3483
3063
  /** Package version */
3484
- const version = "0.1.0-next.4";
3064
+ const version = "0.1.0-next.5";
3485
3065
 
3486
3066
  //#endregion
3487
3067
  //#region src/Seedcord.ts
@@ -3499,12 +3079,9 @@ var Seedcord = class Seedcord extends _seedcord_core_node_internal.Pluggable {
3499
3079
  version = version;
3500
3080
  healthCheck;
3501
3081
  hmrManager;
3502
- /** @see {@link CoordinatedShutdown} */
3503
- shutdown;
3504
- /** @see {@link CoordinatedStartup} */
3505
- startup;
3506
3082
  /** @see {@link Bus} */
3507
3083
  bus;
3084
+ subscribers;
3508
3085
  /** @see {@link Bot} */
3509
3086
  bot;
3510
3087
  /** @see {@link IRateLimiter} */
@@ -3514,17 +3091,14 @@ var Seedcord = class Seedcord extends _seedcord_core_node_internal.Pluggable {
3514
3091
  * @throws A **SeedcordError** When attempting to create multiple instances (singleton)
3515
3092
  */
3516
3093
  constructor(config) {
3517
- const shutdown = new _seedcord_core_node_internal.CoordinatedShutdown();
3518
- const startup = new _seedcord_core_node_internal.CoordinatedStartup();
3519
- super(shutdown, startup);
3094
+ super(new _seedcord_core_node_internal.CoordinatedShutdown(), new _seedcord_core_node_internal.CoordinatedStartup());
3520
3095
  this.config = config;
3521
3096
  (0, _seedcord_logger_node.installNodeDefaults)(config.logger);
3522
3097
  (0, _seedcord_core_internal.setBotColor)(config.botColor);
3523
- this.shutdown = shutdown;
3524
- this.startup = startup;
3525
3098
  this.hmrManager = new _seedcord_core_internal.HmrManager();
3526
3099
  this.hmrManager.init();
3527
- this.bus = new Bus(this);
3100
+ this.bus = new _seedcord_core.Bus(this);
3101
+ this.subscribers = new _seedcord_core_node_internal.SubscriberLoader(this.bus, config.subscribers.path);
3528
3102
  this.bot = new Bot(this);
3529
3103
  this.rateLimiter = config.store ?? new _seedcord_rate_limiter.MemoryRateLimiter();
3530
3104
  this.healthCheck = _seedcord_core_node_internal.HealthCheck.fromOption(this.shutdown, config.healthCheck);
@@ -3547,18 +3121,18 @@ var Seedcord = class Seedcord extends _seedcord_core_node_internal.Pluggable {
3547
3121
  }
3548
3122
  registerStartupTasks() {
3549
3123
  if (envapt.Envapter.isDevelopment || envapt.Envapter.isTest) this.registerHmrAwareModules();
3550
- this.startup.addTask(_seedcord_core_node_internal.StartupPhase.Configuration, "Bus Initialization", async () => {
3124
+ this.startup.addTask(_seedcord_core_node_internal.StartupPhase.Configuration, "bus-initialization", async () => {
3551
3125
  this.bus.logger.utils.initialization("Subscribers", "start");
3552
- await this.bus.init();
3126
+ await this.subscribers.init();
3553
3127
  this.bus.logger.utils.initialization("Subscribers", "end");
3554
3128
  });
3555
- this.startup.addTask(_seedcord_core_node_internal.StartupPhase.Login, "Bot Initialization", async () => {
3129
+ this.startup.addTask(_seedcord_core_node_internal.StartupPhase.Login, "bot-initialization", async () => {
3556
3130
  this.bot.logger.utils.initialization("Bot", "start");
3557
3131
  await this.bot.init();
3558
3132
  this.bot.logger.utils.initialization("Bot", "end");
3559
3133
  });
3560
3134
  const { healthCheck } = this;
3561
- if (healthCheck) this.startup.addTask(_seedcord_core_node_internal.StartupPhase.Ready, "Health Check", async () => {
3135
+ if (healthCheck) this.startup.addTask(_seedcord_core_node_internal.StartupPhase.Ready, "health-check", async () => {
3562
3136
  healthCheck.logger.utils.initialization("HealthCheck", "start");
3563
3137
  await healthCheck.init();
3564
3138
  healthCheck.logger.utils.initialization("HealthCheck", "end");
@@ -3580,9 +3154,9 @@ var Seedcord = class Seedcord extends _seedcord_core_node_internal.Pluggable {
3580
3154
  return this;
3581
3155
  }
3582
3156
  registerHmrAwareModules() {
3583
- this.startup.addTask(_seedcord_core_node_internal.StartupPhase.Configuration, "HMR Registration", async () => {
3157
+ this.startup.addTask(_seedcord_core_node_internal.StartupPhase.Configuration, "hmr-registration", async () => {
3584
3158
  this.hmrManager.register(this.bot);
3585
- this.hmrManager.register(this.bus);
3159
+ this.hmrManager.register(this.subscribers);
3586
3160
  for (const plugin of this.plugins) this.hmrManager.register(plugin);
3587
3161
  await Promise.resolve();
3588
3162
  });
@@ -3608,15 +3182,14 @@ exports.MiddlewareType = MiddlewareType;
3608
3182
  exports.ModalHandler = ModalHandler;
3609
3183
  exports.Nsfw = Nsfw;
3610
3184
  exports.Paginator = Paginator;
3185
+ exports.Plugin = Plugin;
3611
3186
  exports.RegisterEvent = RegisterEvent;
3612
3187
  exports.RepliableHandler = RepliableHandler;
3613
3188
  exports.Seedcord = Seedcord;
3614
3189
  exports.SelectMenuHandler = SelectMenuHandler;
3615
3190
  exports.SlashHandler = SlashHandler;
3616
- exports.Subscribe = Subscribe;
3617
3191
  exports.Subscriber = Subscriber;
3618
3192
  exports.WebhookLog = WebhookLog;
3619
- exports.WebhookUrl = WebhookUrl;
3620
3193
  exports.checkBotPermissions = checkBotPermissions;
3621
3194
  exports.checkPermissions = checkPermissions;
3622
3195
  exports.fetchGuildMember = fetchGuildMember;
@@ -3644,6 +3217,7 @@ Object.keys(_seedcord_errors).forEach(function (k) {
3644
3217
  });
3645
3218
  });
3646
3219
 
3220
+ var _seedcord_event_emitter = require("@seedcord/event-emitter");
3647
3221
  Object.keys(_seedcord_event_emitter).forEach(function (k) {
3648
3222
  if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
3649
3223
  enumerable: true,