chapterjs 0.2.3 → 0.4.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 (87) hide show
  1. package/README.md +65 -0
  2. package/dist/cli/build.js +14 -1
  3. package/dist/cli/cli.js +41 -6
  4. package/dist/cli/dev.js +29 -1
  5. package/dist/cli/env.d.ts +5 -0
  6. package/dist/cli/env.js +39 -3
  7. package/dist/cli/project.d.ts +49 -7
  8. package/dist/cli/project.js +103 -19
  9. package/dist/cli/redis.d.ts +23 -0
  10. package/dist/cli/redis.js +57 -0
  11. package/dist/cli/start.js +43 -17
  12. package/dist/cli/test.d.ts +29 -0
  13. package/dist/cli/test.js +446 -0
  14. package/dist/commands/autocomplete.d.ts +1 -1
  15. package/dist/commands/autocomplete.js +37 -41
  16. package/dist/commands/router.d.ts +4 -1
  17. package/dist/commands/router.js +15 -17
  18. package/dist/components/router.d.ts +7 -2
  19. package/dist/components/router.js +16 -16
  20. package/dist/discord/types/channel.d.ts +9 -1
  21. package/dist/discord/types/guild.d.ts +5 -5
  22. package/dist/discord/types/permissions.d.ts +6 -0
  23. package/dist/discord/types/poll.d.ts +7 -1
  24. package/dist/events/registry.d.ts +252 -239
  25. package/dist/events/registry.js +125 -100
  26. package/dist/events/router.d.ts +9 -4
  27. package/dist/events/router.js +24 -11
  28. package/dist/index.d.ts +4 -2
  29. package/dist/index.js +1 -0
  30. package/dist/interactions/dispatch.d.ts +5 -1
  31. package/dist/interactions/dispatch.js +6 -2
  32. package/dist/loader/hot.js +22 -2
  33. package/dist/loader/loader.d.ts +7 -0
  34. package/dist/loader/loader.js +22 -12
  35. package/dist/ranking/declaration.d.ts +12 -0
  36. package/dist/ranking/declaration.js +48 -0
  37. package/dist/ranking/ranking.d.ts +102 -0
  38. package/dist/ranking/ranking.js +129 -0
  39. package/dist/redis/client.d.ts +36 -0
  40. package/dist/redis/client.js +252 -0
  41. package/dist/redis/protocol.d.ts +22 -0
  42. package/dist/redis/protocol.js +99 -0
  43. package/dist/redis/url.d.ts +24 -0
  44. package/dist/redis/url.js +50 -0
  45. package/dist/store/backend.d.ts +49 -4
  46. package/dist/store/backend.js +66 -11
  47. package/dist/store/declaration.d.ts +3 -0
  48. package/dist/store/declaration.js +8 -3
  49. package/dist/store/file-backend.d.ts +3 -1
  50. package/dist/store/file-backend.js +29 -0
  51. package/dist/store/ipc-backend.d.ts +24 -1
  52. package/dist/store/ipc-backend.js +49 -0
  53. package/dist/store/memory-backend.d.ts +17 -0
  54. package/dist/store/memory-backend.js +82 -0
  55. package/dist/store/ranking-backend.d.ts +45 -0
  56. package/dist/store/ranking-backend.js +82 -0
  57. package/dist/store/redis-backend.d.ts +38 -0
  58. package/dist/store/redis-backend.js +245 -0
  59. package/dist/store/redis-ranking.d.ts +15 -0
  60. package/dist/store/redis-ranking.js +69 -0
  61. package/dist/store/scope.d.ts +11 -0
  62. package/dist/store/scope.js +23 -0
  63. package/dist/store/store.d.ts +39 -5
  64. package/dist/store/store.js +83 -33
  65. package/dist/structures/invite.d.ts +6 -0
  66. package/dist/structures/invite.js +11 -0
  67. package/dist/tasks/scheduler.d.ts +6 -0
  68. package/dist/tasks/scheduler.js +9 -3
  69. package/dist/testing/failure.d.ts +9 -0
  70. package/dist/testing/failure.js +20 -0
  71. package/dist/testing/fake-discord.d.ts +59 -0
  72. package/dist/testing/fake-discord.js +120 -0
  73. package/dist/testing/fake-gateway.d.ts +59 -0
  74. package/dist/testing/fake-gateway.js +123 -0
  75. package/dist/testing/harness.d.ts +387 -0
  76. package/dist/testing/harness.js +1418 -0
  77. package/dist/testing/index.d.ts +4 -0
  78. package/dist/testing/index.js +5 -0
  79. package/dist/testing/scenario.d.ts +39 -0
  80. package/dist/testing/scenario.js +58 -0
  81. package/dist/testing/smoke.d.ts +20 -0
  82. package/dist/testing/smoke.js +659 -0
  83. package/dist/testing/websocket.d.ts +34 -0
  84. package/dist/testing/websocket.js +167 -0
  85. package/dist/testing/world.d.ts +145 -0
  86. package/dist/testing/world.js +1479 -0
  87. package/package.json +20 -3
