@gakr-gakr/discord 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (353) hide show
  1. package/account-inspect-api.ts +6 -0
  2. package/action-runtime-api.ts +1 -0
  3. package/api.ts +130 -0
  4. package/autobot.plugin.json +15 -0
  5. package/channel-config-api.ts +1 -0
  6. package/channel-plugin-api.ts +3 -0
  7. package/config-api.ts +4 -0
  8. package/configured-state.ts +6 -0
  9. package/contract-api.ts +21 -0
  10. package/directory-contract-api.ts +4 -0
  11. package/doctor-contract-api.ts +1 -0
  12. package/index.ts +24 -0
  13. package/package.json +79 -0
  14. package/runtime-api.actions.ts +15 -0
  15. package/runtime-api.lookup.ts +22 -0
  16. package/runtime-api.monitor.ts +50 -0
  17. package/runtime-api.send.ts +79 -0
  18. package/runtime-api.threads.ts +31 -0
  19. package/runtime-api.ts +181 -0
  20. package/runtime-setter-api.ts +3 -0
  21. package/secret-contract-api.ts +4 -0
  22. package/security-audit-contract-api.ts +1 -0
  23. package/security-contract-api.ts +4 -0
  24. package/session-key-api.ts +1 -0
  25. package/setup-entry.ts +9 -0
  26. package/setup-plugin-api.ts +3 -0
  27. package/src/account-inspect.ts +131 -0
  28. package/src/accounts.ts +205 -0
  29. package/src/actions/handle-action.guild-admin.ts +421 -0
  30. package/src/actions/handle-action.ts +402 -0
  31. package/src/actions/runtime.guild.ts +446 -0
  32. package/src/actions/runtime.messaging.messages.ts +226 -0
  33. package/src/actions/runtime.messaging.reactions.ts +67 -0
  34. package/src/actions/runtime.messaging.runtime.ts +73 -0
  35. package/src/actions/runtime.messaging.send.ts +336 -0
  36. package/src/actions/runtime.messaging.shared.ts +97 -0
  37. package/src/actions/runtime.messaging.ts +37 -0
  38. package/src/actions/runtime.moderation-shared.ts +48 -0
  39. package/src/actions/runtime.moderation.ts +116 -0
  40. package/src/actions/runtime.presence.ts +117 -0
  41. package/src/actions/runtime.shared.ts +86 -0
  42. package/src/actions/runtime.ts +87 -0
  43. package/src/api.ts +219 -0
  44. package/src/approval-handler.runtime.ts +636 -0
  45. package/src/approval-native.ts +219 -0
  46. package/src/approval-runtime.ts +14 -0
  47. package/src/approval-shared.ts +56 -0
  48. package/src/audit-core.ts +178 -0
  49. package/src/audit.ts +32 -0
  50. package/src/channel-actions.runtime.ts +1 -0
  51. package/src/channel-actions.ts +254 -0
  52. package/src/channel-api.ts +29 -0
  53. package/src/channel.conversation.ts +159 -0
  54. package/src/channel.loaders.ts +50 -0
  55. package/src/channel.runtime.ts +1 -0
  56. package/src/channel.setup.ts +12 -0
  57. package/src/channel.ts +728 -0
  58. package/src/chunk.ts +321 -0
  59. package/src/client.ts +143 -0
  60. package/src/component-custom-id.ts +72 -0
  61. package/src/components-registry.ts +356 -0
  62. package/src/components.builders.ts +410 -0
  63. package/src/components.modal.ts +124 -0
  64. package/src/components.parse.ts +407 -0
  65. package/src/components.ts +54 -0
  66. package/src/components.types.ts +187 -0
  67. package/src/config-schema.ts +6 -0
  68. package/src/config-ui-hints.ts +354 -0
  69. package/src/conversation-identity.ts +58 -0
  70. package/src/delivery-retry.ts +56 -0
  71. package/src/directory-cache.ts +116 -0
  72. package/src/directory-config.ts +58 -0
  73. package/src/directory-live.ts +135 -0
  74. package/src/doctor-contract.ts +477 -0
  75. package/src/doctor-shared.ts +5 -0
  76. package/src/doctor.ts +340 -0
  77. package/src/draft-chunking.ts +43 -0
  78. package/src/draft-stream.ts +162 -0
  79. package/src/error-body.ts +38 -0
  80. package/src/exec-approvals.ts +110 -0
  81. package/src/gateway-logging.ts +67 -0
  82. package/src/group-policy.ts +113 -0
  83. package/src/guilds.ts +29 -0
  84. package/src/inbound-event-delivery.ts +135 -0
  85. package/src/interactive-dispatch.ts +104 -0
  86. package/src/internal/api.commands.ts +51 -0
  87. package/src/internal/api.guild.ts +164 -0
  88. package/src/internal/api.interactions.ts +53 -0
  89. package/src/internal/api.messages.ts +113 -0
  90. package/src/internal/api.reactions.ts +38 -0
  91. package/src/internal/api.ts +61 -0
  92. package/src/internal/api.users.ts +19 -0
  93. package/src/internal/api.webhooks.ts +13 -0
  94. package/src/internal/client.ts +310 -0
  95. package/src/internal/command-deploy.ts +352 -0
  96. package/src/internal/commands.ts +188 -0
  97. package/src/internal/components.base.ts +65 -0
  98. package/src/internal/components.message.ts +279 -0
  99. package/src/internal/components.modal.ts +95 -0
  100. package/src/internal/components.ts +31 -0
  101. package/src/internal/discord.ts +11 -0
  102. package/src/internal/embeds.ts +35 -0
  103. package/src/internal/entity-cache.ts +98 -0
  104. package/src/internal/event-queue.ts +185 -0
  105. package/src/internal/gateway-close-codes.ts +25 -0
  106. package/src/internal/gateway-dispatch.ts +96 -0
  107. package/src/internal/gateway-identify-limiter.ts +26 -0
  108. package/src/internal/gateway-lifecycle.ts +75 -0
  109. package/src/internal/gateway-rate-limit.ts +104 -0
  110. package/src/internal/gateway.ts +479 -0
  111. package/src/internal/interaction-dispatch.ts +162 -0
  112. package/src/internal/interaction-options.ts +98 -0
  113. package/src/internal/interaction-response.ts +53 -0
  114. package/src/internal/interactions.ts +378 -0
  115. package/src/internal/listeners.ts +91 -0
  116. package/src/internal/modal-fields.ts +95 -0
  117. package/src/internal/payload.ts +69 -0
  118. package/src/internal/rest-body.ts +115 -0
  119. package/src/internal/rest-errors.ts +88 -0
  120. package/src/internal/rest-routes.ts +50 -0
  121. package/src/internal/rest-scheduler.ts +557 -0
  122. package/src/internal/rest.ts +322 -0
  123. package/src/internal/schemas.ts +36 -0
  124. package/src/internal/structures.ts +280 -0
  125. package/src/internal/test-builders.test-support.ts +167 -0
  126. package/src/internal/voice.ts +49 -0
  127. package/src/media-detection.ts +28 -0
  128. package/src/mentions.ts +147 -0
  129. package/src/monitor/ack-reactions.ts +70 -0
  130. package/src/monitor/agent-components-auth.ts +7 -0
  131. package/src/monitor/agent-components-context.ts +154 -0
  132. package/src/monitor/agent-components-data.ts +224 -0
  133. package/src/monitor/agent-components-dm-auth.ts +177 -0
  134. package/src/monitor/agent-components-guild-auth.ts +322 -0
  135. package/src/monitor/agent-components-helpers.runtime.ts +3 -0
  136. package/src/monitor/agent-components-helpers.ts +34 -0
  137. package/src/monitor/agent-components-reply.ts +10 -0
  138. package/src/monitor/agent-components.deps.runtime.ts +2 -0
  139. package/src/monitor/agent-components.dispatch.ts +359 -0
  140. package/src/monitor/agent-components.handlers.ts +303 -0
  141. package/src/monitor/agent-components.modal.ts +160 -0
  142. package/src/monitor/agent-components.plugin-interactive.ts +187 -0
  143. package/src/monitor/agent-components.runtime.ts +14 -0
  144. package/src/monitor/agent-components.system-controls.ts +215 -0
  145. package/src/monitor/agent-components.ts +70 -0
  146. package/src/monitor/agent-components.types.ts +58 -0
  147. package/src/monitor/agent-components.wildcard-controls.ts +171 -0
  148. package/src/monitor/allow-list.ts +631 -0
  149. package/src/monitor/auto-presence.ts +356 -0
  150. package/src/monitor/channel-access.ts +102 -0
  151. package/src/monitor/commands.ts +9 -0
  152. package/src/monitor/dm-command-auth.ts +259 -0
  153. package/src/monitor/dm-command-decision.ts +49 -0
  154. package/src/monitor/exec-approvals.ts +161 -0
  155. package/src/monitor/format.ts +45 -0
  156. package/src/monitor/gateway-handle.ts +34 -0
  157. package/src/monitor/gateway-metadata.ts +298 -0
  158. package/src/monitor/gateway-plugin.ts +302 -0
  159. package/src/monitor/gateway-registry.ts +37 -0
  160. package/src/monitor/gateway-supervisor.ts +206 -0
  161. package/src/monitor/inbound-context.ts +95 -0
  162. package/src/monitor/inbound-dedupe.ts +79 -0
  163. package/src/monitor/inbound-job.ts +118 -0
  164. package/src/monitor/listeners.queue.ts +91 -0
  165. package/src/monitor/listeners.reactions.ts +594 -0
  166. package/src/monitor/listeners.ts +150 -0
  167. package/src/monitor/message-channel-info.ts +96 -0
  168. package/src/monitor/message-forwarded.ts +114 -0
  169. package/src/monitor/message-handler.batch-gate.ts +19 -0
  170. package/src/monitor/message-handler.context.ts +492 -0
  171. package/src/monitor/message-handler.dm-preflight.ts +119 -0
  172. package/src/monitor/message-handler.draft-preview.ts +436 -0
  173. package/src/monitor/message-handler.hydration.ts +198 -0
  174. package/src/monitor/message-handler.module-test-helpers.ts +31 -0
  175. package/src/monitor/message-handler.preflight-channel-access.ts +86 -0
  176. package/src/monitor/message-handler.preflight-channel-context.ts +58 -0
  177. package/src/monitor/message-handler.preflight-context.ts +54 -0
  178. package/src/monitor/message-handler.preflight-helpers.ts +164 -0
  179. package/src/monitor/message-handler.preflight-history.ts +23 -0
  180. package/src/monitor/message-handler.preflight-logging.ts +36 -0
  181. package/src/monitor/message-handler.preflight-pluralkit.ts +28 -0
  182. package/src/monitor/message-handler.preflight-runtime.ts +28 -0
  183. package/src/monitor/message-handler.preflight-thread.ts +49 -0
  184. package/src/monitor/message-handler.preflight.ts +822 -0
  185. package/src/monitor/message-handler.preflight.types.ts +115 -0
  186. package/src/monitor/message-handler.process.ts +1033 -0
  187. package/src/monitor/message-handler.routing-preflight.ts +112 -0
  188. package/src/monitor/message-handler.ts +309 -0
  189. package/src/monitor/message-media.ts +536 -0
  190. package/src/monitor/message-run-queue.ts +101 -0
  191. package/src/monitor/message-text.ts +171 -0
  192. package/src/monitor/message-utils.ts +34 -0
  193. package/src/monitor/model-picker-preferences.ts +184 -0
  194. package/src/monitor/model-picker.state.ts +364 -0
  195. package/src/monitor/model-picker.test-utils.ts +26 -0
  196. package/src/monitor/model-picker.ts +38 -0
  197. package/src/monitor/model-picker.view.ts +722 -0
  198. package/src/monitor/native-command-agent-reply.ts +125 -0
  199. package/src/monitor/native-command-arg-ui.ts +233 -0
  200. package/src/monitor/native-command-auth.ts +309 -0
  201. package/src/monitor/native-command-bypass.ts +13 -0
  202. package/src/monitor/native-command-context.ts +109 -0
  203. package/src/monitor/native-command-dispatch.ts +35 -0
  204. package/src/monitor/native-command-model-picker-apply.ts +209 -0
  205. package/src/monitor/native-command-model-picker-interaction.ts +516 -0
  206. package/src/monitor/native-command-model-picker-ui.ts +357 -0
  207. package/src/monitor/native-command-reply.ts +185 -0
  208. package/src/monitor/native-command-route.ts +91 -0
  209. package/src/monitor/native-command-status.ts +76 -0
  210. package/src/monitor/native-command-ui.ts +26 -0
  211. package/src/monitor/native-command-ui.types.ts +20 -0
  212. package/src/monitor/native-command.args.ts +45 -0
  213. package/src/monitor/native-command.options.ts +153 -0
  214. package/src/monitor/native-command.runtime.ts +51 -0
  215. package/src/monitor/native-command.ts +747 -0
  216. package/src/monitor/native-command.types.ts +9 -0
  217. package/src/monitor/native-interaction-channel-context.ts +50 -0
  218. package/src/monitor/preflight-audio.runtime.ts +9 -0
  219. package/src/monitor/preflight-audio.ts +130 -0
  220. package/src/monitor/presence-cache.ts +61 -0
  221. package/src/monitor/presence.ts +50 -0
  222. package/src/monitor/provider-session.runtime.ts +12 -0
  223. package/src/monitor/provider.acp.ts +89 -0
  224. package/src/monitor/provider.allowlist.ts +398 -0
  225. package/src/monitor/provider.cleanup.ts +41 -0
  226. package/src/monitor/provider.commands.ts +129 -0
  227. package/src/monitor/provider.config-log.ts +45 -0
  228. package/src/monitor/provider.deploy-errors.ts +362 -0
  229. package/src/monitor/provider.deploy.ts +221 -0
  230. package/src/monitor/provider.interactions.ts +160 -0
  231. package/src/monitor/provider.lifecycle.ts +562 -0
  232. package/src/monitor/provider.runtime.ts +1 -0
  233. package/src/monitor/provider.startup-log.ts +32 -0
  234. package/src/monitor/provider.startup.ts +323 -0
  235. package/src/monitor/provider.ts +688 -0
  236. package/src/monitor/reply-context.ts +64 -0
  237. package/src/monitor/reply-delivery.ts +216 -0
  238. package/src/monitor/reply-safety.ts +96 -0
  239. package/src/monitor/rest-fetch.ts +97 -0
  240. package/src/monitor/route-resolution.ts +140 -0
  241. package/src/monitor/sender-identity.ts +81 -0
  242. package/src/monitor/startup-status.ts +10 -0
  243. package/src/monitor/status.ts +22 -0
  244. package/src/monitor/system-events.ts +55 -0
  245. package/src/monitor/thread-bindings.config.ts +35 -0
  246. package/src/monitor/thread-bindings.discord-api.ts +310 -0
  247. package/src/monitor/thread-bindings.lifecycle.ts +354 -0
  248. package/src/monitor/thread-bindings.manager.ts +554 -0
  249. package/src/monitor/thread-bindings.messages.ts +6 -0
  250. package/src/monitor/thread-bindings.persona.ts +25 -0
  251. package/src/monitor/thread-bindings.session-adapter.ts +229 -0
  252. package/src/monitor/thread-bindings.session-shared.ts +59 -0
  253. package/src/monitor/thread-bindings.session-updates.ts +35 -0
  254. package/src/monitor/thread-bindings.state.ts +540 -0
  255. package/src/monitor/thread-bindings.ts +48 -0
  256. package/src/monitor/thread-bindings.types.ts +83 -0
  257. package/src/monitor/thread-channel-context.ts +112 -0
  258. package/src/monitor/thread-session-close.ts +63 -0
  259. package/src/monitor/thread-title.ts +181 -0
  260. package/src/monitor/threading.auto-thread.ts +287 -0
  261. package/src/monitor/threading.cache.ts +45 -0
  262. package/src/monitor/threading.starter.ts +288 -0
  263. package/src/monitor/threading.ts +20 -0
  264. package/src/monitor/threading.types.ts +102 -0
  265. package/src/monitor/timeouts.ts +84 -0
  266. package/src/monitor/typing.ts +17 -0
  267. package/src/monitor.gateway.ts +75 -0
  268. package/src/monitor.ts +28 -0
  269. package/src/network-config.ts +79 -0
  270. package/src/normalize.ts +86 -0
  271. package/src/outbound-adapter.ts +327 -0
  272. package/src/outbound-approval.ts +29 -0
  273. package/src/outbound-components.ts +86 -0
  274. package/src/outbound-payload.ts +208 -0
  275. package/src/outbound-send-context.ts +92 -0
  276. package/src/outbound-session-route.ts +72 -0
  277. package/src/pluralkit.ts +58 -0
  278. package/src/preview-streaming.ts +18 -0
  279. package/src/probe.runtime.ts +1 -0
  280. package/src/probe.ts +237 -0
  281. package/src/proxy-fetch.ts +92 -0
  282. package/src/proxy-request-client.ts +21 -0
  283. package/src/recipient-resolution.ts +39 -0
  284. package/src/resolve-allowlist-common.ts +39 -0
  285. package/src/resolve-channels.ts +369 -0
  286. package/src/resolve-users.ts +184 -0
  287. package/src/retry.ts +98 -0
  288. package/src/runtime-api.ts +64 -0
  289. package/src/runtime-config.ts +16 -0
  290. package/src/runtime.ts +23 -0
  291. package/src/secret-config-contract.ts +140 -0
  292. package/src/security-audit.runtime.ts +1 -0
  293. package/src/security-audit.ts +208 -0
  294. package/src/security-contract.ts +47 -0
  295. package/src/security-doctor.ts +20 -0
  296. package/src/security.ts +60 -0
  297. package/src/send-target-parsing.ts +14 -0
  298. package/src/send.channels.ts +139 -0
  299. package/src/send.components.ts +391 -0
  300. package/src/send.emojis-stickers.ts +57 -0
  301. package/src/send.guild.ts +170 -0
  302. package/src/send.message-request.ts +112 -0
  303. package/src/send.messages.ts +229 -0
  304. package/src/send.outbound.ts +459 -0
  305. package/src/send.permissions.ts +283 -0
  306. package/src/send.reactions.ts +155 -0
  307. package/src/send.receipt.ts +69 -0
  308. package/src/send.shared.ts +469 -0
  309. package/src/send.ts +82 -0
  310. package/src/send.types.ts +191 -0
  311. package/src/send.typing.ts +9 -0
  312. package/src/send.voice.ts +140 -0
  313. package/src/send.webhook.ts +137 -0
  314. package/src/session-contract.ts +3 -0
  315. package/src/session-key-normalization.ts +47 -0
  316. package/src/setup-account-state.ts +144 -0
  317. package/src/setup-adapter.ts +14 -0
  318. package/src/setup-core.ts +215 -0
  319. package/src/setup-runtime-helpers.ts +10 -0
  320. package/src/setup-surface.ts +132 -0
  321. package/src/shared-interactive.ts +167 -0
  322. package/src/shared.ts +197 -0
  323. package/src/status-issues.ts +201 -0
  324. package/src/subagent-hooks.ts +232 -0
  325. package/src/target-parsing.ts +70 -0
  326. package/src/target-resolver.ts +129 -0
  327. package/src/targets.ts +12 -0
  328. package/src/token.ts +107 -0
  329. package/src/ui-colors.ts +27 -0
  330. package/src/ui.ts +20 -0
  331. package/src/voice/access.ts +126 -0
  332. package/src/voice/audio.ts +249 -0
  333. package/src/voice/capture-state.ts +120 -0
  334. package/src/voice/command.ts +284 -0
  335. package/src/voice/config.ts +8 -0
  336. package/src/voice/ingress.ts +164 -0
  337. package/src/voice/manager.runtime.ts +14 -0
  338. package/src/voice/manager.ts +1155 -0
  339. package/src/voice/prompt.ts +22 -0
  340. package/src/voice/realtime.ts +1370 -0
  341. package/src/voice/receive-recovery.ts +159 -0
  342. package/src/voice/sanitize.ts +29 -0
  343. package/src/voice/sdk-runtime.ts +14 -0
  344. package/src/voice/segment.ts +160 -0
  345. package/src/voice/session.ts +81 -0
  346. package/src/voice/speaker-context.ts +127 -0
  347. package/src/voice/tts.ts +151 -0
  348. package/src/voice-message.ts +474 -0
  349. package/subagent-hooks-api.ts +27 -0
  350. package/test-api.ts +4 -0
  351. package/thread-binding-api.ts +1 -0
  352. package/timeouts.ts +6 -0
  353. package/tsconfig.json +16 -0
