chapterjs 0.0.0 → 0.1.1
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.
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.js +33 -0
- package/dist/cli/dev.d.ts +1 -0
- package/dist/cli/dev.js +206 -0
- package/dist/components.d.ts +97 -0
- package/dist/components.js +260 -0
- package/dist/context.d.ts +72 -0
- package/dist/context.js +273 -0
- package/dist/discord/client.d.ts +35 -0
- package/dist/discord/client.js +66 -0
- package/dist/discord/constants.d.ts +49 -0
- package/dist/discord/constants.js +61 -0
- package/dist/discord/events.d.ts +129 -0
- package/dist/discord/events.js +102 -0
- package/dist/discord/gateway.d.ts +61 -0
- package/dist/discord/gateway.js +320 -0
- package/dist/discord/payloads.d.ts +275 -0
- package/dist/discord/payloads.js +3 -0
- package/dist/discord/presence.d.ts +42 -0
- package/dist/discord/presence.js +51 -0
- package/dist/discord/rest.d.ts +49 -0
- package/dist/discord/rest.js +271 -0
- package/dist/discord/types.d.ts +334 -0
- package/dist/discord/types.js +4 -0
- package/dist/events.d.ts +94 -0
- package/dist/events.js +255 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +12 -0
- package/dist/presence.d.ts +92 -0
- package/dist/presence.js +250 -0
- package/dist/structures/Channel.d.ts +110 -0
- package/dist/structures/Channel.js +171 -0
- package/dist/structures/Guild.d.ts +70 -0
- package/dist/structures/Guild.js +105 -0
- package/dist/structures/Interaction.d.ts +113 -0
- package/dist/structures/Interaction.js +274 -0
- package/dist/structures/Member.d.ts +70 -0
- package/dist/structures/Member.js +124 -0
- package/dist/structures/Message.d.ts +87 -0
- package/dist/structures/Message.js +182 -0
- package/dist/structures/Reaction.d.ts +38 -0
- package/dist/structures/Reaction.js +64 -0
- package/dist/structures/Role.d.ts +43 -0
- package/dist/structures/Role.js +77 -0
- package/dist/structures/User.d.ts +41 -0
- package/dist/structures/User.js +79 -0
- package/dist/structures/utils.d.ts +72 -0
- package/dist/structures/utils.js +59 -0
- package/dist/utils/errors.d.ts +5 -0
- package/dist/utils/errors.js +28 -0
- package/dist/utils/modules.d.ts +39 -0
- package/dist/utils/modules.js +156 -0
- package/package.json +30 -5
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { GatewayIntents } from './constants.js';
|
|
2
|
+
const { Guilds, GuildMembers, GuildModeration, GuildExpressions, GuildIntegrations, GuildWebhooks, GuildInvites, GuildVoiceStates, GuildPresences, GuildMessages, GuildMessageReactions, GuildMessageTyping, DirectMessages, DirectMessageReactions, DirectMessageTyping, GuildScheduledEvents, AutoModerationConfiguration, AutoModerationExecution, GuildMessagePolls, DirectMessagePolls, } = GatewayIntents;
|
|
3
|
+
/**
|
|
4
|
+
* Every dispatch event an app can receive, by camelCase name, with the intents
|
|
5
|
+
* that make Discord send it (0 when it is always sent). An event listed under
|
|
6
|
+
* several intents (guild and DM) needs any of them; all are requested.
|
|
7
|
+
*
|
|
8
|
+
* https://docs.discord.com/developers/events/gateway-events#receive-events
|
|
9
|
+
* https://docs.discord.com/developers/events/gateway#list-of-intents
|
|
10
|
+
*/
|
|
11
|
+
export const GATEWAY_EVENTS = {
|
|
12
|
+
ready: 0,
|
|
13
|
+
resumed: 0,
|
|
14
|
+
rateLimited: 0,
|
|
15
|
+
applicationCommandPermissionsUpdate: 0,
|
|
16
|
+
autoModerationRuleCreate: AutoModerationConfiguration,
|
|
17
|
+
autoModerationRuleUpdate: AutoModerationConfiguration,
|
|
18
|
+
autoModerationRuleDelete: AutoModerationConfiguration,
|
|
19
|
+
autoModerationActionExecution: AutoModerationExecution,
|
|
20
|
+
channelCreate: Guilds,
|
|
21
|
+
channelUpdate: Guilds,
|
|
22
|
+
channelDelete: Guilds,
|
|
23
|
+
channelInfo: 0,
|
|
24
|
+
channelPinsUpdate: Guilds | DirectMessages,
|
|
25
|
+
voiceChannelStatusUpdate: Guilds,
|
|
26
|
+
voiceChannelStartTimeUpdate: Guilds,
|
|
27
|
+
threadCreate: Guilds,
|
|
28
|
+
threadUpdate: Guilds,
|
|
29
|
+
threadDelete: Guilds,
|
|
30
|
+
threadListSync: Guilds,
|
|
31
|
+
threadMemberUpdate: Guilds,
|
|
32
|
+
// Other members' updates also need the privileged GuildMembers intent
|
|
33
|
+
threadMembersUpdate: Guilds,
|
|
34
|
+
entitlementCreate: 0,
|
|
35
|
+
entitlementUpdate: 0,
|
|
36
|
+
entitlementDelete: 0,
|
|
37
|
+
guildCreate: Guilds,
|
|
38
|
+
guildUpdate: Guilds,
|
|
39
|
+
guildDelete: Guilds,
|
|
40
|
+
guildAuditLogEntryCreate: GuildModeration,
|
|
41
|
+
guildBanAdd: GuildModeration,
|
|
42
|
+
guildBanRemove: GuildModeration,
|
|
43
|
+
guildEmojisUpdate: GuildExpressions,
|
|
44
|
+
guildStickersUpdate: GuildExpressions,
|
|
45
|
+
guildIntegrationsUpdate: GuildIntegrations,
|
|
46
|
+
guildMemberAdd: GuildMembers,
|
|
47
|
+
guildMemberRemove: GuildMembers,
|
|
48
|
+
guildMemberUpdate: GuildMembers,
|
|
49
|
+
guildMembersChunk: 0,
|
|
50
|
+
guildRoleCreate: Guilds,
|
|
51
|
+
guildRoleUpdate: Guilds,
|
|
52
|
+
guildRoleDelete: Guilds,
|
|
53
|
+
guildScheduledEventCreate: GuildScheduledEvents,
|
|
54
|
+
guildScheduledEventUpdate: GuildScheduledEvents,
|
|
55
|
+
guildScheduledEventDelete: GuildScheduledEvents,
|
|
56
|
+
guildScheduledEventUserAdd: GuildScheduledEvents,
|
|
57
|
+
guildScheduledEventUserRemove: GuildScheduledEvents,
|
|
58
|
+
guildSoundboardSoundCreate: GuildExpressions,
|
|
59
|
+
guildSoundboardSoundUpdate: GuildExpressions,
|
|
60
|
+
guildSoundboardSoundDelete: GuildExpressions,
|
|
61
|
+
guildSoundboardSoundsUpdate: GuildExpressions,
|
|
62
|
+
soundboardSounds: 0,
|
|
63
|
+
integrationCreate: GuildIntegrations,
|
|
64
|
+
integrationUpdate: GuildIntegrations,
|
|
65
|
+
integrationDelete: GuildIntegrations,
|
|
66
|
+
interactionCreate: 0,
|
|
67
|
+
inviteCreate: GuildInvites,
|
|
68
|
+
inviteDelete: GuildInvites,
|
|
69
|
+
messageCreate: GuildMessages | DirectMessages,
|
|
70
|
+
messageUpdate: GuildMessages | DirectMessages,
|
|
71
|
+
messageDelete: GuildMessages | DirectMessages,
|
|
72
|
+
messageDeleteBulk: GuildMessages,
|
|
73
|
+
messageReactionAdd: GuildMessageReactions | DirectMessageReactions,
|
|
74
|
+
messageReactionRemove: GuildMessageReactions | DirectMessageReactions,
|
|
75
|
+
messageReactionRemoveAll: GuildMessageReactions | DirectMessageReactions,
|
|
76
|
+
messageReactionRemoveEmoji: GuildMessageReactions | DirectMessageReactions,
|
|
77
|
+
presenceUpdate: GuildPresences,
|
|
78
|
+
stageInstanceCreate: Guilds,
|
|
79
|
+
stageInstanceUpdate: Guilds,
|
|
80
|
+
stageInstanceDelete: Guilds,
|
|
81
|
+
subscriptionCreate: 0,
|
|
82
|
+
subscriptionUpdate: 0,
|
|
83
|
+
subscriptionDelete: 0,
|
|
84
|
+
typingStart: GuildMessageTyping | DirectMessageTyping,
|
|
85
|
+
userUpdate: 0,
|
|
86
|
+
voiceChannelEffectSend: GuildVoiceStates,
|
|
87
|
+
voiceStateUpdate: GuildVoiceStates,
|
|
88
|
+
voiceServerUpdate: 0,
|
|
89
|
+
webhooksUpdate: GuildWebhooks,
|
|
90
|
+
messagePollVoteAdd: GuildMessagePolls | DirectMessagePolls,
|
|
91
|
+
messagePollVoteRemove: GuildMessagePolls | DirectMessagePolls,
|
|
92
|
+
};
|
|
93
|
+
/** `messageCreate` → `MESSAGE_CREATE`, as sent by Discord. */
|
|
94
|
+
export function toDispatchName(name) {
|
|
95
|
+
return name.replace(/[A-Z]/g, letter => `_${letter}`).toUpperCase();
|
|
96
|
+
}
|
|
97
|
+
/** Intents only granted once enabled in the Developer Portal (Bot > Privileged Gateway Intents). */
|
|
98
|
+
export const PRIVILEGED_INTENTS = {
|
|
99
|
+
GuildMembers: GatewayIntents.GuildMembers,
|
|
100
|
+
GuildPresences: GatewayIntents.GuildPresences,
|
|
101
|
+
MessageContent: GatewayIntents.MessageContent,
|
|
102
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { type Presence } from './presence.js';
|
|
3
|
+
import { RestClient } from './rest.js';
|
|
4
|
+
import { type Client } from './client.js';
|
|
5
|
+
import { User } from '../structures/User.js';
|
|
6
|
+
/** Error emitted with `fatal` when Discord closed the connection for good. */
|
|
7
|
+
export declare class GatewayCloseError extends Error {
|
|
8
|
+
/** https://docs.discord.com/developers/topics/opcodes-and-status-codes#gateway-gateway-close-event-codes */
|
|
9
|
+
readonly code: number;
|
|
10
|
+
constructor(message: string,
|
|
11
|
+
/** https://docs.discord.com/developers/topics/opcodes-and-status-codes#gateway-gateway-close-event-codes */
|
|
12
|
+
code: number);
|
|
13
|
+
}
|
|
14
|
+
export interface GatewayOptions {
|
|
15
|
+
token: string;
|
|
16
|
+
/** Bitfield of gateway intents, see `GatewayIntents`. Defaults to `Guilds`. */
|
|
17
|
+
intents?: number;
|
|
18
|
+
/** Guild reserved for development, see `dev`. */
|
|
19
|
+
devGuildId?: string;
|
|
20
|
+
/**
|
|
21
|
+
* In dev mode, only events from `devGuildId` reach your handlers.
|
|
22
|
+
* Otherwise, events from `devGuildId` are ignored so a dev process and a
|
|
23
|
+
* production process can share the same bot.
|
|
24
|
+
*/
|
|
25
|
+
dev?: boolean;
|
|
26
|
+
/** Status and activity shown when the bot connects, see `setPresence`. */
|
|
27
|
+
presence?: Presence;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The gateway connection, internal to the framework: only `chapterjs dev`
|
|
31
|
+
* drives it (login, intents, presence, shutdown). User code gets its
|
|
32
|
+
* read-only `client`.
|
|
33
|
+
*/
|
|
34
|
+
export declare class Gateway extends EventEmitter {
|
|
35
|
+
#private;
|
|
36
|
+
readonly rest: RestClient;
|
|
37
|
+
/** What handlers receive, see `Client`. */
|
|
38
|
+
readonly client: Client;
|
|
39
|
+
/** The bot's user, set once connected. */
|
|
40
|
+
user: User | null;
|
|
41
|
+
applicationId: string | null;
|
|
42
|
+
/** Ids of the guilds the bot is in, as sent in READY. */
|
|
43
|
+
guildIds: Set<string>;
|
|
44
|
+
constructor(options: GatewayOptions);
|
|
45
|
+
/** Connects to the gateway and resolves once the bot is ready. */
|
|
46
|
+
login(): Promise<User>;
|
|
47
|
+
/** Bitfield of the gateway intents of the current session. */
|
|
48
|
+
get intents(): number;
|
|
49
|
+
/**
|
|
50
|
+
* Changes the gateway intents. Intents are only sent when identifying, so a
|
|
51
|
+
* connected client starts a new session (READY is dispatched again).
|
|
52
|
+
*/
|
|
53
|
+
setIntents(intents: number): void;
|
|
54
|
+
/** Updates the bot's status and activity, e.g. `{ activity: { type: 'playing', name: 'Minecraft' } }`. */
|
|
55
|
+
setPresence(presence: Presence): void;
|
|
56
|
+
/**
|
|
57
|
+
* Closes the connection. Resolves once Discord acknowledged the close, so the
|
|
58
|
+
* session ends right away instead of lingering online until it times out.
|
|
59
|
+
*/
|
|
60
|
+
destroy(): Promise<void>;
|
|
61
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { API_VERSION, FATAL_CLOSE_CODES, GatewayCloseCodes, GatewayIntents, GatewayOpcodes, } from './constants.js';
|
|
3
|
+
import { toGatewayPresence } from './presence.js';
|
|
4
|
+
import { RestClient } from './rest.js';
|
|
5
|
+
import { createClient } from './client.js';
|
|
6
|
+
import { User } from '../structures/User.js';
|
|
7
|
+
/** Error emitted with `fatal` when Discord closed the connection for good. */
|
|
8
|
+
export class GatewayCloseError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
constructor(message,
|
|
11
|
+
/** https://docs.discord.com/developers/topics/opcodes-and-status-codes#gateway-gateway-close-event-codes */
|
|
12
|
+
code) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.code = code;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Events that don't belong to any guild and must always go through. */
|
|
18
|
+
const SYSTEM_EVENTS = new Set([
|
|
19
|
+
'READY',
|
|
20
|
+
'RESUMED',
|
|
21
|
+
'USER_UPDATE',
|
|
22
|
+
'RATE_LIMITED',
|
|
23
|
+
]);
|
|
24
|
+
/** Guild events carry the guild id in `id` instead of `guild_id`. */
|
|
25
|
+
const GUILD_EVENTS = new Set(['GUILD_CREATE', 'GUILD_UPDATE', 'GUILD_DELETE']);
|
|
26
|
+
/**
|
|
27
|
+
* Minimum delay between two presence updates. The gateway allows 120 events
|
|
28
|
+
* per minute per connection and disconnects apps going over it.
|
|
29
|
+
*/
|
|
30
|
+
const PRESENCE_UPDATE_INTERVAL = 5_000;
|
|
31
|
+
/** Any non-1000/1001 close code keeps the session resumable. */
|
|
32
|
+
const RECONNECT_CLOSE_CODE = 4000;
|
|
33
|
+
/**
|
|
34
|
+
* The gateway connection, internal to the framework: only `chapterjs dev`
|
|
35
|
+
* drives it (login, intents, presence, shutdown). User code gets its
|
|
36
|
+
* read-only `client`.
|
|
37
|
+
*/
|
|
38
|
+
export class Gateway extends EventEmitter {
|
|
39
|
+
rest;
|
|
40
|
+
/** What handlers receive, see `Client`. */
|
|
41
|
+
client;
|
|
42
|
+
/** The bot's user, set once connected. */
|
|
43
|
+
user = null;
|
|
44
|
+
applicationId = null;
|
|
45
|
+
/** Ids of the guilds the bot is in, as sent in READY. */
|
|
46
|
+
guildIds = new Set();
|
|
47
|
+
#token;
|
|
48
|
+
#intents;
|
|
49
|
+
#devGuildId;
|
|
50
|
+
#dev;
|
|
51
|
+
#presence;
|
|
52
|
+
/** Last presence Discord received, to skip identical updates. */
|
|
53
|
+
#sentPresence = null;
|
|
54
|
+
#lastPresenceUpdate = 0;
|
|
55
|
+
#presenceTimeout = null;
|
|
56
|
+
#ws = null;
|
|
57
|
+
#gatewayUrl = '';
|
|
58
|
+
#resumeGatewayUrl = null;
|
|
59
|
+
#sessionId = null;
|
|
60
|
+
#sequence = null;
|
|
61
|
+
#heartbeatTimeout = null;
|
|
62
|
+
#heartbeatInterval = null;
|
|
63
|
+
#heartbeatAcked = true;
|
|
64
|
+
#destroyed = false;
|
|
65
|
+
constructor(options) {
|
|
66
|
+
super();
|
|
67
|
+
this.#token = options.token;
|
|
68
|
+
this.#intents = options.intents ?? GatewayIntents.Guilds;
|
|
69
|
+
this.#devGuildId = options.devGuildId ?? null;
|
|
70
|
+
this.#dev = options.dev ?? false;
|
|
71
|
+
this.#presence = options.presence
|
|
72
|
+
? toGatewayPresence(options.presence)
|
|
73
|
+
: null;
|
|
74
|
+
this.rest = new RestClient(this.#token);
|
|
75
|
+
this.client = createClient(this);
|
|
76
|
+
}
|
|
77
|
+
/** Connects to the gateway and resolves once the bot is ready. */
|
|
78
|
+
async login() {
|
|
79
|
+
// https://docs.discord.com/developers/events/gateway#get-gateway-bot
|
|
80
|
+
const { url } = await this.rest.get('/gateway/bot');
|
|
81
|
+
this.#gatewayUrl = url;
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const onReady = (user) => {
|
|
84
|
+
this.off('fatal', onFatal);
|
|
85
|
+
resolve(user);
|
|
86
|
+
};
|
|
87
|
+
const onFatal = (error) => {
|
|
88
|
+
this.off('ready', onReady);
|
|
89
|
+
reject(error);
|
|
90
|
+
};
|
|
91
|
+
this.once('ready', onReady);
|
|
92
|
+
this.once('fatal', onFatal);
|
|
93
|
+
this.#connect(url);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/** Bitfield of the gateway intents of the current session. */
|
|
97
|
+
get intents() {
|
|
98
|
+
return this.#intents;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Changes the gateway intents. Intents are only sent when identifying, so a
|
|
102
|
+
* connected client starts a new session (READY is dispatched again).
|
|
103
|
+
*/
|
|
104
|
+
setIntents(intents) {
|
|
105
|
+
if (intents === this.#intents)
|
|
106
|
+
return;
|
|
107
|
+
this.#intents = intents;
|
|
108
|
+
if (this.#ws)
|
|
109
|
+
this.#reconnect(false);
|
|
110
|
+
}
|
|
111
|
+
/** Updates the bot's status and activity, e.g. `{ activity: { type: 'playing', name: 'Minecraft' } }`. */
|
|
112
|
+
setPresence(presence) {
|
|
113
|
+
this.#presence = toGatewayPresence(presence);
|
|
114
|
+
this.#flushPresence();
|
|
115
|
+
}
|
|
116
|
+
/** Sends the latest presence, at most once every `PRESENCE_UPDATE_INTERVAL`. */
|
|
117
|
+
#flushPresence() {
|
|
118
|
+
if (!this.#sessionId || this.#presenceTimeout)
|
|
119
|
+
return;
|
|
120
|
+
const wait = this.#lastPresenceUpdate + PRESENCE_UPDATE_INTERVAL - Date.now();
|
|
121
|
+
if (wait > 0) {
|
|
122
|
+
this.#presenceTimeout = setTimeout(() => {
|
|
123
|
+
this.#presenceTimeout = null;
|
|
124
|
+
this.#flushPresence();
|
|
125
|
+
}, wait);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const presence = JSON.stringify(this.#presence);
|
|
129
|
+
if (presence === this.#sentPresence)
|
|
130
|
+
return;
|
|
131
|
+
if (this.#send(GatewayOpcodes.PresenceUpdate, this.#presence)) {
|
|
132
|
+
this.#sentPresence = presence;
|
|
133
|
+
this.#lastPresenceUpdate = Date.now();
|
|
134
|
+
this.emit('presenceUpdate', this.#presence);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Closes the connection. Resolves once Discord acknowledged the close, so the
|
|
139
|
+
* session ends right away instead of lingering online until it times out.
|
|
140
|
+
*/
|
|
141
|
+
destroy() {
|
|
142
|
+
this.#destroyed = true;
|
|
143
|
+
this.#stopHeartbeat();
|
|
144
|
+
if (this.#presenceTimeout)
|
|
145
|
+
clearTimeout(this.#presenceTimeout);
|
|
146
|
+
const ws = this.#ws;
|
|
147
|
+
this.#ws = null;
|
|
148
|
+
if (!ws || ws.readyState === WebSocket.CLOSED)
|
|
149
|
+
return Promise.resolve();
|
|
150
|
+
return new Promise(resolve => {
|
|
151
|
+
const timeout = setTimeout(resolve, 1000);
|
|
152
|
+
ws.addEventListener('close', () => {
|
|
153
|
+
clearTimeout(timeout);
|
|
154
|
+
resolve();
|
|
155
|
+
});
|
|
156
|
+
ws.close(1000);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
#connect(baseUrl) {
|
|
160
|
+
const ws = new WebSocket(`${baseUrl}/?v=${API_VERSION}&encoding=json`);
|
|
161
|
+
this.#ws = ws;
|
|
162
|
+
ws.addEventListener('message', event => {
|
|
163
|
+
if (ws !== this.#ws)
|
|
164
|
+
return;
|
|
165
|
+
this.#onPayload(JSON.parse(String(event.data)));
|
|
166
|
+
});
|
|
167
|
+
ws.addEventListener('close', event => {
|
|
168
|
+
if (ws !== this.#ws)
|
|
169
|
+
return;
|
|
170
|
+
this.#onClose(event.code);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
#send(op, d) {
|
|
174
|
+
if (this.#ws?.readyState !== WebSocket.OPEN)
|
|
175
|
+
return false;
|
|
176
|
+
this.#ws.send(JSON.stringify({ op, d }));
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
#onPayload(payload) {
|
|
180
|
+
if (payload.s !== null)
|
|
181
|
+
this.#sequence = payload.s;
|
|
182
|
+
switch (payload.op) {
|
|
183
|
+
case GatewayOpcodes.Hello:
|
|
184
|
+
this.#startHeartbeat(payload.d.heartbeat_interval);
|
|
185
|
+
if (this.#sessionId)
|
|
186
|
+
this.#resume();
|
|
187
|
+
else
|
|
188
|
+
this.#identify();
|
|
189
|
+
break;
|
|
190
|
+
case GatewayOpcodes.Heartbeat:
|
|
191
|
+
this.#heartbeat();
|
|
192
|
+
break;
|
|
193
|
+
case GatewayOpcodes.HeartbeatAck:
|
|
194
|
+
this.#heartbeatAcked = true;
|
|
195
|
+
break;
|
|
196
|
+
case GatewayOpcodes.Reconnect:
|
|
197
|
+
this.#reconnect(true);
|
|
198
|
+
break;
|
|
199
|
+
case GatewayOpcodes.InvalidSession:
|
|
200
|
+
// d tells whether the session can be resumed; wait 1-5s before re-identifying
|
|
201
|
+
if (payload.d)
|
|
202
|
+
this.#reconnect(true);
|
|
203
|
+
else
|
|
204
|
+
setTimeout(() => this.#reconnect(false), 1000 + Math.random() * 4000);
|
|
205
|
+
break;
|
|
206
|
+
case GatewayOpcodes.Dispatch:
|
|
207
|
+
this.#onDispatch(payload.t, payload.d);
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
#onDispatch(event, data) {
|
|
212
|
+
if (event === 'READY') {
|
|
213
|
+
this.#sessionId = data.session_id;
|
|
214
|
+
this.#resumeGatewayUrl = data.resume_gateway_url;
|
|
215
|
+
this.user = new User(this.client, data.user);
|
|
216
|
+
this.applicationId = data.application.id;
|
|
217
|
+
this.guildIds = new Set(data.guilds.map((guild) => guild.id));
|
|
218
|
+
this.emit('ready', this.user);
|
|
219
|
+
}
|
|
220
|
+
else if (event === 'RESUMED') {
|
|
221
|
+
// An update may have been dropped while disconnected
|
|
222
|
+
this.#flushPresence();
|
|
223
|
+
this.emit('resumed');
|
|
224
|
+
}
|
|
225
|
+
if (this.#shouldDispatch(event, data))
|
|
226
|
+
this.emit('dispatch', event, data);
|
|
227
|
+
}
|
|
228
|
+
#shouldDispatch(event, data) {
|
|
229
|
+
if (!this.#devGuildId || SYSTEM_EVENTS.has(event))
|
|
230
|
+
return true;
|
|
231
|
+
const guildId = GUILD_EVENTS.has(event)
|
|
232
|
+
? data?.id
|
|
233
|
+
: data?.guild_id;
|
|
234
|
+
// In dev, events without a guild (DMs) are left to the production process
|
|
235
|
+
return this.#dev
|
|
236
|
+
? guildId === this.#devGuildId
|
|
237
|
+
: guildId !== this.#devGuildId;
|
|
238
|
+
}
|
|
239
|
+
// https://docs.discord.com/developers/events/gateway#identifying
|
|
240
|
+
#identify() {
|
|
241
|
+
this.#send(GatewayOpcodes.Identify, {
|
|
242
|
+
token: this.#token,
|
|
243
|
+
intents: this.#intents,
|
|
244
|
+
properties: {
|
|
245
|
+
os: process.platform,
|
|
246
|
+
browser: 'chapterjs',
|
|
247
|
+
device: 'chapterjs',
|
|
248
|
+
},
|
|
249
|
+
...(this.#presence && { presence: this.#presence }),
|
|
250
|
+
});
|
|
251
|
+
this.#sentPresence = JSON.stringify(this.#presence);
|
|
252
|
+
}
|
|
253
|
+
// https://docs.discord.com/developers/events/gateway#resuming
|
|
254
|
+
#resume() {
|
|
255
|
+
this.#send(GatewayOpcodes.Resume, {
|
|
256
|
+
token: this.#token,
|
|
257
|
+
session_id: this.#sessionId,
|
|
258
|
+
seq: this.#sequence,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
// https://docs.discord.com/developers/events/gateway#sending-heartbeats
|
|
262
|
+
#startHeartbeat(interval) {
|
|
263
|
+
this.#stopHeartbeat();
|
|
264
|
+
this.#heartbeatAcked = true;
|
|
265
|
+
this.#heartbeatTimeout = setTimeout(() => {
|
|
266
|
+
this.#heartbeat();
|
|
267
|
+
this.#heartbeatInterval = setInterval(() => this.#heartbeat(), interval);
|
|
268
|
+
}, interval * Math.random());
|
|
269
|
+
}
|
|
270
|
+
#stopHeartbeat() {
|
|
271
|
+
if (this.#heartbeatTimeout)
|
|
272
|
+
clearTimeout(this.#heartbeatTimeout);
|
|
273
|
+
if (this.#heartbeatInterval)
|
|
274
|
+
clearInterval(this.#heartbeatInterval);
|
|
275
|
+
this.#heartbeatTimeout = this.#heartbeatInterval = null;
|
|
276
|
+
}
|
|
277
|
+
#heartbeat() {
|
|
278
|
+
if (!this.#heartbeatAcked) {
|
|
279
|
+
// No ACK since the last heartbeat: the connection is zombied
|
|
280
|
+
this.#reconnect(true);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
this.#heartbeatAcked = false;
|
|
284
|
+
this.#send(GatewayOpcodes.Heartbeat, this.#sequence);
|
|
285
|
+
}
|
|
286
|
+
#reconnect(resume) {
|
|
287
|
+
if (this.#destroyed)
|
|
288
|
+
return;
|
|
289
|
+
this.#stopHeartbeat();
|
|
290
|
+
if (!resume)
|
|
291
|
+
this.#resetSession();
|
|
292
|
+
const previous = this.#ws;
|
|
293
|
+
this.#ws = null;
|
|
294
|
+
// https://docs.discord.com/developers/events/gateway#initiating-a-disconnect
|
|
295
|
+
// 1000 ends the session right away, so it doesn't linger next to the new one
|
|
296
|
+
previous?.close(resume ? RECONNECT_CLOSE_CODE : 1000);
|
|
297
|
+
this.#connect(this.#sessionId && this.#resumeGatewayUrl
|
|
298
|
+
? this.#resumeGatewayUrl
|
|
299
|
+
: this.#gatewayUrl);
|
|
300
|
+
}
|
|
301
|
+
#resetSession() {
|
|
302
|
+
this.#sessionId = null;
|
|
303
|
+
this.#resumeGatewayUrl = null;
|
|
304
|
+
this.#sequence = null;
|
|
305
|
+
}
|
|
306
|
+
#onClose(code) {
|
|
307
|
+
this.#stopHeartbeat();
|
|
308
|
+
if (this.#destroyed)
|
|
309
|
+
return;
|
|
310
|
+
const fatal = FATAL_CLOSE_CODES[code];
|
|
311
|
+
if (fatal) {
|
|
312
|
+
this.#ws = null;
|
|
313
|
+
this.emit('fatal', new GatewayCloseError(`${fatal} (close code ${code})`, code));
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const resumable = code !== GatewayCloseCodes.InvalidSeq &&
|
|
317
|
+
code !== GatewayCloseCodes.SessionTimedOut;
|
|
318
|
+
setTimeout(() => this.#reconnect(resumable), 1000);
|
|
319
|
+
}
|
|
320
|
+
}
|