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,92 @@
1
+ import { JSON_ERROR_CODES } from '../discord/codes.js';
2
+ /**
3
+ * Turns the nested `errors` object of an error response into a flat list.
4
+ * A problem is an `_errors` array placed under the key of the field, at any
5
+ * depth; array items are keyed by their index.
6
+ * @see https://docs.discord.com/developers/reference#error-messages
7
+ */
8
+ export function flattenFieldErrors(errors, path = '') {
9
+ if (typeof errors !== 'object' || errors === null)
10
+ return [];
11
+ const found = [];
12
+ for (const [key, value] of Object.entries(errors)) {
13
+ if (key === '_errors') {
14
+ if (!Array.isArray(value))
15
+ continue;
16
+ for (const error of value) {
17
+ found.push({
18
+ path,
19
+ code: String(error?.code ?? ''),
20
+ message: String(error?.message ?? ''),
21
+ });
22
+ }
23
+ continue;
24
+ }
25
+ const child = /^\d+$/.test(key)
26
+ ? `${path}[${key}]`
27
+ : path === ''
28
+ ? key
29
+ : `${path}.${key}`;
30
+ found.push(...flattenFieldErrors(value, child));
31
+ }
32
+ return found;
33
+ }
34
+ /** Discord answered a request with an error. */
35
+ export class DiscordApiError extends Error {
36
+ name = 'DiscordApiError';
37
+ /** The HTTP status of the response (403, 404...). */
38
+ status;
39
+ /**
40
+ * The JSON error code of Discord (50013 for "Missing Permissions"...), or
41
+ * 0 when the response had none.
42
+ * @see https://docs.discord.com/developers/topics/opcodes-and-status-codes#json-json-error-codes
43
+ */
44
+ code;
45
+ /** The fields of the request Discord refused, when it says which. */
46
+ fieldErrors;
47
+ /** The HTTP method of the request that failed. */
48
+ method;
49
+ /** The path of the endpoint as written in the documentation. */
50
+ route;
51
+ constructor(input) {
52
+ const body = typeof input.body === 'object' && input.body !== null
53
+ ? input.body
54
+ : {};
55
+ const code = typeof body.code === 'number' ? body.code : 0;
56
+ const fieldErrors = flattenFieldErrors(body.errors);
57
+ const reason = typeof body.message === 'string' && body.message !== ''
58
+ ? body.message
59
+ : ((typeof body.code === 'number'
60
+ ? JSON_ERROR_CODES[code]
61
+ : undefined) ?? `HTTP ${input.status}`);
62
+ const details = fieldErrors
63
+ .map(error => `\n ${error.path || 'request'}: ${error.message}`)
64
+ .join('');
65
+ super(`Discord refused ${input.method} ${input.route}: ${reason} (${code || input.status})${details}`);
66
+ this.status = input.status;
67
+ this.code = code;
68
+ this.fieldErrors = fieldErrors;
69
+ this.method = input.method;
70
+ this.route = input.route;
71
+ }
72
+ }
73
+ /** A request could not reach Discord, or Discord kept failing. */
74
+ export class DiscordUnavailableError extends Error {
75
+ name = 'DiscordUnavailableError';
76
+ /** The HTTP method of the request that failed. */
77
+ method;
78
+ /** The path of the endpoint as written in the documentation. */
79
+ route;
80
+ constructor(method, route, cause) {
81
+ super(`Discord could not be reached for ${method} ${route}. Check your internet connection and https://discordstatus.com, then try again.`, { cause });
82
+ this.method = method;
83
+ this.route = route;
84
+ }
85
+ }
86
+ /** The bot token was refused: no request can succeed until it is fixed. */
87
+ export class InvalidTokenError extends Error {
88
+ name = 'InvalidTokenError';
89
+ constructor() {
90
+ super('Discord refused the bot token. Copy it again from the Developer Portal (your application → Bot → Reset Token) into BOT_TOKEN in your .env file.');
91
+ }
92
+ }
@@ -0,0 +1,46 @@
1
+ /** What the headers of a response say about the rate limit of its route. */
2
+ export interface RateLimitHeaders {
3
+ /** `X-RateLimit-Bucket`: routes sharing a limit share this value. */
4
+ bucket: string | null;
5
+ /** `X-RateLimit-Remaining` */
6
+ remaining: number | null;
7
+ /** `X-RateLimit-Reset-After`, in seconds. */
8
+ resetAfter: number | null;
9
+ }
10
+ /** A 429 response. */
11
+ export interface RateLimitHit {
12
+ /** `Retry-After` / `retry_after`, in seconds. */
13
+ retryAfter: number;
14
+ /** `X-RateLimit-Scope`: `user`, `global` or `shared`. */
15
+ scope: string | null;
16
+ /** `X-RateLimit-Global` */
17
+ global: boolean;
18
+ }
19
+ export declare function readRateLimitHeaders(headers: Headers): RateLimitHeaders;
20
+ export interface RateLimiterOptions {
21
+ /** Requests per second for the whole bot. Default: 50. */
22
+ globalLimit?: number;
23
+ /** Called every time a request has to wait. */
24
+ onWait?: (info: {
25
+ route: string;
26
+ ms: number;
27
+ global: boolean;
28
+ }) => void;
29
+ }
30
+ export declare class RateLimiter {
31
+ #private;
32
+ constructor(options?: RateLimiterOptions);
33
+ /**
34
+ * Runs `send` when the limits allow it. Requests of the same bucket run
35
+ * one after the other; different buckets run in parallel. `send` reports
36
+ * what the response said through `update` and returns its result, or a
37
+ * `RateLimitHit` to be run again after the delay Discord asked for.
38
+ */
39
+ run<T>(input: {
40
+ route: string;
41
+ major: string;
42
+ /** Interaction endpoints are not bound to the global rate limit. */
43
+ countsForGlobal: boolean;
44
+ }, send: (update: (headers: RateLimitHeaders) => void) => Promise<T | RateLimitHit>): Promise<T>;
45
+ }
46
+ export declare function rateLimitHit(hit: RateLimitHit): RateLimitHit;
@@ -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
+ }