chapterjs 0.4.0 → 0.5.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 (84) hide show
  1. package/README.md +3 -1
  2. package/dist/assets/public.js +4 -4
  3. package/dist/cli/build.d.ts +6 -2
  4. package/dist/cli/build.js +3 -3
  5. package/dist/cli/dev.js +8 -5
  6. package/dist/cli/project.d.ts +31 -2
  7. package/dist/cli/project.js +166 -34
  8. package/dist/cli/redis.js +14 -2
  9. package/dist/cli/start.js +11 -1
  10. package/dist/cli/test.js +108 -16
  11. package/dist/commands/command.d.ts +140 -5
  12. package/dist/commands/command.js +17 -2
  13. package/dist/commands/declaration.d.ts +17 -4
  14. package/dist/commands/declaration.js +128 -2
  15. package/dist/commands/router.js +78 -18
  16. package/dist/commands/tree.d.ts +25 -4
  17. package/dist/commands/tree.js +95 -26
  18. package/dist/commands/twins.d.ts +8 -0
  19. package/dist/commands/twins.js +24 -6
  20. package/dist/components/button.d.ts +8 -3
  21. package/dist/components/declaration.d.ts +2 -2
  22. package/dist/components/declaration.js +16 -12
  23. package/dist/components/render.js +5 -0
  24. package/dist/components/router.js +41 -0
  25. package/dist/components/select.d.ts +12 -5
  26. package/dist/core/bot.d.ts +7 -0
  27. package/dist/core/bot.js +44 -0
  28. package/dist/discord/api.d.ts +2 -0
  29. package/dist/discord/api.js +2 -0
  30. package/dist/discord/permissions.d.ts +4 -4
  31. package/dist/discord/permissions.js +13 -0
  32. package/dist/events/registry.d.ts +19 -10
  33. package/dist/events/registry.js +10 -4
  34. package/dist/index.d.ts +3 -2
  35. package/dist/index.js +1 -1
  36. package/dist/interactions/dispatch.js +4 -2
  37. package/dist/loader/loader.d.ts +16 -0
  38. package/dist/loader/loader.js +52 -7
  39. package/dist/loader/usage.d.ts +15 -0
  40. package/dist/loader/usage.js +40 -0
  41. package/dist/loader/watch.d.ts +5 -3
  42. package/dist/loader/watch.js +88 -17
  43. package/dist/messages/commands.js +3 -0
  44. package/dist/messages/messages.d.ts +1 -0
  45. package/dist/messages/translate.js +5 -1
  46. package/dist/presence/declaration.d.ts +49 -2
  47. package/dist/presence/declaration.js +122 -39
  48. package/dist/presence/presence.d.ts +65 -6
  49. package/dist/presence/presence.js +11 -0
  50. package/dist/presence/rotation.d.ts +32 -0
  51. package/dist/presence/rotation.js +167 -0
  52. package/dist/rest/errors.d.ts +0 -1
  53. package/dist/rest/errors.js +15 -1
  54. package/dist/rest/rest.js +10 -1
  55. package/dist/store/file-backend.d.ts +0 -1
  56. package/dist/store/file-backend.js +18 -1
  57. package/dist/store/scope.d.ts +6 -1
  58. package/dist/store/scope.js +9 -3
  59. package/dist/structures/channel.d.ts +13 -0
  60. package/dist/structures/channel.js +24 -0
  61. package/dist/structures/context.d.ts +3 -0
  62. package/dist/structures/entities.js +1 -0
  63. package/dist/structures/interaction.d.ts +6 -0
  64. package/dist/structures/interaction.js +20 -0
  65. package/dist/structures/message.d.ts +17 -0
  66. package/dist/structures/message.js +33 -0
  67. package/dist/structures/wait.d.ts +105 -0
  68. package/dist/structures/wait.js +85 -0
  69. package/dist/testing/harness.d.ts +48 -8
  70. package/dist/testing/harness.js +156 -36
  71. package/dist/testing/index.d.ts +1 -1
  72. package/dist/testing/smoke.js +112 -1
  73. package/dist/testing/world.js +51 -2
  74. package/dist/util/call-site.d.ts +4 -0
  75. package/dist/util/call-site.js +22 -0
  76. package/dist/voice/connection.d.ts +2 -0
  77. package/dist/voice/connection.js +15 -1
  78. package/dist/voice/usage.d.ts +3 -2
  79. package/dist/voice/usage.js +7 -19
  80. package/dist/waiting/usage.d.ts +5 -0
  81. package/dist/waiting/usage.js +19 -0
  82. package/dist/waiting/waiters.d.ts +48 -0
  83. package/dist/waiting/waiters.js +107 -0
  84. package/package.json +1 -1