@@ -0,0 +1,298 @@
1
+ import type { APIGatewayBotInfo } from "discord-api-types/v10";
2
+ import { formatErrorMessage } from "autobot/plugin-sdk/error-runtime";
3
+ import { captureHttpExchange } from "autobot/plugin-sdk/proxy-capture";
4
+ import type { RuntimeEnv } from "autobot/plugin-sdk/runtime-env";
5
+ import { fetchWithSsrFGuard } from "autobot/plugin-sdk/ssrf-runtime";
6
+ import { Type } from "typebox";
7
+ import { Check, Errors } from "typebox/value";
8
+ import { isDiscordRateLimitResponseBody, summarizeDiscordResponseBody } from "../error-body.js";
9
+ import { withAbortTimeout } from "./timeouts.js";
10
+
11
+ const DISCORD_GATEWAY_BOT_URL = "https://discord.com/api/v10/gateway/bot";
12
+ const DISCORD_API_HOST = "discord.com";
13
+ const DEFAULT_DISCORD_GATEWAY_URL = "wss://gateway.discord.gg/";
14
+ const DEFAULT_DISCORD_GATEWAY_INFO_TIMEOUT_MS = 30_000;
15
+ const MAX_DISCORD_GATEWAY_INFO_TIMEOUT_MS = 120_000;
16
+ const DISCORD_GATEWAY_INFO_TIMEOUT_ENV = "AUTOBOT_DISCORD_GATEWAY_INFO_TIMEOUT_MS";
17
+ const DISCORD_GATEWAY_METADATA_FALLBACK_LOG_INTERVAL_MS = 60_000;
18
+
19
+ type DiscordGatewayMetadataResponse = Pick<Response, "ok" | "status" | "text">;
20
+ export type DiscordGatewayFetchInit = Record<string, unknown> & {
21
+ headers?: Record<string, string>;
22
+ };
23
+ export type DiscordGatewayFetch = (
24
+ input: string,
25
+ init?: DiscordGatewayFetchInit,
26
+ ) => Promise<DiscordGatewayMetadataResponse>;
27
+
28
+ type DiscordGatewayMetadataError = Error & { transient?: boolean };
29
+
30
+ const discordGatewayBotInfoSchema = Type.Object({
31
+ url: Type.String({ minLength: 1 }),
32
+ shards: Type.Integer({ minimum: 1 }),
33
+ session_start_limit: Type.Object({
34
+ total: Type.Integer({ minimum: 0 }),
35
+ remaining: Type.Integer({ minimum: 0 }),
36
+ reset_after: Type.Number({ minimum: 0 }),
37
+ max_concurrency: Type.Integer({ minimum: 1 }),
38
+ }),
39
+ });
40
+
41
+ const gatewayMetadataFallbackLogLastAt = new WeakMap<RuntimeEnv, number>();
42
+
43
+ function resolveFetchInputUrl(input: RequestInfo | URL): string {
44
+ if (typeof input === "string") {
45
+ return input;
46
+ }
47
+ if (input instanceof URL) {
48
+ return input.toString();
49
+ }
50
+ return input.url;
51
+ }
52
+
53
+ async function materializeGuardedResponse(response: Response): Promise<Response> {
54
+ const body = await response.arrayBuffer();
55
+ return new Response(body, {
56
+ status: response.status,
57
+ statusText: response.statusText,
58
+ headers: response.headers,
59
+ });
60
+ }
61
+
62
+ function normalizeGatewayInfoTimeoutMs(value: unknown): number | undefined {
63
+ const numeric =
64
+ typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
65
+ if (!Number.isFinite(numeric) || numeric <= 0) {
66
+ return undefined;
67
+ }
68
+ return Math.min(Math.floor(numeric), MAX_DISCORD_GATEWAY_INFO_TIMEOUT_MS);
69
+ }
70
+
71
+ export function resolveDiscordGatewayInfoTimeoutMs(params?: {
72
+ configuredTimeoutMs?: number;
73
+ env?: NodeJS.ProcessEnv;
74
+ }): number {
75
+ return (
76
+ normalizeGatewayInfoTimeoutMs(params?.configuredTimeoutMs) ??
77
+ normalizeGatewayInfoTimeoutMs(params?.env?.[DISCORD_GATEWAY_INFO_TIMEOUT_ENV]) ??
78
+ DEFAULT_DISCORD_GATEWAY_INFO_TIMEOUT_MS
79
+ );
80
+ }
81
+
82
+ function summarizeGatewayResponseBody(body: string): string {
83
+ return summarizeDiscordResponseBody(body, { emptyText: "<empty>" }) ?? "<empty>";
84
+ }
85
+
86
+ function isDiscordGatewayRateLimitResponse(status: number, body: string): boolean {
87
+ return status === 429 && isDiscordRateLimitResponseBody(body);
88
+ }
89
+
90
+ function isTransientDiscordGatewayResponse(status: number, body: string): boolean {
91
+ if (status >= 500) {
92
+ return true;
93
+ }
94
+ if (isDiscordGatewayRateLimitResponse(status, body)) {
95
+ return true;
96
+ }
97
+ const normalized = body.toLowerCase();
98
+ return (
99
+ normalized.includes("upstream connect error") ||
100
+ normalized.includes("disconnect/reset before headers") ||
101
+ normalized.includes("reset reason:")
102
+ );
103
+ }
104
+
105
+ function createGatewayMetadataError(params: {
106
+ detail: string;
107
+ transient: boolean;
108
+ cause?: unknown;
109
+ }): Error {
110
+ const error = new Error(
111
+ params.transient
112
+ ? "Failed to get gateway information from Discord: fetch failed"
113
+ : `Failed to get gateway information from Discord: ${params.detail}`,
114
+ {
115
+ cause: params.cause ?? (params.transient ? new Error(params.detail) : undefined),
116
+ },
117
+ ) as DiscordGatewayMetadataError;
118
+ Object.defineProperty(error, "transient", {
119
+ value: params.transient,
120
+ enumerable: false,
121
+ });
122
+ return error;
123
+ }
124
+
125
+ function isTransientGatewayMetadataError(error: unknown): boolean {
126
+ return Boolean((error as DiscordGatewayMetadataError | undefined)?.transient);
127
+ }
128
+
129
+ function createDefaultGatewayInfo(): APIGatewayBotInfo {
130
+ return {
131
+ url: DEFAULT_DISCORD_GATEWAY_URL,
132
+ shards: 1,
133
+ session_start_limit: {
134
+ total: 1,
135
+ remaining: 1,
136
+ reset_after: 0,
137
+ max_concurrency: 1,
138
+ },
139
+ };
140
+ }
141
+
142
+ function summarizeGatewaySchemaErrors(value: unknown): string {
143
+ const errors = Errors(discordGatewayBotInfoSchema, value);
144
+ if (errors.length === 0) {
145
+ return "unknown schema mismatch";
146
+ }
147
+ return errors
148
+ .slice(0, 3)
149
+ .map((error) => `${error.instancePath || "/"} ${error.message}`)
150
+ .join("; ");
151
+ }
152
+
153
+ export function parseDiscordGatewayInfoBody(body: string): APIGatewayBotInfo {
154
+ const parsed = JSON.parse(body) as unknown;
155
+ if (!Check(discordGatewayBotInfoSchema, parsed)) {
156
+ throw new Error(summarizeGatewaySchemaErrors(parsed));
157
+ }
158
+ return parsed;
159
+ }
160
+
161
+ export async function fetchDiscordGatewayInfo(params: {
162
+ token: string;
163
+ fetchImpl: DiscordGatewayFetch;
164
+ fetchInit?: DiscordGatewayFetchInit;
165
+ }): Promise<APIGatewayBotInfo> {
166
+ let response: DiscordGatewayMetadataResponse;
167
+ try {
168
+ response = await params.fetchImpl(DISCORD_GATEWAY_BOT_URL, {
169
+ ...params.fetchInit,
170
+ headers: {
171
+ ...params.fetchInit?.headers,
172
+ Authorization: `Bot ${params.token}`,
173
+ },
174
+ });
175
+ } catch (error) {
176
+ throw createGatewayMetadataError({
177
+ detail: formatErrorMessage(error),
178
+ transient: true,
179
+ cause: error,
180
+ });
181
+ }
182
+
183
+ let body: string;
184
+ try {
185
+ body = await response.text();
186
+ } catch (error) {
187
+ throw createGatewayMetadataError({
188
+ detail: formatErrorMessage(error),
189
+ transient: true,
190
+ cause: error,
191
+ });
192
+ }
193
+ const summary = summarizeGatewayResponseBody(body);
194
+ const transient = isTransientDiscordGatewayResponse(response.status, body);
195
+
196
+ if (!response.ok) {
197
+ throw createGatewayMetadataError({
198
+ detail: `Discord API /gateway/bot failed (${response.status}): ${summary}`,
199
+ transient,
200
+ });
201
+ }
202
+
203
+ try {
204
+ return parseDiscordGatewayInfoBody(body);
205
+ } catch (error) {
206
+ throw createGatewayMetadataError({
207
+ detail: `Discord API /gateway/bot returned invalid metadata: ${formatErrorMessage(error)} (${summary})`,
208
+ transient,
209
+ cause: error,
210
+ });
211
+ }
212
+ }
213
+
214
+ export async function fetchDiscordGatewayInfoWithTimeout(params: {
215
+ token: string;
216
+ fetchImpl: DiscordGatewayFetch;
217
+ fetchInit?: DiscordGatewayFetchInit;
218
+ timeoutMs?: number;
219
+ }): Promise<APIGatewayBotInfo> {
220
+ const timeoutMs = Math.max(1, params.timeoutMs ?? DEFAULT_DISCORD_GATEWAY_INFO_TIMEOUT_MS);
221
+ return await withAbortTimeout({
222
+ timeoutMs,
223
+ createTimeoutError: () =>
224
+ createGatewayMetadataError({
225
+ detail: `Discord API /gateway/bot timed out after ${timeoutMs}ms`,
226
+ transient: true,
227
+ cause: new Error("gateway metadata timeout"),
228
+ }),
229
+ run: async (signal) =>
230
+ await fetchDiscordGatewayInfo({
231
+ token: params.token,
232
+ fetchImpl: params.fetchImpl,
233
+ fetchInit: {
234
+ ...params.fetchInit,
235
+ signal,
236
+ },
237
+ }),
238
+ });
239
+ }
240
+
241
+ export function resolveGatewayInfoWithFallback(params: { runtime?: RuntimeEnv; error: unknown }): {
242
+ info: APIGatewayBotInfo;
243
+ usedFallback: boolean;
244
+ } {
245
+ if (!isTransientGatewayMetadataError(params.error)) {
246
+ throw params.error;
247
+ }
248
+ const message = formatErrorMessage(params.error);
249
+ const now = Date.now();
250
+ if (params.runtime) {
251
+ const previous = gatewayMetadataFallbackLogLastAt.get(params.runtime);
252
+ if (
253
+ previous === undefined ||
254
+ now - previous >= DISCORD_GATEWAY_METADATA_FALLBACK_LOG_INTERVAL_MS
255
+ ) {
256
+ params.runtime.log?.(
257
+ `discord: gateway metadata lookup failed transiently; using default gateway url (${message})`,
258
+ );
259
+ gatewayMetadataFallbackLogLastAt.set(params.runtime, now);
260
+ }
261
+ }
262
+ return {
263
+ info: createDefaultGatewayInfo(),
264
+ usedFallback: true,
265
+ };
266
+ }
267
+
268
+ export async function fetchDiscordGatewayMetadataDirect(
269
+ input: string,
270
+ init?: DiscordGatewayFetchInit,
271
+ capture?: false | { flowId: string; meta: Record<string, unknown> },
272
+ ): Promise<Response> {
273
+ const guarded = await fetchWithSsrFGuard({
274
+ url: resolveFetchInputUrl(input),
275
+ init: init as RequestInit,
276
+ policy: { allowedHostnames: [DISCORD_API_HOST] },
277
+ capture: false,
278
+ auditContext: "discord.gateway.metadata",
279
+ });
280
+ let response: Response;
281
+ try {
282
+ response = await materializeGuardedResponse(guarded.response);
283
+ } finally {
284
+ await guarded.release();
285
+ }
286
+ if (capture) {
287
+ captureHttpExchange({
288
+ url: input,
289
+ method: (init?.method as string | undefined) ?? "GET",
290
+ requestHeaders: init?.headers as Headers | Record<string, string> | undefined,
291
+ requestBody: (init as RequestInit & { body?: BodyInit | null })?.body ?? null,
292
+ response,
293
+ flowId: capture.flowId,
294
+ meta: capture.meta,
295
+ });
296
+ }
297
+ return response;
298
+ }
@@ -0,0 +1,302 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { Agent as HttpsAgent } from "node:https";
3
+ import * as httpsProxyAgent from "https-proxy-agent";
4
+ import type { DiscordAccountConfig } from "autobot/plugin-sdk/config-contracts";
5
+ import {
6
+ captureWsEvent,
7
+ resolveEffectiveDebugProxyUrl,
8
+ resolveDebugProxySettings,
9
+ } from "autobot/plugin-sdk/proxy-capture";
10
+ import { danger } from "autobot/plugin-sdk/runtime-env";
11
+ import type { RuntimeEnv } from "autobot/plugin-sdk/runtime-env";
12
+ import * as ws from "ws";
13
+ import * as discordGateway from "../internal/gateway.js";
14
+ import { createDiscordDnsLookup } from "../network-config.js";
15
+ import { validateDiscordProxyUrl } from "../proxy-fetch.js";
16
+ import { resolveDiscordVoiceEnabled } from "../voice/config.js";
17
+ import { DISCORD_GATEWAY_TRANSPORT_ACTIVITY_EVENT } from "./gateway-handle.js";
18
+ import {
19
+ fetchDiscordGatewayInfoWithTimeout,
20
+ fetchDiscordGatewayMetadataDirect,
21
+ resolveDiscordGatewayInfoTimeoutMs,
22
+ resolveGatewayInfoWithFallback,
23
+ type DiscordGatewayFetch,
24
+ type DiscordGatewayFetchInit,
25
+ } from "./gateway-metadata.js";
26
+
27
+ export {
28
+ parseDiscordGatewayInfoBody,
29
+ resolveDiscordGatewayInfoTimeoutMs,
30
+ } from "./gateway-metadata.js";
31
+
32
+ const DISCORD_GATEWAY_HANDSHAKE_TIMEOUT_MS = 30_000;
33
+ const discordDnsLookup = createDiscordDnsLookup();
34
+
35
+ type DiscordGatewayWebSocketCtor = new (
36
+ url: string,
37
+ options?: { agent?: unknown; handshakeTimeout?: number },
38
+ ) => ws.WebSocket;
39
+ type DiscordGatewayWebSocketAgent =
40
+ | InstanceType<typeof HttpsAgent>
41
+ | InstanceType<typeof httpsProxyAgent.HttpsProxyAgent<string>>;
42
+ const registrationPromises = new WeakMap<discordGateway.GatewayPlugin, Promise<void>>();
43
+ type DiscordGatewayClient = Parameters<discordGateway.GatewayPlugin["registerClient"]>[0];
44
+ type GatewayPluginTestingOptions = {
45
+ registerClient?: (
46
+ plugin: discordGateway.GatewayPlugin,
47
+ client: DiscordGatewayClient,
48
+ ) => Promise<void>;
49
+ webSocketCtor?: DiscordGatewayWebSocketCtor;
50
+ };
51
+ type CreateDiscordGatewayPluginTestingOptions = GatewayPluginTestingOptions & {
52
+ HttpsProxyAgentCtor?: typeof httpsProxyAgent.HttpsProxyAgent;
53
+ };
54
+ type DiscordGatewayRegistrationState = {
55
+ client?: DiscordGatewayClient;
56
+ ws?: unknown;
57
+ isConnecting?: boolean;
58
+ };
59
+
60
+ function assignGatewayClient(
61
+ plugin: discordGateway.GatewayPlugin,
62
+ client: DiscordGatewayClient,
63
+ ): void {
64
+ (plugin as unknown as DiscordGatewayRegistrationState).client = client;
65
+ }
66
+
67
+ function hasGatewaySocketStarted(plugin: discordGateway.GatewayPlugin): boolean {
68
+ const state = plugin as unknown as DiscordGatewayRegistrationState;
69
+ return state.ws != null || state.isConnecting === true;
70
+ }
71
+
72
+ type ResolveDiscordGatewayIntentsParams = {
73
+ intentsConfig?: import("autobot/plugin-sdk/config-contracts").DiscordIntentsConfig;
74
+ voiceEnabled?: boolean;
75
+ };
76
+
77
+ export function resolveDiscordGatewayIntents(params?: ResolveDiscordGatewayIntentsParams): number {
78
+ const intentsConfig = params?.intentsConfig;
79
+ const voiceEnabled = params?.voiceEnabled;
80
+ const voiceStatesEnabled = intentsConfig?.voiceStates ?? voiceEnabled ?? false;
81
+ let intents =
82
+ discordGateway.GatewayIntents.Guilds |
83
+ discordGateway.GatewayIntents.GuildMessages |
84
+ discordGateway.GatewayIntents.MessageContent |
85
+ discordGateway.GatewayIntents.DirectMessages |
86
+ discordGateway.GatewayIntents.GuildMessageReactions |
87
+ discordGateway.GatewayIntents.DirectMessageReactions;
88
+ if (voiceStatesEnabled) {
89
+ intents |= discordGateway.GatewayIntents.GuildVoiceStates;
90
+ }
91
+ if (intentsConfig?.presence) {
92
+ intents |= discordGateway.GatewayIntents.GuildPresences;
93
+ }
94
+ if (intentsConfig?.guildMembers) {
95
+ intents |= discordGateway.GatewayIntents.GuildMembers;
96
+ }
97
+ return intents;
98
+ }
99
+
100
+ function createGatewayPlugin(params: {
101
+ options: {
102
+ reconnect: { maxAttempts: number };
103
+ intents: number;
104
+ autoInteractions: boolean;
105
+ };
106
+ gatewayInfoTimeoutMs: number;
107
+ fetchImpl: DiscordGatewayFetch;
108
+ fetchInit?: DiscordGatewayFetchInit;
109
+ wsAgent?: DiscordGatewayWebSocketAgent;
110
+ runtime?: RuntimeEnv;
111
+ testing?: GatewayPluginTestingOptions;
112
+ }): discordGateway.GatewayPlugin {
113
+ class AutoBotGatewayPlugin extends discordGateway.GatewayPlugin {
114
+ private gatewayInfoUsedFallback = false;
115
+
116
+ constructor() {
117
+ super(params.options);
118
+ }
119
+
120
+ override registerClient(client: DiscordGatewayClient) {
121
+ const registration = this.registerClientInternal(client);
122
+ // Client construction starts plugin hooks without awaiting them. Mark the
123
+ // promise handled immediately, then let startup await the original promise.
124
+ registration.catch(() => {});
125
+ registrationPromises.set(this, registration);
126
+ return registration;
127
+ }
128
+
129
+ private async registerClientInternal(client: DiscordGatewayClient) {
130
+ // Publish the client reference before the metadata fetch can yield, so an external
131
+ // connect()->identify() cannot silently drop IDENTIFY (#52372).
132
+ assignGatewayClient(this, client);
133
+
134
+ if (!this.gatewayInfo || this.gatewayInfoUsedFallback) {
135
+ const resolved = await fetchDiscordGatewayInfoWithTimeout({
136
+ token: client.options.token,
137
+ fetchImpl: params.fetchImpl,
138
+ fetchInit: params.fetchInit,
139
+ timeoutMs: params.gatewayInfoTimeoutMs,
140
+ })
141
+ .then((info) => ({
142
+ info,
143
+ usedFallback: false,
144
+ }))
145
+ .catch((error) => resolveGatewayInfoWithFallback({ runtime: params.runtime, error }));
146
+ this.gatewayInfo = resolved.info;
147
+ this.gatewayInfoUsedFallback = resolved.usedFallback;
148
+ }
149
+ if (params.testing?.registerClient) {
150
+ await params.testing.registerClient(this, client);
151
+ return;
152
+ }
153
+ // If the lifecycle timeout already started a socket while metadata was
154
+ // loading, do not register again; it would close that socket and open another one.
155
+ if (hasGatewaySocketStarted(this)) {
156
+ return;
157
+ }
158
+ return super.registerClient(client);
159
+ }
160
+
161
+ override createWebSocket(url: string) {
162
+ if (!url) {
163
+ throw new Error("Gateway URL is required");
164
+ }
165
+ const wsFlowId = randomUUID();
166
+ // Avoid Node's undici-backed global WebSocket here. We have seen late
167
+ // close-path crashes during Discord gateway teardown; the ws transport is
168
+ // already our proxy path and behaves predictably for lifecycle cleanup.
169
+ const WebSocketCtor = params.testing?.webSocketCtor ?? ws.default;
170
+ const socket = new WebSocketCtor(url, {
171
+ handshakeTimeout: DISCORD_GATEWAY_HANDSHAKE_TIMEOUT_MS,
172
+ ...(params.wsAgent ? { agent: params.wsAgent } : {}),
173
+ });
174
+ const emitTransportActivity = () => {
175
+ if ((this as unknown as { ws?: unknown }).ws !== socket) {
176
+ return;
177
+ }
178
+ this.emitter.emit(DISCORD_GATEWAY_TRANSPORT_ACTIVITY_EVENT, { at: Date.now() });
179
+ };
180
+ captureWsEvent({
181
+ url,
182
+ direction: "local",
183
+ kind: "ws-open",
184
+ flowId: wsFlowId,
185
+ meta: { subsystem: "discord-gateway" },
186
+ });
187
+ socket.on?.("message", (data: unknown) => {
188
+ emitTransportActivity();
189
+ captureWsEvent({
190
+ url,
191
+ direction: "inbound",
192
+ kind: "ws-frame",
193
+ flowId: wsFlowId,
194
+ payload: Buffer.isBuffer(data) ? data : Buffer.from(String(data)),
195
+ meta: { subsystem: "discord-gateway" },
196
+ });
197
+ });
198
+ socket.on?.("close", (code: number, reason: Buffer) => {
199
+ captureWsEvent({
200
+ url,
201
+ direction: "local",
202
+ kind: "ws-close",
203
+ flowId: wsFlowId,
204
+ closeCode: code,
205
+ payload: reason,
206
+ meta: { subsystem: "discord-gateway" },
207
+ });
208
+ });
209
+ socket.on?.("error", (error: Error) => {
210
+ captureWsEvent({
211
+ url,
212
+ direction: "local",
213
+ kind: "error",
214
+ flowId: wsFlowId,
215
+ errorText: error.message,
216
+ meta: { subsystem: "discord-gateway" },
217
+ });
218
+ });
219
+ if ("binaryType" in socket) {
220
+ try {
221
+ socket.binaryType = "arraybuffer";
222
+ } catch {
223
+ // Ignore runtimes that expose a readonly binaryType.
224
+ }
225
+ }
226
+ return socket;
227
+ }
228
+ }
229
+
230
+ return new AutoBotGatewayPlugin();
231
+ }
232
+
233
+ function createDiscordGatewayMetadataFetch(debugCaptureEnabled: boolean): DiscordGatewayFetch {
234
+ return (input, init) =>
235
+ fetchDiscordGatewayMetadataDirect(
236
+ input,
237
+ init,
238
+ debugCaptureEnabled
239
+ ? false
240
+ : {
241
+ flowId: randomUUID(),
242
+ meta: { subsystem: "discord-gateway-metadata" },
243
+ },
244
+ );
245
+ }
246
+
247
+ export function waitForDiscordGatewayPluginRegistration(
248
+ plugin: unknown,
249
+ ): Promise<void> | undefined {
250
+ if (typeof plugin !== "object" || plugin === null) {
251
+ return undefined;
252
+ }
253
+ return registrationPromises.get(plugin as discordGateway.GatewayPlugin);
254
+ }
255
+
256
+ export function createDiscordGatewayPlugin(params: {
257
+ discordConfig: DiscordAccountConfig;
258
+ runtime: RuntimeEnv;
259
+ testing?: CreateDiscordGatewayPluginTestingOptions;
260
+ }): discordGateway.GatewayPlugin {
261
+ const intents = resolveDiscordGatewayIntents({
262
+ intentsConfig: params.discordConfig?.intents,
263
+ voiceEnabled: resolveDiscordVoiceEnabled(params.discordConfig?.voice),
264
+ });
265
+ const proxy = resolveEffectiveDebugProxyUrl(params.discordConfig?.proxy);
266
+ const debugProxySettings = resolveDebugProxySettings();
267
+ const gatewayInfoTimeoutMs = resolveDiscordGatewayInfoTimeoutMs({
268
+ configuredTimeoutMs: params.discordConfig?.gatewayInfoTimeoutMs,
269
+ env: process.env,
270
+ });
271
+ let fetchImpl = createDiscordGatewayMetadataFetch(debugProxySettings.enabled);
272
+ let wsAgent: DiscordGatewayWebSocketAgent = new HttpsAgent({
273
+ lookup: discordDnsLookup,
274
+ });
275
+
276
+ if (proxy) {
277
+ try {
278
+ validateDiscordProxyUrl(proxy);
279
+ const HttpsProxyAgentCtor =
280
+ params.testing?.HttpsProxyAgentCtor ?? httpsProxyAgent.HttpsProxyAgent;
281
+ wsAgent = new HttpsProxyAgentCtor<string>(proxy);
282
+ params.runtime.log?.("discord: gateway proxy enabled");
283
+ } catch (err) {
284
+ params.runtime.error?.(danger(`discord: invalid gateway proxy: ${String(err)}`));
285
+ fetchImpl = (input, init) => fetchDiscordGatewayMetadataDirect(input, init, false);
286
+ }
287
+ }
288
+
289
+ return createGatewayPlugin({
290
+ options: {
291
+ reconnect: { maxAttempts: 50 },
292
+ intents,
293
+ // AutoBot registers its own async interaction listener.
294
+ autoInteractions: false,
295
+ },
296
+ gatewayInfoTimeoutMs,
297
+ fetchImpl,
298
+ runtime: params.runtime,
299
+ testing: params.testing,
300
+ ...(wsAgent ? { wsAgent } : {}),
301
+ });
302
+ }
@@ -0,0 +1,37 @@
1
+ import type { GatewayPlugin } from "../internal/gateway.js";
2
+
3
+ /**
4
+ * Module-level registry of active Discord GatewayPlugin instances.
5
+ * Bridges the gap between agent tool handlers (which only have REST access)
6
+ * and the gateway WebSocket (needed for operations like updatePresence).
7
+ * Follows the same pattern as presence-cache.ts.
8
+ */
9
+ const gatewayRegistry = new Map<string, GatewayPlugin>();
10
+
11
+ // Sentinel key for the default (unnamed) account. Uses a prefix that cannot
12
+ // collide with user-configured account IDs.
13
+ const DEFAULT_ACCOUNT_KEY = "\0__default__";
14
+
15
+ function resolveAccountKey(accountId?: string): string {
16
+ return accountId ?? DEFAULT_ACCOUNT_KEY;
17
+ }
18
+
19
+ /** Register a GatewayPlugin instance for an account. */
20
+ export function registerGateway(accountId: string | undefined, gateway: GatewayPlugin): void {
21
+ gatewayRegistry.set(resolveAccountKey(accountId), gateway);
22
+ }
23
+
24
+ /** Unregister a GatewayPlugin instance for an account. */
25
+ export function unregisterGateway(accountId?: string): void {
26
+ gatewayRegistry.delete(resolveAccountKey(accountId));
27
+ }
28
+
29
+ /** Get the GatewayPlugin for an account. Returns undefined if not registered. */
30
+ export function getGateway(accountId?: string): GatewayPlugin | undefined {
31
+ return gatewayRegistry.get(resolveAccountKey(accountId));
32
+ }
33
+
34
+ /** Clear all registered gateways (for testing). */
35
+ export function clearGateways(): void {
36
+ gatewayRegistry.clear();
37
+ }