chapterjs 0.1.2 → 0.2.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 (318) 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 +205 -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 +253 -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 +135 -0
  25. package/dist/cli/project.js +517 -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 +360 -0
  31. package/dist/commands/command.js +28 -0
  32. package/dist/commands/declaration.d.ts +77 -0
  33. package/dist/commands/declaration.js +492 -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 +47 -0
  39. package/dist/commands/tree.js +216 -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 +106 -0
  44. package/dist/components/component.d.ts +140 -0
  45. package/dist/components/component.js +39 -0
  46. package/dist/components/custom-id.d.ts +72 -0
  47. package/dist/components/custom-id.js +128 -0
  48. package/dist/components/declaration.d.ts +42 -0
  49. package/dist/components/declaration.js +342 -0
  50. package/dist/components/declared.d.ts +29 -0
  51. package/dist/components/declared.js +49 -0
  52. package/dist/components/embed.d.ts +29 -0
  53. package/dist/components/embed.js +11 -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 +245 -0
  59. package/dist/components/modal.js +398 -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 +19 -0
  65. package/dist/components/router.js +251 -0
  66. package/dist/components/select.d.ts +146 -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/declaration.d.ts +16 -0
  155. package/dist/events/declaration.js +80 -0
  156. package/dist/events/event.d.ts +45 -0
  157. package/dist/events/event.js +34 -0
  158. package/dist/events/registry.d.ts +697 -0
  159. package/dist/events/registry.js +839 -0
  160. package/dist/events/router.d.ts +46 -0
  161. package/dist/events/router.js +133 -0
  162. package/dist/gateway/errors.d.ts +18 -0
  163. package/dist/gateway/errors.js +47 -0
  164. package/dist/gateway/gateway.d.ts +64 -0
  165. package/dist/gateway/gateway.js +117 -0
  166. package/dist/gateway/identify-queue.d.ts +25 -0
  167. package/dist/gateway/identify-queue.js +29 -0
  168. package/dist/gateway/shard.d.ts +94 -0
  169. package/dist/gateway/shard.js +414 -0
  170. package/dist/gateway/state.d.ts +11 -0
  171. package/dist/gateway/state.js +184 -0
  172. package/dist/index.d.ts +57 -18
  173. package/dist/index.js +40 -12
  174. package/dist/interactions/cooldown.d.ts +33 -0
  175. package/dist/interactions/cooldown.js +74 -0
  176. package/dist/interactions/dispatch.d.ts +91 -0
  177. package/dist/interactions/dispatch.js +110 -0
  178. package/dist/loader/generated.d.ts +10 -0
  179. package/dist/loader/generated.js +80 -0
  180. package/dist/loader/hot.d.ts +13 -0
  181. package/dist/loader/hot.js +85 -0
  182. package/dist/loader/loader.d.ts +85 -0
  183. package/dist/loader/loader.js +119 -0
  184. package/dist/loader/locate.d.ts +10 -0
  185. package/dist/loader/locate.js +36 -0
  186. package/dist/loader/watch.d.ts +17 -0
  187. package/dist/loader/watch.js +66 -0
  188. package/dist/main.d.ts +2 -0
  189. package/dist/main.js +5 -0
  190. package/dist/messages/commands.d.ts +23 -0
  191. package/dist/messages/commands.js +142 -0
  192. package/dist/messages/declaration.d.ts +89 -0
  193. package/dist/messages/declaration.js +455 -0
  194. package/dist/messages/messages.d.ts +241 -0
  195. package/dist/messages/messages.js +31 -0
  196. package/dist/messages/phrases.d.ts +23 -0
  197. package/dist/messages/phrases.js +44 -0
  198. package/dist/messages/translate.d.ts +100 -0
  199. package/dist/messages/translate.js +249 -0
  200. package/dist/presence/declaration.d.ts +19 -0
  201. package/dist/presence/declaration.js +118 -0
  202. package/dist/presence/presence.d.ts +57 -0
  203. package/dist/presence/presence.js +27 -0
  204. package/dist/rest/errors.d.ts +55 -0
  205. package/dist/rest/errors.js +92 -0
  206. package/dist/rest/rate-limiter.d.ts +46 -0
  207. package/dist/rest/rate-limiter.js +153 -0
  208. package/dist/rest/rest.d.ts +63 -0
  209. package/dist/rest/rest.js +194 -0
  210. package/dist/structures/base.d.ts +48 -0
  211. package/dist/structures/base.js +84 -0
  212. package/dist/structures/channel.d.ts +380 -0
  213. package/dist/structures/channel.js +627 -0
  214. package/dist/structures/context.d.ts +30 -0
  215. package/dist/structures/context.js +7 -0
  216. package/dist/structures/emoji.d.ts +46 -0
  217. package/dist/structures/emoji.js +65 -0
  218. package/dist/structures/entities.d.ts +62 -0
  219. package/dist/structures/entities.js +226 -0
  220. package/dist/structures/guild.d.ts +348 -0
  221. package/dist/structures/guild.js +466 -0
  222. package/dist/structures/interaction.d.ts +197 -0
  223. package/dist/structures/interaction.js +396 -0
  224. package/dist/structures/invite.d.ts +38 -0
  225. package/dist/structures/invite.js +57 -0
  226. package/dist/structures/known.d.ts +28 -0
  227. package/dist/structures/known.js +58 -0
  228. package/dist/structures/member.d.ts +140 -0
  229. package/dist/structures/member.js +260 -0
  230. package/dist/structures/message.d.ts +197 -0
  231. package/dist/structures/message.js +268 -0
  232. package/dist/structures/payload.d.ts +91 -0
  233. package/dist/structures/payload.js +184 -0
  234. package/dist/structures/role.d.ts +69 -0
  235. package/dist/structures/role.js +112 -0
  236. package/dist/structures/user.d.ts +59 -0
  237. package/dist/structures/user.js +94 -0
  238. package/dist/structures/webhook.d.ts +46 -0
  239. package/dist/structures/webhook.js +79 -0
  240. package/dist/tasks/declaration.d.ts +16 -0
  241. package/dist/tasks/declaration.js +56 -0
  242. package/dist/tasks/schedule.d.ts +54 -0
  243. package/dist/tasks/schedule.js +236 -0
  244. package/dist/tasks/scheduler.d.ts +35 -0
  245. package/dist/tasks/scheduler.js +107 -0
  246. package/dist/tasks/task.d.ts +75 -0
  247. package/dist/tasks/task.js +27 -0
  248. package/dist/util/case.d.ts +13 -0
  249. package/dist/util/case.js +54 -0
  250. package/dist/util/duration.d.ts +8 -0
  251. package/dist/util/duration.js +29 -0
  252. package/dist/util/websocket.d.ts +9 -0
  253. package/dist/util/websocket.js +31 -0
  254. package/dist/voice/connection.d.ts +90 -0
  255. package/dist/voice/connection.js +494 -0
  256. package/dist/voice/dave.d.ts +109 -0
  257. package/dist/voice/dave.js +245 -0
  258. package/dist/voice/opus.d.ts +19 -0
  259. package/dist/voice/opus.js +273 -0
  260. package/dist/voice/session.d.ts +55 -0
  261. package/dist/voice/session.js +405 -0
  262. package/dist/voice/source.d.ts +22 -0
  263. package/dist/voice/source.js +195 -0
  264. package/dist/voice/transport.d.ts +43 -0
  265. package/dist/voice/transport.js +140 -0
  266. package/dist/voice/usage.d.ts +4 -0
  267. package/dist/voice/usage.js +33 -0
  268. package/package.json +26 -7
  269. package/vendor/dave/LICENSE +231 -0
  270. package/vendor/dave/libdave.d.mts +99 -0
  271. package/vendor/dave/libdave.mjs +2 -0
  272. package/vendor/dave/libdave.wasm +0 -0
  273. package/dist/components.d.ts +0 -97
  274. package/dist/components.js +0 -260
  275. package/dist/context.d.ts +0 -72
  276. package/dist/context.js +0 -273
  277. package/dist/discord/client.d.ts +0 -35
  278. package/dist/discord/client.js +0 -66
  279. package/dist/discord/constants.d.ts +0 -49
  280. package/dist/discord/constants.js +0 -61
  281. package/dist/discord/events.d.ts +0 -129
  282. package/dist/discord/events.js +0 -102
  283. package/dist/discord/gateway.d.ts +0 -61
  284. package/dist/discord/gateway.js +0 -320
  285. package/dist/discord/payloads.d.ts +0 -275
  286. package/dist/discord/payloads.js +0 -3
  287. package/dist/discord/presence.d.ts +0 -42
  288. package/dist/discord/presence.js +0 -51
  289. package/dist/discord/rest.d.ts +0 -49
  290. package/dist/discord/rest.js +0 -271
  291. package/dist/discord/types.d.ts +0 -334
  292. package/dist/discord/types.js +0 -4
  293. package/dist/events.d.ts +0 -94
  294. package/dist/events.js +0 -255
  295. package/dist/presence.d.ts +0 -92
  296. package/dist/presence.js +0 -250
  297. package/dist/structures/Channel.d.ts +0 -110
  298. package/dist/structures/Channel.js +0 -171
  299. package/dist/structures/Guild.d.ts +0 -70
  300. package/dist/structures/Guild.js +0 -105
  301. package/dist/structures/Interaction.d.ts +0 -113
  302. package/dist/structures/Interaction.js +0 -274
  303. package/dist/structures/Member.d.ts +0 -70
  304. package/dist/structures/Member.js +0 -124
  305. package/dist/structures/Message.d.ts +0 -87
  306. package/dist/structures/Message.js +0 -182
  307. package/dist/structures/Reaction.d.ts +0 -38
  308. package/dist/structures/Reaction.js +0 -64
  309. package/dist/structures/Role.d.ts +0 -43
  310. package/dist/structures/Role.js +0 -77
  311. package/dist/structures/User.d.ts +0 -41
  312. package/dist/structures/User.js +0 -79
  313. package/dist/structures/utils.d.ts +0 -72
  314. package/dist/structures/utils.js +0 -59
  315. package/dist/utils/errors.d.ts +0 -5
  316. package/dist/utils/errors.js +0 -28
  317. package/dist/utils/modules.d.ts +0 -39
  318. package/dist/utils/modules.js +0 -156