@@ -31,8 +31,12 @@ export interface ButtonLook<Data extends DataShape = DataShape> {
31
31
  export type ButtonContext<Data extends DataShape = {}, Where extends ComponentWhere = 'guild'> = InteractiveContext<Data> & TranslationContext & PlaceOf<Where, ComponentInteraction>;
32
32
  /** What a file gives to `button()`. */
33
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;
34
+ /**
35
+ * What to do when someone clicks the button. Leave it out for a button
36
+ * that is only waited for (`message.waitForClick()`): once nothing waits,
37
+ * a click answers that it is not available any more.
38
+ */
39
+ run?: (context: ButtonContext<Data, Where>) => unknown;
36
40
  }
37
41
  /**
38
42
  * What `button()` returns: a declaration the framework finds in the exports
@@ -71,7 +75,8 @@ export interface LoadedButton {
71
75
  readonly where: ComponentWhere;
72
76
  readonly who: 'everyone' | 'author';
73
77
  readonly ephemeral: boolean;
74
- readonly run: (context: ButtonContext<DataShape, ComponentWhere>) => unknown;
78
+ /** `undefined` for a button that is only waited for. */
79
+ readonly run: ((context: ButtonContext<DataShape, ComponentWhere>) => unknown) | undefined;
75
80
  }
76
81
  /** Checks what a message gives to change the look of a button. */
77
82
  export declare function readLook(what: string, look: unknown): ButtonLook;
@@ -1,4 +1,4 @@
1
- import type { Declaration } from '../loader/loader.js';
1
+ import { type Declaration } from '../loader/loader.js';
2
2
  import { type LoadedButton } from './button.js';
3
3
  import { isEmbedDeclaration } from './embed.js';
4
4
  import { type ComponentKind } from './declared.js';
@@ -29,7 +29,7 @@ export declare function findDuplicates<T extends {
29
29
  kind: ComponentKind;
30
30
  name: string;
31
31
  };
