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,228 @@
1
+ // Runs the command someone used: finds its file, turns what Discord sent
2
+ // into what `run` receives, and makes sure the person always gets an answer.
3
+ import { Permissions } from '../discord/permissions.js';
4
+ import { ApplicationCommandOptionType, ApplicationCommandType, } from '../discord/types/application-command.js';
5
+ import { InteractionType, } from '../discord/types/interaction.js';
6
+ import { timestamp, TimestampStyle } from '../discord/formatting.js';
7
+ import { checkCooldown, MemoryCooldowns, } from '../interactions/cooldown.js';
8
+ import { authorOf, placeContext, refuse, resolvePlace, runInteraction, } from '../interactions/dispatch.js';
9
+ import { audienceLocale, translation } from '../messages/translate.js';
10
+ import { CommandInteraction } from '../structures/interaction.js';
11
+ import { remember } from '../structures/known.js';
12
+ import { toCamelCase } from '../util/case.js';
13
+ import { suggest } from './autocomplete.js';
14
+ import { commandName } from './tree.js';
15
+ /**
16
+ * How long a refusal for a cooldown may wait before deleting itself: the
17
+ * token of an interaction lives 15 minutes, with room for the request.
18
+ * @see https://docs.discord.com/developers/interactions/receiving-and-responding#interaction-object
19
+ */
20
+ const DELETE_WITHIN = 14 * 60_000;
21
+ const SUB = ApplicationCommandOptionType.SubCommand;
22
+ const GROUP = ApplicationCommandOptionType.SubCommandGroup;
23
+ export class CommandRouter {
24
+ #commands = new Map();
25
+ #options;
26
+ // Kept by the router, not the entries: a reload keeps the cooldowns.
27
+ #cooldowns;
28
+ // What was already said about suggestions, which come on every keystroke.
29
+ #warnedOnce = new Set();
30
+ constructor(options) {
31
+ this.#options = options;
32
+ this.#cooldowns = options.cooldowns ?? new MemoryCooldowns();
33
+ }
34
+ /** Replaces every command (after a load or a reload). */
35
+ set(entries) {
36
+ const commands = new Map();
37
+ for (const entry of entries) {
38
+ commands.set(entry.command.path.join(' '), entry);
39
+ }
40
+ this.#commands = commands;
41
+ }
42
+ /**
43
+ * Handles an Interaction Create event, if it is a slash command being
44
+ * used, or someone typing in one of its options with suggestions.
45
+ */
46
+ dispatch(ctx, raw) {
47
+ const autocomplete = raw.type === InteractionType.ApplicationCommandAutocomplete;
48
+ if (raw.type !== InteractionType.ApplicationCommand && !autocomplete) {
49
+ return;
50
+ }
51
+ const data = raw.data;
52
+ if (!data || data.type !== ApplicationCommandType.ChatInput)
53
+ return;
54
+ // `/mod roles add` comes as a command with nested options.
55
+ const path = [data.name];
56
+ let given = data.options ?? [];
57
+ while (given[0] && (given[0].type === SUB || given[0].type === GROUP)) {
58
+ path.push(given[0].name);
59
+ given = given[0].options ?? [];
60
+ }
61
+ const entry = this.#commands.get(path.join(' '));
62
+ if (autocomplete) {
63
+ suggest(ctx, raw, entry, given, this.#options, this.#warnedOnce);
64
+ return;
65
+ }
66
+ const name = commandName(path);
67
+ const author = authorOf(raw);
68
+ if (!author)
69
+ return;
70
+ const { user: _user, member: _member, message: _message, ...rest } = raw;
71
+ const user = ctx.entities.user(author.user);
72
+ const interaction = new CommandInteraction(ctx, rest, user, {
73
+ ephemeral: entry?.command.ephemeral ?? false,
74
+ commandName: name,
75
+ });
76
+ if (!entry) {
77
+ // Still registered on Discord, but its file is gone or broken.
78
+ refuse(interaction, 'unavailable');
79
+ return;
80
+ }
81
+ const { command, file } = entry;
82
+ const placed = resolvePlace(ctx, raw, user, author.member, command.where, message => this.#options.onWarning(file, `${name} ${message}`));
83
+ if (!placed.place) {
84
+ refuse(interaction, placed.refusal, { what: 'command' });
85
+ return;
86
+ }
87
+ const { place } = placed;
88
+ // What the member can do where the command was used, overwrites
89
+ // included: Discord computes it for us.
90
+ if (command.permissions !== 0n && author.member) {
91
+ const missing = new Permissions(author.member.permissions ?? '0').missing(command.permissions);
92
+ if (missing.length > 0) {
93
+ refuse(interaction, 'needsPermissions', {
94
+ permissions: missing.join(', '),
95
+ count: missing.length,
96
+ });
97
+ return;
98
+ }
99
+ }
100
+ // Used too soon: the person is told when, and nothing runs.
101
+ const again = checkCooldown(this.#cooldowns, path.join(' '), command.cooldown, {
102
+ user: user.id,
103
+ channel: place.channel.id,
104
+ guild: place.guild?.id,
105
+ });
106
+ if (again > 0) {
107
+ this.#refuseUntil(interaction, again);
108
+ return;
109
+ }
110
+ let options;
111
+ try {
112
+ options = this.#readOptions(ctx, raw, data, given, entry);
113
+ }
114
+ catch (error) {
115
+ this.#options.onError(file, error);
116
+ refuse(interaction, 'failed', { what: 'command' });
117
+ return;
118
+ }
119
+ remember(interaction, {
120
+ guild: place.guild ?? undefined,
121
+ member: place.member ?? undefined,
122
+ channel: place.channel,
123
+ });
124
+ const context = Object.freeze({
125
+ interaction,
126
+ options: Object.freeze(options),
127
+ user,
128
+ ...placeContext(place, command.where),
129
+ // `t` speaks the language of who will read the answer.
130
+ ...translation(ctx, audienceLocale({
131
+ person: raw.locale,
132
+ guild: place.guild,
133
+ ephemeral: command.ephemeral,
134
+ })),
135
+ });
136
+ runInteraction({
137
+ interaction,
138
+ file,
139
+ name,
140
+ what: 'command',
141
+ reporter: this.#options,
142
+ defer: () => interaction.defer(),
143
+ unanswered: 'finished without answering: the person sees "The application did not respond". Call interaction.reply() in run.',
144
+ run: () => command.run(context),
145
+ });
146
+ }
147
+ /**
148
+ * Tells the person when they can use the command again. Discord shows a
149
+ * relative time ("in 5 seconds") that counts down, but keeps counting
150
+ * ("52 seconds ago") once passed: the answer is deleted when the
151
+ * cooldown ends, as long as the interaction can still be edited (15
152
+ * minutes); beyond that, the date and time are shown instead.
153
+ */
154
+ #refuseUntil(interaction, until) {
155
+ const at = Math.ceil(until / 1000) * 1000;
156
+ const wait = at - Date.now();
157
+ const deletable = wait <= DELETE_WITHIN;
158
+ const when = timestamp(at, deletable ? TimestampStyle.Relative : TimestampStyle.LongDateShortTime);
159
+ const refused = refuse(interaction, 'cooldown', { what: 'command', when });
160
+ if (!deletable)
161
+ return;
162
+ const timer = setTimeout(() => {
163
+ refused.then(() => interaction.delete()).catch(() => { });
164
+ }, wait);
165
+ // Nothing of the bot waits for it: the process may stop meanwhile.
166
+ timer.unref();
167
+ }
168
+ /** The value of each declared option, as structures where it applies. */
169
+ #readOptions(ctx, raw, data, given, { command }) {
170
+ const values = new Map(given.map(option => [option.name, option.value]));
171
+ const resolved = data.resolved ?? {};
172
+ const guildId = raw.guild_id;
173
+ const userOf = (id) => {
174
+ const rawUser = resolved.users?.[id];
175
+ if (!rawUser)
176
+ return undefined;
177
+ const member = resolved.members?.[id];
178
+ // The member comes along: remembered, so `guild.members` has it.
179
+ if (member && guildId) {
180
+ ctx.entities.member(guildId, member, rawUser);
181
+ }
182
+ return ctx.entities.user(rawUser);
183
+ };
184
+ const roleOf = (id) => {
185
+ const role = resolved.roles?.[id];
186
+ return role && guildId ? ctx.entities.role(guildId, role) : undefined;
187
+ };
188
+ const options = {};
189
+ for (const option of command.options) {
190
+ const value = values.get(option.name);
191
+ if (value === undefined) {
192
+ options[option.name] = undefined;
193
+ continue;
194
+ }
195
+ const id = String(value);
196
+ switch (option.type) {
197
+ case 'user':
198
+ options[option.name] = userOf(id);
199
+ break;
200
+ case 'role':
201
+ options[option.name] = roleOf(id);
202
+ break;
203
+ case 'mentionable':
204
+ options[option.name] = userOf(id) ?? roleOf(id);
205
+ break;
206
+ case 'channel': {
207
+ const partial = resolved.channels?.[id];
208
+ options[option.name] =
209
+ ctx.cache.channels.get(id) ??
210
+ (partial
211
+ ? ctx.entities.channel(partial, guildId)
212
+ : undefined);
213
+ break;
214
+ }
215
+ case 'attachment': {
216
+ const attachment = resolved.attachments?.[id];
217
+ options[option.name] = attachment
218
+ ? toCamelCase(attachment)
219
+ : undefined;
220
+ break;
221
+ }
222
+ default:
223
+ options[option.name] = value;
224
+ }
225
+ }
226
+ return options;
227
+ }
228
+ }
@@ -0,0 +1,47 @@
1
+ import { type BulkOverwriteGuildApplicationCommandsJSONParams } from '../discord/types/application-command.js';
2
+ import type { LoadedCommand } from './declaration.js';
3
+ /** A command with the file it comes from. */
4
+ export interface CommandEntry {
5
+ file: string;
6
+ /** The export it is: `'default'`, or its name. */
7
+ export: string;
8
+ command: LoadedCommand;
9
+ }
10
+ /** Two declarations that can't both be commands. */
11
+ export interface CommandConflict {
12
+ file: string;
13
+ export: string;
14
+ message: string;
15
+ }
16
+ export type CommandPayload = BulkOverwriteGuildApplicationCommandsJSONParams;
17
+ /** `/mod ban` for `['mod', 'ban']`. */
18
+ export declare const commandName: (path: readonly string[]) => string;
19
+ /**
20
+ * Finds the declarations that can't be commands together, and leaves them
21
+ * out:
22
+ * - two commands with the same name;
23
+ * - a command that also has subcommands: Discord only lets people use the
24
+ * subcommands.
25
+ */
26
+ export declare function findConflicts(entries: readonly CommandEntry[]): {
27
+ valid: CommandEntry[];
28
+ conflicts: CommandConflict[];
29
+ };
30
+ /**
31
+ * The commands that only work in private messages. Discord does not offer
32
+ * the commands of one server there, so they can't be used with those.
33
+ * @see https://docs.discord.com/developers/interactions/application-commands#permissions
34
+ */
35
+ export declare const privateOnly: (entries: readonly CommandEntry[]) => CommandEntry[];
36
+ /**
37
+ * What to register for a set of commands. `guild` is for commands of one
38
+ * server (dev), where Discord takes no contexts: the commands that only
39
+ * work in private messages are left out, they could never run there.
40
+ *
41
+ * A folder has no file: Discord asks for its description but never shows
42
+ * it, so it is generated, and it is shown under the name of the folder in
43
+ * every language.
44
+ */
45
+ export declare function buildCommands(entries: readonly CommandEntry[], { guild }: {
46
+ guild: boolean;
47
+ }): CommandPayload[];
@@ -0,0 +1,216 @@
1
+ // Turns the commands of a project into what Discord registers: one
2
+ // application command per top-level name, with its subcommands and groups.
3
+ // https://docs.discord.com/developers/interactions/application-commands#subcommands-and-subcommand-groups
4
+ import { ApplicationCommandOptionType, ApplicationCommandType, } from '../discord/types/application-command.js';
5
+ import { ApplicationIntegrationType } from '../discord/types/application.js';
6
+ import { InteractionContextType } from '../discord/types/interaction.js';
7
+ import { siteName } from '../loader/loader.js';
8
+ const TYPES = {
9
+ string: ApplicationCommandOptionType.String,
10
+ integer: ApplicationCommandOptionType.Integer,
11
+ number: ApplicationCommandOptionType.Number,
12
+ boolean: ApplicationCommandOptionType.Boolean,
13
+ user: ApplicationCommandOptionType.User,
14
+ channel: ApplicationCommandOptionType.Channel,
15
+ role: ApplicationCommandOptionType.Role,
16
+ mentionable: ApplicationCommandOptionType.Mentionable,
17
+ attachment: ApplicationCommandOptionType.Attachment,
18
+ };
19
+ /** A dictionary of translations, or nothing when there is none. */
20
+ const localized = (translations) => translations && Object.keys(translations).length > 0
21
+ ? translations
22
+ : undefined;
23
+ /** Adds the translations of a name and a description, when there are some. */
24
+ function withLocales(target, names, descriptions) {
25
+ const nameLocalizations = localized(names);
26
+ const descriptionLocalizations = localized(descriptions);
27
+ return {
28
+ ...target,
29
+ ...(nameLocalizations ? { name_localizations: nameLocalizations } : {}),
30
+ ...(descriptionLocalizations
31
+ ? { description_localizations: descriptionLocalizations }
32
+ : {}),
33
+ };
34
+ }
35
+ function toOption(option, locales, autocomplete) {
36
+ const raw = withLocales({
37
+ type: TYPES[option.type],
38
+ name: option.name,
39
+ description: option.description,
40
+ }, locales?.names, locales?.descriptions);
41
+ if (option.required)
42
+ raw.required = true;
43
+ // Discord then asks the bot for suggestions while the person types.
44
+ // https://docs.discord.com/developers/interactions/application-commands#autocomplete
45
+ if (autocomplete)
46
+ raw.autocomplete = true;
47
+ if ('choices' in option && option.choices) {
48
+ const entries = Array.isArray(option.choices)
49
+ ? option.choices.map((value) => [String(value), value])
50
+ : Object.entries(option.choices);
51
+ raw.choices = entries.map(([name, value]) => {
52
+ const translations = localized(locales?.choices[name]);
53
+ return {
54
+ name,
55
+ value,
56
+ ...(translations ? { name_localizations: translations } : {}),
57
+ };
58
+ });
59
+ }
60
+ if (option.type === 'string') {
61
+ if (option.minLength !== undefined)
62
+ raw.min_length = option.minLength;
63
+ if (option.maxLength !== undefined)
64
+ raw.max_length = option.maxLength;
65
+ }
66
+ else if (option.type === 'integer' || option.type === 'number') {
67
+ if (option.min !== undefined)
68
+ raw.min_value = option.min;
69
+ if (option.max !== undefined)
70
+ raw.max_value = option.max;
71
+ }
72
+ else if (option.type === 'channel' && option.channelTypes) {
73
+ raw.channel_types = [...option.channelTypes];
74
+ }
75
+ return raw;
76
+ }
77
+ /** `/mod ban` for `['mod', 'ban']`. */
78
+ export const commandName = (path) => `/${path.join(' ')}`;
79
+ /**
80
+ * Finds the declarations that can't be commands together, and leaves them
81
+ * out:
82
+ * - two commands with the same name;
83
+ * - a command that also has subcommands: Discord only lets people use the
84
+ * subcommands.
85
+ */
86
+ export function findConflicts(entries) {
87
+ const conflicts = [];
88
+ const seen = new Map();
89
+ const unique = [...entries]
90
+ .sort((a, b) => a.file === b.file
91
+ ? a.export < b.export
92
+ ? -1
93
+ : 1
94
+ : a.file < b.file
95
+ ? -1
96
+ : 1)
97
+ .filter(entry => {
98
+ const key = entry.command.path.join(' ');
99
+ const first = seen.get(key);
100
+ if (!first) {
101
+ seen.set(key, entry);
102
+ return true;
103
+ }
104
+ conflicts.push({
105
+ file: entry.file,
106
+ export: entry.export,
107
+ message: `${commandName(entry.command.path)} is already declared in ${siteName(first)}: two commands can't have the same name.`,
108
+ });
109
+ return false;
110
+ });
111
+ const valid = unique.filter(entry => {
112
+ const { path } = entry.command;
113
+ const child = unique.find(other => other !== entry &&
114
+ other.command.path.length > path.length &&
115
+ path.every((part, index) => other.command.path[index] === part));
116
+ if (!child)
117
+ return true;
118
+ conflicts.push({
119
+ file: entry.file,
120
+ export: entry.export,
121
+ message: `${commandName(path)} can't be a command and have subcommands (${siteName(child)} is ${commandName(child.command.path)}): Discord only lets people use the subcommands. Make this one a subcommand too, for example name: '${[...path, 'run'].join(' ')}'.`,
122
+ });
123
+ return false;
124
+ });
125
+ return { valid, conflicts };
126
+ }
127
+ /**
128
+ * The commands that only work in private messages. Discord does not offer
129
+ * the commands of one server there, so they can't be used with those.
130
+ * @see https://docs.discord.com/developers/interactions/application-commands#permissions
131
+ */
132
+ export const privateOnly = (entries) => entries.filter(entry => entry.command.where === 'dm');
133
+ /**
134
+ * What to register for a set of commands. `guild` is for commands of one
135
+ * server (dev), where Discord takes no contexts: the commands that only
136
+ * work in private messages are left out, they could never run there.
137
+ *
138
+ * A folder has no file: Discord asks for its description but never shows
139
+ * it, so it is generated, and it is shown under the name of the folder in
140
+ * every language.
141
+ */
142
+ export function buildCommands(entries, { guild }) {
143
+ const optionsOf = (command) => command.options.map(option => toOption(option, command.optionLocales[option.name], option.name in command.autocomplete));
144
+ const sub = (command) => withLocales({
145
+ type: ApplicationCommandOptionType.SubCommand,
146
+ name: command.path.at(-1),
147
+ description: command.description,
148
+ ...(command.options.length > 0 ? { options: optionsOf(command) } : {}),
149
+ }, command.names, command.descriptions);
150
+ const roots = new Map();
151
+ for (const { command } of entries) {
152
+ if (guild && command.where === 'dm')
153
+ continue;
154
+ const list = roots.get(command.path[0]) ?? [];
155
+ list.push(command);
156
+ roots.set(command.path[0], list);
157
+ }
158
+ return [...roots]
159
+ .sort(([a], [b]) => (a < b ? -1 : 1))
160
+ .map(([name, commands]) => {
161
+ const [first] = commands;
162
+ const leaf = commands.length === 1 && first.path.length === 1 ? first : null;
163
+ // What the server shows the command to by default: what every one of
164
+ // its subcommands asks for. Each subcommand checks its own on use.
165
+ const permissions = commands.reduce((bits, command) => bits & command.permissions, commands[0].permissions);
166
+ const payload = withLocales({
167
+ type: ApplicationCommandType.ChatInput,
168
+ name,
169
+ description: leaf?.description ?? `${name} commands`,
170
+ default_member_permissions: permissions === 0n ? null : String(permissions),
171
+ // Only for globally-scoped commands.
172
+ ...(guild
173
+ ? {}
174
+ : {
175
+ integration_types: [ApplicationIntegrationType.GuildInstall],
176
+ // A command is offered wherever one of its subcommands
177
+ // works; each one refuses the place it does not work in.
178
+ contexts: [
179
+ ...(commands.some(command => command.where !== 'dm')
180
+ ? [InteractionContextType.Guild]
181
+ : []),
182
+ ...(commands.some(command => command.where !== 'guild')
183
+ ? [InteractionContextType.BotDm]
184
+ : []),
185
+ ],
186
+ }),
187
+ ...(commands.some(command => command.nsfw) ? { nsfw: true } : {}),
188
+ }, leaf?.names, leaf?.descriptions);
189
+ if (leaf) {
190
+ if (leaf.options.length > 0)
191
+ payload.options = optionsOf(leaf);
192
+ return payload;
193
+ }
194
+ const inGroups = new Map();
195
+ const options = [];
196
+ for (const command of [...commands].sort((a, b) => a.path.join('/') < b.path.join('/') ? -1 : 1)) {
197
+ if (command.path.length === 2)
198
+ options.push(sub(command));
199
+ else {
200
+ const list = inGroups.get(command.path[1]) ?? [];
201
+ list.push(command);
202
+ inGroups.set(command.path[1], list);
203
+ }
204
+ }
205
+ for (const [group, members] of inGroups) {
206
+ options.push({
207
+ type: ApplicationCommandOptionType.SubCommandGroup,
208
+ name: group,
209
+ description: `${group} commands`,
210
+ options: members.map(sub),
211
+ });
212
+ }
213
+ payload.options = options;
214
+ return payload;
215
+ });
216
+ }
@@ -0,0 +1,15 @@
1
+ import type { RawApplicationCommand } from '../discord/types/application-command.js';
2
+ import type { CommandPayload } from './tree.js';
3
+ /** Whether a command of the project is the one Discord has for everyone. */
4
+ export declare function sameDefinition(ours: CommandPayload, theirs: RawApplicationCommand): boolean;
5
+ /**
6
+ * The commands to give one server, without the ones that are already there
7
+ * for everyone exactly as declared (`twins`: shown once, by the other).
8
+ * A command that changed is kept: its new form must be there to be tried,
9
+ * next to the one in production until that one is updated.
10
+ */
11
+ export declare function withoutGlobalTwins(commands: readonly CommandPayload[], global: readonly RawApplicationCommand[]): {
12
+ kept: CommandPayload[];
13
+ twins: string[];
14
+ changed: string[];
15
+ };
@@ -0,0 +1,78 @@
1
+ // A bot can have a command for everyone and a command of one server with
2
+ // the same name: Discord then shows both in that server. A dev bot that
3
+ // shares its token with the bot in production would show every command
4
+ // twice in the dev server. So the dev server only gets the commands that
5
+ // are not already there for everyone, exactly as the project declares them.
6
+ // https://docs.discord.com/developers/interactions/application-commands#registering-a-command
7
+ /** What says how a command looks and works: the rest is not compared. */
8
+ const DEFINITION = new Set([
9
+ 'type',
10
+ 'name',
11
+ 'description',
12
+ 'name_localizations',
13
+ 'description_localizations',
14
+ 'default_member_permissions',
15
+ 'nsfw',
16
+ 'options',
17
+ 'required',
18
+ 'choices',
19
+ 'value',
20
+ 'min_value',
21
+ 'max_value',
22
+ 'min_length',
23
+ 'max_length',
24
+ 'channel_types',
25
+ 'autocomplete',
26
+ ]);
27
+ const isEmpty = (value) => value === null ||
28
+ value === undefined ||
29
+ value === false ||
30
+ (Array.isArray(value) && value.length === 0) ||
31
+ (typeof value === 'object' && Object.keys(value).length === 0);
32
+ /**
33
+ * A definition in one form, whoever wrote it: Discord sends back what it
34
+ * was given with more fields, and says "nothing" in several ways (a field
35
+ * left out, `null`, `false`, an empty list).
36
+ */
37
+ function canonical(value, top = true) {
38
+ if (Array.isArray(value))
39
+ return value.map(item => canonical(item, top));
40
+ if (typeof value !== 'object' || value === null)
41
+ return value;
42
+ const entries = Object.entries(value)
43
+ // Translations are dictionaries: their keys are languages, all kept.
44
+ .filter(([key]) => !top || DEFINITION.has(key))
45
+ .map(([key, child]) => [key, canonical(child, !key.endsWith('_localizations'))])
46
+ .filter(([, child]) => !isEmpty(child))
47
+ .sort(([a], [b]) => (a < b ? -1 : 1));
48
+ return Object.fromEntries(entries);
49
+ }
50
+ /** Whether a command of the project is the one Discord has for everyone. */
51
+ export function sameDefinition(ours, theirs) {
52
+ return JSON.stringify(canonical(ours)) === JSON.stringify(canonical(theirs));
53
+ }
54
+ /**
55
+ * The commands to give one server, without the ones that are already there
56
+ * for everyone exactly as declared (`twins`: shown once, by the other).
57
+ * A command that changed is kept: its new form must be there to be tried,
58
+ * next to the one in production until that one is updated.
59
+ */
60
+ export function withoutGlobalTwins(commands, global) {
61
+ const byName = new Map(global
62
+ .filter(command => (command.type ?? 1) === 1)
63
+ .map(command => [command.name, command]));
64
+ const kept = [];
65
+ const twins = [];
66
+ const changed = [];
67
+ for (const command of commands) {
68
+ const other = byName.get(command.name);
69
+ if (other && sameDefinition(command, other)) {
70
+ twins.push(command.name);
71
+ continue;
72
+ }
73
+ if (other)
74
+ changed.push(command.name);
75
+ kept.push(command);
76
+ }
77
+ return { kept, twins, changed };
78
+ }
@@ -0,0 +1,79 @@
1
+ import { ButtonStyle } from '../discord/types/component.js';
2
+ import type { TranslationContext } from '../messages/messages.js';
3
+ import type { ComponentInteraction } from '../structures/interaction.js';
4
+ import type { EmojiInput } from '../structures/message.js';
5
+ import { type DynamicText } from './component.js';
6
+ import type { ComponentWhere, InteractiveConfig, InteractiveContext, PlaceOf } from './component.js';
7
+ import { type DataShape, type DataInputOf } from './custom-id.js';
8
+ import { type ButtonComponent, type Rendered } from './instance.js';
9
+ /**
10
+ * The color of a button: `'primary'` (blurple), `'secondary'` (grey),
11
+ * `'success'` (green) or `'danger'` (red).
12
+ * @see https://docs.discord.com/developers/components/reference#button-button-styles
13
+ */
14
+ export type ButtonStyleName = 'primary' | 'secondary' | 'success' | 'danger';
15
+ export declare const BUTTON_STYLES: Record<ButtonStyleName, ButtonStyle>;
16
+ /** What a button looks like. Everything is optional when it is put in a message: what the file says is the default. */
17
+ export interface ButtonLook<Data extends DataShape = DataShape> {
18
+ /**
19
+ * The text on the button (80 characters at most): written as is, or a
20
+ * function of `t` and the data, run when the message is sent.
21
+ */
22
+ label?: DynamicText<Data>;
23
+ /** An emoji on the button, before the label. */
24
+ emoji?: EmojiInput;
25
+ /** The color of the button. `'primary'` by default. */
26
+ style?: ButtonStyleName;
27
+ /** Shows the button greyed out, impossible to click. */
28
+ disabled?: boolean;
29
+ }
30
+ /** What `run` receives when the button is clicked. */
31
+ export type ButtonContext<Data extends DataShape = {}, Where extends ComponentWhere = 'guild'> = InteractiveContext<Data> & TranslationContext & PlaceOf<Where, ComponentInteraction>;
32
+ /** What a file gives to `button()`. */
33
+ export interface ButtonConfig<Data extends DataShape = {}, Where extends ComponentWhere = 'guild'> extends ButtonLook<Data>, InteractiveConfig<Data, Where> {
34
+ /** What to do when someone clicks the button. */
35
+ run: (context: ButtonContext<Data, Where>) => unknown;
36
+ }
37
+ /**
38
+ * What `button()` returns: a declaration the framework finds in the exports
39
+ * of a file. Import it where you send a message, and call it with the data
40
+ * the button carries (`ban({ userId })`); a button without data is used as
41
+ * is (`confirm`), or called with a look (`confirm({ disabled: true })`).
42
+ */
43
+ export type ButtonDeclaration<Data extends DataShape = {}> = {
44
+ /** What the file gave to `button()`, not checked yet. */
45
+ readonly config: unknown;
46
+ } & ({} extends Data ? ((look?: ButtonLook) => ButtonComponent) & ButtonComponent : {} extends DataInputOf<Data> ? ((data?: DataInputOf<Data>, look?: ButtonLook) => ButtonComponent) & ButtonComponent : (data: DataInputOf<Data>, look?: ButtonLook) => ButtonComponent);
47
+ /**
48
+ * Declares a button. Export the result from any file of `src/`: the name
49
+ * of the export is what tells the button apart, so you never write an id.
50
+ *
51
+ * ```ts
52
+ * import { button } from 'chapterjs';
53
+ *
54
+ * export const confirm = button({
55
+ * label: 'Confirm',
56
+ * style: 'success',
57
+ * async run({ interaction }) {
58
+ * await interaction.update({ content: 'Confirmed!', components: [] });
59
+ * },
60
+ * });
61
+ * ```
62
+ */
63
+ export declare function button<const Data extends DataShape = {}, const Where extends ComponentWhere = 'guild'>(config: ButtonConfig<Data, Where>): ButtonDeclaration<Data>;
64
+ /** A button, checked. */
65
+ export interface LoadedButton {
66
+ readonly kind: 'button';
67
+ /** The name of the export that declares it: its id for Discord. */
68
+ readonly name: string;
69
+ readonly look: Required<Pick<ButtonLook, 'style' | 'disabled'>> & Pick<ButtonLook, 'label' | 'emoji'>;
70
+ readonly data: DataShape;
71
+ readonly where: ComponentWhere;
72
+ readonly who: 'everyone' | 'author';
73
+ readonly ephemeral: boolean;
74
+ readonly run: (context: ButtonContext<DataShape, ComponentWhere>) => unknown;
75
+ }
76
+ /** Checks what a message gives to change the look of a button. */
77
+ export declare function readLook(what: string, look: unknown): ButtonLook;
78
+ /** The button as Discord takes it, for the data and the look given. */
79
+ export declare function renderButton(loaded: LoadedButton, args: readonly unknown[]): Rendered<'button'>;