@@ -0,0 +1,153 @@
1
+ // Waits before each request so Discord's rate limits are never exceeded.
2
+ // Limits are never hard coded: they are read from the headers of each
3
+ // response, as the documentation asks.
4
+ // https://docs.discord.com/developers/topics/rate-limits
5
+ import { setTimeout as sleep } from 'node:timers/promises';
6
+ import { GLOBAL_REQUESTS_PER_SECOND } from '../discord/api.js';
7
+ export function readRateLimitHeaders(headers) {
8
+ const number = (name) => {
9
+ const value = headers.get(name);
10
+ if (value === null || value === '')
11
+ return null;
12
+ const parsed = Number(value);
13
+ return Number.isFinite(parsed) ? parsed : null;
14
+ };
15
+ return {
16
+ bucket: headers.get('x-ratelimit-bucket'),
17
+ remaining: number('x-ratelimit-remaining'),
18
+ resetAfter: number('x-ratelimit-reset-after'),
19
+ };
20
+ }
21
+ /** The state of one bucket for one top-level resource. */
22
+ class Bucket {
23
+ remaining = 1;
24
+ /** When `remaining` goes back to its maximum (ms since epoch). */
25
+ resetAt = 0;
26
+ /** Requests of this bucket run one at a time, in order. */
27
+ tail = Promise.resolve();
28
+ pending = 0;
29
+ }
30
+ export class RateLimiter {
31
+ /** `METHOD /path/template` → the bucket Discord said it belongs to. */
32
+ #bucketOfRoute = new Map();
33
+ /** `bucket:majorParameter` → its state. */
34
+ #buckets = new Map();
35
+ #globalLimit;
36
+ #onWait;
37
+ /** Start of the current one-second window of the global limit. */
38
+ #windowStart = 0;
39
+ #windowCount = 0;
40
+ /** Until when every request is paused after a global 429. */
41
+ #globalBlockedUntil = 0;
42
+ constructor(options = {}) {
43
+ this.#globalLimit = options.globalLimit ?? GLOBAL_REQUESTS_PER_SECOND;
44
+ this.#onWait = options.onWait;
45
+ }
46
+ #bucket(route, major) {
47
+ // Until Discord names the bucket of a route, the route is its own bucket.
48
+ const key = `${this.#bucketOfRoute.get(route) ?? route}:${major}`;
49
+ let bucket = this.#buckets.get(key);
50
+ if (!bucket) {
51
+ bucket = new Bucket();
52
+ this.#buckets.set(key, bucket);
53
+ }
54
+ return bucket;
55
+ }
56
+ async #wait(route, ms, global) {
57
+ if (ms <= 0)
58
+ return;
59
+ this.#onWait?.({ route, ms, global });
60
+ await sleep(ms);
61
+ }
62
+ /** Waits for a slot in the global limit, then takes it. */
63
+ async #takeGlobalSlot(route) {
64
+ for (;;) {
65
+ const now = Date.now();
66
+ if (now < this.#globalBlockedUntil) {
67
+ await this.#wait(route, this.#globalBlockedUntil - now, true);
68
+ continue;
69
+ }
70
+ if (now - this.#windowStart >= 1000) {
71
+ this.#windowStart = now;
72
+ this.#windowCount = 0;
73
+ }
74
+ if (this.#windowCount < this.#globalLimit) {
75
+ this.#windowCount++;
76
+ return;
77
+ }
78
+ await this.#wait(route, this.#windowStart + 1000 - now, true);
79
+ }
80
+ }
81
+ /**
82
+ * Runs `send` when the limits allow it. Requests of the same bucket run
83
+ * one after the other; different buckets run in parallel. `send` reports
84
+ * what the response said through `update` and returns its result, or a
85
+ * `RateLimitHit` to be run again after the delay Discord asked for.
86
+ */
87
+ async run(input, send) {
88
+ const { route, major } = input;
89
+ const bucket = this.#bucket(route, major);
90
+ const previous = bucket.tail;
91
+ let release;
92
+ bucket.tail = new Promise(resolve => (release = resolve));
93
+ bucket.pending++;
94
+ await previous;
95
+ try {
96
+ for (;;) {
97
+ // The bucket may have been renamed by an earlier response.
98
+ const current = this.#bucket(route, major);
99
+ const now = Date.now();
100
+ if (current.remaining <= 0 && now < current.resetAt) {
101
+ await this.#wait(route, current.resetAt - now, false);
102
+ }
103
+ if (input.countsForGlobal)
104
+ await this.#takeGlobalSlot(route);
105
+ const result = await send(headers => {
106
+ if (headers.bucket !== null) {
107
+ this.#bucketOfRoute.set(route, headers.bucket);
108
+ }
109
+ const target = this.#bucket(route, major);
110
+ if (headers.remaining !== null)
111
+ target.remaining = headers.remaining;
112
+ if (headers.resetAfter !== null) {
113
+ target.resetAt = Date.now() + headers.resetAfter * 1000;
114
+ }
115
+ });
116
+ if (!isRateLimitHit(result))
117
+ return result;
118
+ const ms = Math.ceil(result.retryAfter * 1000);
119
+ if (result.global || result.scope === 'global') {
120
+ this.#globalBlockedUntil = Date.now() + ms;
121
+ }
122
+ else {
123
+ const target = this.#bucket(route, major);
124
+ target.remaining = 0;
125
+ target.resetAt = Date.now() + ms;
126
+ }
127
+ }
128
+ }
129
+ finally {
130
+ bucket.pending--;
131
+ release();
132
+ this.#sweep();
133
+ }
134
+ }
135
+ /** Forgets buckets that are idle and reset, so memory doesn't grow. */
136
+ #sweep() {
137
+ if (this.#buckets.size < 1000)
138
+ return;
139
+ const now = Date.now();
140
+ for (const [key, bucket] of this.#buckets) {
141
+ if (bucket.pending === 0 && bucket.resetAt <= now) {
142
+ this.#buckets.delete(key);
143
+ }
144
+ }
145
+ }
146
+ }
147
+ const HIT = Symbol('rate limit hit');
148
+ export function rateLimitHit(hit) {
149
+ return Object.assign(hit, { [HIT]: true });
150
+ }
151
+ function isRateLimitHit(value) {
152
+ return typeof value === 'object' && value !== null && HIT in value;
153
+ }
@@ -0,0 +1,63 @@
1
+ import { type Endpoint, type EndpointBody, type EndpointQuery, type EndpointResult, type PathParams } from '../discord/endpoint.js';
2
+ /** A file to upload with a request. */
3
+ /** The content of a file, or how to read it when it is sent. */
4
+ export type RestFileData = Blob | Uint8Array | ArrayBuffer | string;
5
+ export interface RestFile {
6
+ /** The name of the file, with its extension: `chart.png`. */
7
+ name: string;
8
+ /** The content, or a function that reads it when the request is sent. */
9
+ data: RestFileData | (() => Promise<RestFileData>);
10
+ /** The media type; guessed by Discord from the name when missing. */
11
+ contentType?: string;
12
+ /**
13
+ * The name of the form field. By default `files[n]`, which is what
14
+ * message attachments use.
15
+ * @see https://docs.discord.com/developers/reference#uploading-files
16
+ */
17
+ field?: string;
18
+ }
19
+ export interface RequestOptions<E extends Endpoint = Endpoint> {
20
+ body?: EndpointBody<E>;
21
+ query?: EndpointQuery<E>;
22
+ /**
23
+ * Why the action is taken, shown in the audit log of the server.
24
+ * @see https://docs.discord.com/developers/resources/audit-log#audit-log-entry-object
25
+ */
26
+ reason?: string;
27
+ files?: readonly RestFile[];
28
+ /**
29
+ * `false` for endpoints authenticated by a token in their path (webhooks,
30
+ * interactions): the bot token is not sent and the global limit is not
31
+ * counted.
32
+ */
33
+ auth?: boolean;
34
+ signal?: AbortSignal;
35
+ }
36
+ export interface RestOptions {
37
+ token: string;
38
+ /** The version of the framework, sent in the User-Agent header. */
39
+ version: string;
40
+ /** Where Discord is. Only tests change it. */
41
+ baseUrl?: string;
42
+ /** How many times a request is tried again after a 5xx or a network error. Default: 3. */
43
+ retries?: number;
44
+ /** How long to wait for a response, in milliseconds. Default: 15000. */
45
+ timeout?: number;
46
+ /** Requests per second for the whole bot. Default: 50. */
47
+ globalLimit?: number;
48
+ /** Called every time a request has to wait for a rate limit. */
49
+ onRateLimit?: (info: {
50
+ route: string;
51
+ ms: number;
52
+ global: boolean;
53
+ }) => void;
54
+ }
55
+ /** The interface features use to talk to Discord over HTTP. */
56
+ export interface Rest {
57
+ request<E extends Endpoint>(endpoint: E, params: PathParams<E['path']>, options?: RequestOptions<E>): Promise<EndpointResult<E>>;
58
+ }
59
+ export declare class RestClient implements Rest {
60
+ #private;
61
+ constructor(options: RestOptions);
62
+ request<E extends Endpoint>(endpoint: E, params: PathParams<E['path']>, options?: RequestOptions<E>): Promise<EndpointResult<E>>;
63
+ }
@@ -0,0 +1,194 @@
1
+ // The only place that sends HTTP requests to Discord.
2
+ // https://docs.discord.com/developers/reference#http-api
3
+ import { setTimeout as sleep } from 'node:timers/promises';
4
+ import { API_BASE_URL, API_VERSION, AUDIT_LOG_REASON_MAX_LENGTH, userAgent, } from '../discord/api.js';
5
+ import { HttpStatus } from '../discord/codes.js';
6
+ import { fillPath, majorParameter, } from '../discord/endpoint.js';
7
+ import { DiscordApiError, DiscordUnavailableError, InvalidTokenError, } from './errors.js';
8
+ import { RateLimiter, rateLimitHit, readRateLimitHeaders, } from './rate-limiter.js';
9
+ /** A 429 that is still there after this many tries is reported as an error. */
10
+ const MAX_RATE_LIMIT_RETRIES = 5;
11
+ function toQueryString(query) {
12
+ if (typeof query !== 'object' || query === null)
13
+ return '';
14
+ const search = new URLSearchParams();
15
+ for (const [key, value] of Object.entries(query)) {
16
+ if (value === undefined || value === null)
17
+ continue;
18
+ // Arrays are sent as several instances of the same parameter.
19
+ // https://docs.discord.com/developers/reference#array-query-strings
20
+ for (const item of Array.isArray(value) ? value : [value]) {
21
+ search.append(key, String(item));
22
+ }
23
+ }
24
+ const text = search.toString();
25
+ return text === '' ? '' : `?${text}`;
26
+ }
27
+ function toBlob(file, data) {
28
+ if (data instanceof Blob) {
29
+ return file.contentType
30
+ ? new Blob([data], { type: file.contentType })
31
+ : data;
32
+ }
33
+ const part = typeof data === 'string' || data instanceof ArrayBuffer
34
+ ? data
35
+ : new Uint8Array(data);
36
+ return new Blob([part], file.contentType ? { type: file.contentType } : {});
37
+ }
38
+ function encodeReason(reason) {
39
+ const trimmed = reason.trim();
40
+ if (trimmed === '') {
41
+ throw new TypeError('The reason is empty: leave it out or write one.');
42
+ }
43
+ if (trimmed.length > AUDIT_LOG_REASON_MAX_LENGTH) {
44
+ throw new RangeError(`The reason is ${trimmed.length} characters long: Discord accepts ${AUDIT_LOG_REASON_MAX_LENGTH} at most.`);
45
+ }
46
+ return encodeURIComponent(trimmed);
47
+ }
48
+ export class RestClient {
49
+ #token;
50
+ #baseUrl;
51
+ #userAgent;
52
+ #retries;
53
+ #timeout;
54
+ #limiter;
55
+ /** Set after a 401: no further request is sent with a refused token. */
56
+ #tokenRefused = false;
57
+ constructor(options) {
58
+ if (typeof options.token !== 'string' || options.token.trim() === '') {
59
+ throw new InvalidTokenError();
60
+ }
61
+ this.#token = options.token.trim().replace(/^Bot\s+/i, '');
62
+ this.#baseUrl = `${(options.baseUrl ?? API_BASE_URL).replace(/\/+$/, '')}/v${API_VERSION}`;
63
+ this.#userAgent = userAgent(options.version);
64
+ this.#retries = options.retries ?? 3;
65
+ this.#timeout = options.timeout ?? 15_000;
66
+ this.#limiter = new RateLimiter({
67
+ globalLimit: options.globalLimit,
68
+ onWait: options.onRateLimit,
69
+ });
70
+ }
71
+ async request(endpoint, params, options = {}) {
72
+ const auth = options.auth ?? true;
73
+ if (auth && this.#tokenRefused)
74
+ throw new InvalidTokenError();
75
+ const { method, path } = endpoint;
76
+ const url = this.#baseUrl + fillPath(path, params) + toQueryString(options.query);
77
+ const headers = { 'User-Agent': this.#userAgent };
78
+ if (auth)
79
+ headers.Authorization = `Bot ${this.#token}`;
80
+ if (options.reason !== undefined) {
81
+ headers['X-Audit-Log-Reason'] = encodeReason(options.reason);
82
+ }
83
+ // Built once: a retry sends the same thing.
84
+ let body;
85
+ if (options.files && options.files.length > 0) {
86
+ // A file read on demand is read once, now: a retry sends the same.
87
+ const blobs = await Promise.all(options.files.map(async (file) => toBlob(file, typeof file.data === 'function' ? await file.data() : file.data)));
88
+ const files = options.files;
89
+ const json = options.body === undefined ? undefined : JSON.stringify(options.body);
90
+ body = () => {
91
+ const form = new FormData();
92
+ if (json !== undefined)
93
+ form.set('payload_json', json);
94
+ files.forEach((file, index) => {
95
+ form.set(file.field ?? `files[${index}]`, blobs[index], file.name);
96
+ });
97
+ return form;
98
+ };
99
+ }
100
+ else if (options.body !== undefined) {
101
+ body = JSON.stringify(options.body);
102
+ headers['Content-Type'] = 'application/json';
103
+ }
104
+ const route = `${method} ${path}`;
105
+ let failures = 0;
106
+ let rateLimits = 0;
107
+ return this.#limiter.run({ route, major: majorParameter(path, params), countsForGlobal: auth }, async (update) => {
108
+ for (;;) {
109
+ let response;
110
+ try {
111
+ response = await fetch(url, {
112
+ method,
113
+ headers,
114
+ body: typeof body === 'function' ? body() : body,
115
+ signal: options.signal
116
+ ? AbortSignal.any([
117
+ options.signal,
118
+ AbortSignal.timeout(this.#timeout),
119
+ ])
120
+ : AbortSignal.timeout(this.#timeout),
121
+ });
122
+ }
123
+ catch (error) {
124
+ if (options.signal?.aborted)
125
+ throw options.signal.reason;
126
+ if (failures++ >= this.#retries) {
127
+ throw new DiscordUnavailableError(method, path, error);
128
+ }
129
+ await sleep(250 * 2 ** (failures - 1));
130
+ continue;
131
+ }
132
+ update(readRateLimitHeaders(response.headers));
133
+ if (response.status === HttpStatus.TooManyRequests) {
134
+ const data = await readBody(response);
135
+ const retryAfter = typeof data?.retry_after ===
136
+ 'number'
137
+ ? data.retry_after
138
+ : Number(response.headers.get('retry-after') ?? 1);
139
+ if (rateLimits++ >= MAX_RATE_LIMIT_RETRIES) {
140
+ throw new DiscordApiError({
141
+ status: response.status,
142
+ method,
143
+ route: path,
144
+ body: data,
145
+ });
146
+ }
147
+ return rateLimitHit({
148
+ retryAfter: Number.isFinite(retryAfter) ? retryAfter : 1,
149
+ scope: response.headers.get('x-ratelimit-scope'),
150
+ global: response.headers.get('x-ratelimit-global') === 'true' ||
151
+ data?.global === true,
152
+ });
153
+ }
154
+ if (response.status >= 500) {
155
+ if (failures++ >= this.#retries) {
156
+ throw new DiscordUnavailableError(method, path, new Error(`HTTP ${response.status}`));
157
+ }
158
+ await response.body?.cancel();
159
+ await sleep(250 * 2 ** (failures - 1));
160
+ continue;
161
+ }
162
+ const data = await readBody(response);
163
+ if (response.ok)
164
+ return data;
165
+ if (response.status === HttpStatus.Unauthorized && auth) {
166
+ this.#tokenRefused = true;
167
+ throw new InvalidTokenError();
168
+ }
169
+ throw new DiscordApiError({
170
+ status: response.status,
171
+ method,
172
+ route: path,
173
+ body: data,
174
+ });
175
+ }
176
+ });
177
+ }
178
+ }
179
+ async function readBody(response) {
180
+ if (response.status === HttpStatus.NoContent)
181
+ return undefined;
182
+ const text = await response.text();
183
+ if (text === '')
184
+ return undefined;
185
+ if (response.headers.get('content-type')?.includes('application/json')) {
186
+ try {
187
+ return JSON.parse(text);
188
+ }
189
+ catch {
190
+ return text;
191
+ }
192
+ }
193
+ return text;
194
+ }
@@ -0,0 +1,48 @@
1
+ import { inspect } from 'node:util';
2
+ import type { Snowflake } from '../discord/types/common.js';
3
+ import { type Camelize } from '../util/case.js';
4
+ import type { Context } from './context.js';
5
+ /** Only the framework reads the context and the data of a structure. */
6
+ export declare let ctxOf: (structure: Structure<object>) => Context;
7
+ export declare let dataOf: <Raw extends object>(structure: Structure<Raw>) => Raw;
8
+ /** What a structure was created with: see `known.ts`, which owns it. */
9
+ export declare let originOf: (structure: Structure<object>) => object | undefined;
10
+ export declare let setOrigin: (structure: Structure<object>, origin: object | undefined) => void;
11
+ /**
12
+ * Something of Discord (a server, a member, a message...) as user code sees
13
+ * it: the data Discord sent, read through camelCase properties, and the
14
+ * actions that can be done on it. Structures are only created by the
15
+ * framework.
16
+ */
17
+ export declare abstract class Structure<Raw extends object> {
18
+ #private;
19
+ constructor(ctx: Context, data: Raw);
20
+ /** The data of Discord as a plain object, with camelCase names. */
21
+ toJSON(): Camelize<Raw>;
22
+ /** What `console.log` shows: the name of the class, then `toJSON()`. */
23
+ [inspect.custom](_depth: number, options: Parameters<typeof inspect>[1]): string;
24
+ }
25
+ /** A structure Discord identifies with an id. */
26
+ export declare abstract class IdStructure<Raw extends {
27
+ id: Snowflake;
28
+ }> extends Structure<Raw> {
29
+ /** The unique id Discord gave it. */
30
+ get id(): Snowflake;
31
+ /** When it was created: a Discord id contains its creation date. */
32
+ get createdAt(): Date;
33
+ }
34
+ /**
35
+ * Updates the data of a structure with what Discord just sent. Fields
36
+ * Discord left out keep their value.
37
+ */
38
+ export declare function patch<Raw extends object>(structure: Structure<Raw>, data: Partial<Raw>): void;
39
+ /** Copies shared methods onto a class (see `TextBasedChannel`). */
40
+ export declare function mixin(target: {
41
+ prototype: object;
42
+ }, methods: object): void;
43
+ /** Parses a date of Discord, which may be missing. */
44
+ export declare function toDate(value: string | null | undefined): Date | null;
45
+ /** The id of something given as an id or as a structure. */
46
+ export declare function idOf(value: Snowflake | {
47
+ id: Snowflake;
48
+ }): Snowflake;
@@ -0,0 +1,84 @@
1
+ import { inspect } from 'node:util';
2
+ import { snowflakeTimestamp } from '../discord/snowflake.js';
3
+ import { toCamelCase } from '../util/case.js';
4
+ /** Only the framework reads the context and the data of a structure. */
5
+ export let ctxOf;
6
+ export let dataOf;
7
+ /** What a structure was created with: see `known.ts`, which owns it. */
8
+ export let originOf;
9
+ export let setOrigin;
10
+ /**
11
+ * Something of Discord (a server, a member, a message...) as user code sees
12
+ * it: the data Discord sent, read through camelCase properties, and the
13
+ * actions that can be done on it. Structures are only created by the
14
+ * framework.
15
+ */
16
+ export class Structure {
17
+ #ctx;
18
+ #data;
19
+ // One reference, in the structure itself: nothing is allocated to keep it.
20
+ #origin = undefined;
21
+ static {
22
+ originOf = structure => structure.#origin;
23
+ setOrigin = (structure, origin) => {
24
+ structure.#origin = origin;
25
+ };
26
+ ctxOf = structure => structure.#ctx;
27
+ dataOf = (structure) => structure.#data;
28
+ }
29
+ constructor(ctx, data) {
30
+ this.#ctx = ctx;
31
+ this.#data = data;
32
+ }
33
+ /** The data of Discord as a plain object, with camelCase names. */
34
+ toJSON() {
35
+ return toCamelCase(this.#data);
36
+ }
37
+ /** What `console.log` shows: the name of the class, then `toJSON()`. */
38
+ [inspect.custom](_depth, options) {
39
+ return `${this.constructor.name} ${inspect(this.toJSON(), options)}`;
40
+ }
41
+ }
42
+ /** A structure Discord identifies with an id. */
43
+ export class IdStructure extends Structure {
44
+ /** The unique id Discord gave it. */
45
+ get id() {
46
+ return dataOf(this).id;
47
+ }
48
+ /** When it was created: a Discord id contains its creation date. */
49
+ get createdAt() {
50
+ return new Date(snowflakeTimestamp(this.id));
51
+ }
52
+ }
53
+ /**
54
+ * Updates the data of a structure with what Discord just sent. Fields
55
+ * Discord left out keep their value.
56
+ */
57
+ export function patch(structure, data) {
58
+ const target = dataOf(structure);
59
+ for (const [key, value] of Object.entries(data)) {
60
+ if (value !== undefined)
61
+ target[key] = value;
62
+ }
63
+ }
64
+ /** Copies shared methods onto a class (see `TextBasedChannel`). */
65
+ export function mixin(target, methods) {
66
+ for (const [name, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(methods))) {
67
+ Object.defineProperty(target.prototype, name, {
68
+ ...descriptor,
69
+ enumerable: false,
70
+ });
71
+ }
72
+ }
73
+ /** Parses a date of Discord, which may be missing. */
74
+ export function toDate(value) {
75
+ return value ? new Date(value) : null;
76
+ }
77
+ /** The id of something given as an id or as a structure. */
78
+ export function idOf(value) {
79
+ const id = typeof value === 'string' ? value : value?.id;
80
+ if (typeof id !== 'string' || id === '') {
81
+ throw new TypeError(`Expected an id or something with an id, got ${value === null ? 'null' : typeof value}.`);
82
+ }
83
+ return id;
84
+ }