chapterjs 0.3.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 (115) hide show
  1. package/README.md +5 -3
  2. package/dist/assets/public.js +4 -4
  3. package/dist/cli/build.d.ts +6 -2
  4. package/dist/cli/build.js +17 -4
  5. package/dist/cli/dev.js +37 -6
  6. package/dist/cli/env.d.ts +5 -0
  7. package/dist/cli/env.js +39 -3
  8. package/dist/cli/project.d.ts +61 -9
  9. package/dist/cli/project.js +250 -44
  10. package/dist/cli/redis.d.ts +23 -0
  11. package/dist/cli/redis.js +69 -0
  12. package/dist/cli/start.js +54 -18
  13. package/dist/cli/test.js +112 -18
  14. package/dist/commands/command.d.ts +140 -5
  15. package/dist/commands/command.js +17 -2
  16. package/dist/commands/declaration.d.ts +17 -4
  17. package/dist/commands/declaration.js +128 -2
  18. package/dist/commands/router.js +78 -18
  19. package/dist/commands/tree.d.ts +25 -4
  20. package/dist/commands/tree.js +95 -26
  21. package/dist/commands/twins.d.ts +8 -0
  22. package/dist/commands/twins.js +24 -6
  23. package/dist/components/button.d.ts +8 -3
  24. package/dist/components/declaration.d.ts +2 -2
  25. package/dist/components/declaration.js +16 -12
  26. package/dist/components/render.js +5 -0
  27. package/dist/components/router.js +41 -0
  28. package/dist/components/select.d.ts +12 -5
  29. package/dist/core/bot.d.ts +7 -0
  30. package/dist/core/bot.js +44 -0
  31. package/dist/discord/api.d.ts +2 -0
  32. package/dist/discord/api.js +2 -0
  33. package/dist/discord/permissions.d.ts +4 -4
  34. package/dist/discord/permissions.js +13 -0
  35. package/dist/events/registry.d.ts +265 -243
  36. package/dist/events/registry.js +131 -100
  37. package/dist/index.d.ts +7 -4
  38. package/dist/index.js +2 -1
  39. package/dist/interactions/dispatch.js +4 -2
  40. package/dist/loader/loader.d.ts +16 -0
  41. package/dist/loader/loader.js +52 -7
  42. package/dist/loader/usage.d.ts +15 -0
  43. package/dist/loader/usage.js +40 -0
  44. package/dist/loader/watch.d.ts +5 -3
  45. package/dist/loader/watch.js +88 -17
  46. package/dist/messages/commands.js +3 -0
  47. package/dist/messages/messages.d.ts +1 -0
  48. package/dist/messages/translate.js +5 -1
  49. package/dist/presence/declaration.d.ts +49 -2
  50. package/dist/presence/declaration.js +122 -39
  51. package/dist/presence/presence.d.ts +65 -6
  52. package/dist/presence/presence.js +11 -0
  53. package/dist/presence/rotation.d.ts +32 -0
  54. package/dist/presence/rotation.js +167 -0
  55. package/dist/ranking/declaration.d.ts +12 -0
  56. package/dist/ranking/declaration.js +48 -0
  57. package/dist/ranking/ranking.d.ts +102 -0
  58. package/dist/ranking/ranking.js +129 -0
  59. package/dist/redis/client.d.ts +36 -0
  60. package/dist/redis/client.js +252 -0
  61. package/dist/redis/protocol.d.ts +22 -0
  62. package/dist/redis/protocol.js +99 -0
  63. package/dist/redis/url.d.ts +24 -0
  64. package/dist/redis/url.js +50 -0
  65. package/dist/rest/errors.d.ts +0 -1
  66. package/dist/rest/errors.js +15 -1
  67. package/dist/rest/rest.js +10 -1
  68. package/dist/store/backend.d.ts +49 -4
  69. package/dist/store/backend.js +66 -11
  70. package/dist/store/declaration.d.ts +3 -0
  71. package/dist/store/declaration.js +8 -3
  72. package/dist/store/file-backend.d.ts +3 -2
  73. package/dist/store/file-backend.js +47 -1
  74. package/dist/store/ipc-backend.d.ts +24 -1
  75. package/dist/store/ipc-backend.js +49 -0
  76. package/dist/store/memory-backend.d.ts +4 -2
  77. package/dist/store/memory-backend.js +27 -1
  78. package/dist/store/ranking-backend.d.ts +45 -0
  79. package/dist/store/ranking-backend.js +82 -0
  80. package/dist/store/redis-backend.d.ts +38 -0
  81. package/dist/store/redis-backend.js +245 -0
  82. package/dist/store/redis-ranking.d.ts +15 -0
  83. package/dist/store/redis-ranking.js +69 -0
  84. package/dist/store/scope.d.ts +17 -1
  85. package/dist/store/scope.js +32 -3
  86. package/dist/store/store.d.ts +39 -5
  87. package/dist/store/store.js +79 -32
  88. package/dist/structures/channel.d.ts +13 -0
  89. package/dist/structures/channel.js +24 -0
  90. package/dist/structures/context.d.ts +3 -0
  91. package/dist/structures/entities.js +1 -0
  92. package/dist/structures/interaction.d.ts +6 -0
  93. package/dist/structures/interaction.js +20 -0
  94. package/dist/structures/invite.d.ts +6 -0
  95. package/dist/structures/invite.js +11 -0
  96. package/dist/structures/message.d.ts +17 -0
  97. package/dist/structures/message.js +33 -0
  98. package/dist/structures/wait.d.ts +105 -0
  99. package/dist/structures/wait.js +85 -0
  100. package/dist/testing/harness.d.ts +48 -8
  101. package/dist/testing/harness.js +156 -36
  102. package/dist/testing/index.d.ts +1 -1
  103. package/dist/testing/smoke.js +112 -1
  104. package/dist/testing/world.js +51 -2
  105. package/dist/util/call-site.d.ts +4 -0
  106. package/dist/util/call-site.js +22 -0
  107. package/dist/voice/connection.d.ts +2 -0
  108. package/dist/voice/connection.js +15 -1
  109. package/dist/voice/usage.d.ts +3 -2
  110. package/dist/voice/usage.js +7 -19
  111. package/dist/waiting/usage.d.ts +5 -0
  112. package/dist/waiting/usage.js +19 -0
  113. package/dist/waiting/waiters.d.ts +48 -0
  114. package/dist/waiting/waiters.js +107 -0
  115. 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;