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,492 @@
1
+ // What a file declares with `command()`, checked when it loads against the
2
+ // limits of Discord, so a mistake is explained before Discord refuses it.
3
+ // https://docs.discord.com/developers/interactions/application-commands
4
+ import { ALL_PERMISSIONS } from '../discord/permissions.js';
5
+ import { ChannelType } from '../discord/types/channel.js';
6
+ import { Locale } from '../discord/types/common.js';
7
+ import { PermissionFlags } from '../discord/types/permissions.js';
8
+ import { COOLDOWN_SCOPES, NO_COOLDOWN, } from '../interactions/cooldown.js';
9
+ import { readFile } from 'node:fs/promises';
10
+ import { listSources } from '../loader/loader.js';
11
+ import { DURATION_EXAMPLE, parseDuration } from '../util/duration.js';
12
+ import { isCommandDeclaration, } from './command.js';
13
+ /**
14
+ * The names of commands and options: lowercase letters of any language,
15
+ * digits, `-` and `_`, 32 characters at most.
16
+ * @see https://docs.discord.com/developers/interactions/application-commands#application-command-object-application-command-naming
17
+ */
18
+ const NAME = /^[-_\u02BC\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u;
19
+ const isName = (name) => NAME.test(name) && name === name.toLowerCase();
20
+ const MAX_OPTIONS = 25;
21
+ const MAX_CHOICES = 25;
22
+ const MAX_DESCRIPTION = 100;
23
+ const MAX_STRING_LENGTH = 6000;
24
+ const OPTION_TYPES = [
25
+ 'string',
26
+ 'integer',
27
+ 'number',
28
+ 'boolean',
29
+ 'user',
30
+ 'role',
31
+ 'channel',
32
+ 'mentionable',
33
+ 'attachment',
34
+ ];
35
+ /** The keys each kind of option accepts, besides the common ones. */
36
+ const OPTION_KEYS = {
37
+ string: ['choices', 'minLength', 'maxLength'],
38
+ integer: ['choices', 'min', 'max'],
39
+ number: ['choices', 'min', 'max'],
40
+ channel: ['channelTypes'],
41
+ };
42
+ const COMMAND_KEYS = [
43
+ 'name',
44
+ 'description',
45
+ 'options',
46
+ 'locales',
47
+ 'permissions',
48
+ 'where',
49
+ 'nsfw',
50
+ 'ephemeral',
51
+ 'cooldown',
52
+ 'autocomplete',
53
+ 'run',
54
+ ];
55
+ /** The kinds of options whose suggestions can come from the bot. */
56
+ const SUGGESTABLE_TYPES = ['string', 'integer', 'number'];
57
+ const fail = (message) => {
58
+ throw new TypeError(message);
59
+ };
60
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
61
+ function checkDescription(what, description) {
62
+ if (typeof description !== 'string' || description.trim() === '') {
63
+ return fail(`${what} needs a description: a short text that says what it is for, like description: 'Bans a member'.`);
64
+ }
65
+ if (description.length > MAX_DESCRIPTION) {
66
+ return fail(`The description of ${what} is ${description.length} characters long: Discord accepts ${MAX_DESCRIPTION} at most.`);
67
+ }
68
+ return description;
69
+ }
70
+ function checkKeys(what, value, allowed) {
71
+ for (const key of Object.keys(value)) {
72
+ if (!allowed.includes(key)) {
73
+ fail(`"${key}" is not something ${what} has. It can have: ${allowed.join(', ')}.`);
74
+ }
75
+ }
76
+ }
77
+ function checkBoolean(what, value) {
78
+ if (value === undefined)
79
+ return false;
80
+ if (typeof value !== 'boolean') {
81
+ return fail(`${what} is true or false, got ${JSON.stringify(value)}.`);
82
+ }
83
+ return value;
84
+ }
85
+ const PLACES = ['guild', 'dm', 'both'];
86
+ function checkWhere(value) {
87
+ if (value === undefined)
88
+ return 'guild';
89
+ if (!PLACES.includes(value)) {
90
+ return fail(`"where" says where the command can be used: 'guild' (in servers, which is the default), 'dm' (in private messages with the bot) or 'both'. Got ${JSON.stringify(value) ?? typeof value}.`);
91
+ }
92
+ return value;
93
+ }
94
+ /** The shortest cooldown accepted, in milliseconds. */
95
+ const MIN_COOLDOWN = 1000;
96
+ function checkCooldown(value, where) {
97
+ if (value === undefined)
98
+ return NO_COOLDOWN;
99
+ const example = `cooldown: '10s' (${DURATION_EXAMPLE}) for each person, or cooldown: { user: '10s', channel: '5s', guild: '1m' } for each place`;
100
+ const duration = (what, text) => {
101
+ if (typeof text !== 'string' || text.trim() === '') {
102
+ return fail(`${what} is a duration, got ${JSON.stringify(text) ?? typeof text}: ${example}.`);
103
+ }
104
+ const ms = parseDuration(text);
105
+ if (ms === null) {
106
+ return fail(`${what} is ${JSON.stringify(text)}, which is not a duration: ${example}.`);
107
+ }
108
+ if (ms < MIN_COOLDOWN) {
109
+ return fail(`${what} is ${JSON.stringify(text)}: a cooldown is 1 second at least. Leave it out for none.`);
110
+ }
111
+ return ms;
112
+ };
113
+ if (typeof value === 'string') {
114
+ return Object.freeze({
115
+ ...NO_COOLDOWN,
116
+ user: duration('"cooldown"', value),
117
+ });
118
+ }
119
+ if (!isRecord(value)) {
120
+ return fail(`"cooldown" says how long to wait before the command can be used again: ${example}.`);
121
+ }
122
+ checkKeys('"cooldown"', value, COOLDOWN_SCOPES);
123
+ const limits = { ...NO_COOLDOWN };
124
+ for (const scope of COOLDOWN_SCOPES) {
125
+ if (value[scope] === undefined)
126
+ continue;
127
+ limits[scope] = duration(`"${scope}" of "cooldown"`, value[scope]);
128
+ }
129
+ if (limits.user + limits.channel + limits.guild === 0) {
130
+ return fail(`"cooldown" is empty: ${example}. Leave it out for none.`);
131
+ }
132
+ if (where === 'dm' && limits.guild > 0) {
133
+ return fail(`"cooldown" has "guild", but this command only works in private messages (where: 'dm'), where there is no server: use "user".`);
134
+ }
135
+ return Object.freeze(limits);
136
+ }
137
+ function checkOption(name, option, described) {
138
+ const what = `the option "${name}"`;
139
+ if (!isName(name)) {
140
+ fail(`"${name}" can't be the name of an option: use lowercase letters, digits, - and _ only, 32 characters at most.`);
141
+ }
142
+ if (!isRecord(option)) {
143
+ return fail(`${what} must be an object like { type: 'string', description: '...' }.`);
144
+ }
145
+ const type = option.type;
146
+ if (!OPTION_TYPES.includes(type)) {
147
+ return fail(`The type of ${what} is ${JSON.stringify(type)}, which does not exist. Types are: ${OPTION_TYPES.join(', ')}.`);
148
+ }
149
+ checkKeys(what, option, [
150
+ 'type',
151
+ 'description',
152
+ 'required',
153
+ ...(OPTION_KEYS[type] ?? []),
154
+ ]);
155
+ // Described in the file with the command, or in the default language.
156
+ if (described || option.description !== undefined) {
157
+ checkDescription(what, option.description);
158
+ }
159
+ checkBoolean(`"required" of ${what}`, option.required);
160
+ const isText = type === 'string';
161
+ if (option.choices !== undefined) {
162
+ const values = Array.isArray(option.choices)
163
+ ? option.choices
164
+ : isRecord(option.choices)
165
+ ? Object.values(option.choices)
166
+ : fail(`The choices of ${what} are a list like ['a', 'b'] or an object like { 'Shown name': 'value' }.`);
167
+ const labels = Array.isArray(option.choices)
168
+ ? values.map(String)
169
+ : Object.keys(option.choices);
170
+ if (values.length === 0 || values.length > MAX_CHOICES) {
171
+ fail(`${what} has ${values.length} choices: Discord accepts between 1 and ${MAX_CHOICES}.`);
172
+ }
173
+ for (const [index, value] of values.entries()) {
174
+ if (typeof value !== (isText ? 'string' : 'number')) {
175
+ fail(`The choices of ${what} must be ${isText ? 'texts' : 'numbers'}, since its type is ${String(type)}: got ${JSON.stringify(value)}.`);
176
+ }
177
+ if (type === 'integer' && !Number.isInteger(value)) {
178
+ fail(`The choice ${String(value)} of ${what} is not a whole number.`);
179
+ }
180
+ const label = labels[index];
181
+ if (label.length < 1 || label.length > MAX_DESCRIPTION) {
182
+ fail(`A choice of ${what} is ${label.length} characters long: Discord accepts between 1 and ${MAX_DESCRIPTION}.`);
183
+ }
184
+ }
185
+ if (new Set(labels).size !== labels.length) {
186
+ fail(`${what} has the same choice twice.`);
187
+ }
188
+ }
189
+ const range = (low, high, limit) => {
190
+ for (const key of [low, high]) {
191
+ const value = option[key];
192
+ if (value === undefined)
193
+ continue;
194
+ if (typeof value !== 'number' || Number.isNaN(value)) {
195
+ fail(`"${key}" of ${what} is a number, got ${JSON.stringify(value)}.`);
196
+ }
197
+ if ((limit !== undefined || type === 'integer') &&
198
+ !Number.isInteger(value)) {
199
+ fail(`"${key}" of ${what} is a whole number, got ${String(value)}.`);
200
+ }
201
+ if (limit !== undefined &&
202
+ (value < 0 || value > limit)) {
203
+ fail(`"${key}" of ${what} is between 0 and ${limit}, got ${String(value)}.`);
204
+ }
205
+ }
206
+ if (typeof option[low] === 'number' &&
207
+ typeof option[high] === 'number' &&
208
+ option[low] > option[high]) {
209
+ fail(`${what} can never be filled in: its "${low}" (${option[low]}) is greater than its "${high}" (${option[high]}).`);
210
+ }
211
+ };
212
+ if (isText)
213
+ range('minLength', 'maxLength', MAX_STRING_LENGTH);
214
+ else
215
+ range('min', 'max');
216
+ if (option.channelTypes !== undefined) {
217
+ const known = Object.values(ChannelType);
218
+ if (!Array.isArray(option.channelTypes) ||
219
+ !option.channelTypes.every(one => known.includes(one))) {
220
+ fail(`"channelTypes" of ${what} is a list of ChannelType values, like [ChannelType.GuildText].`);
221
+ }
222
+ }
223
+ return {
224
+ ...option,
225
+ name,
226
+ description: option.description ?? '',
227
+ };
228
+ }
229
+ const LOCALES = Object.values(Locale);
230
+ /** Checks `locales` and returns it as the dictionaries Discord takes. */
231
+ export function checkLocales(locales, options) {
232
+ const names = {};
233
+ const descriptions = {};
234
+ const optionLocales = {};
235
+ if (locales === undefined) {
236
+ return { names, descriptions, optionLocales };
237
+ }
238
+ if (!isRecord(locales)) {
239
+ return fail(`"locales" is an object whose keys are languages: locales: { fr: { description: '...' } }`);
240
+ }
241
+ for (const [locale, translation] of Object.entries(locales)) {
242
+ const where = `the "${locale}" translation`;
243
+ if (!LOCALES.includes(locale)) {
244
+ fail(`"${locale}" is not a language Discord knows. Languages are: ${LOCALES.join(', ')}.`);
245
+ }
246
+ if (!isRecord(translation)) {
247
+ fail(`${where} must be an object like { name: '...', description: '...' }.`);
248
+ }
249
+ const fields = translation;
250
+ checkKeys(where, fields, ['name', 'description', 'options']);
251
+ const readName = (what, name) => {
252
+ if (name === undefined)
253
+ return undefined;
254
+ if (typeof name !== 'string' || !isName(name)) {
255
+ return fail(`${JSON.stringify(name)} can't be the name of ${what} in ${where}: use lowercase letters, digits, - and _ only, 32 characters at most.`);
256
+ }
257
+ return name;
258
+ };
259
+ const readDescription = (what, text) => text === undefined
260
+ ? undefined
261
+ : checkDescription(`${what} in ${where}`, text);
262
+ const name = readName('this command', fields.name);
263
+ if (name !== undefined)
264
+ names[locale] = name;
265
+ const description = readDescription('this command', fields.description);
266
+ if (description !== undefined)
267
+ descriptions[locale] = description;
268
+ if (fields.options === undefined)
269
+ continue;
270
+ if (!isRecord(fields.options)) {
271
+ fail(`"options" of ${where} is an object whose keys are the names of the options.`);
272
+ }
273
+ for (const [optionName, optionTranslation] of Object.entries(fields.options)) {
274
+ const option = options.find(one => one.name === optionName);
275
+ if (!option) {
276
+ fail(`${where} translates an option "${optionName}" that this command does not have.${options.length > 0 ? ` Its options are: ${options.map(one => one.name).join(', ')}.` : ''}`);
277
+ }
278
+ const what = `the option "${optionName}"`;
279
+ if (!isRecord(optionTranslation)) {
280
+ fail(`${what} of ${where} must be an object like { name: '...', description: '...' }.`);
281
+ }
282
+ const translated = optionTranslation;
283
+ checkKeys(`${what} of ${where}`, translated, [
284
+ 'name',
285
+ 'description',
286
+ 'choices',
287
+ ]);
288
+ const target = (optionLocales[optionName] ??= {
289
+ names: {},
290
+ descriptions: {},
291
+ choices: {},
292
+ });
293
+ const translatedName = readName(what, translated.name);
294
+ if (translatedName !== undefined)
295
+ target.names[locale] = translatedName;
296
+ const translatedDescription = readDescription(what, translated.description);
297
+ if (translatedDescription !== undefined) {
298
+ target.descriptions[locale] = translatedDescription;
299
+ }
300
+ if (translated.choices === undefined)
301
+ continue;
302
+ const declared = choiceLabels(option);
303
+ if (!isRecord(translated.choices) || declared.length === 0) {
304
+ fail(declared.length === 0
305
+ ? `${where} translates choices of ${what}, which has no choices.`
306
+ : `"choices" of ${what} in ${where} is an object like { ${JSON.stringify(declared[0])}: '...' }.`);
307
+ }
308
+ for (const [label, text] of Object.entries(translated.choices)) {
309
+ if (!declared.includes(label)) {
310
+ fail(`${where} translates a choice "${label}" that ${what} does not have. Its choices are: ${declared.join(', ')}.`);
311
+ }
312
+ if (typeof text !== 'string' ||
313
+ text.length < 1 ||
314
+ text.length > MAX_DESCRIPTION) {
315
+ fail(`The choice "${label}" of ${what} in ${where} must be a text of 1 to ${MAX_DESCRIPTION} characters.`);
316
+ }
317
+ (target.choices[label] ??= {})[locale] = text;
318
+ }
319
+ }
320
+ // A translated option name must not be the name of another option, in
321
+ // that language or by default.
322
+ const taken = new Map();
323
+ for (const option of options) {
324
+ const shown = optionLocales[option.name]?.names[locale] ?? option.name;
325
+ const other = taken.get(shown);
326
+ if (other !== undefined) {
327
+ fail(`In ${where}, the options "${other}" and "${option.name}" would both be called "${shown}".`);
328
+ }
329
+ taken.set(shown, option.name);
330
+ if (shown !== option.name && options.some(one => one.name === shown)) {
331
+ fail(`In ${where}, the option "${option.name}" is called "${shown}", which is the name of another option.`);
332
+ }
333
+ }
334
+ }
335
+ return { names, descriptions, optionLocales };
336
+ }
337
+ /** The choices of an option, as they are named by default. */
338
+ function choiceLabels(option) {
339
+ if (!('choices' in option) || !option.choices)
340
+ return [];
341
+ return Array.isArray(option.choices)
342
+ ? option.choices.map(String)
343
+ : Object.keys(option.choices);
344
+ }
345
+ /**
346
+ * Checks `autocomplete`: one function per option, for an option that can
347
+ * have suggestions (a text or a number, without choices).
348
+ * @see https://docs.discord.com/developers/interactions/application-commands#autocomplete
349
+ */
350
+ function checkAutocomplete(value, options) {
351
+ if (value === undefined)
352
+ return Object.freeze({});
353
+ const example = `autocomplete: { ${options[0]?.name ?? 'name'}: ({ value }) => [...] }`;
354
+ if (!isRecord(value)) {
355
+ return fail(`"autocomplete" is an object whose keys are the names of the options to suggest for, each a function returning the suggestions: ${example}`);
356
+ }
357
+ const functions = {};
358
+ for (const [name, fn] of Object.entries(value)) {
359
+ const option = options.find(one => one.name === name);
360
+ const what = `"${name}" of "autocomplete"`;
361
+ if (!option) {
362
+ fail(`${what} is not an option of this command.${options.length > 0 ? ` Its options are: ${options.map(one => one.name).join(', ')}.` : ' Declare it in "options" first.'}`);
363
+ }
364
+ if (!SUGGESTABLE_TYPES.includes(option.type)) {
365
+ fail(`${what} can't have suggestions: the option "${name}" is a ${option.type}. Only a ${SUGGESTABLE_TYPES.join(', ')} option can.`);
366
+ }
367
+ if ('choices' in option && option.choices) {
368
+ fail(`${what} can't have suggestions: the option "${name}" has "choices", and Discord takes one or the other. Remove its choices to suggest them from here, or take it out of "autocomplete".`);
369
+ }
370
+ if (typeof fn !== 'function') {
371
+ fail(`${what} is a function that returns the suggestions, like ${name}: ({ value }) => [...]: got ${JSON.stringify(fn) ?? typeof fn}.`);
372
+ }
373
+ functions[name] = fn;
374
+ }
375
+ return Object.freeze(functions);
376
+ }
377
+ function checkPermissions(permissions) {
378
+ if (permissions === undefined)
379
+ return 0n;
380
+ if (!Array.isArray(permissions)) {
381
+ return fail(`"permissions" is a list of permission names, like ['BanMembers'].`);
382
+ }
383
+ let bits = 0n;
384
+ for (const name of permissions) {
385
+ const flag = PermissionFlags[name];
386
+ if (typeof name !== 'string' || flag === undefined) {
387
+ return fail(`${JSON.stringify(name)} is not a permission. Permissions are: ${Object.keys(PermissionFlags).join(', ')}.`);
388
+ }
389
+ bits |= flag;
390
+ }
391
+ return bits & ALL_PERMISSIONS;
392
+ }
393
+ const EXAMPLE = `command({ name: 'ping', description: '...', run({ interaction }) { ... } })`;
394
+ /**
395
+ * The parts of the name of a command: `'mod ban'` is `['mod', 'ban']`.
396
+ * One part is a command, two a subcommand, three a subcommand in a group:
397
+ * Discord allows nothing deeper.
398
+ */
399
+ export function readCommandName(name) {
400
+ if (typeof name !== 'string' || name.trim() === '') {
401
+ return fail(`This command has no "name": what people type after the /, like ${EXAMPLE}. A subcommand is written with its parents: name: 'mod ban' is /mod ban.`);
402
+ }
403
+ const parts = name.trim().split(/\s+/);
404
+ if (parts.length > 3) {
405
+ fail(`The name ${JSON.stringify(name)} has ${parts.length} words: Discord allows a command, a group and a subcommand, so 3 at most ('mod roles add' is /mod roles add).`);
406
+ }
407
+ for (const part of parts) {
408
+ if (!isName(part)) {
409
+ fail(`"${part}" can't be in the name of a command: use lowercase letters, digits, - and _ only, 32 characters at most.`);
410
+ }
411
+ }
412
+ return parts;
413
+ }
414
+ /**
415
+ * A slash command, declared anywhere in `src/` with `command()`: `name` is
416
+ * what people type after the `/`, with its parents for a subcommand
417
+ * (`'mod ban'` is `/mod ban`, `'mod roles add'` is `/mod roles add`). A
418
+ * file may declare several.
419
+ */
420
+ export const commandDeclaration = {
421
+ one: 'command',
422
+ many: 'commands',
423
+ is: isCommandDeclaration,
424
+ list: true,
425
+ read(value) {
426
+ const config = value.config;
427
+ if (!isRecord(config)) {
428
+ return fail(`command() needs an object: ${EXAMPLE}`);
429
+ }
430
+ checkKeys('a command', config, COMMAND_KEYS);
431
+ const parts = readCommandName(config.name);
432
+ // No description: the language files describe the command, options
433
+ // included, and hold its translations.
434
+ const described = config.description !== undefined;
435
+ const description = described
436
+ ? checkDescription('this command', config.description)
437
+ : '';
438
+ if (!described && config.locales !== undefined) {
439
+ fail(`This command has no "description", so its texts come from the language files: put its "locales" there too (commands: { ... }), or give it a description here.`);
440
+ }
441
+ if (typeof config.run !== 'function') {
442
+ fail(`This command has no "run": the function to run when someone uses it, like async run({ interaction }) { await interaction.reply('Pong!'); }`);
443
+ }
444
+ const rawOptions = config.options ?? {};
445
+ if (!isRecord(rawOptions)) {
446
+ fail(`"options" is an object whose keys are the names of the options: options: { name: { type: 'string', description: '...' } }`);
447
+ }
448
+ const options = Object.entries(rawOptions).map(([name, option]) => checkOption(name, option, described));
449
+ if (options.length > MAX_OPTIONS) {
450
+ fail(`This command has ${options.length} options: Discord accepts ${MAX_OPTIONS} at most.`);
451
+ }
452
+ // Required options must be listed before optional options.
453
+ options.sort((a, b) => Number(b.required === true) - Number(a.required === true));
454
+ const where = checkWhere(config.where);
455
+ return Object.freeze({
456
+ ...checkLocales(config.locales, options),
457
+ path: Object.freeze(parts),
458
+ described,
459
+ description,
460
+ options: Object.freeze(options),
461
+ permissions: checkPermissions(config.permissions),
462
+ where,
463
+ nsfw: checkBoolean('"nsfw"', config.nsfw),
464
+ ephemeral: checkBoolean('"ephemeral"', config.ephemeral),
465
+ cooldown: checkCooldown(config.cooldown, where),
466
+ autocomplete: checkAutocomplete(config.autocomplete, options),
467
+ run: config.run,
468
+ });
469
+ },
470
+ };
471
+ /** `command({ ... name: '...' ... })`, as written in a file. */
472
+ const DECLARED = /\bcommand\s*\(\s*\{[^]*?\bname\s*:\s*['"`]([^'"`]+)['"`]/g;
473
+ /**
474
+ * The names of the commands of a project, found without running its
475
+ * files: a text search for `command({ name: '...' })`. Approximate on
476
+ * purpose, for `sync`, which runs nothing: `dev` and `build` then write
477
+ * the exact ones. Whether a command is described is not known: none is.
478
+ */
479
+ export async function scanCommands(cwd) {
480
+ const names = new Set();
481
+ for (const { path } of await listSources(cwd)) {
482
+ const code = await readFile(path, 'utf8').catch(() => '');
483
+ if (!code.includes('command('))
484
+ continue;
485
+ for (const match of code.matchAll(DECLARED)) {
486
+ const name = match[1].trim().split(/\s+/).join(' ');
487
+ if (name !== '')
488
+ names.add(name);
489
+ }
490
+ }
491
+ return [...names].sort();
492
+ }
@@ -0,0 +1,18 @@
1
+ import type { Snowflake } from '../discord/types/common.js';
2
+ import type { Rest } from '../rest/rest.js';
3
+ import type { CommandPayload } from './tree.js';
4
+ /**
5
+ * Registers the commands of the bot, replacing what was there: on one
6
+ * server when `guildId` is given (they show up there at once), for
7
+ * everyone otherwise.
8
+ * @returns whether Discord was told (false when nothing changed)
9
+ * @see https://docs.discord.com/developers/interactions/application-commands#bulk-overwrite-guild-application-commands
10
+ * @see https://docs.discord.com/developers/interactions/application-commands#bulk-overwrite-global-application-commands
11
+ */
12
+ export declare function registerCommands(input: {
13
+ rest: Rest;
14
+ projectDir: string;
15
+ applicationId: Snowflake;
16
+ guildId?: Snowflake;
17
+ commands: readonly CommandPayload[];
18
+ }): Promise<boolean>;
@@ -0,0 +1,42 @@
1
+ // Tells Discord which commands the bot has. Done only when they changed:
2
+ // what was last registered is remembered in the `.chapterjs/` folder of the
3
+ // project, so starting the bot again costs no request.
4
+ import { createHash } from 'node:crypto';
5
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
6
+ import { dirname, join } from 'node:path';
7
+ import { BulkOverwriteGlobalApplicationCommands, BulkOverwriteGuildApplicationCommands, } from '../discord/endpoints.js';
8
+ /**
9
+ * Registers the commands of the bot, replacing what was there: on one
10
+ * server when `guildId` is given (they show up there at once), for
11
+ * everyone otherwise.
12
+ * @returns whether Discord was told (false when nothing changed)
13
+ * @see https://docs.discord.com/developers/interactions/application-commands#bulk-overwrite-guild-application-commands
14
+ * @see https://docs.discord.com/developers/interactions/application-commands#bulk-overwrite-global-application-commands
15
+ */
16
+ export async function registerCommands(input) {
17
+ const { rest, applicationId, guildId, commands } = input;
18
+ // One memory per kind: running the bot for everyone and on its dev server
19
+ // from the same folder must not make each forget what the other did.
20
+ const file = join(input.projectDir, '.chapterjs', 'cache', guildId ? 'commands.json' : 'commands.global.json');
21
+ // The same commands for another bot or another server are not the same.
22
+ const hash = createHash('sha256')
23
+ .update(JSON.stringify({ applicationId, guildId, commands }))
24
+ .digest('hex');
25
+ const previous = await readFile(file, 'utf8').catch(() => null);
26
+ if (previous === hash)
27
+ return false;
28
+ // A project that never had commands has nothing to tell Discord.
29
+ if (previous === null && commands.length === 0)
30
+ return false;
31
+ if (guildId) {
32
+ await rest.request(BulkOverwriteGuildApplicationCommands, [applicationId, guildId], { body: [...commands] });
33
+ }
34
+ else {
35
+ await rest.request(BulkOverwriteGlobalApplicationCommands, [applicationId], {
36
+ body: [...commands],
37
+ });
38
+ }
39
+ await mkdir(dirname(file), { recursive: true });
40
+ await writeFile(file, hash);
41
+ return true;
42
+ }
@@ -0,0 +1,20 @@
1
+ import { type RawInteraction } from '../discord/types/interaction.js';
2
+ import { type CooldownStore } from '../interactions/cooldown.js';
3
+ import { type Reporter } from '../interactions/dispatch.js';
4
+ import type { Context } from '../structures/context.js';
5
+ import { type CommandEntry } from './tree.js';
6
+ export interface CommandRouterOptions extends Reporter {
7
+ /** Where the last uses of the commands with a `cooldown` are kept. */
8
+ cooldowns?: CooldownStore;
9
+ }
10
+ export declare class CommandRouter {
11
+ #private;
12
+ constructor(options: CommandRouterOptions);
13
+ /** Replaces every command (after a load or a reload). */
14
+ set(entries: Iterable<CommandEntry>): void;
15
+ /**
16
+ * Handles an Interaction Create event, if it is a slash command being
17
+ * used, or someone typing in one of its options with suggestions.
18
+ */
19
+ dispatch(ctx: Context, raw: RawInteraction): void;
20
+ }