chapterjs 0.1.2 → 0.1.4

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 (320) hide show
  1. package/dist/assets/asset.d.ts +46 -0
  2. package/dist/assets/asset.js +86 -0
  3. package/dist/assets/public.d.ts +19 -0
  4. package/dist/assets/public.js +120 -0
  5. package/dist/cache/cache.d.ts +62 -0
  6. package/dist/cache/cache.js +80 -0
  7. package/dist/cache/store.d.ts +45 -0
  8. package/dist/cache/store.js +53 -0
  9. package/dist/cli/build.d.ts +41 -0
  10. package/dist/cli/build.js +206 -0
  11. package/dist/cli/cli.js +108 -25
  12. package/dist/cli/cluster.d.ts +70 -0
  13. package/dist/cli/cluster.js +213 -0
  14. package/dist/cli/dev.d.ts +18 -1
  15. package/dist/cli/dev.js +252 -198
  16. package/dist/cli/env.d.ts +31 -0
  17. package/dist/cli/env.js +93 -0
  18. package/dist/cli/log.d.ts +13 -0
  19. package/dist/cli/log.js +17 -0
  20. package/dist/cli/node-version.d.ts +8 -0
  21. package/dist/cli/node-version.js +13 -0
  22. package/dist/cli/preflight.d.ts +36 -0
  23. package/dist/cli/preflight.js +122 -0
  24. package/dist/cli/project.d.ts +122 -0
  25. package/dist/cli/project.js +452 -0
  26. package/dist/cli/start.d.ts +25 -0
  27. package/dist/cli/start.js +225 -0
  28. package/dist/commands/autocomplete.d.ts +13 -0
  29. package/dist/commands/autocomplete.js +159 -0
  30. package/dist/commands/command.d.ts +346 -0
  31. package/dist/commands/command.js +28 -0
  32. package/dist/commands/convention.d.ts +64 -0
  33. package/dist/commands/convention.js +464 -0
  34. package/dist/commands/register.d.ts +18 -0
  35. package/dist/commands/register.js +42 -0
  36. package/dist/commands/router.d.ts +20 -0
  37. package/dist/commands/router.js +228 -0
  38. package/dist/commands/tree.d.ts +43 -0
  39. package/dist/commands/tree.js +206 -0
  40. package/dist/commands/twins.d.ts +15 -0
  41. package/dist/commands/twins.js +78 -0
  42. package/dist/components/button.d.ts +79 -0
  43. package/dist/components/button.js +108 -0
  44. package/dist/components/component.d.ts +140 -0
  45. package/dist/components/component.js +39 -0
  46. package/dist/components/convention.d.ts +35 -0
  47. package/dist/components/convention.js +339 -0
  48. package/dist/components/custom-id.d.ts +72 -0
  49. package/dist/components/custom-id.js +128 -0
  50. package/dist/components/embed.d.ts +28 -0
  51. package/dist/components/embed.js +11 -0
  52. package/dist/components/file.d.ts +30 -0
  53. package/dist/components/file.js +52 -0
  54. package/dist/components/instance.d.ts +100 -0
  55. package/dist/components/instance.js +86 -0
  56. package/dist/components/layout.d.ts +103 -0
  57. package/dist/components/layout.js +279 -0
  58. package/dist/components/modal.d.ts +244 -0
  59. package/dist/components/modal.js +399 -0
  60. package/dist/components/options.d.ts +41 -0
  61. package/dist/components/options.js +101 -0
  62. package/dist/components/render.d.ts +12 -0
  63. package/dist/components/render.js +58 -0
  64. package/dist/components/router.d.ts +18 -0
  65. package/dist/components/router.js +256 -0
  66. package/dist/components/select.d.ts +144 -0
  67. package/dist/components/select.js +179 -0
  68. package/dist/core/bot.d.ts +99 -0
  69. package/dist/core/bot.js +174 -0
  70. package/dist/core/memory.d.ts +66 -0
  71. package/dist/core/memory.js +115 -0
  72. package/dist/discord/api.d.ts +112 -0
  73. package/dist/discord/api.js +115 -0
  74. package/dist/discord/cdn.d.ts +67 -0
  75. package/dist/discord/cdn.js +100 -0
  76. package/dist/discord/codes.d.ts +171 -0
  77. package/dist/discord/codes.js +421 -0
  78. package/dist/discord/endpoint.d.ts +56 -0
  79. package/dist/discord/endpoint.js +53 -0
  80. package/dist/discord/endpoints.d.ts +1529 -0
  81. package/dist/discord/endpoints.js +1139 -0
  82. package/dist/discord/formatting.d.ts +67 -0
  83. package/dist/discord/formatting.js +87 -0
  84. package/dist/discord/index.d.ts +10 -0
  85. package/dist/discord/index.js +13 -0
  86. package/dist/discord/intents.d.ts +48 -0
  87. package/dist/discord/intents.js +128 -0
  88. package/dist/discord/permissions.d.ts +94 -0
  89. package/dist/discord/permissions.js +185 -0
  90. package/dist/discord/snowflake.d.ts +21 -0
  91. package/dist/discord/snowflake.js +36 -0
  92. package/dist/discord/types/application-command.d.ts +332 -0
  93. package/dist/discord/types/application-command.js +48 -0
  94. package/dist/discord/types/application-role-connection-metadata.d.ts +42 -0
  95. package/dist/discord/types/application-role-connection-metadata.js +22 -0
  96. package/dist/discord/types/application.d.ts +208 -0
  97. package/dist/discord/types/application.js +48 -0
  98. package/dist/discord/types/audit-log.d.ts +255 -0
  99. package/dist/discord/types/audit-log.js +144 -0
  100. package/dist/discord/types/auto-moderation.d.ts +167 -0
  101. package/dist/discord/types/auto-moderation.js +52 -0
  102. package/dist/discord/types/channel.d.ts +557 -0
  103. package/dist/discord/types/channel.js +88 -0
  104. package/dist/discord/types/common.d.ts +54 -0
  105. package/dist/discord/types/common.js +38 -0
  106. package/dist/discord/types/component.d.ts +775 -0
  107. package/dist/discord/types/component.js +76 -0
  108. package/dist/discord/types/emoji.d.ts +74 -0
  109. package/dist/discord/types/emoji.js +1 -0
  110. package/dist/discord/types/entitlement.d.ts +84 -0
  111. package/dist/discord/types/entitlement.js +22 -0
  112. package/dist/discord/types/gateway-events.d.ts +1148 -0
  113. package/dist/discord/types/gateway-events.js +54 -0
  114. package/dist/discord/types/gateway.d.ts +38 -0
  115. package/dist/discord/types/gateway.js +1 -0
  116. package/dist/discord/types/guild-scheduled-event.d.ts +257 -0
  117. package/dist/discord/types/guild-scheduled-event.js +68 -0
  118. package/dist/discord/types/guild-template.d.ts +51 -0
  119. package/dist/discord/types/guild-template.js +1 -0
  120. package/dist/discord/types/guild.d.ts +910 -0
  121. package/dist/discord/types/guild.js +153 -0
  122. package/dist/discord/types/index.d.ts +31 -0
  123. package/dist/discord/types/index.js +31 -0
  124. package/dist/discord/types/interaction.d.ts +321 -0
  125. package/dist/discord/types/interaction.js +47 -0
  126. package/dist/discord/types/invite.d.ts +158 -0
  127. package/dist/discord/types/invite.js +39 -0
  128. package/dist/discord/types/message.d.ts +834 -0
  129. package/dist/discord/types/message.js +159 -0
  130. package/dist/discord/types/oauth2.d.ts +22 -0
  131. package/dist/discord/types/oauth2.js +1 -0
  132. package/dist/discord/types/permissions.d.ts +183 -0
  133. package/dist/discord/types/permissions.js +118 -0
  134. package/dist/discord/types/poll.d.ts +106 -0
  135. package/dist/discord/types/poll.js +8 -0
  136. package/dist/discord/types/sku.d.ts +47 -0
  137. package/dist/discord/types/sku.js +26 -0
  138. package/dist/discord/types/soundboard.d.ts +71 -0
  139. package/dist/discord/types/soundboard.js +1 -0
  140. package/dist/discord/types/stage-instance.d.ts +58 -0
  141. package/dist/discord/types/stage-instance.js +10 -0
  142. package/dist/discord/types/sticker.d.ts +115 -0
  143. package/dist/discord/types/sticker.js +20 -0
  144. package/dist/discord/types/subscription.d.ts +54 -0
  145. package/dist/discord/types/subscription.js +12 -0
  146. package/dist/discord/types/team.d.ts +55 -0
  147. package/dist/discord/types/team.js +21 -0
  148. package/dist/discord/types/user.d.ts +273 -0
  149. package/dist/discord/types/user.js +83 -0
  150. package/dist/discord/types/voice.d.ts +72 -0
  151. package/dist/discord/types/voice.js +1 -0
  152. package/dist/discord/types/webhook.d.ts +184 -0
  153. package/dist/discord/types/webhook.js +12 -0
  154. package/dist/events/convention.d.ts +25 -0
  155. package/dist/events/convention.js +97 -0
  156. package/dist/events/event.d.ts +8 -0
  157. package/dist/events/event.js +20 -0
  158. package/dist/events/registry.d.ts +706 -0
  159. package/dist/events/registry.js +839 -0
  160. package/dist/events/router.d.ts +45 -0
  161. package/dist/events/router.js +133 -0
  162. package/dist/events/types.d.ts +7 -0
  163. package/dist/events/types.js +48 -0
  164. package/dist/gateway/errors.d.ts +18 -0
  165. package/dist/gateway/errors.js +47 -0
  166. package/dist/gateway/gateway.d.ts +64 -0
  167. package/dist/gateway/gateway.js +117 -0
  168. package/dist/gateway/identify-queue.d.ts +25 -0
  169. package/dist/gateway/identify-queue.js +29 -0
  170. package/dist/gateway/shard.d.ts +94 -0
  171. package/dist/gateway/shard.js +414 -0
  172. package/dist/gateway/state.d.ts +11 -0
  173. package/dist/gateway/state.js +184 -0
  174. package/dist/index.d.ts +55 -18
  175. package/dist/index.js +38 -12
  176. package/dist/interactions/cooldown.d.ts +33 -0
  177. package/dist/interactions/cooldown.js +74 -0
  178. package/dist/interactions/dispatch.d.ts +91 -0
  179. package/dist/interactions/dispatch.js +110 -0
  180. package/dist/loader/generated.d.ts +37 -0
  181. package/dist/loader/generated.js +114 -0
  182. package/dist/loader/hot.d.ts +13 -0
  183. package/dist/loader/hot.js +85 -0
  184. package/dist/loader/loader.d.ts +72 -0
  185. package/dist/loader/loader.js +135 -0
  186. package/dist/loader/locate.d.ts +10 -0
  187. package/dist/loader/locate.js +36 -0
  188. package/dist/loader/watch.d.ts +17 -0
  189. package/dist/loader/watch.js +66 -0
  190. package/dist/main.d.ts +3 -0
  191. package/dist/main.js +6 -0
  192. package/dist/messages/commands.d.ts +23 -0
  193. package/dist/messages/commands.js +123 -0
  194. package/dist/messages/convention.d.ts +89 -0
  195. package/dist/messages/convention.js +347 -0
  196. package/dist/messages/messages.d.ts +219 -0
  197. package/dist/messages/messages.js +30 -0
  198. package/dist/messages/phrases.d.ts +23 -0
  199. package/dist/messages/phrases.js +44 -0
  200. package/dist/messages/translate.d.ts +100 -0
  201. package/dist/messages/translate.js +249 -0
  202. package/dist/presence/convention.d.ts +26 -0
  203. package/dist/presence/convention.js +133 -0
  204. package/dist/presence/presence.d.ts +57 -0
  205. package/dist/presence/presence.js +27 -0
  206. package/dist/rest/errors.d.ts +55 -0
  207. package/dist/rest/errors.js +92 -0
  208. package/dist/rest/rate-limiter.d.ts +46 -0
  209. package/dist/rest/rate-limiter.js +153 -0
  210. package/dist/rest/rest.d.ts +63 -0
  211. package/dist/rest/rest.js +194 -0
  212. package/dist/structures/base.d.ts +48 -0
  213. package/dist/structures/base.js +84 -0
  214. package/dist/structures/channel.d.ts +380 -0
  215. package/dist/structures/channel.js +627 -0
  216. package/dist/structures/context.d.ts +30 -0
  217. package/dist/structures/context.js +7 -0
  218. package/dist/structures/emoji.d.ts +46 -0
  219. package/dist/structures/emoji.js +65 -0
  220. package/dist/structures/entities.d.ts +62 -0
  221. package/dist/structures/entities.js +226 -0
  222. package/dist/structures/guild.d.ts +348 -0
  223. package/dist/structures/guild.js +466 -0
  224. package/dist/structures/interaction.d.ts +197 -0
  225. package/dist/structures/interaction.js +396 -0
  226. package/dist/structures/invite.d.ts +38 -0
  227. package/dist/structures/invite.js +57 -0
  228. package/dist/structures/known.d.ts +28 -0
  229. package/dist/structures/known.js +58 -0
  230. package/dist/structures/member.d.ts +140 -0
  231. package/dist/structures/member.js +260 -0
  232. package/dist/structures/message.d.ts +197 -0
  233. package/dist/structures/message.js +268 -0
  234. package/dist/structures/payload.d.ts +91 -0
  235. package/dist/structures/payload.js +184 -0
  236. package/dist/structures/role.d.ts +69 -0
  237. package/dist/structures/role.js +112 -0
  238. package/dist/structures/user.d.ts +59 -0
  239. package/dist/structures/user.js +94 -0
  240. package/dist/structures/webhook.d.ts +46 -0
  241. package/dist/structures/webhook.js +79 -0
  242. package/dist/tasks/convention.d.ts +26 -0
  243. package/dist/tasks/convention.js +78 -0
  244. package/dist/tasks/schedule.d.ts +54 -0
  245. package/dist/tasks/schedule.js +236 -0
  246. package/dist/tasks/scheduler.d.ts +33 -0
  247. package/dist/tasks/scheduler.js +107 -0
  248. package/dist/tasks/task.d.ts +76 -0
  249. package/dist/tasks/task.js +28 -0
  250. package/dist/util/case.d.ts +13 -0
  251. package/dist/util/case.js +54 -0
  252. package/dist/util/duration.d.ts +8 -0
  253. package/dist/util/duration.js +29 -0
  254. package/dist/util/websocket.d.ts +9 -0
  255. package/dist/util/websocket.js +31 -0
  256. package/dist/voice/connection.d.ts +90 -0
  257. package/dist/voice/connection.js +494 -0
  258. package/dist/voice/dave.d.ts +109 -0
  259. package/dist/voice/dave.js +245 -0
  260. package/dist/voice/opus.d.ts +19 -0
  261. package/dist/voice/opus.js +273 -0
  262. package/dist/voice/session.d.ts +55 -0
  263. package/dist/voice/session.js +405 -0
  264. package/dist/voice/source.d.ts +22 -0
  265. package/dist/voice/source.js +195 -0
  266. package/dist/voice/transport.d.ts +43 -0
  267. package/dist/voice/transport.js +140 -0
  268. package/dist/voice/usage.d.ts +4 -0
  269. package/dist/voice/usage.js +33 -0
  270. package/package.json +26 -7
  271. package/vendor/dave/LICENSE +231 -0
  272. package/vendor/dave/libdave.d.mts +99 -0
  273. package/vendor/dave/libdave.mjs +2 -0
  274. package/vendor/dave/libdave.wasm +0 -0
  275. package/dist/components.d.ts +0 -97
  276. package/dist/components.js +0 -260
  277. package/dist/context.d.ts +0 -72
  278. package/dist/context.js +0 -273
  279. package/dist/discord/client.d.ts +0 -35
  280. package/dist/discord/client.js +0 -66
  281. package/dist/discord/constants.d.ts +0 -49
  282. package/dist/discord/constants.js +0 -61
  283. package/dist/discord/events.d.ts +0 -129
  284. package/dist/discord/events.js +0 -102
  285. package/dist/discord/gateway.d.ts +0 -61
  286. package/dist/discord/gateway.js +0 -320
  287. package/dist/discord/payloads.d.ts +0 -275
  288. package/dist/discord/payloads.js +0 -3
  289. package/dist/discord/presence.d.ts +0 -42
  290. package/dist/discord/presence.js +0 -51
  291. package/dist/discord/rest.d.ts +0 -49
  292. package/dist/discord/rest.js +0 -271
  293. package/dist/discord/types.d.ts +0 -334
  294. package/dist/discord/types.js +0 -4
  295. package/dist/events.d.ts +0 -94
  296. package/dist/events.js +0 -255
  297. package/dist/presence.d.ts +0 -92
  298. package/dist/presence.js +0 -250
  299. package/dist/structures/Channel.d.ts +0 -110
  300. package/dist/structures/Channel.js +0 -171
  301. package/dist/structures/Guild.d.ts +0 -70
  302. package/dist/structures/Guild.js +0 -105
  303. package/dist/structures/Interaction.d.ts +0 -113
  304. package/dist/structures/Interaction.js +0 -274
  305. package/dist/structures/Member.d.ts +0 -70
  306. package/dist/structures/Member.js +0 -124
  307. package/dist/structures/Message.d.ts +0 -87
  308. package/dist/structures/Message.js +0 -182
  309. package/dist/structures/Reaction.d.ts +0 -38
  310. package/dist/structures/Reaction.js +0 -64
  311. package/dist/structures/Role.d.ts +0 -43
  312. package/dist/structures/Role.js +0 -77
  313. package/dist/structures/User.d.ts +0 -41
  314. package/dist/structures/User.js +0 -79
  315. package/dist/structures/utils.d.ts +0 -72
  316. package/dist/structures/utils.js +0 -59
  317. package/dist/utils/errors.d.ts +0 -5
  318. package/dist/utils/errors.js +0 -28
  319. package/dist/utils/modules.d.ts +0 -39
  320. package/dist/utils/modules.js +0 -156
