@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,479 @@
1
+ import { EventEmitter } from "node:events";
2
+ import {
3
+ GatewayCloseCodes,
4
+ GatewayDispatchEvents,
5
+ GatewayIntentBits,
6
+ GatewayOpcodes,
7
+ type APIGatewayBotInfo,
8
+ type GatewayDispatchPayload,
9
+ type GatewayHeartbeat,
10
+ type GatewayIdentify,
11
+ type GatewayPresenceUpdateData,
12
+ type GatewayReceivePayload,
13
+ type GatewaySendPayload,
14
+ type GatewayVoiceStateUpdateData,
15
+ } from "discord-api-types/v10";
16
+ import * as ws from "ws";
17
+ import { Plugin, type Client } from "./client.js";
18
+ import { canResumeAfterGatewayClose, isFatalGatewayCloseCode } from "./gateway-close-codes.js";
19
+ import { dispatchVoiceGatewayEvent, mapGatewayDispatchData } from "./gateway-dispatch.js";
20
+ import { sharedGatewayIdentifyLimiter } from "./gateway-identify-limiter.js";
21
+ import { GatewayHeartbeatTimers, GatewayReconnectTimer } from "./gateway-lifecycle.js";
22
+ import { GatewaySendLimiter } from "./gateway-rate-limit.js";
23
+
24
+ export { GatewayCloseCodes };
25
+ export const GatewayIntents = GatewayIntentBits;
26
+ export type Activity = NonNullable<GatewayPresenceUpdateData["activities"]>[number];
27
+ export type UpdatePresenceData = Omit<GatewayPresenceUpdateData, "status"> & {
28
+ status: "online" | "idle" | "dnd" | "invisible" | "offline";
29
+ };
30
+ type UpdateVoiceStateData = GatewayVoiceStateUpdateData;
31
+ type RequestGuildMembersData = {
32
+ guild_id: string;
33
+ query?: string;
34
+ limit: number;
35
+ presences?: boolean;
36
+ user_ids?: string | string[];
37
+ nonce?: string;
38
+ };
39
+ type GatewayPluginOptions = {
40
+ reconnect?: { maxAttempts?: number };
41
+ intents?: number;
42
+ autoInteractions?: boolean;
43
+ shard?: [number, number];
44
+ url?: string;
45
+ };
46
+
47
+ const READY_STATE_OPEN = 1;
48
+ const DEFAULT_GATEWAY_URL = "wss://gateway.discord.gg/";
49
+ const DISCORD_GATEWAY_PAYLOAD_LIMIT_BYTES = 4096;
50
+ const INVALID_SESSION_MIN_DELAY_MS = 1_000;
51
+ const INVALID_SESSION_JITTER_MS = 4_000;
52
+
53
+ function ensureGatewayParams(url: string): string {
54
+ const parsed = new URL(url);
55
+ parsed.searchParams.set("v", parsed.searchParams.get("v") ?? "10");
56
+ parsed.searchParams.set("encoding", parsed.searchParams.get("encoding") ?? "json");
57
+ return parsed.toString();
58
+ }
59
+
60
+ function decodeGatewayMessage(incoming: unknown): GatewayReceivePayload | null {
61
+ const text = Buffer.isBuffer(incoming)
62
+ ? incoming.toString("utf8")
63
+ : incoming instanceof ArrayBuffer
64
+ ? Buffer.from(incoming).toString("utf8")
65
+ : Array.isArray(incoming)
66
+ ? Buffer.concat(incoming.map((entry) => Buffer.from(entry))).toString("utf8")
67
+ : String(incoming);
68
+ try {
69
+ return JSON.parse(text) as GatewayReceivePayload;
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ export class GatewayPlugin extends Plugin {
76
+ readonly id = "gateway";
77
+ protected client?: Client;
78
+ readonly options: Required<Pick<GatewayPluginOptions, "autoInteractions">> & GatewayPluginOptions;
79
+ public ws: ws.WebSocket | null = null;
80
+ public sequence: number | null = null;
81
+ public lastHeartbeatAck = true;
82
+ public emitter = new EventEmitter();
83
+ public shardId?: number;
84
+ public totalShards?: number;
85
+ protected gatewayInfo?: APIGatewayBotInfo;
86
+ public isConnected = false;
87
+ private sessionId: string | null = null;
88
+ private resumeGatewayUrl: string | null = null;
89
+ private reconnectAttempts = 0;
90
+ private shouldReconnect = false;
91
+ private isConnecting = false;
92
+ private readonly heartbeatTimers = new GatewayHeartbeatTimers();
93
+ private readonly reconnectTimer = new GatewayReconnectTimer();
94
+ private outboundLimiter = new GatewaySendLimiter(
95
+ (payload) => this.sendSerializedGatewayEvent(payload),
96
+ (error) => this.emitter.emit("error", error),
97
+ );
98
+
99
+ constructor(options: GatewayPluginOptions, gatewayInfo?: APIGatewayBotInfo) {
100
+ super();
101
+ this.options = {
102
+ ...options,
103
+ reconnect: { maxAttempts: 50, ...options.reconnect },
104
+ autoInteractions: options.autoInteractions ?? true,
105
+ intents: options.intents ?? 0,
106
+ };
107
+ this.gatewayInfo = gatewayInfo;
108
+ }
109
+
110
+ get ping(): number | null {
111
+ return null;
112
+ }
113
+
114
+ get heartbeatInterval(): NodeJS.Timeout | undefined {
115
+ return this.heartbeatTimers.heartbeatInterval;
116
+ }
117
+
118
+ set heartbeatInterval(timer: NodeJS.Timeout | undefined) {
119
+ this.heartbeatTimers.heartbeatInterval = timer;
120
+ }
121
+
122
+ get firstHeartbeatTimeout(): NodeJS.Timeout | undefined {
123
+ return this.heartbeatTimers.firstHeartbeatTimeout;
124
+ }
125
+
126
+ set firstHeartbeatTimeout(timer: NodeJS.Timeout | undefined) {
127
+ this.heartbeatTimers.firstHeartbeatTimeout = timer;
128
+ }
129
+
130
+ override async registerClient(client: Client): Promise<void> {
131
+ this.client = client;
132
+ if (this.options.shard) {
133
+ client.shardId = this.options.shard[0];
134
+ client.totalShards = this.options.shard[1];
135
+ this.shardId = this.options.shard[0];
136
+ this.totalShards = this.options.shard[1];
137
+ }
138
+ this.shouldReconnect = true;
139
+ this.connect(false);
140
+ }
141
+
142
+ connect(resume = false): void {
143
+ this.stopReconnectTimer();
144
+ this.stopHeartbeat();
145
+ if (this.isConnecting) {
146
+ return;
147
+ }
148
+ this.shouldReconnect = true;
149
+ this.lastHeartbeatAck = true;
150
+ this.ws?.close(1000, "Reconnecting");
151
+ const baseUrl =
152
+ resume && this.resumeGatewayUrl
153
+ ? this.resumeGatewayUrl
154
+ : (this.gatewayInfo?.url ?? this.options.url ?? DEFAULT_GATEWAY_URL);
155
+ this.ws = this.createWebSocket(ensureGatewayParams(baseUrl));
156
+ this.isConnecting = true;
157
+ this.isConnected = false;
158
+ this.setupWebSocket(resume);
159
+ }
160
+
161
+ disconnect(): void {
162
+ this.shouldReconnect = false;
163
+ this.stopReconnectTimer();
164
+ this.stopHeartbeat();
165
+ this.outboundLimiter.clear();
166
+ this.ws?.close(1000, "Client disconnect");
167
+ this.ws = null;
168
+ this.isConnecting = false;
169
+ this.isConnected = false;
170
+ this.reconnectAttempts = 0;
171
+ }
172
+
173
+ protected createWebSocket(url: string): ws.WebSocket {
174
+ return new ws.WebSocket(url);
175
+ }
176
+
177
+ private setupWebSocket(resume: boolean): void {
178
+ const socket = this.ws;
179
+ if (!socket) {
180
+ return;
181
+ }
182
+ socket.on("open", () => {
183
+ if (socket !== this.ws) {
184
+ return;
185
+ }
186
+ this.isConnecting = false;
187
+ this.emitter.emit("debug", "Gateway websocket opened");
188
+ });
189
+ socket.on("message", (incoming) => {
190
+ if (socket !== this.ws) {
191
+ return;
192
+ }
193
+ const payload = decodeGatewayMessage(incoming);
194
+ if (!payload) {
195
+ this.emitter.emit("error", new Error("Invalid gateway payload"));
196
+ return;
197
+ }
198
+ this.handlePayload(payload, resume, socket);
199
+ });
200
+ socket.on("close", (code) => {
201
+ if (socket !== this.ws) {
202
+ return;
203
+ }
204
+ const closeCode = code as GatewayCloseCodes;
205
+ this.stopHeartbeat();
206
+ this.outboundLimiter.clear();
207
+ this.isConnecting = false;
208
+ this.isConnected = false;
209
+ this.emitter.emit("debug", `Gateway websocket closed: ${code}`);
210
+ if (!this.shouldReconnect) {
211
+ return;
212
+ }
213
+ if (isFatalGatewayCloseCode(closeCode)) {
214
+ this.shouldReconnect = false;
215
+ this.emitter.emit("error", new Error(`Fatal gateway close code: ${code}`));
216
+ return;
217
+ }
218
+ const canResume = canResumeAfterGatewayClose(closeCode);
219
+ if (!canResume) {
220
+ this.resetSessionState();
221
+ }
222
+ this.scheduleReconnect(canResume, closeCode);
223
+ });
224
+ socket.on("error", (error) => {
225
+ if (socket !== this.ws) {
226
+ return;
227
+ }
228
+ this.emitter.emit("error", error);
229
+ });
230
+ }
231
+
232
+ private handlePayload(
233
+ payload: GatewayReceivePayload,
234
+ resume: boolean,
235
+ sourceSocket?: ws.WebSocket,
236
+ ): void {
237
+ if (payload.s !== null && payload.s !== undefined) {
238
+ this.sequence = payload.s;
239
+ }
240
+ switch (payload.op) {
241
+ case GatewayOpcodes.Hello:
242
+ this.startHeartbeat(
243
+ (payload.d as { heartbeat_interval?: number }).heartbeat_interval ?? 45_000,
244
+ );
245
+ if (resume && this.sessionId) {
246
+ this.send(
247
+ {
248
+ op: GatewayOpcodes.Resume,
249
+ d: {
250
+ token: this.client?.options.token ?? "",
251
+ session_id: this.sessionId,
252
+ seq: this.sequence ?? 0,
253
+ },
254
+ } as GatewaySendPayload,
255
+ true,
256
+ );
257
+ } else {
258
+ void this.identifyWithConcurrency(sourceSocket).catch((error: unknown) => {
259
+ this.emitter.emit(
260
+ "error",
261
+ error instanceof Error ? error : new Error(String(error), { cause: error }),
262
+ );
263
+ });
264
+ }
265
+ break;
266
+ case GatewayOpcodes.HeartbeatAck:
267
+ this.lastHeartbeatAck = true;
268
+ break;
269
+ case GatewayOpcodes.Heartbeat:
270
+ this.sendHeartbeat();
271
+ break;
272
+ case GatewayOpcodes.Dispatch:
273
+ void this.handleDispatch(payload).catch((error: unknown) => {
274
+ this.emitter.emit(
275
+ "error",
276
+ error instanceof Error ? error : new Error(String(error), { cause: error }),
277
+ );
278
+ });
279
+ break;
280
+ case GatewayOpcodes.InvalidSession:
281
+ if (!payload.d) {
282
+ this.resetSessionState();
283
+ }
284
+ this.scheduleReconnect(
285
+ payload.d,
286
+ undefined,
287
+ INVALID_SESSION_MIN_DELAY_MS + Math.floor(Math.random() * INVALID_SESSION_JITTER_MS),
288
+ );
289
+ break;
290
+ case GatewayOpcodes.Reconnect:
291
+ this.scheduleReconnect(true);
292
+ break;
293
+ }
294
+ }
295
+
296
+ private startHeartbeat(intervalMs: number): void {
297
+ this.heartbeatTimers.start({
298
+ intervalMs,
299
+ isAcked: () => this.lastHeartbeatAck,
300
+ onHeartbeat: () => this.sendHeartbeat(),
301
+ onAckTimeout: () => {
302
+ this.emitter.emit("error", new Error("Gateway heartbeat ACK timeout"));
303
+ this.scheduleReconnect(true);
304
+ },
305
+ });
306
+ }
307
+
308
+ private stopHeartbeat(): void {
309
+ this.heartbeatTimers.stop();
310
+ }
311
+
312
+ private stopReconnectTimer(): void {
313
+ this.reconnectTimer.stop();
314
+ }
315
+
316
+ private sendHeartbeat(): void {
317
+ if (!this.ws || this.ws.readyState !== READY_STATE_OPEN) {
318
+ return;
319
+ }
320
+ this.lastHeartbeatAck = false;
321
+ this.send({ op: GatewayOpcodes.Heartbeat, d: this.sequence } as GatewayHeartbeat, true);
322
+ }
323
+
324
+ private identify(): void {
325
+ this.send(
326
+ {
327
+ op: GatewayOpcodes.Identify,
328
+ d: {
329
+ token: this.client?.options.token ?? "",
330
+ intents: this.options.intents ?? 0,
331
+ properties: { os: process.platform, browser: "autobot", device: "autobot" },
332
+ shard: this.options.shard,
333
+ },
334
+ } as GatewayIdentify,
335
+ true,
336
+ );
337
+ }
338
+
339
+ private async identifyWithConcurrency(sourceSocket?: ws.WebSocket): Promise<void> {
340
+ await sharedGatewayIdentifyLimiter.wait({
341
+ shardId: this.shardId,
342
+ maxConcurrency: this.gatewayInfo?.session_start_limit.max_concurrency,
343
+ });
344
+ const socket = sourceSocket ?? this.ws;
345
+ if (!socket || socket !== this.ws) {
346
+ return;
347
+ }
348
+ if (socket.readyState !== READY_STATE_OPEN) {
349
+ this.scheduleReconnect(false);
350
+ return;
351
+ }
352
+ this.identify();
353
+ }
354
+
355
+ send(payload: GatewaySendPayload | GatewayReceivePayload, skipRateLimit = false): void {
356
+ if (!this.ws || this.ws.readyState !== READY_STATE_OPEN) {
357
+ throw new Error("Discord gateway socket is not open");
358
+ }
359
+ const serialized = JSON.stringify(payload);
360
+ const payloadSize =
361
+ typeof Buffer !== "undefined"
362
+ ? Buffer.byteLength(serialized, "utf8")
363
+ : new TextEncoder().encode(serialized).byteLength;
364
+ if (payloadSize > DISCORD_GATEWAY_PAYLOAD_LIMIT_BYTES) {
365
+ throw new Error(
366
+ `Discord gateway payload exceeds ${DISCORD_GATEWAY_PAYLOAD_LIMIT_BYTES}-byte limit`,
367
+ );
368
+ }
369
+ this.outboundLimiter.send(serialized, { critical: skipRateLimit });
370
+ }
371
+
372
+ private sendSerializedGatewayEvent(serialized: string): void {
373
+ if (!this.ws || this.ws.readyState !== READY_STATE_OPEN) {
374
+ throw new Error("Discord gateway socket is not open");
375
+ }
376
+ this.ws.send(serialized);
377
+ }
378
+
379
+ private async handleDispatch(payload: GatewayDispatchPayload): Promise<void> {
380
+ if (!this.client || !payload.t) {
381
+ return;
382
+ }
383
+ if (payload.t === GatewayDispatchEvents.Ready) {
384
+ const ready = payload.d as { session_id?: string; resume_gateway_url?: string };
385
+ this.sessionId = ready.session_id ?? null;
386
+ this.resumeGatewayUrl = ready.resume_gateway_url ?? null;
387
+ this.reconnectAttempts = 0;
388
+ this.isConnected = true;
389
+ }
390
+ if (payload.t === GatewayDispatchEvents.Resumed) {
391
+ this.reconnectAttempts = 0;
392
+ this.isConnected = true;
393
+ }
394
+ dispatchVoiceGatewayEvent(this.client, payload.t, payload.d);
395
+ const data = mapGatewayDispatchData(this.client, payload.t, payload.d);
396
+ await this.client.dispatchGatewayEvent(payload.t, data);
397
+ if (payload.t === GatewayDispatchEvents.InteractionCreate && this.options.autoInteractions) {
398
+ await this.client.handleInteraction(payload.d);
399
+ }
400
+ }
401
+
402
+ private resetSessionState(): void {
403
+ this.sessionId = null;
404
+ this.resumeGatewayUrl = null;
405
+ this.sequence = null;
406
+ }
407
+
408
+ private scheduleReconnect(resume: boolean, closeCode?: number, minDelayMs = 0): void {
409
+ if (!this.shouldReconnect) {
410
+ return;
411
+ }
412
+ this.stopHeartbeat();
413
+ this.stopReconnectTimer();
414
+ this.ws?.close();
415
+ this.ws = null;
416
+ this.isConnecting = false;
417
+ this.isConnected = false;
418
+ this.outboundLimiter.clear();
419
+ this.reconnectAttempts += 1;
420
+ if (this.reconnectAttempts > (this.options.reconnect?.maxAttempts ?? 50)) {
421
+ const maxAttempts = this.options.reconnect?.maxAttempts ?? 50;
422
+ this.emitter.emit(
423
+ "error",
424
+ new Error(
425
+ `Max reconnect attempts (${maxAttempts}) reached${closeCode !== undefined ? ` after close code ${closeCode}` : ""}`,
426
+ ),
427
+ );
428
+ return;
429
+ }
430
+ const delay = Math.max(
431
+ minDelayMs,
432
+ Math.min(30_000, 1_000 * 2 ** Math.min(this.reconnectAttempts, 5)),
433
+ );
434
+ this.reconnectTimer.schedule(delay, () => {
435
+ this.connect(resume);
436
+ });
437
+ }
438
+
439
+ updatePresence(data: UpdatePresenceData): void {
440
+ this.send({ op: GatewayOpcodes.PresenceUpdate, d: data } as GatewaySendPayload);
441
+ }
442
+
443
+ updateVoiceState(data: UpdateVoiceStateData): void {
444
+ this.send({ op: GatewayOpcodes.VoiceStateUpdate, d: data } as GatewaySendPayload, true);
445
+ }
446
+
447
+ requestGuildMembers(data: RequestGuildMembersData): void {
448
+ if (!this.hasIntent(GatewayIntentBits.GuildMembers)) {
449
+ throw new Error("GUILD_MEMBERS intent is required for requestGuildMembers");
450
+ }
451
+ if (data.presences && !this.hasIntent(GatewayIntentBits.GuildPresences)) {
452
+ throw new Error("GUILD_PRESENCES intent is required when requesting presences");
453
+ }
454
+ if (!data.query && data.query !== "" && !data.user_ids) {
455
+ throw new Error("Either query or user_ids is required for requestGuildMembers");
456
+ }
457
+ this.send({ op: GatewayOpcodes.RequestGuildMembers, d: data } as GatewaySendPayload);
458
+ }
459
+
460
+ getRateLimitStatus() {
461
+ return this.outboundLimiter.getStatus();
462
+ }
463
+
464
+ getIntentsInfo() {
465
+ const intents = this.options.intents ?? 0;
466
+ return {
467
+ intents,
468
+ hasGuilds: this.hasIntent(GatewayIntentBits.Guilds),
469
+ hasGuildMembers: this.hasIntent(GatewayIntentBits.GuildMembers),
470
+ hasGuildPresences: this.hasIntent(GatewayIntentBits.GuildPresences),
471
+ hasGuildMessages: this.hasIntent(GatewayIntentBits.GuildMessages),
472
+ hasMessageContent: this.hasIntent(GatewayIntentBits.MessageContent),
473
+ };
474
+ }
475
+
476
+ hasIntent(intent: number): boolean {
477
+ return Boolean((this.options.intents ?? 0) & intent);
478
+ }
479
+ }
@@ -0,0 +1,162 @@
1
+ import { InteractionType, type APIInteraction } from "discord-api-types/v10";
2
+ import {
3
+ type BaseCommand,
4
+ deferCommandInteractionIfNeeded,
5
+ resolveFocusedCommandOptionAutocompleteHandler,
6
+ } from "./commands.js";
7
+ import {
8
+ AutocompleteInteraction,
9
+ BaseComponentInteraction,
10
+ CommandInteraction,
11
+ ModalInteraction,
12
+ createInteraction,
13
+ parseComponentInteractionData,
14
+ type RawInteraction,
15
+ } from "./interactions.js";
16
+
17
+ type DispatchComponent = {
18
+ defer: boolean | ((interaction: BaseComponentInteraction) => boolean);
19
+ ephemeral: boolean | ((interaction: BaseComponentInteraction) => boolean);
20
+ run(interaction: BaseComponentInteraction, data: Record<string, unknown>): unknown;
21
+ customIdParser(id: string): { data: Record<string, unknown> };
22
+ };
23
+
24
+ type DispatchModal = {
25
+ run(interaction: ModalInteraction, data: Record<string, unknown>): unknown;
26
+ customIdParser(id: string): { data: Record<string, unknown> };
27
+ };
28
+
29
+ type DispatchClient = Parameters<typeof createInteraction>[0] & {
30
+ commands: BaseCommand[];
31
+ componentHandler: {
32
+ resolve(customId: string, options?: { componentType?: number }): DispatchComponent | undefined;
33
+ resolveOneOffComponent(params: {
34
+ channelId?: string;
35
+ customId: string;
36
+ messageId?: string;
37
+ values?: string[];
38
+ }): boolean;
39
+ };
40
+ modalHandler: { resolve(customId: string): DispatchModal | undefined };
41
+ };
42
+
43
+ export async function dispatchInteraction(
44
+ client: DispatchClient,
45
+ rawData: APIInteraction,
46
+ ): Promise<void> {
47
+ const interaction = createInteraction(client, rawData as RawInteraction);
48
+ if (rawData.type === InteractionType.ApplicationCommandAutocomplete) {
49
+ const command = client.commands.find((entry) => entry.name === readInteractionName(rawData));
50
+ if (!command) {
51
+ return;
52
+ }
53
+ const autocompleteInteraction = interaction as AutocompleteInteraction;
54
+ const optionAutocomplete = resolveFocusedCommandOptionAutocompleteHandler(
55
+ command,
56
+ autocompleteInteraction,
57
+ );
58
+ if (optionAutocomplete) {
59
+ await optionAutocomplete(autocompleteInteraction);
60
+ return;
61
+ }
62
+ if ("autocomplete" in command) {
63
+ await (
64
+ command as { autocomplete: (interaction: AutocompleteInteraction) => Promise<void> }
65
+ ).autocomplete(autocompleteInteraction);
66
+ }
67
+ return;
68
+ }
69
+ if (rawData.type === InteractionType.ApplicationCommand) {
70
+ const command = client.commands.find((entry) => entry.name === readInteractionName(rawData));
71
+ if (command && "run" in command) {
72
+ await deferCommandInteractionIfNeeded(command, interaction as CommandInteraction);
73
+ await (command as { run: (interaction: CommandInteraction) => Promise<void> }).run(
74
+ interaction as CommandInteraction,
75
+ );
76
+ }
77
+ return;
78
+ }
79
+ if (rawData.type === InteractionType.MessageComponent) {
80
+ const customId = readCustomId(rawData);
81
+ if (!customId) {
82
+ return;
83
+ }
84
+ const componentInteraction = interaction as BaseComponentInteraction;
85
+ if (
86
+ client.componentHandler.resolveOneOffComponent({
87
+ channelId: readMessageChannelId(rawData),
88
+ customId,
89
+ messageId: readMessageId(rawData),
90
+ values: readComponentValues(rawData),
91
+ })
92
+ ) {
93
+ await componentInteraction.acknowledge();
94
+ return;
95
+ }
96
+ const component = client.componentHandler.resolve(customId, {
97
+ componentType: (rawData as { data?: { component_type?: number } }).data?.component_type,
98
+ });
99
+ if (component) {
100
+ await deferComponentInteractionIfNeeded(component, componentInteraction);
101
+ await component.run(componentInteraction, parseComponentInteractionData(component, customId));
102
+ }
103
+ return;
104
+ }
105
+ if (rawData.type === InteractionType.ModalSubmit) {
106
+ const customId = readCustomId(rawData);
107
+ if (!customId) {
108
+ return;
109
+ }
110
+ const modal = client.modalHandler.resolve(customId);
111
+ if (modal) {
112
+ await modal.run(interaction as ModalInteraction, modal.customIdParser(customId).data);
113
+ }
114
+ }
115
+ }
116
+
117
+ function resolveConditionalComponentOption(
118
+ value: boolean | ((interaction: BaseComponentInteraction) => boolean),
119
+ interaction: BaseComponentInteraction,
120
+ ): boolean {
121
+ return typeof value === "function" ? value(interaction) : value;
122
+ }
123
+
124
+ async function deferComponentInteractionIfNeeded(
125
+ component: {
126
+ defer: boolean | ((interaction: BaseComponentInteraction) => boolean);
127
+ ephemeral: boolean | ((interaction: BaseComponentInteraction) => boolean);
128
+ },
129
+ interaction: BaseComponentInteraction,
130
+ ): Promise<void> {
131
+ if (!resolveConditionalComponentOption(component.defer, interaction)) {
132
+ return;
133
+ }
134
+ if (resolveConditionalComponentOption(component.ephemeral, interaction)) {
135
+ await interaction.defer({ ephemeral: true });
136
+ return;
137
+ }
138
+ await interaction.acknowledge();
139
+ }
140
+
141
+ function readInteractionName(rawData: APIInteraction): string | undefined {
142
+ return (rawData as { data?: { name?: string } }).data?.name;
143
+ }
144
+
145
+ function readCustomId(rawData: APIInteraction): string | undefined {
146
+ return (rawData as { data?: { custom_id?: string } }).data?.custom_id;
147
+ }
148
+
149
+ function readComponentValues(rawData: APIInteraction): string[] | undefined {
150
+ const values = (rawData as { data?: { values?: unknown } }).data?.values;
151
+ return Array.isArray(values) ? values.map(String) : undefined;
152
+ }
153
+
154
+ function readMessageId(rawData: APIInteraction): string | undefined {
155
+ const messageId = (rawData as { message?: { id?: unknown } }).message?.id;
156
+ return typeof messageId === "string" ? messageId : undefined;
157
+ }
158
+
159
+ function readMessageChannelId(rawData: APIInteraction): string | undefined {
160
+ const channelId = (rawData as { message?: { channel_id?: unknown } }).message?.channel_id;
161
+ return typeof channelId === "string" ? channelId : undefined;
162
+ }