@@ -0,0 +1,167 @@
1
+ // A WebSocket server on Node built-ins, for the fake servers of Discord
2
+ // (the gateway, voice): the real connection code of the framework runs
3
+ // against it. Masked and fragmented client frames, text and binary, pings.
4
+ // https://datatracker.ietf.org/doc/html/rfc6455#section-5
5
+ import { createHash } from 'node:crypto';
6
+ import { createServer } from 'node:http';
7
+ const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
8
+ function frame(opcode, payload) {
9
+ const length = payload.length;
10
+ let header;
11
+ if (length < 126) {
12
+ header = Buffer.from([0x80 | opcode, length]);
13
+ }
14
+ else if (length < 65536) {
15
+ header = Buffer.alloc(4);
16
+ header[0] = 0x80 | opcode;
17
+ header[1] = 126;
18
+ header.writeUInt16BE(length, 2);
19
+ }
20
+ else {
21
+ header = Buffer.alloc(10);
22
+ header[0] = 0x80 | opcode;
23
+ header[1] = 127;
24
+ header.writeBigUInt64BE(BigInt(length), 2);
25
+ }
26
+ return Buffer.concat([header, payload]);
27
+ }
28
+ /**
29
+ * A local WebSocket server: it accepts every client, reads their (masked,
30
+ * possibly fragmented) messages and answers pings. The caller closes it.
31
+ */
32
+ export async function webSocketServer(onConnection) {
33
+ const sockets = new Set();
34
+ const server = createServer((_, res) => res.writeHead(426).end());
35
+ server.on('upgrade', (req, socket) => {
36
+ const key = req.headers['sec-websocket-key'];
37
+ const accept = createHash('sha1')
38
+ .update(key + GUID)
39
+ .digest('base64');
40
+ socket.write('HTTP/1.1 101 Switching Protocols\r\n' +
41
+ 'Upgrade: websocket\r\n' +
42
+ 'Connection: Upgrade\r\n' +
43
+ `Sec-WebSocket-Accept: ${accept}\r\n\r\n`);
44
+ sockets.add(socket);
45
+ const closeWaiters = [];
46
+ const markClosed = (code) => {
47
+ if (peer.closed)
48
+ return;
49
+ peer.closed = { code };
50
+ sockets.delete(socket);
51
+ for (const resolve of closeWaiters.splice(0))
52
+ resolve(code);
53
+ };
54
+ let handler = () => { };
55
+ const peer = {
56
+ query: Object.fromEntries(new URL(req.url ?? '/', 'http://localhost').searchParams),
57
+ closed: null,
58
+ sendText(text) {
59
+ if (!peer.closed)
60
+ socket.write(frame(0x1, Buffer.from(text)));
61
+ },
62
+ sendBinary(data) {
63
+ if (!peer.closed)
64
+ socket.write(frame(0x2, data));
65
+ },
66
+ close(code) {
67
+ if (peer.closed)
68
+ return;
69
+ const body = Buffer.alloc(2);
70
+ body.writeUInt16BE(code);
71
+ socket.end(frame(0x8, body));
72
+ markClosed(null);
73
+ },
74
+ drop() {
75
+ socket.destroy();
76
+ markClosed(null);
77
+ },
78
+ waitForClose(timeout = 2000) {
79
+ if (peer.closed)
80
+ return Promise.resolve(peer.closed.code);
81
+ return new Promise((resolve, reject) => {
82
+ const timer = setTimeout(() => reject(new Error(`The fake server waited ${timeout}ms for the connection to close.`)), timeout);
83
+ closeWaiters.push(code => {
84
+ clearTimeout(timer);
85
+ resolve(code);
86
+ });
87
+ });
88
+ },
89
+ };
90
+ // Client frames are masked; a message may come in several frames.
91
+ let buffer = Buffer.alloc(0);
92
+ let fragments = [];
93
+ let binary = false;
94
+ socket.on('data', (chunk) => {
95
+ buffer = Buffer.concat([buffer, chunk]);
96
+ for (;;) {
97
+ if (buffer.length < 2)
98
+ return;
99
+ const fin = (buffer[0] & 0x80) !== 0;
100
+ const opcode = buffer[0] & 0x0f;
101
+ let length = buffer[1] & 0x7f;
102
+ let offset = 2;
103
+ if (length === 126) {
104
+ if (buffer.length < 4)
105
+ return;
106
+ length = buffer.readUInt16BE(2);
107
+ offset = 4;
108
+ }
109
+ else if (length === 127) {
110
+ if (buffer.length < 10)
111
+ return;
112
+ length = Number(buffer.readBigUInt64BE(2));
113
+ offset = 10;
114
+ }
115
+ if (buffer.length < offset + 4 + length)
116
+ return;
117
+ const mask = buffer.subarray(offset, offset + 4);
118
+ const data = Buffer.from(buffer.subarray(offset + 4, offset + 4 + length));
119
+ for (let index = 0; index < data.length; index++) {
120
+ data[index] ^= mask[index % 4];
121
+ }
122
+ buffer = buffer.subarray(offset + 4 + length);
123
+ if (opcode === 0x8) {
124
+ const code = data.length >= 2 ? data.readUInt16BE(0) : null;
125
+ if (!peer.closed)
126
+ socket.end(frame(0x8, data.subarray(0, 2)));
127
+ markClosed(code);
128
+ return;
129
+ }
130
+ if (opcode === 0x9) {
131
+ socket.write(frame(0xa, data));
132
+ continue;
133
+ }
134
+ if (opcode === 0xa)
135
+ continue;
136
+ if (opcode === 0x1 || opcode === 0x2)
137
+ binary = opcode === 0x2;
138
+ fragments.push(data);
139
+ if (fin) {
140
+ const message = Buffer.concat(fragments);
141
+ fragments = [];
142
+ handler(binary ? message : message.toString('utf8'));
143
+ }
144
+ }
145
+ });
146
+ socket.on('close', () => markClosed(null));
147
+ socket.on('error', () => markClosed(null));
148
+ onConnection(peer, req, {
149
+ message: next => {
150
+ handler = next;
151
+ },
152
+ });
153
+ });
154
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
155
+ const { port } = server.address();
156
+ return {
157
+ url: `ws://127.0.0.1:${port}`,
158
+ host: '127.0.0.1',
159
+ port,
160
+ close: () => new Promise(resolve => {
161
+ for (const socket of sockets)
162
+ socket.destroy();
163
+ server.closeAllConnections();
164
+ server.close(() => resolve());
165
+ }),
166
+ };
167
+ }
@@ -0,0 +1,145 @@
1
+ import type { RawApplicationCommand } from '../discord/types/application-command.js';
2
+ import { ChannelType, type RawChannel } from '../discord/types/channel.js';
3
+ import type { Locale, Snowflake } from '../discord/types/common.js';
4
+ import type { RawGuild, RawGuildMember } from '../discord/types/guild.js';
5
+ import { type RawInteraction, type RawInteractionCallbackAutocompleteData, type RawInteractionCallbackModalData, type RawInteractionResponse } from '../discord/types/interaction.js';
6
+ import { type RawMessage } from '../discord/types/message.js';
7
+ import { type RawRole } from '../discord/types/permissions.js';
8
+ import type { RawUser } from '../discord/types/user.js';
9
+ import { type FakeDiscord, type FakeDiscordRequest } from './fake-discord.js';
10
+ import { type FakeGateway } from './fake-gateway.js';
11
+ /** An interaction someone made, and everything the bot answered to it. */
12
+ export interface InteractionRecord {
13
+ readonly id: Snowflake;
14
+ readonly token: string;
15
+ readonly raw: RawInteraction;
16
+ /** The message the component was on, for a component or a form it opened. */
17
+ message: RawMessage | null;
18
+ /** Every callback sent to `/callback`, in order (Discord takes one). */
19
+ readonly callbacks: RawInteractionResponse[];
20
+ /** The first answer, as a message: sent, deferred, or the updated one. */
21
+ original: RawMessage | null;
22
+ /** The messages sent after the first answer. */
23
+ readonly followUps: RawMessage[];
24
+ /** The form opened, when the answer was one. */
25
+ modal: RawInteractionCallbackModalData | null;
26
+ /** The suggestions given, for an autocomplete. */
27
+ suggestions: RawInteractionCallbackAutocompleteData['choices'] | null;
28
+ /** Whether the first answer was "thinking…" (deferred). */
29
+ deferred: boolean;
30
+ /** Whether the answer changed the message of the component. */
31
+ updated: boolean;
32
+ }
33
+ export interface WorldOptions {
34
+ /** The name of the server. Default: "Test Server". */
35
+ guildName?: string;
36
+ /** The name of the bot. Default: "Test Bot". */
37
+ botName?: string;
38
+ }
39
+ export interface WorldUserOptions {
40
+ bot?: boolean;
41
+ locale?: Locale;
42
+ }
43
+ export interface WorldMemberOptions {
44
+ roles?: readonly Snowflake[];
45
+ nick?: string | null;
46
+ }
47
+ export interface WorldRoleOptions {
48
+ permissions?: bigint;
49
+ position?: number;
50
+ color?: number;
51
+ }
52
+ export interface WorldChannelOptions {
53
+ type?: ChannelType;
54
+ parentId?: Snowflake | null;
55
+ topic?: string | null;
56
+ }
57
+ /**
58
+ * The Discord of a test: one server, the bot in it, people who come and go,
59
+ * and everything the bot does kept where a test can read it.
60
+ */
61
+ export declare class World {
62
+ #private;
63
+ readonly botId: Snowflake;
64
+ readonly applicationId: Snowflake;
65
+ readonly guildId: Snowflake;
66
+ readonly ownerId: Snowflake;
67
+ guildName: string;
68
+ readonly users: Map<string, RawUser>;
69
+ /** The members of the server, by user id. */
70
+ readonly members: Map<string, RawGuildMember>;
71
+ readonly roles: Map<string, RawRole>;
72
+ /** The channels of the server, its threads and the private channels. */
73
+ readonly channels: Map<string, RawChannel>;
74
+ /** The messages of each channel, oldest first. */
75
+ readonly messages: Map<string, RawMessage[]>;
76
+ readonly bans: Map<string, {
77
+ user: RawUser;
78
+ reason: string | null;
79
+ }>;
80
+ /** Who is in each thread. */
81
+ readonly threadMembers: Map<string, Set<string>>;
82
+ /** Every interaction made, by id and by token. */
83
+ readonly interactions: Map<string, InteractionRecord>;
84
+ /** The commands registered, for the server and for everyone. */
85
+ guildCommands: RawApplicationCommand[];
86
+ globalCommands: RawApplicationCommand[];
87
+ /** Every gateway event the world sent to the bot, in order. */
88
+ readonly sent: {
89
+ type: string;
90
+ data: unknown;
91
+ }[];
92
+ discord: FakeDiscord;
93
+ gateway: FakeGateway;
94
+ constructor(options?: WorldOptions);
95
+ /** The environment that points the CLI at this Discord. */
96
+ get env(): Record<string, string>;
97
+ /** The user of the bot. */
98
+ get bot(): RawUser;
99
+ /** Starts the fake REST API and gateway. */
100
+ start(): Promise<void>;
101
+ close(): Promise<void>;
102
+ /** A new id, later than every id given before, in this world or another. */
103
+ snowflake(): Snowflake;
104
+ addUser(name: string, options?: WorldUserOptions): RawUser;
105
+ /** The user named so, if any. */
106
+ findUser(name: string): RawUser | undefined;
107
+ addMember(userId: Snowflake, options?: WorldMemberOptions): RawGuildMember;
108
+ addRole(name: string, options?: WorldRoleOptions): RawRole;
109
+ addChannel(name: string, options?: WorldChannelOptions): RawChannel;
110
+ /** The channel of the server named so, if any. */
111
+ findChannel(name: string): RawChannel | undefined;
112
+ /** The private channel with a user, made the first time it is asked. */
113
+ dm(userId: Snowflake): RawChannel;
114
+ /** The permissions of a member in the server (channel overwrites left aside). */
115
+ permissionsOf(userId: Snowflake): bigint;
116
+ /** The server as Discord sends it in Guild Create. */
117
+ guildPayload(): RawGuild & Record<string, unknown>;
118
+ /** The server as Get Guild returns it. */
119
+ guildFields(): RawGuild;
120
+ isThread(channel: RawChannel): boolean;
121
+ /** The connection of the bot, once it identified. */
122
+ get connection(): import("./fake-gateway.js").FakeGatewayConnection;
123
+ /** Sends a gateway event to the bot. */
124
+ dispatch(type: string, data: unknown): void;
125
+ /** Makes an interaction happen: records it and sends it to the bot. */
126
+ interaction(raw: RawInteraction): InteractionRecord;
127
+ /** Puts a message in its channel and tells the bot, as Discord does. */
128
+ createMessage(channelId: Snowflake, author: RawUser, data: Record<string, unknown>, files?: FakeDiscordRequest['files'], extra?: Partial<RawMessage>): RawMessage;
129
+ /** The message with this id, wherever it is. */
130
+ findMessage(id: Snowflake): RawMessage | undefined;
131
+ editMessage(message: RawMessage, data: Record<string, unknown>): RawMessage;
132
+ deleteMessage(message: RawMessage): void;
133
+ removeMember(userId: Snowflake): void;
134
+ memberUpdated(member: RawGuildMember): void;
135
+ }
136
+ /** The raw interaction type of a kind of thing someone does. */
137
+ export declare const INTERACTION_TYPES: {
138
+ readonly Ping: 1;
139
+ readonly ApplicationCommand: 2;
140
+ readonly MessageComponent: 3;
141
+ readonly ApplicationCommandAutocomplete: 4;
142
+ readonly ModalSubmit: 5;
143
+ };
144
+ /** Every permission, for the roles of tests. */
145
+ export declare const EVERY_PERMISSION: bigint;