@@ -0,0 +1,405 @@
1
+ // One session with a voice server: its WebSocket (voice gateway v8), its UDP
2
+ // socket, the transport key and the DAVE state. A session lives as long as
3
+ // its token: moving to another channel or losing it makes a new one, while
4
+ // the voice connection users hold stays the same object.
5
+ // https://docs.discord.com/developers/topics/voice-connections
6
+ import { createSocket } from 'node:dgram';
7
+ import { VoiceCloseCode, VoiceOpcode } from '../discord/codes.js';
8
+ import { onSocketEnd } from '../util/websocket.js';
9
+ import { DaveSession } from './dave.js';
10
+ import { pickMode, rtpHeader, TRANSPORT_MODES, TransportEncryptor, } from './transport.js';
11
+ export const VOICE_GATEWAY_VERSION = 8;
12
+ /** The close codes after which Discord says not to reconnect. */
13
+ const ENDED = [
14
+ VoiceCloseCode.Disconnected,
15
+ VoiceCloseCode.DisconnectedRateLimited,
16
+ VoiceCloseCode.DisconnectedCallTerminated,
17
+ ];
18
+ /** The session is gone: a new one is needed, from the main gateway. */
19
+ const LOST = [
20
+ VoiceCloseCode.SessionNoLongerValid,
21
+ VoiceCloseCode.SessionTimeout,
22
+ VoiceCloseCode.ServerNotFound,
23
+ ];
24
+ /** What these codes mean for the developer. */
25
+ const REFUSED = {
26
+ [VoiceCloseCode.UnknownOpcode]: 'it received an opcode it does not know',
27
+ [VoiceCloseCode.FailedToDecodePayload]: 'it could not read what it received',
28
+ [VoiceCloseCode.NotAuthenticated]: 'it was sent something before Identify',
29
+ [VoiceCloseCode.AuthenticationFailed]: 'the voice token was refused',
30
+ [VoiceCloseCode.AlreadyAuthenticated]: 'it was identified twice',
31
+ [VoiceCloseCode.UnknownProtocol]: 'it does not know the protocol asked',
32
+ [VoiceCloseCode.UnknownEncryptionMode]: 'it does not know the encryption mode asked',
33
+ [VoiceCloseCode.E2eeDaveProtocolRequired]: 'the channel requires end-to-end encryption (DAVE)',
34
+ [VoiceCloseCode.BadRequest]: 'it received a malformed request',
35
+ };
36
+ /** Five frames of silence, sent before stopping. */
37
+ const SILENCE = Buffer.from([0xf8, 0xff, 0xfe]);
38
+ export class VoiceSession {
39
+ server;
40
+ #options;
41
+ dave;
42
+ #ws = null;
43
+ #udp = null;
44
+ #remote = null;
45
+ #ssrc = 0;
46
+ #encryptor = null;
47
+ #sequence = Math.floor(Math.random() * 0xffff);
48
+ #timestamp = Math.floor(Math.random() * 0xffffffff);
49
+ #seqAck = -1;
50
+ #heartbeat = null;
51
+ #acked = true;
52
+ #resuming = false;
53
+ #closed = false;
54
+ #speaking = false;
55
+ #ready = null;
56
+ #retries = 0;
57
+ /** Packets handed to the socket and not sent yet. */
58
+ #sending = 0;
59
+ #afterSends = null;
60
+ constructor(server, options) {
61
+ this.server = server;
62
+ this.#options = options;
63
+ this.dave = new DaveSession(options.dave, {
64
+ userId: server.userId,
65
+ channelId: server.channelId,
66
+ warn: options.warn,
67
+ transport: {
68
+ json: (op, data) => this.#send(op, data),
69
+ binary: (op, payload) => this.#sendBinary(op, payload),
70
+ },
71
+ });
72
+ }
73
+ /** Resolves once the session can send audio. */
74
+ connect() {
75
+ const timeout = this.#options.timeout ?? 15_000;
76
+ return new Promise((resolve, reject) => {
77
+ const timer = setTimeout(() => reject(new Error(`The voice server did not answer within ${timeout / 1000} s.`)), timeout);
78
+ this.#ready = {
79
+ resolve: () => {
80
+ clearTimeout(timer);
81
+ resolve();
82
+ },
83
+ reject: error => {
84
+ clearTimeout(timer);
85
+ reject(error);
86
+ },
87
+ };
88
+ this.#open();
89
+ });
90
+ }
91
+ /** Sends one Opus packet that plays `samples` samples. */
92
+ sendOpus(packet, samples) {
93
+ const encryptor = this.#encryptor;
94
+ if (!encryptor || !this.#udp || !this.#remote || this.#closed)
95
+ return;
96
+ if (!this.dave.canSend) {
97
+ // The call is end-to-end encrypted and the bot has no key yet: what
98
+ // it would send could not be heard. Time goes on all the same.
99
+ this.#timestamp = (this.#timestamp + samples) >>> 0;
100
+ return;
101
+ }
102
+ if (!this.#speaking)
103
+ this.setSpeaking(true);
104
+ const frame = this.dave.encrypt(packet);
105
+ const header = rtpHeader(this.#sequence, this.#timestamp, this.#ssrc);
106
+ this.#sequence = (this.#sequence + 1) & 0xffff;
107
+ this.#timestamp = (this.#timestamp + samples) >>> 0;
108
+ this.#sending++;
109
+ this.#udp.send(encryptor.seal(header, frame), this.#remote.port, this.#remote.ip, () => {
110
+ this.#sending--;
111
+ if (this.#sending === 0)
112
+ this.#afterSends?.();
113
+ });
114
+ }
115
+ /** The five frames of silence Discord asks for before a pause. */
116
+ sendSilence() {
117
+ for (let i = 0; i < 5; i++)
118
+ this.sendOpus(SILENCE, 960);
119
+ }
120
+ /** Speaking (5): needed before audio, and shown in Discord. */
121
+ setSpeaking(speaking) {
122
+ if (speaking === this.#speaking)
123
+ return;
124
+ this.#speaking = speaking;
125
+ this.#send(VoiceOpcode.Speaking, {
126
+ speaking: speaking ? 1 : 0,
127
+ delay: 0,
128
+ ssrc: this.#ssrc,
129
+ });
130
+ }
131
+ /** Closes the session for good. */
132
+ close() {
133
+ if (this.#closed)
134
+ return;
135
+ this.#closed = true;
136
+ this.#stopHeartbeat();
137
+ this.#ws?.close(1000);
138
+ this.#ws = null;
139
+ // What was handed to the socket (the silence that ends a song) still
140
+ // leaves before it closes.
141
+ const udp = this.#udp;
142
+ this.#udp = null;
143
+ if (udp && this.#sending > 0)
144
+ this.#afterSends = () => udp.close();
145
+ else
146
+ udp?.close();
147
+ this.dave.close();
148
+ this.#ready?.reject(new Error('The voice connection was closed.'));
149
+ this.#ready = null;
150
+ }
151
+ #open() {
152
+ const scheme = this.#options.secure === false ? 'ws' : 'wss';
153
+ const ws = new WebSocket(`${scheme}://${this.server.endpoint}/?v=${VOICE_GATEWAY_VERSION}`);
154
+ ws.binaryType = 'arraybuffer';
155
+ this.#ws = ws;
156
+ ws.addEventListener('message', event => {
157
+ if (ws !== this.#ws)
158
+ return;
159
+ try {
160
+ if (typeof event.data === 'string')
161
+ this.#onJson(event.data);
162
+ else
163
+ this.#onBinary(new Uint8Array(event.data));
164
+ }
165
+ catch (error) {
166
+ this.#options.warn(`a message of the voice server could not be read: ${error.message}`);
167
+ }
168
+ });
169
+ onSocketEnd(ws, code => {
170
+ if (ws === this.#ws)
171
+ this.#onClose(code);
172
+ });
173
+ }
174
+ #onJson(text) {
175
+ const { op, d, seq } = JSON.parse(text);
176
+ if (typeof seq === 'number')
177
+ this.#seqAck = seq;
178
+ const data = d ?? {};
179
+ switch (op) {
180
+ case VoiceOpcode.Hello:
181
+ this.#startHeartbeat(data.heartbeat_interval);
182
+ if (this.#resuming) {
183
+ this.#send(VoiceOpcode.Resume, {
184
+ server_id: this.server.guildId,
185
+ session_id: this.server.sessionId,
186
+ token: this.server.token,
187
+ seq_ack: this.#seqAck,
188
+ });
189
+ }
190
+ else {
191
+ this.#send(VoiceOpcode.Identify, {
192
+ server_id: this.server.guildId,
193
+ user_id: this.server.userId,
194
+ session_id: this.server.sessionId,
195
+ token: this.server.token,
196
+ max_dave_protocol_version: this.dave.maxVersion,
197
+ });
198
+ }
199
+ break;
200
+ case VoiceOpcode.Ready:
201
+ void this.#selectProtocol(data);
202
+ break;
203
+ case VoiceOpcode.SessionDescription: {
204
+ const { mode, secret_key: key, dave_protocol_version: version, } = data;
205
+ const chosen = pickMode([mode]);
206
+ if (!chosen) {
207
+ this.#fail(new Error(`The voice server chose an encryption mode chapterjs does not know: ${mode}.`));
208
+ return;
209
+ }
210
+ this.#encryptor = new TransportEncryptor(chosen, Uint8Array.from(key));
211
+ this.dave.start(version ?? 0);
212
+ this.#retries = 0;
213
+ this.#ready?.resolve();
214
+ this.#ready = null;
215
+ break;
216
+ }
217
+ case VoiceOpcode.HeartbeatAck:
218
+ this.#acked = true;
219
+ break;
220
+ case VoiceOpcode.Resumed:
221
+ this.#resuming = false;
222
+ this.#retries = 0;
223
+ break;
224
+ case VoiceOpcode.ClientsConnect:
225
+ this.dave.clientsConnect(data.user_ids ?? []);
226
+ break;
227
+ case VoiceOpcode.ClientDisconnect:
228
+ this.dave.clientDisconnect(data.user_id);
229
+ break;
230
+ case VoiceOpcode.DavePrepareTransition:
231
+ this.dave.prepareTransition(data.transition_id, data.protocol_version);
232
+ break;
233
+ case VoiceOpcode.DaveExecuteTransition:
234
+ this.dave.executeTransition(data.transition_id);
235
+ break;
236
+ case VoiceOpcode.DavePrepareEpoch:
237
+ this.dave.prepareEpoch(data.epoch, data.protocol_version);
238
+ break;
239
+ default:
240
+ // Speaking of others, video, and what the bot does not use.
241
+ break;
242
+ }
243
+ }
244
+ /** Binary messages: a sequence number, an opcode, then the payload. */
245
+ #onBinary(bytes) {
246
+ if (bytes.length < 3)
247
+ return;
248
+ this.#seqAck = (bytes[0] << 8) | bytes[1];
249
+ const op = bytes[2];
250
+ const payload = bytes.subarray(3);
251
+ const transition = () => (payload[0] << 8) | payload[1];
252
+ switch (op) {
253
+ case VoiceOpcode.DaveMlsExternalSender:
254
+ this.dave.externalSender(payload);
255
+ break;
256
+ case VoiceOpcode.DaveMlsProposals:
257
+ this.dave.proposals(payload);
258
+ break;
259
+ case VoiceOpcode.DaveMlsAnnounceCommitTransition:
260
+ this.dave.announceCommit(transition(), payload.subarray(2));
261
+ break;
262
+ case VoiceOpcode.DaveMlsWelcome:
263
+ this.dave.welcome(transition(), payload.subarray(2));
264
+ break;
265
+ default:
266
+ break;
267
+ }
268
+ }
269
+ async #selectProtocol(ready) {
270
+ const mode = pickMode(ready.modes);
271
+ if (!mode) {
272
+ this.#fail(new Error(`The voice server offers none of the encryption modes chapterjs speaks (${TRANSPORT_MODES.join(', ')}): it offered ${ready.modes.join(', ')}.`));
273
+ return;
274
+ }
275
+ this.#ssrc = ready.ssrc;
276
+ this.dave.setSsrc(ready.ssrc);
277
+ this.#remote = { ip: ready.ip, port: ready.port };
278
+ try {
279
+ const self = await this.#discover(ready);
280
+ this.#send(VoiceOpcode.SelectProtocol, {
281
+ protocol: 'udp',
282
+ data: { address: self.ip, port: self.port, mode },
283
+ });
284
+ }
285
+ catch (error) {
286
+ this.#fail(error);
287
+ }
288
+ }
289
+ /**
290
+ * IP Discovery: asks the voice server which address and port it sees the
291
+ * bot from, through the socket audio will go through.
292
+ * https://docs.discord.com/developers/topics/voice-connections#ip-discovery
293
+ */
294
+ #discover(ready) {
295
+ this.#udp?.close();
296
+ const udp = createSocket('udp4');
297
+ this.#udp = udp;
298
+ return new Promise((resolve, reject) => {
299
+ const timer = setTimeout(() => {
300
+ udp.off('message', onMessage);
301
+ reject(new Error(`The voice server did not answer over UDP (${ready.ip}:${ready.port}): a firewall may block it.`));
302
+ }, 5000);
303
+ const onMessage = (message) => {
304
+ if (message.length < 74 || message.readUInt16BE(0) !== 2)
305
+ return;
306
+ clearTimeout(timer);
307
+ udp.off('message', onMessage);
308
+ const end = message.indexOf(0, 8);
309
+ resolve({
310
+ ip: message.toString('utf8', 8, end > 8 && end < 72 ? end : 72),
311
+ port: message.readUInt16BE(72),
312
+ });
313
+ };
314
+ udp.on('message', onMessage);
315
+ udp.on('error', error => {
316
+ clearTimeout(timer);
317
+ reject(error);
318
+ });
319
+ const request = Buffer.alloc(74);
320
+ request.writeUInt16BE(1, 0);
321
+ request.writeUInt16BE(70, 2);
322
+ request.writeUInt32BE(ready.ssrc >>> 0, 4);
323
+ udp.send(request, ready.port, ready.ip);
324
+ });
325
+ }
326
+ #onClose(code) {
327
+ this.#stopHeartbeat();
328
+ this.#ws = null;
329
+ if (this.#closed)
330
+ return;
331
+ if (ENDED.includes(code))
332
+ return this.#end({ type: 'ended', code });
333
+ if (LOST.includes(code))
334
+ return this.#end({ type: 'lost', code });
335
+ const reason = REFUSED[code];
336
+ if (reason)
337
+ return this.#end({ type: 'refused', code, reason });
338
+ // Anything else (a network error, a crash of the voice server, a
339
+ // missing heartbeat ACK): resume, waiting longer each time.
340
+ if (this.#ready || !this.#encryptor) {
341
+ // Not even ready yet: there is nothing to resume.
342
+ return this.#end({ type: 'lost', code });
343
+ }
344
+ this.#resuming = true;
345
+ const delay = Math.min(1000 * 2 ** this.#retries, 30_000);
346
+ this.#retries++;
347
+ setTimeout(() => {
348
+ if (!this.#closed)
349
+ this.#open();
350
+ }, delay).unref();
351
+ }
352
+ #end(end) {
353
+ const ready = this.#ready;
354
+ this.close();
355
+ if (ready) {
356
+ ready.reject(new Error(end.type === 'refused'
357
+ ? `The voice server refused the bot (${end.code}): ${end.reason}.`
358
+ : `The voice server closed the connection (${end.code}).`));
359
+ }
360
+ this.#options.onEnd(end);
361
+ }
362
+ #fail(error) {
363
+ const ready = this.#ready;
364
+ this.#ready = null;
365
+ this.close();
366
+ ready?.reject(error);
367
+ if (!ready)
368
+ this.#options.onEnd({ type: 'lost', code: 0 });
369
+ }
370
+ #startHeartbeat(interval) {
371
+ this.#stopHeartbeat();
372
+ this.#acked = true;
373
+ this.#heartbeat = setInterval(() => {
374
+ if (!this.#acked) {
375
+ // No answer to the last one: the connection is dead.
376
+ this.#ws?.close(4000);
377
+ return;
378
+ }
379
+ this.#acked = false;
380
+ this.#send(VoiceOpcode.Heartbeat, {
381
+ t: Date.now(),
382
+ seq_ack: this.#seqAck,
383
+ });
384
+ }, interval);
385
+ this.#heartbeat.unref();
386
+ }
387
+ #stopHeartbeat() {
388
+ if (this.#heartbeat)
389
+ clearInterval(this.#heartbeat);
390
+ this.#heartbeat = null;
391
+ }
392
+ #send(op, d) {
393
+ if (this.#ws?.readyState === WebSocket.OPEN) {
394
+ this.#ws.send(JSON.stringify({ op, d }));
395
+ }
396
+ }
397
+ #sendBinary(op, payload) {
398
+ if (this.#ws?.readyState !== WebSocket.OPEN)
399
+ return;
400
+ const message = new Uint8Array(payload.length + 1);
401
+ message[0] = op;
402
+ message.set(payload, 1);
403
+ this.#ws.send(message);
404
+ }
405
+ }
@@ -0,0 +1,22 @@
1
+ import { type AssetFile } from '../assets/asset.js';
2
+ import type { Attachment } from '../structures/message.js';
3
+ /**
4
+ * What `play()` takes: a file of `public/` (`asset('music/intro.ogg')`), a
5
+ * file sent on Discord (the `attachment` option of a command, or one of
6
+ * `message.attachments`), or a link starting with `https://`.
7
+ */
8
+ export type AudioSource = AssetFile | Attachment | string;
9
+ /** Opus packets being read, and how to stop reading them. */
10
+ export interface OpusStream {
11
+ readonly packets: AsyncIterable<Buffer>;
12
+ /** What is played, as the developer named it. */
13
+ readonly name: string;
14
+ close(): void;
15
+ }
16
+ /** The container of a file, from its first bytes. */
17
+ export declare function sniff(head: Buffer): 'ogg' | 'webm' | null;
18
+ /**
19
+ * Opens what is to be played. Fails before anything is sent when the file
20
+ * is missing, or when ffmpeg is needed and is not installed.
21
+ */
22
+ export declare function openAudio(source: AudioSource): Promise<OpusStream>;
@@ -0,0 +1,195 @@
1
+ // What `play()` takes, turned into Opus packets: a file of `public/`, a
2
+ // link or a file sent on Discord that is already Opus is read as is;
3
+ // anything else is converted by ffmpeg, when it is installed.
4
+ import { spawn } from 'node:child_process';
5
+ import { createReadStream } from 'node:fs';
6
+ import { open } from 'node:fs/promises';
7
+ import { assetPath, isAssetFile, missingAsset, } from '../assets/asset.js';
8
+ import { oggOpusPackets, webmOpusPackets } from './opus.js';
9
+ /** How much of a file tells what it holds. */
10
+ const HEAD = 4096;
11
+ /** The container of a file, from its first bytes. */
12
+ export function sniff(head) {
13
+ if (head.toString('latin1', 0, 4) === 'OggS' &&
14
+ head.includes('OpusHead', 0, 'latin1')) {
15
+ return 'ogg';
16
+ }
17
+ if (head.readUInt32BE(0) === 0x1a45dfa3 && head.includes('A_OPUS')) {
18
+ return 'webm';
19
+ }
20
+ return null;
21
+ }
22
+ /**
23
+ * Opens what is to be played. Fails before anything is sent when the file
24
+ * is missing, or when ffmpeg is needed and is not installed.
25
+ */
26
+ export async function openAudio(source) {
27
+ if (isAssetFile(source)) {
28
+ const path = assetPath(source);
29
+ const name = `public/${source.path}`;
30
+ let head;
31
+ try {
32
+ const handle = await open(path, 'r');
33
+ try {
34
+ const buffer = Buffer.alloc(4096);
35
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
36
+ head = buffer.subarray(0, bytesRead);
37
+ }
38
+ finally {
39
+ await handle.close();
40
+ }
41
+ }
42
+ catch (error) {
43
+ throw missingAsset(source, error);
44
+ }
45
+ const container = head.length >= 4 ? sniff(head) : null;
46
+ if (container) {
47
+ const stream = createReadStream(path);
48
+ // Reading reports what goes wrong; a stream never read must not.
49
+ stream.on('error', () => { });
50
+ const demux = container === 'ogg' ? oggOpusPackets : webmOpusPackets;
51
+ return {
52
+ name,
53
+ packets: demux(stream),
54
+ close: () => stream.destroy(),
55
+ };
56
+ }
57
+ return ffmpeg(path, name);
58
+ }
59
+ if (isAttachment(source))
60
+ return openLink(source.url, source.filename);
61
+ if (typeof source === 'string' && /^https?:\/\//i.test(source)) {
62
+ return openLink(source, source);
63
+ }
64
+ throw new TypeError(`play() takes a file of public/, like play(asset('music/intro.ogg')), a file sent on Discord, or a link starting with https://. Got ${JSON.stringify(source) ?? String(source)}.`);
65
+ }
66
+ /** A file sent on Discord, as a command option or a message gives it. */
67
+ function isAttachment(value) {
68
+ return (typeof value === 'object' &&
69
+ value !== null &&
70
+ typeof value.url === 'string' &&
71
+ typeof value.filename === 'string');
72
+ }
73
+ /**
74
+ * A link: downloaded as it plays. Its first bytes say whether it is Opus,
75
+ * read as is, or something ffmpeg converts.
76
+ */
77
+ async function openLink(url, name) {
78
+ let response;
79
+ try {
80
+ response = await fetch(url);
81
+ }
82
+ catch (error) {
83
+ throw new Error(`${name} could not be downloaded: ${error.message}.`);
84
+ }
85
+ if (!response.ok || !response.body) {
86
+ throw new Error(`${name} could not be downloaded: the server answered ${response.status}.`);
87
+ }
88
+ // A page (YouTube, a site): what plays is not at this address.
89
+ if (response.headers.get('content-type')?.startsWith('text/html')) {
90
+ void response.body.cancel().catch(() => { });
91
+ throw new Error(`${name} is a web page, not a sound: give the link of an audio file or of a stream, or send the file.`);
92
+ }
93
+ const reader = response.body.getReader();
94
+ const start = [];
95
+ let length = 0;
96
+ let ended = false;
97
+ while (length < HEAD) {
98
+ const next = await reader.read();
99
+ if (next.done) {
100
+ ended = true;
101
+ break;
102
+ }
103
+ start.push(next.value);
104
+ length += next.value.length;
105
+ }
106
+ const head = Buffer.concat(start);
107
+ const container = head.length >= 4 ? sniff(head) : null;
108
+ if (!container) {
109
+ // ffmpeg downloads it itself.
110
+ void reader.cancel().catch(() => { });
111
+ return ffmpeg(url, name);
112
+ }
113
+ let closed = false;
114
+ const body = async function* () {
115
+ yield head;
116
+ if (ended)
117
+ return;
118
+ while (!closed) {
119
+ const next = await reader.read();
120
+ if (next.done)
121
+ return;
122
+ yield next.value;
123
+ }
124
+ };
125
+ const demux = container === 'ogg' ? oggOpusPackets : webmOpusPackets;
126
+ return {
127
+ name,
128
+ packets: demux(body()),
129
+ close: () => {
130
+ closed = true;
131
+ void reader.cancel().catch(() => { });
132
+ },
133
+ };
134
+ }
135
+ /**
136
+ * Converts anything ffmpeg reads into Ogg Opus, 48 kHz stereo in frames
137
+ * of 20 ms: what Discord expects.
138
+ */
139
+ async function ffmpeg(input, name) {
140
+ const child = spawn('ffmpeg', [
141
+ '-hide_banner',
142
+ '-loglevel',
143
+ 'error',
144
+ '-i',
145
+ input,
146
+ '-vn',
147
+ '-c:a',
148
+ 'libopus',
149
+ '-ar',
150
+ '48000',
151
+ '-ac',
152
+ '2',
153
+ '-b:a',
154
+ '128k',
155
+ '-frame_duration',
156
+ '20',
157
+ '-f',
158
+ 'ogg',
159
+ 'pipe:1',
160
+ ], { stdio: ['ignore', 'pipe', 'pipe'] });
161
+ await new Promise((resolve, reject) => {
162
+ child.once('spawn', resolve);
163
+ child.once('error', error => {
164
+ reject(error.code === 'ENOENT'
165
+ ? new Error(`${name} is not an Opus file (.ogg or .webm), and ffmpeg is not installed to convert it. Install ffmpeg (https://ffmpeg.org/download.html), or convert the file to .ogg Opus.`)
166
+ : error);
167
+ });
168
+ });
169
+ let errors = '';
170
+ child.stderr.setEncoding('utf8');
171
+ child.stderr.on('data', (text) => {
172
+ errors = (errors + text).slice(-2000);
173
+ });
174
+ const exited = new Promise(resolve => child.once('close', code => resolve(code)));
175
+ const packets = (async function* () {
176
+ let count = 0;
177
+ for await (const packet of oggOpusPackets(child.stdout)) {
178
+ count++;
179
+ yield packet;
180
+ }
181
+ const code = await exited;
182
+ if (code !== 0 && code !== null && count === 0) {
183
+ const reason = errors.trim().split('\n').at(-1) ?? `exit code ${code}`;
184
+ throw new Error(`ffmpeg could not read ${name}: ${reason}`);
185
+ }
186
+ })();
187
+ return {
188
+ name,
189
+ packets,
190
+ close: () => {
191
+ if (child.exitCode === null)
192
+ child.kill();
193
+ },
194
+ };
195
+ }
@@ -0,0 +1,43 @@
1
+ /** The transport encryption modes, best first. */
2
+ export declare const TRANSPORT_MODES: readonly ['aead_aes256_gcm_rtpsize', 'aead_xchacha20_poly1305_rtpsize'];
3
+ export type TransportMode = (typeof TRANSPORT_MODES)[number];
4
+ /**
5
+ * The mode to use among the ones the voice server offers: AES256-GCM when
6
+ * it is there (Discord asks to prefer it), XChaCha20-Poly1305 otherwise
7
+ * (always offered); `null` when neither is.
8
+ * https://docs.discord.com/developers/topics/voice-connections#transport-encryption-modes
9
+ */
10
+ export declare function pickMode(offered: readonly string[]): TransportMode | null;
11
+ /** Audio sampled at 48 kHz: what an RTP timestamp counts. */
12
+ export declare const SAMPLE_RATE = 48000;
13
+ /**
14
+ * The RTP header of a voice packet: version 2, payload type 0x78, then
15
+ * sequence, timestamp and SSRC, big endian.
16
+ * https://docs.discord.com/developers/topics/voice-connections#transport-encryption-modes-voice-packet-structure
17
+ */
18
+ export declare function rtpHeader(sequence: number, timestamp: number, ssrc: number): Buffer;
19
+ /**
20
+ * HChaCha20: derives the key XChaCha20 uses for a 24-byte nonce, from the
21
+ * key and the first 16 bytes of that nonce.
22
+ * https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha#section-2.2
23
+ */
24
+ export declare function hchacha20(key: Uint8Array, nonce16: Uint8Array): Buffer;
25
+ /**
26
+ * XChaCha20-Poly1305: ChaCha20-Poly1305 (which Node has) with the key
27
+ * HChaCha20 derives and the last 8 bytes of the 24-byte nonce.
28
+ * https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha#section-2.3
29
+ */
30
+ export declare function xchacha20poly1305Seal(key: Uint8Array, nonce24: Uint8Array, plaintext: Uint8Array, aad: Uint8Array): Buffer;
31
+ /**
32
+ * Encrypts packets for one voice session. In the `rtpsize` modes the RTP
33
+ * header stays clear and authenticated, and a 32-bit counter, appended to
34
+ * the packet, is the nonce: the first 4 bytes of a nonce that is zero
35
+ * otherwise.
36
+ */
37
+ export declare class TransportEncryptor {
38
+ #private;
39
+ constructor(mode: TransportMode, key: Uint8Array);
40
+ get mode(): TransportMode;
41
+ /** A whole packet: the clear header, the sealed audio, the counter. */
42
+ seal(header: Buffer, payload: Uint8Array): Buffer;
43
+ }