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