32
- }>(entries: readonly T[]): {
32
+ }>(entries: readonly T[], running?: ReadonlySet<string>): {
33
33
  valid: T[];
34
34
  conflicts: {
35
35
  file: string;
@@ -4,6 +4,7 @@
4
4
  // bound to the name of its export, which is its id and what makes its
5
5
  // instances.
6
6
  import { Limits } from '../discord/api.js';
7
+ import { bySite } from '../loader/loader.js';
7
8
  import { checkEmbed } from '../structures/payload.js';
8
9
  import { isDynamic } from './component.js';
9
10
  import { readLook, renderButton, } from './button.js';
@@ -93,6 +94,17 @@ function checkRun(what, value, example) {
93
94
  fail(`${what} has no "run": the function to run when someone uses it, like ${example}`);
94
95
  }
95
96
  }
97
+ /**
98
+ * The `run` of a button or a menu: optional, since one may only be waited
99
+ * for (`message.waitForClick()`); when given, a function.
100
+ */
101
+ function checkOptionalRun(what, value, example) {
102
+ if (value === undefined)
103
+ return;
104
+ if (typeof value !== 'function') {
105
+ fail(`"run" of ${what} is the function to run when someone uses it, like ${example}. Leave it out for one that is only waited for with message.waitForClick().`);
106
+ }
107
+ }
96
108
  function readButton(name, config) {
97
109
  if (!isRecord(config)) {
98
110
  return fail(`button() needs an object: button({ label: '...', run({ interaction }) { ... } })`);
@@ -118,7 +130,7 @@ function readButton(name, config) {
118
130
  if (look.label === undefined && look.emoji === undefined) {
119
131
  fail('A button needs a label or an emoji.');
120
132
  }
121
- checkRun(what, config.run, `async run({ interaction }) { await interaction.update('Done!'); }`);
133
+ checkOptionalRun(what, config.run, `async run({ interaction }) { await interaction.update('Done!'); }`);
122
134
  return Object.freeze({
123
135
  kind: 'button',
124
136
  name,
@@ -168,7 +180,7 @@ function readSelect(name, config) {
168
180
  }
169
181
  const options = type === 'string' ? readOptions(what, config.options) : [];
170
182
  const placeholder = readPlaceholder(what, config.placeholder);
171
- checkRun(what, config.run, `async run({ interaction, values }) { ... }`);
183
+ checkOptionalRun(what, config.run, `async run({ interaction, values }) { ... }`);
172
184
  return Object.freeze({
173
185
  kind: 'select',
174
186
  name,
@@ -310,18 +322,10 @@ export const componentDeclaration = {
310
322
  },
311
323
  };
312
324
  /** Two components of the same kind with the same name: the later one is left out. */
313
- export function findDuplicates(entries) {
325
+ export function findDuplicates(entries, running) {
314
326
  const seen = new Map();
315
327
  const conflicts = [];
316
- const valid = [...entries]
317
- .sort((a, b) => a.file === b.file
318
- ? a.export < b.export
319
- ? -1
320
- : 1
321
- : a.file < b.file
322
- ? -1
323
- : 1)
324
- .filter(entry => {
328
+ const valid = bySite(entries, running).filter(entry => {
325
329
  const key = `${entry.component.kind}:${entry.component.name}`;
326
330
  const first = seen.get(key);
327
331
  if (!first) {
@@ -46,6 +46,11 @@ export function renderComponents(components, t = missingTranslator(null)) {
46
46
  const v2 = rows.some(one => one.kind !== 'row');
47
47
  // The texts of a piece may be computed now, for who will read the message.
48
48
  const raw = rows.map(one => resolve(one, t).raw);
49
+ // Without the pieces of the new layout, a message is rows only, and
50
+ // Discord takes 5 of them.
51
+ if (!v2 && raw.length > Limits.MessageActionRows) {
52
+ throw new RangeError(`The message has ${raw.length} rows of components: Discord accepts ${Limits.MessageActionRows} at most. A menu takes a row by itself and up to 5 buttons share one: send the rest in another message.`);
53
+ }
49
54
  const total = count(raw);
50
55
  if (total > Limits.MessageComponents) {
51
56
  throw new RangeError(`The message has ${total} components once its rows are made: Discord accepts ${Limits.MessageComponents} at most.`);
@@ -10,6 +10,7 @@ import { ComponentInteraction, ModalInteraction, } from '../structures/interacti
10
10
  import { remember } from '../structures/known.js';
11
11
  import { toCamelCase } from '../util/case.js';
12
12
  import { decodeCustomId, readData } from './custom-id.js';
13
+ import { componentStateOf } from './declared.js';
13
14
  import { SELECT_TYPES } from './select.js';
14
15
  /** What a component is called, for the person and the developer. */
15
16
  const WHAT_OF = {
@@ -130,6 +131,32 @@ export class ComponentRouter {
130
131
  this.#options.onError(file, error);
131
132
  return refuse(interaction, 'failed', { what });
132
133
  }
134
+ // A click a function of the files waits for (`message.waitForClick()`)
135
+ // is theirs: the `run` of the component does not run.
136
+ if (component.kind !== 'modal' && ctx.waiters?.wants('click')) {
137
+ const click = Object.freeze({
138
+ messageId: message.id,
139
+ interaction: interaction,
140
+ user,
141
+ member: place.member ?? null,
142
+ name,
143
+ data: Object.freeze(values),
144
+ values: Object.freeze([
145
+ ...(data.values ?? []),
146
+ ]),
147
+ is: (declared) => {
148
+ const state = componentStateOf(declared);
149
+ return state?.kind === component.kind && state.bound?.name === name;
150
+ },
151
+ });
152
+ if (ctx.waiters.offer('click', click)) {
153
+ return this.#waited(interaction);
154
+ }
155
+ }
156
+ if (!component.run) {
157
+ // Only waited for, and nothing waits any more.
158
+ return refuse(interaction, 'gone', { what });
159
+ }
133
160
  const context = Object.freeze({
134
161
  interaction,
135
162
  user,
@@ -160,6 +187,20 @@ export class ComponentRouter {
160
187
  run: () => component.run(context),
161
188
  });
162
189
  }
190
+ /**
191
+ * A click taken by a wait: the files answer it where they waited. When
192
+ * they did not after 2 seconds, nothing changes on screen
193
+ * (`deferUpdate()`), so the person never sees "This interaction failed".
194
+ */
195
+ #waited(interaction) {
196
+ const timer = setTimeout(() => {
197
+ if (!interaction.answered && !interaction.deferred) {
198
+ interaction.deferUpdate().catch(() => { });
199
+ }
200
+ }, this.#options.deferAfter ?? 2000);
201
+ timer.unref();
202
+ return Promise.resolve();
203
+ }
163
204
  /** What the person picked in a menu, as structures where it applies. */
164
205
  #readValues(ctx, raw, type, data) {
165
206
  const values = resolveValues(ctx, raw, type, data.values ?? [], data.resolved ?? {});
@@ -67,8 +67,11 @@ export interface StringSelectConfig<Options extends SelectOptions, Data extends
67
67
  * shown to what your code receives, or a list of `RichOption`.
68
68
  */
69
69
  options: Options;
70
- /** What to do when something is picked. */
71
- run: (context: SelectContext<OptionValue<Options>, Data, Where>) => unknown;
70
+ /**
71
+ * What to do when something is picked. Leave it out for a menu that is
72
+ * only waited for (`message.waitForClick()`).
73
+ */
74
+ run?: (context: SelectContext<OptionValue<Options>, Data, Where>) => unknown;
72
75
  }
73
76
  /** A menu of users, roles, users and roles, or channels of the server. */
74
77
  export interface EntitySelectConfig<Type extends EntitySelectType, Data extends DataShape = {}, Where extends ComponentWhere = 'guild'> extends SelectConfigBase<Data, Where> {
@@ -76,8 +79,11 @@ export interface EntitySelectConfig<Type extends EntitySelectType, Data extends
76
79
  type: Type;
77
80
  /** For a menu of channels: the kinds of channels listed. All by default. */
78
81
  channelTypes?: readonly ChannelType[];
79
- /** What to do when something is picked. */
80
- run: (context: SelectContext<EntityValues[Type], Data, Where>) => unknown;
82
+ /**
83
+ * What to do when something is picked. Leave it out for a menu that is
84
+ * only waited for (`message.waitForClick()`).
85
+ */
86
+ run?: (context: SelectContext<EntityValues[Type], Data, Where>) => unknown;
81
87
  }
82
88
  /** What a select file gives to `select()`: a menu of texts, or of things of the server. */
83
89
  export type SelectConfig<Options extends SelectOptions = SelectOptions, Type extends EntitySelectType = EntitySelectType, Data extends DataShape = {}, Where extends ComponentWhere = 'guild'> = StringSelectConfig<Options, Data, Where> | EntitySelectConfig<Type, Data, Where>;
@@ -126,7 +132,8 @@ export interface LoadedSelect {
126
132
  readonly where: ComponentWhere;
127
133
  readonly who: 'everyone' | 'author';
128
134
  readonly ephemeral: boolean;
129
- readonly run: (context: SelectContext<unknown, DataShape, ComponentWhere>) => unknown;
135
+ /** `undefined` for a menu that is only waited for. */
136
+ readonly run: ((context: SelectContext<unknown, DataShape, ComponentWhere>) => unknown) | undefined;
130
137
  }
131
138
  /** Checks `min` and `max` of a menu, a group of checkboxes or a file upload. */
132
139
  export declare function readRange(what: string, given: {
@@ -5,6 +5,7 @@ import { Gateway, type GatewayOptions } from '../gateway/gateway.js';
5
5
  import type { ShardEvent } from '../gateway/shard.js';
6
6
  import { type RestOptions } from '../rest/rest.js';
7
7
  import type { Context } from '../structures/context.js';
8
+ import { type Waiters } from '../waiting/waiters.js';
8
9
  /** What a dispatched event is, besides its data. */
9
10
  export interface DispatchInfo {
10
11
  shardId: number;
@@ -71,11 +72,17 @@ export interface BotOptions {
71
72
  * (an outage of Discord). Default: 15000.
72
73
  */
73
74
  guildsTimeout?: number;
75
+ /**
76
+ * What the files wait for (`channel.waitForMessage()`...): in memory by
77
+ * default.
78
+ */
79
+ waiters?: Waiters;
74
80
  /** Only tests change these. */
75
81
  rest?: Partial<RestOptions>;
76
82
  gateway?: Pick<GatewayOptions, 'backoff' | 'identifyInterval'>;
77
83
  voice?: {
78
84
  secure?: boolean;
85
+ unavailable?: string;
79
86
  };
80
87
  }
81
88
  export interface Bot {
package/dist/core/bot.js CHANGED
@@ -9,6 +9,34 @@ import { createContext } from '../structures/entities.js';
9
9
  import { GatewayOpcode } from '../discord/codes.js';
10
10
  import { GatewayIntent } from '../discord/intents.js';
11
11
  import { VoiceManager } from '../voice/connection.js';
12
+ import { MemoryWaiters } from '../waiting/waiters.js';
13
+ import { GetUser } from '../discord/endpoints.js';
14
+ import { toCamelCase } from '../util/case.js';
15
+ /**
16
+ * Gives a reaction to what waits for one, with who reacted: the member
17
+ * Discord sends in a server, else the user the bot knows, else asked.
18
+ * @see https://docs.discord.com/developers/events/gateway-events#message-reaction-add
19
+ */
20
+ function offerReaction(ctx, data) {
21
+ const member = data.member && data.guild_id
22
+ ? ctx.entities.member(data.guild_id, data.member)
23
+ : null;
24
+ const known = member?.user ?? ctx.cache.users.get(data.user_id);
25
+ const user = known
26
+ ? Promise.resolve(known)
27
+ : ctx.rest
28
+ .request(GetUser, [data.user_id])
29
+ .then(raw => ctx.entities.user(raw));
30
+ user
31
+ .then(user => ctx.waiters?.offer('reaction', {
32
+ messageId: data.message_id,
33
+ emoji: toCamelCase(data.emoji),
34
+ user,
35
+ member,
36
+ }))
37
+ // Who reacted could not be known: nothing to give.
38
+ .catch(() => { });
39
+ }
12
40
  /** The server a gateway event belongs to, when it belongs to one. */
13
41
  function guildIdOf(event, data) {
14
42
  if (typeof data !== 'object' || data === null)
@@ -38,6 +66,8 @@ export function createBot(options) {
38
66
  ...options.rest,
39
67
  });
40
68
  const ctx = createContext({ rest, cache: new Cache(options.cache) });
69
+ const waiters = options.waiters ?? new MemoryWaiters();
70
+ ctx.waiters = waiters;
41
71
  /** Servers announced by Ready whose data has not come yet, per shard. */
42
72
  const pending = new Map();
43
73
  let guildsReady = null;
@@ -129,6 +159,15 @@ export function createBot(options) {
129
159
  else if (event === 'VOICE_SERVER_UPDATE') {
130
160
  voice.onVoiceServer(data);
131
161
  }
162
+ // What the files wait for: built only when a wait wants it.
163
+ if (event === 'MESSAGE_CREATE' && waiters.wants('message')) {
164
+ const message = data;
165
+ waiters.offer('message', ctx.entities.message(message));
166
+ }
167
+ else if (event === 'MESSAGE_REACTION_ADD' &&
168
+ waiters.wants('reaction')) {
169
+ offerReaction(ctx, data);
170
+ }
132
171
  options.onDispatch?.(event, data, { shardId, joined, before });
133
172
  if (awaited)
134
173
  checkGuilds();
@@ -143,6 +182,9 @@ export function createBot(options) {
143
182
  ...(options.voice?.secure === undefined
144
183
  ? {}
145
184
  : { secure: options.voice.secure }),
185
+ ...(options.voice?.unavailable === undefined
186
+ ? {}
187
+ : { unavailable: options.voice.unavailable }),
146
188
  });
147
189
  ctx.voice = voice;
148
190
  return {
@@ -167,6 +209,8 @@ export function createBot(options) {
167
209
  clearTimeout(guildsTimer);
168
210
  guildsTimer = null;
169
211
  voice.closeAll();
212
+ // What the files waited for will not come: they go on with nothing.
213
+ waiters.close();
170
214
  await gateway.close();
171
215
  },
172
216
  setPresence: presence => gateway.setPresence(presence),
@@ -74,6 +74,8 @@ export declare const Limits: {
74
74
  readonly CustomId: 100;
75
75
  /** @see https://docs.discord.com/developers/components/reference#component-object */
76
76
  readonly MessageComponents: 40;
77
+ /** @see https://docs.discord.com/developers/components/reference#legacy-message-component-behavior */
78
+ readonly MessageActionRows: 5;
77
79
  /** @see https://docs.discord.com/developers/components/reference#action-row */
78
80
  readonly ActionRowButtons: 5;
79
81
  /** @see https://docs.discord.com/developers/components/reference#button-button-structure */
@@ -77,6 +77,8 @@ export const Limits = {
77
77
  CustomId: 100,
78
78
  /** @see https://docs.discord.com/developers/components/reference#component-object */
79
79
  MessageComponents: 40,
80
+ /** @see https://docs.discord.com/developers/components/reference#legacy-message-component-behavior */
81
+ MessageActionRows: 5,
80
82
  /** @see https://docs.discord.com/developers/components/reference#action-row */
81
83
  ActionRowButtons: 5,
82
84
  /** @see https://docs.discord.com/developers/components/reference#button-button-structure */
@@ -12,11 +12,11 @@ export declare const ALL_PERMISSIONS: bigint;
12
12
  */
13
13
  export declare const ELEVATED_PERMISSIONS: readonly PermissionName[];
14
14
  /**
15
- * A set of permissions that never changes: every operation returns a new
16
- * set. Discord serializes permissions as text because they don't fit in a
17
- * regular number.
18
- * @see https://docs.discord.com/developers/topics/permissions
15
+ * A permission as people read it: `BanMembers` is "Ban Members". For what
16
+ * the bot says to the people of a server, never to the developer, who
17
+ * writes the names as they are.
19
18
  */
19
+ export declare function permissionWords(name: PermissionName): string;
20
20
  export declare class Permissions {
21
21
  /** The permissions as one number, each bit being a permission. */
22
22
  readonly bits: bigint;
@@ -42,6 +42,19 @@ function resolve(permission) {
42
42
  * regular number.
43
43
  * @see https://docs.discord.com/developers/topics/permissions
44
44
  */
45
+ /** Letters that are said one by one in the name of a permission. */
46
+ const ACRONYMS = { Tts: 'TTS', Vad: 'VAD' };
47
+ /**
48
+ * A permission as people read it: `BanMembers` is "Ban Members". For what
49
+ * the bot says to the people of a server, never to the developer, who
50
+ * writes the names as they are.
51
+ */
52
+ export function permissionWords(name) {
53
+ return name
54
+ .split(/(?=[A-Z])/)
55
+ .map(word => ACRONYMS[word] ?? word)
56
+ .join(' ');
57
+ }
45
58
  export class Permissions {
46
59
  /** The permissions as one number, each bit being a permission. */
47
60
  bits;
@@ -1,7 +1,7 @@
1
1
  import type { CacheLimits } from '../cache/cache.js';
2
2
  import type { Snowflake } from '../discord/types/common.js';
3
3
  import type { GatewayDispatchEventName, GatewayDispatchEvents } from '../discord/types/gateway-events.js';
4
- import type { Channel, DMChannel, GuildChannel, GuildTextBasedChannel, TextBasedChannel, ThreadChannel, VoiceChannel } from '../structures/channel.js';
4
+ import type { DMChannel, GuildChannel, GuildTextBasedChannel, TextBasedChannel, ThreadChannel, VoiceChannel } from '../structures/channel.js';
5
5
  import type { Context } from '../structures/context.js';
6
6
  import type { GuildEmoji } from '../structures/emoji.js';
7
7
  import type { AuditLogEntry, Guild, ScheduledEvent, VoiceState } from '../structures/guild.js';
@@ -220,23 +220,32 @@ export interface EventContexts {
220
220
  /** A channel was created. */
221
221
  channelCreate: {
222
222
  /** The new channel or thread. */
223
- channel: GuildChannel | ThreadChannel | Channel;
224
- /** The server of the channel; `null` for a private conversation. */
225
- guild: Guild | null;
223
+ channel: GuildChannel | ThreadChannel;
224
+ /**
225
+ * The server of the channel: Discord only sends these events, with the
226
+ * Guilds intent, for the channels of a server.
227
+ */
228
+ guild: Guild;
226
229
  };
227
230
  /** A channel changed: name, topic, permissions... */
228
231
  channelUpdate: {
229
232
  /** The channel or thread, as it is now. */
230
- channel: GuildChannel | ThreadChannel | Channel;
231
- /** The server of the channel; `null` for a private conversation. */
232
- guild: Guild | null;
233
+ channel: GuildChannel | ThreadChannel;
234
+ /**
235
+ * The server of the channel: Discord only sends these events, with the
236
+ * Guilds intent, for the channels of a server.
237
+ */
238
+ guild: Guild;
233
239
  };
234
240
  /** A channel was deleted. */
235
241
  channelDelete: {
236
242
  /** The channel or thread as it was. */
237
- channel: GuildChannel | ThreadChannel | Channel;
238
- /** The server of the channel; `null` for a private conversation. */
239
- guild: Guild | null;
243
+ channel: GuildChannel | ThreadChannel;
244
+ /**
245
+ * The server of the channel: Discord only sends these events, with the
246
+ * Guilds intent, for the channels of a server.
247
+ */
248
+ guild: Guild;
240
249
  };
241
250
  /** A role was created. */
242
251
  roleCreate: {
@@ -15,16 +15,22 @@ const ofGuild = ({ guild }) => guild;
15
15
  const channel = (on) => ({
16
16
  on,
17
17
  build: (ctx, data) => {
18
+ // A server's channel only: what the types promise.
18
19
  const found = ctx.cache.channels.get(data.id);
19
- return found ? { channel: found, guild: found.guild } : null;
20
+ return found?.isGuildChannel() || found?.isThread()
21
+ ? { channel: found, guild: found.guild }
22
+ : null;
20
23
  },
21
24
  });
22
25
  const deletedChannel = (on) => ({
23
26
  on,
24
27
  before: (ctx, data) => ctx.cache.channels.get(data.id) ?? null,
25
- build: (_ctx, _data, { before }) => before
26
- ? { channel: before, guild: before.guild }
27
- : null,
28
+ build: (_ctx, _data, { before }) => {
29
+ const channel = before;
30
+ return channel?.isGuildChannel() || channel?.isThread()
31
+ ? { channel, guild: channel.guild }
32
+ : null;
33
+ },
28
34
  });
29
35
  /**
30
36
  * Discord refused what an event needs before it can be given to its files:
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { command } from './commands/command.js';
2
- export type { AutocompleteContext, ChannelOption, CommandAutocomplete, CommandConfig, CommandContext, CommandInDm, CommandInGuild, CommandInPrivate, CommandWhere, CommandDeclaration, CommandLocales, CommandName, CommandOption, CommandOptions, CommandTranslation, Cooldown, Duration, NumberOption, OptionsSoFar, OptionTranslation, OptionValuesOf, SimpleOption, StringOption, Suggestion, } from './commands/command.js';
2
+ export type { AutocompleteContext, ChannelOption, CommandAutocomplete, CommandConfig, CommandContext, CommandInDm, CommandInGuild, CommandInPrivate, CommandWhere, CommandDeclaration, CommandLocales, CommandName, CommandOption, CommandOptions, CommandTranslation, Cooldown, MenuConfig, MenuContext, MenuDeclaration, MenuLocales, MenuTranslation, MenuType, Duration, NumberOption, OptionsSoFar, OptionTranslation, OptionValuesOf, SimpleOption, StringOption, Suggestion, } from './commands/command.js';
3
3
  export type { CommandInteraction, ComponentInteraction, DmCommandInteraction, GuildCommandInteraction, PrivateCommandInteraction, Interaction, InteractionReply, InteractionReplyOptions, ModalInteraction, } from './structures/interaction.js';
4
4
  export type { DynamicText, TextContext } from './components/component.js';
5
5
  export { button } from './components/button.js';
@@ -25,10 +25,11 @@ export type { AuditLog, AuditLogEntry, AutoModerationRule, Ban, ChannelCreateOpt
25
25
  export type { Invite } from './structures/invite.js';
26
26
  export type { BanOptions, GuildMember, MemberEditOptions, } from './structures/member.js';
27
27
  export type { Attachment, EmojiInput, DmMessage, GuildMessage, MemberMessage, Message, PrivateMessage, MessageFlagName, Reaction, ReactionEmoji, } from './structures/message.js';
28
+ export type { Click, CollectMessagesOptions, PersonInput, WaitedReaction, WaitForClickOptions, WaitForMessageOptions, WaitForReactionOptions, } from './structures/wait.js';
28
29
  export { language } from './messages/messages.js';
29
30
  export type { CommandsTranslations, FrameworkTexts, LanguageConfig, LanguageDeclaration, MergedMessages, MessagesOf, MessageText, MessageTexts, PluralText, ProjectCommands, ProjectLanguageDefaults, ParamsArgs, ParamsOf, Placeholders, ProjectLocale, ProjectLocales, LocaleGiven, ProjectMessages, TranslationContext, Translator, } from './messages/messages.js';
30
31
  export { presence } from './presence/presence.js';
31
- export type { Activity, ActivityKind, PlainActivity, PresenceConfig, PresenceDeclaration, PresenceStatusName, StreamingActivity, } from './presence/presence.js';
32
+ export type { Activities, Activity, ActivityKind, ActivityText, PlainActivity, PresenceConfig, PresenceContext, PresenceDeclaration, PresenceStatusName, RotatingPresenceConfig, SinglePresenceConfig, StreamingActivity, } from './presence/presence.js';
32
33
  export { store } from './store/store.js';
33
34
  export type { ScopedStore, Store, StoreConfig, StoreEntry, StoreKey, StoreScope, StoreSetOptions, StoreStorage, } from './store/store.js';
34
35
  export { ranking } from './ranking/ranking.js';
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@
9
9
  // (in events, commands...) and never creates them: classes are exported as
10
10
  // types only, and nothing here lets user code connect to Discord or send
11
11
  // requests by itself.
12
- // Slash commands: what the files of src/commands/ are written with.
12
+ // Commands (slash and menu): what the files of src/commands/ are written with.
13
13
  export { command } from './commands/command.js';
14
14
  export { button } from './components/button.js';
15
15
  export { select } from './components/select.js';
@@ -8,7 +8,7 @@ import { setTimeout as sleep } from 'node:timers/promises';
8
8
  import { phrase } from '../messages/phrases.js';
9
9
  import { DiscordApiError } from '../rest/errors.js';
10
10
  import { ctxOf } from '../structures/base.js';
11
- import { isUpdating } from '../structures/interaction.js';
11
+ import { answersSent, isUpdating, } from '../structures/interaction.js';
12
12
  /** Who did it, as Discord sent it: the member in a server, the user elsewhere. */
13
13
  export function authorOf(raw) {
14
14
  const user = raw.member?.user ?? raw.user;
@@ -87,7 +87,9 @@ export function runInteraction(options) {
87
87
  .then(() => options.defer())
88
88
  .catch(() => { });
89
89
  return new Promise(resolve => resolve(withGuild(options.guildId, options.run)))
90
- .then(() => {
90
+ .then(async () => {
91
+ // An answer asked without await is on its way: not a missing one.
92
+ await answersSent(interaction);
91
93
  if (!interaction.answered) {
92
94
  reporter.onWarning(file, `${name} ${options.unanswered}`);
93
95
  }
@@ -50,6 +50,22 @@ export declare const siteName: (site: {
50
50
  file: string;
51
51
  export?: string;
52
52
  }) => string;
53
+ /** What tells one export of the project from another: `src/a.ts#ban`. */
54
+ export declare const siteKey: (site: {
55
+ file: string;
56
+ export: string;
57
+ }) => string;
58
+ /**
59
+ * The order in which declarations that can't both be kept (two with one
60
+ * name) are read, the first one winning: what already ran first (`running`,
61
+ * the `siteKey`s of the declarations of the last load), so that a file saved
62
+ * in dev never takes its name from one that works, then by file and export,
63
+ * so that the choice never depends on the order the files were found in.
64
+ */
65
+ export declare function bySite<T extends {
66
+ file: string;
67
+ export: string;
68
+ }>(entries: readonly T[], running?: ReadonlySet<string>): T[];
53
69
  /** A file of the project, found but not run. */
54
70
  export interface FoundFile {
55
71
  /** The path from the project folder, with `/`: `src/ping.ts`. */
@@ -11,6 +11,28 @@ import { importFile } from './hot.js';
11
11
  export const siteName = (site) => site.export === undefined || site.export === 'default'
12
12
  ? site.file
13
13
  : `${site.file} (${site.export})`;
14
+ /** What tells one export of the project from another: `src/a.ts#ban`. */
15
+ export const siteKey = (site) => `${site.file}#${site.export}`;
16
+ /**
17
+ * The order in which declarations that can't both be kept (two with one
18
+ * name) are read, the first one winning: what already ran first (`running`,
19
+ * the `siteKey`s of the declarations of the last load), so that a file saved
20
+ * in dev never takes its name from one that works, then by file and export,
21
+ * so that the choice never depends on the order the files were found in.
22
+ */
23
+ export function bySite(entries, running = new Set()) {
24
+ const before = (entry) => (running.has(siteKey(entry)) ? 0 : 1);
25
+ return [...entries].sort((a, b) => before(a) - before(b) ||
26
+ (a.file === b.file
27
+ ? a.export < b.export
28
+ ? -1
29
+ : a.export > b.export
30
+ ? 1
31
+ : 0
32
+ : a.file < b.file
33
+ ? -1
34
+ : 1));
35
+ }
14
36
  const SOURCE = /\.(?:ts|mts|js|mjs)$/;
15
37
  const DECLARATION = /\.d\.m?ts$/;
16
38
  /** Tests next to the code: they are for a test runner, not for the bot. */
@@ -68,7 +90,7 @@ export async function loadFiles(files) {
68
90
  result.modules.push({ file, exports: await importFile(path) });
69
91
  }
70
92
  catch (error) {
71
- result.failed.push({ file, error });
93
+ result.failed.push({ file, error: readable(error) });
72
94
  }
73
95
  }));
74
96
  const byFile = (a, b) => a.file < b.file ? -1 : 1;
@@ -76,6 +98,19 @@ export async function loadFiles(files) {
76
98
  result.failed.sort(byFile);
77
99
  return result;
78
100
  }
101
+ /**
102
+ * A file Node can't read as TypeScript gets the words of its parser
103
+ * ("Expected ',', got ';'"): what they mean is said after them. The error
104
+ * is the same, so its line is still found.
105
+ */
106
+ function readable(error) {
107
+ if (error instanceof SyntaxError &&
108
+ error.code === 'ERR_INVALID_TYPESCRIPT_SYNTAX' &&
109
+ !error.message.includes('This line can not be read')) {
110
+ error.message = `${error.message.replace(/\.?$/, '.')} This line can not be read: look for a bracket, a comma or a quote missing or in excess, here or just before.`;
111
+ }
112
+ return error;
113
+ }
79
114
  /** The name of a file, without its folders and its extension. */
80
115
  const baseName = (file) => file
81
116
  .split('/')
@@ -103,22 +138,32 @@ export function readDeclarations(modules, declaration) {
103
138
  export: exported,
104
139
  name: exported === 'default' ? baseName(file) : exported,
105
140
  };
106
- const read = (one) => {
141
+ const read = (one, at) => {
107
142
  if (seen.has(one))
108
143
  return;
109
144
  seen.add(one);
110
- result.loaded.push({ ...site, value: declaration.read(one, site) });
145
+ result.loaded.push({ ...at, value: declaration.read(one, at) });
111
146
  };
112
147
  try {
113
148
  if (declaration.is(value))
114
- read(value);
149
+ read(value, site);
115
150
  else if (Array.isArray(value) && value.some(declaration.is)) {
116
151
  if (!declaration.list) {
117
152
  throw new TypeError(`This export is a list with a ${declaration.one} in it: export each ${declaration.one} on its own (export const ${site.name} = ...), its name is the name of its export.`);
118
153
  }
119
- for (const one of value)
120
- if (declaration.is(one))
121
- read(one);
154
+ // Each one of the list is told apart by its place in it (`events[2]`),
155
+ // and read on its own: one that is wrong leaves the others running.
156
+ value.forEach((one, index) => {
157
+ if (!declaration.is(one))
158
+ return;
159
+ const at = { ...site, export: `${exported}[${index}]` };
160
+ try {
161
+ read(one, at);
162
+ }
163
+ catch (error) {
164
+ result.failed.push({ file, export: at.export, error });
165
+ }
166
+ });
122
167
  }
123
168
  }
124
169
  catch (error) {