flarecord 0.0.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/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/core/bot-base.d.ts +26 -0
- package/dist/core/bot-base.d.ts.map +1 -0
- package/dist/core/bot-base.js +60 -0
- package/dist/core/bot-base.js.map +1 -0
- package/dist/core/client.d.ts +26 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/client.js +60 -0
- package/dist/core/client.js.map +1 -0
- package/dist/core/connection.d.ts +52 -0
- package/dist/core/connection.d.ts.map +1 -0
- package/dist/core/connection.js +156 -0
- package/dist/core/connection.js.map +1 -0
- package/dist/core/gateway.d.ts +15 -0
- package/dist/core/gateway.d.ts.map +1 -0
- package/dist/core/gateway.js +50 -0
- package/dist/core/gateway.js.map +1 -0
- package/dist/core/handlers.d.ts +25 -0
- package/dist/core/handlers.d.ts.map +1 -0
- package/dist/core/handlers.js +120 -0
- package/dist/core/handlers.js.map +1 -0
- package/dist/core/heartbeat.d.ts +19 -0
- package/dist/core/heartbeat.d.ts.map +1 -0
- package/dist/core/heartbeat.js +55 -0
- package/dist/core/heartbeat.js.map +1 -0
- package/dist/core/message-handler.d.ts +23 -0
- package/dist/core/message-handler.d.ts.map +1 -0
- package/dist/core/message-handler.js +92 -0
- package/dist/core/message-handler.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/types/constants.d.ts +90 -0
- package/dist/types/constants.d.ts.map +1 -0
- package/dist/types/constants.js +99 -0
- package/dist/types/constants.js.map +1 -0
- package/dist/types/index.d.ts +54 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/logger.d.ts +23 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +44 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/message-helper.d.ts +11 -0
- package/dist/utils/message-helper.d.ts.map +1 -0
- package/dist/utils/message-helper.js +61 -0
- package/dist/utils/message-helper.js.map +1 -0
- package/dist/utils/reconnect.d.ts +19 -0
- package/dist/utils/reconnect.d.ts.map +1 -0
- package/dist/utils/reconnect.js +69 -0
- package/dist/utils/reconnect.js.map +1 -0
- package/dist/utils/session.d.ts +23 -0
- package/dist/utils/session.d.ts.map +1 -0
- package/dist/utils/session.js +58 -0
- package/dist/utils/session.js.map +1 -0
- package/dist/utils/websocket.d.ts +12 -0
- package/dist/utils/websocket.d.ts.map +1 -0
- package/dist/utils/websocket.js +64 -0
- package/dist/utils/websocket.js.map +1 -0
- package/package.json +48 -0
- package/src/core/bot-base.ts +90 -0
- package/src/core/client.ts +90 -0
- package/src/core/connection.ts +238 -0
- package/src/core/gateway.ts +78 -0
- package/src/core/handlers.ts +136 -0
- package/src/core/heartbeat.ts +65 -0
- package/src/core/message-handler.ts +96 -0
- package/src/index.ts +96 -0
- package/src/types/constants.ts +103 -0
- package/src/types/index.ts +65 -0
- package/src/utils/logger.ts +56 -0
- package/src/utils/message-helper.ts +100 -0
- package/src/utils/reconnect.ts +108 -0
- package/src/utils/session.ts +64 -0
- package/src/utils/websocket.ts +90 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { GatewayOpcode } from "../types/constants";
|
|
2
|
+
import { GatewayPayload, GatewayState } from "../types";
|
|
3
|
+
import type { DurableObjectState } from "@cloudflare/workers-types";
|
|
4
|
+
|
|
5
|
+
export class HeartbeatManager {
|
|
6
|
+
private heartbeatInterval: number | null = null;
|
|
7
|
+
private sequence: { value: number | null };
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
private ctx: DurableObjectState,
|
|
11
|
+
private storageKey: string,
|
|
12
|
+
sequence: { value: number | null },
|
|
13
|
+
private sendFn: (payload: GatewayPayload) => void
|
|
14
|
+
) {
|
|
15
|
+
this.sequence = sequence;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
setInterval(interval: number): void {
|
|
19
|
+
this.heartbeatInterval = interval;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async send(): Promise<void> {
|
|
23
|
+
if (!this.heartbeatInterval) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const heartbeat: GatewayPayload = {
|
|
28
|
+
op: GatewayOpcode.HEARTBEAT,
|
|
29
|
+
d: this.sequence.value,
|
|
30
|
+
};
|
|
31
|
+
this.sendFn(heartbeat);
|
|
32
|
+
|
|
33
|
+
const nextHeartbeat = Date.now() + this.heartbeatInterval;
|
|
34
|
+
await this.ctx.storage.setAlarm(nextHeartbeat);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async cancel(): Promise<void> {
|
|
38
|
+
await this.ctx.storage.deleteAlarm().catch(() => {});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async updateAck(): Promise<void> {
|
|
42
|
+
const state = await this.getState();
|
|
43
|
+
if (state) {
|
|
44
|
+
await this.ctx.storage.put(this.storageKey, {
|
|
45
|
+
...state,
|
|
46
|
+
lastHeartbeatAck: Date.now(),
|
|
47
|
+
} satisfies GatewayState);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private async getState(): Promise<GatewayState | null> {
|
|
52
|
+
const state = await this.ctx.storage.get(this.storageKey);
|
|
53
|
+
return this.validateState(state);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private validateState(state: unknown): GatewayState | null {
|
|
57
|
+
if (state === null || state === undefined) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
if (typeof state === "object" && "heartbeatInterval" in state) {
|
|
61
|
+
return state as GatewayState;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GatewayOpcode,
|
|
3
|
+
RESUMEABLE_CLOSE_CODES,
|
|
4
|
+
WebSocketCloseCode,
|
|
5
|
+
} from "../types/constants";
|
|
6
|
+
import { GatewayPayload, HelloData, GatewayState } from "../types";
|
|
7
|
+
import { GatewayHandlers } from "./handlers";
|
|
8
|
+
import { SessionManager } from "../utils/session";
|
|
9
|
+
import { HeartbeatManager } from "./heartbeat";
|
|
10
|
+
import type { DurableObjectState } from "@cloudflare/workers-types";
|
|
11
|
+
import type { Logger } from "../utils/logger";
|
|
12
|
+
|
|
13
|
+
export class MessageHandler {
|
|
14
|
+
constructor(
|
|
15
|
+
private handlers: GatewayHandlers,
|
|
16
|
+
private sessionManager: SessionManager,
|
|
17
|
+
private heartbeatManager: HeartbeatManager,
|
|
18
|
+
private reconnectFn: (shouldResume: boolean) => Promise<void>,
|
|
19
|
+
private ctx: DurableObjectState,
|
|
20
|
+
private storageKey: string,
|
|
21
|
+
private intents: number,
|
|
22
|
+
private logger: Logger
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
async handle(payload: GatewayPayload): Promise<void> {
|
|
26
|
+
const opcode = payload.op;
|
|
27
|
+
|
|
28
|
+
if (opcode === GatewayOpcode.HELLO) {
|
|
29
|
+
await this.handleHello(payload.d as HelloData);
|
|
30
|
+
} else if (opcode === GatewayOpcode.HEARTBEAT_ACK) {
|
|
31
|
+
await this.heartbeatManager.updateAck();
|
|
32
|
+
} else if (opcode === GatewayOpcode.DISPATCH) {
|
|
33
|
+
this.handlers.handleDispatch(payload);
|
|
34
|
+
} else if (opcode === GatewayOpcode.RECONNECT) {
|
|
35
|
+
this.logger.warn("Received RECONNECT opcode");
|
|
36
|
+
await this.reconnectFn(true);
|
|
37
|
+
} else if (opcode === GatewayOpcode.INVALID_SESSION) {
|
|
38
|
+
await this.handleInvalidSession(payload.d as boolean);
|
|
39
|
+
} else {
|
|
40
|
+
this.logger.warn("Unknown opcode received", { opcode });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private async handleHello(data: HelloData): Promise<void> {
|
|
45
|
+
this.logger.debug("Handling HELLO", { heartbeatInterval: data.heartbeat_interval });
|
|
46
|
+
this.heartbeatManager.setInterval(data.heartbeat_interval);
|
|
47
|
+
await this.handlers.handleHello(data);
|
|
48
|
+
|
|
49
|
+
const resumeData = await this.sessionManager.getResumeData();
|
|
50
|
+
const state = await this.getState();
|
|
51
|
+
|
|
52
|
+
if (resumeData && state?.shouldResume) {
|
|
53
|
+
this.logger.debug("Resuming session", { sessionId: resumeData.sessionId });
|
|
54
|
+
this.sessionManager.sendResume(resumeData.sessionId, resumeData.sequence);
|
|
55
|
+
} else {
|
|
56
|
+
this.logger.debug("Identifying with new session");
|
|
57
|
+
this.sessionManager.sendIdentify(this.intents);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await this.heartbeatManager.send();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private async handleInvalidSession(resumable: boolean): Promise<void> {
|
|
64
|
+
this.logger.warn("Invalid session", { resumable });
|
|
65
|
+
if (resumable) {
|
|
66
|
+
await this.reconnectFn(true);
|
|
67
|
+
} else {
|
|
68
|
+
const state = await this.getState();
|
|
69
|
+
if (state) {
|
|
70
|
+
await this.ctx.storage.put(this.storageKey, {
|
|
71
|
+
...state,
|
|
72
|
+
shouldResume: false,
|
|
73
|
+
sessionId: null,
|
|
74
|
+
} satisfies GatewayState);
|
|
75
|
+
}
|
|
76
|
+
await this.reconnectFn(false);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
shouldResume(code: number): boolean {
|
|
81
|
+
const isResumable = RESUMEABLE_CLOSE_CODES.includes(code as WebSocketCloseCode);
|
|
82
|
+
this.logger.debug("Checking if should resume", { code, isResumable });
|
|
83
|
+
return isResumable;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private async getState(): Promise<GatewayState | null> {
|
|
87
|
+
const state = await this.ctx.storage.get(this.storageKey);
|
|
88
|
+
if (state === null || state === undefined) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
if (typeof state === "object" && "heartbeatInterval" in state) {
|
|
92
|
+
return state as GatewayState;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export { DiscordGateway } from "./core/gateway";
|
|
2
|
+
export { DiscordClient, type DiscordClientOptions } from "./core/client";
|
|
3
|
+
export { MessageHelper } from "./utils/message-helper";
|
|
4
|
+
export { GatewayConnection } from "./core/connection";
|
|
5
|
+
export { GatewayHandlers } from "./core/handlers";
|
|
6
|
+
export { WebSocketManager } from "./utils/websocket";
|
|
7
|
+
export { ReconnectManager } from "./utils/reconnect";
|
|
8
|
+
export { SessionManager } from "./utils/session";
|
|
9
|
+
export { Logger, LogLevel, createLogger } from "./utils/logger";
|
|
10
|
+
|
|
11
|
+
export type {
|
|
12
|
+
GatewayPayload,
|
|
13
|
+
HelloData,
|
|
14
|
+
IdentifyData,
|
|
15
|
+
ResumeData,
|
|
16
|
+
ReadyData,
|
|
17
|
+
GatewayState,
|
|
18
|
+
GatewayConfig,
|
|
19
|
+
MessageData,
|
|
20
|
+
} from "./types";
|
|
21
|
+
export {
|
|
22
|
+
GatewayOpcode,
|
|
23
|
+
GatewayEvent,
|
|
24
|
+
GatewayIntents,
|
|
25
|
+
WebSocketCloseCode,
|
|
26
|
+
RESUMEABLE_CLOSE_CODES,
|
|
27
|
+
GATEWAY_BASE_URL,
|
|
28
|
+
GATEWAY_VERSION,
|
|
29
|
+
GATEWAY_ENCODING,
|
|
30
|
+
DEFAULT_RECONNECT_DELAYS,
|
|
31
|
+
WEBSOCKET_READY_STATE_OPEN,
|
|
32
|
+
DEFAULT_STORAGE_KEY,
|
|
33
|
+
DEFAULT_MAX_RECONNECT_ATTEMPTS,
|
|
34
|
+
} from "./types/constants";
|
|
35
|
+
export { DEFAULT_IDENTIFY_PROPERTIES } from "./types";
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
EmbedBuilder,
|
|
39
|
+
ActionRowBuilder,
|
|
40
|
+
ButtonBuilder,
|
|
41
|
+
StringSelectMenuBuilder,
|
|
42
|
+
UserSelectMenuBuilder,
|
|
43
|
+
RoleSelectMenuBuilder,
|
|
44
|
+
ChannelSelectMenuBuilder,
|
|
45
|
+
MentionableSelectMenuBuilder,
|
|
46
|
+
ModalBuilder,
|
|
47
|
+
TextInputBuilder,
|
|
48
|
+
AttachmentBuilder,
|
|
49
|
+
SlashCommandBuilder,
|
|
50
|
+
ContextMenuCommandBuilder,
|
|
51
|
+
SlashCommandSubcommandBuilder,
|
|
52
|
+
SlashCommandSubcommandGroupBuilder,
|
|
53
|
+
SlashCommandOptionsOnlyBuilder,
|
|
54
|
+
SlashCommandBooleanOption,
|
|
55
|
+
SlashCommandChannelOption,
|
|
56
|
+
SlashCommandIntegerOption,
|
|
57
|
+
SlashCommandMentionableOption,
|
|
58
|
+
SlashCommandNumberOption,
|
|
59
|
+
SlashCommandRoleOption,
|
|
60
|
+
SlashCommandStringOption,
|
|
61
|
+
SlashCommandUserOption,
|
|
62
|
+
REST,
|
|
63
|
+
Routes,
|
|
64
|
+
Colors,
|
|
65
|
+
PermissionFlagsBits,
|
|
66
|
+
GatewayIntentBits,
|
|
67
|
+
Events,
|
|
68
|
+
ActivityType,
|
|
69
|
+
ChannelType,
|
|
70
|
+
MessageType,
|
|
71
|
+
InteractionType,
|
|
72
|
+
ApplicationCommandType,
|
|
73
|
+
ButtonStyle,
|
|
74
|
+
TextInputStyle,
|
|
75
|
+
ComponentType,
|
|
76
|
+
Locale,
|
|
77
|
+
type APIEmbed,
|
|
78
|
+
type APIEmbedField,
|
|
79
|
+
type APIButtonComponent,
|
|
80
|
+
type APIActionRowComponent,
|
|
81
|
+
type APIMessageComponent,
|
|
82
|
+
type RESTPostAPIChannelMessageJSONBody,
|
|
83
|
+
type RESTPostAPIChannelMessageResult,
|
|
84
|
+
type APIInteractionResponse,
|
|
85
|
+
type APIMessage,
|
|
86
|
+
type APIUser,
|
|
87
|
+
type APIGuild,
|
|
88
|
+
type APIChannel,
|
|
89
|
+
type APIInteraction,
|
|
90
|
+
type APIChatInputApplicationCommandInteraction,
|
|
91
|
+
type APIMessageApplicationCommandInteraction,
|
|
92
|
+
type APIUserApplicationCommandInteraction,
|
|
93
|
+
type APIMessageComponentInteraction,
|
|
94
|
+
type APIModalSubmitInteraction,
|
|
95
|
+
} from "discord.js";
|
|
96
|
+
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export enum GatewayOpcode {
|
|
2
|
+
DISPATCH = 0,
|
|
3
|
+
HEARTBEAT = 1,
|
|
4
|
+
IDENTIFY = 2,
|
|
5
|
+
PRESENCE_UPDATE = 3,
|
|
6
|
+
VOICE_STATE_UPDATE = 4,
|
|
7
|
+
RESUME = 6,
|
|
8
|
+
RECONNECT = 7,
|
|
9
|
+
REQUEST_GUILD_MEMBERS = 8,
|
|
10
|
+
INVALID_SESSION = 9,
|
|
11
|
+
HELLO = 10,
|
|
12
|
+
HEARTBEAT_ACK = 11,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export enum GatewayEvent {
|
|
16
|
+
READY = "READY",
|
|
17
|
+
RESUMED = "RESUMED",
|
|
18
|
+
MESSAGE_CREATE = "MESSAGE_CREATE",
|
|
19
|
+
MESSAGE_UPDATE = "MESSAGE_UPDATE",
|
|
20
|
+
MESSAGE_DELETE = "MESSAGE_DELETE",
|
|
21
|
+
INTERACTION_CREATE = "INTERACTION_CREATE",
|
|
22
|
+
GUILD_CREATE = "GUILD_CREATE",
|
|
23
|
+
GUILD_UPDATE = "GUILD_UPDATE",
|
|
24
|
+
GUILD_DELETE = "GUILD_DELETE",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Discord Gateway Intents
|
|
29
|
+
* @see https://discord.com/developers/docs/topics/gateway#gateway-intents
|
|
30
|
+
*/
|
|
31
|
+
export enum GatewayIntents {
|
|
32
|
+
GUILDS = 1 << 0,
|
|
33
|
+
GUILD_MEMBERS = 1 << 1,
|
|
34
|
+
GUILD_MODERATION = 1 << 2,
|
|
35
|
+
GUILD_EMOJIS_AND_STICKERS = 1 << 3,
|
|
36
|
+
GUILD_INTEGRATIONS = 1 << 4,
|
|
37
|
+
GUILD_WEBHOOKS = 1 << 5,
|
|
38
|
+
GUILD_INVITES = 1 << 6,
|
|
39
|
+
GUILD_VOICE_STATES = 1 << 7,
|
|
40
|
+
GUILD_PRESENCES = 1 << 8,
|
|
41
|
+
GUILD_MESSAGES = 1 << 9,
|
|
42
|
+
GUILD_MESSAGE_REACTIONS = 1 << 10,
|
|
43
|
+
GUILD_MESSAGE_TYPING = 1 << 11,
|
|
44
|
+
DIRECT_MESSAGES = 1 << 12,
|
|
45
|
+
DIRECT_MESSAGE_REACTIONS = 1 << 13,
|
|
46
|
+
DIRECT_MESSAGE_TYPING = 1 << 14,
|
|
47
|
+
MESSAGE_CONTENT = 1 << 15,
|
|
48
|
+
GUILD_SCHEDULED_EVENTS = 1 << 16,
|
|
49
|
+
AUTO_MODERATION_CONFIGURATION = 1 << 20,
|
|
50
|
+
AUTO_MODERATION_EXECUTION = 1 << 21,
|
|
51
|
+
GUILD_MESSAGE_POLLS = 1 << 24,
|
|
52
|
+
DIRECT_MESSAGE_POLLS = 1 << 25,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export enum WebSocketCloseCode {
|
|
56
|
+
NORMAL_CLOSURE = 1000,
|
|
57
|
+
GOING_AWAY = 1001,
|
|
58
|
+
PROTOCOL_ERROR = 1002,
|
|
59
|
+
UNSUPPORTED_DATA = 1003,
|
|
60
|
+
NO_STATUS_RECEIVED = 1005,
|
|
61
|
+
ABNORMAL_CLOSURE = 1006,
|
|
62
|
+
INVALID_FRAME_PAYLOAD = 1007,
|
|
63
|
+
POLICY_VIOLATION = 1008,
|
|
64
|
+
MESSAGE_TOO_BIG = 1009,
|
|
65
|
+
MISSING_EXTENSION = 1010,
|
|
66
|
+
INTERNAL_ERROR = 1011,
|
|
67
|
+
SERVICE_RESTART = 1012,
|
|
68
|
+
TRY_AGAIN_LATER = 1013,
|
|
69
|
+
BAD_GATEWAY = 1014,
|
|
70
|
+
TLS_HANDSHAKE = 1015,
|
|
71
|
+
UNAUTHENTICATED = 4000,
|
|
72
|
+
INVALID_API_VERSION = 4001,
|
|
73
|
+
INVALID_INTENTS = 4002,
|
|
74
|
+
DISALLOWED_INTENTS = 4003,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const RESUMEABLE_CLOSE_CODES = [
|
|
78
|
+
WebSocketCloseCode.NORMAL_CLOSURE,
|
|
79
|
+
WebSocketCloseCode.GOING_AWAY,
|
|
80
|
+
WebSocketCloseCode.ABNORMAL_CLOSURE,
|
|
81
|
+
WebSocketCloseCode.NO_STATUS_RECEIVED,
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
export const GATEWAY_BASE_URL = "https://discord.com/api/v10/gateway/bot";
|
|
85
|
+
export const GATEWAY_VERSION = 10;
|
|
86
|
+
export const GATEWAY_ENCODING = "json";
|
|
87
|
+
|
|
88
|
+
export const WEBSOCKET_READY_STATE_OPEN = 1;
|
|
89
|
+
export const DEFAULT_STORAGE_KEY = "gatewayState";
|
|
90
|
+
export const DEFAULT_MAX_RECONNECT_ATTEMPTS = 5;
|
|
91
|
+
|
|
92
|
+
export const DEFAULT_RECONNECT_DELAYS = {
|
|
93
|
+
initial: 1000,
|
|
94
|
+
max: 60000,
|
|
95
|
+
backoffMultiplier: 2,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const DEFAULT_IDENTIFY_PROPERTIES = {
|
|
99
|
+
os: "cloudflare-workers",
|
|
100
|
+
browser: "discord-gateway",
|
|
101
|
+
device: "discord-gateway",
|
|
102
|
+
};
|
|
103
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
GatewayHelloData,
|
|
3
|
+
GatewayIdentifyData,
|
|
4
|
+
GatewayResumeData,
|
|
5
|
+
GatewayReadyDispatchData,
|
|
6
|
+
} from "discord-api-types/v10";
|
|
7
|
+
import type { Logger } from "../utils/logger";
|
|
8
|
+
|
|
9
|
+
export interface GatewayPayload {
|
|
10
|
+
op: number;
|
|
11
|
+
d?: unknown;
|
|
12
|
+
s?: number | null;
|
|
13
|
+
t?: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type HelloData = GatewayHelloData;
|
|
17
|
+
export type IdentifyData = GatewayIdentifyData;
|
|
18
|
+
export type ResumeData = GatewayResumeData;
|
|
19
|
+
export type ReadyData = GatewayReadyDispatchData;
|
|
20
|
+
|
|
21
|
+
export interface MessageData extends Record<string, unknown> {
|
|
22
|
+
_gatewayMetadata?: {
|
|
23
|
+
opcode: number;
|
|
24
|
+
opcodeName: string;
|
|
25
|
+
event: string;
|
|
26
|
+
sequence: number | null;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface GatewayState {
|
|
31
|
+
heartbeatInterval: number;
|
|
32
|
+
sequence: number | null;
|
|
33
|
+
sessionId: string | null;
|
|
34
|
+
lastHeartbeat?: number;
|
|
35
|
+
lastHeartbeatAck?: number;
|
|
36
|
+
reconnectAttempts: number;
|
|
37
|
+
shouldResume: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface GatewayConfig {
|
|
41
|
+
token: string;
|
|
42
|
+
intents: number;
|
|
43
|
+
storageKey?: string;
|
|
44
|
+
identifyProperties?: {
|
|
45
|
+
os: string;
|
|
46
|
+
browser: string;
|
|
47
|
+
device: string;
|
|
48
|
+
};
|
|
49
|
+
maxReconnectAttempts?: number;
|
|
50
|
+
reconnectDelays?: {
|
|
51
|
+
initial: number;
|
|
52
|
+
max: number;
|
|
53
|
+
backoffMultiplier: number;
|
|
54
|
+
};
|
|
55
|
+
onDispatch?: (event: string, data: unknown) => void;
|
|
56
|
+
logger?: Logger;
|
|
57
|
+
debug?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const DEFAULT_IDENTIFY_PROPERTIES = {
|
|
61
|
+
os: "cloudflare-workers",
|
|
62
|
+
browser: "discord-gateway",
|
|
63
|
+
device: "discord-gateway",
|
|
64
|
+
} as const;
|
|
65
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export enum LogLevel {
|
|
2
|
+
DEBUG = 0,
|
|
3
|
+
INFO = 1,
|
|
4
|
+
WARN = 2,
|
|
5
|
+
ERROR = 3,
|
|
6
|
+
NONE = 4,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface LoggerConfig {
|
|
10
|
+
level: LogLevel;
|
|
11
|
+
prefix?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class Logger {
|
|
15
|
+
private level: LogLevel;
|
|
16
|
+
private prefix: string;
|
|
17
|
+
|
|
18
|
+
constructor(config: LoggerConfig = { level: LogLevel.INFO }) {
|
|
19
|
+
this.level = config.level;
|
|
20
|
+
this.prefix = config.prefix || "[flarecord]";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
debug(message: string, ...args: unknown[]): void {
|
|
24
|
+
if (this.level <= LogLevel.DEBUG) {
|
|
25
|
+
console.log(`${this.prefix} [DEBUG] ${message}`, ...args);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
info(message: string, ...args: unknown[]): void {
|
|
30
|
+
if (this.level <= LogLevel.INFO) {
|
|
31
|
+
console.log(`${this.prefix} [INFO] ${message}`, ...args);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
warn(message: string, ...args: unknown[]): void {
|
|
36
|
+
if (this.level <= LogLevel.WARN) {
|
|
37
|
+
console.warn(`${this.prefix} [WARN] ${message}`, ...args);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
error(message: string, error?: Error | unknown, ...args: unknown[]): void {
|
|
42
|
+
if (this.level <= LogLevel.ERROR) {
|
|
43
|
+
const errorDetails = error instanceof Error ? error.stack : error;
|
|
44
|
+
console.error(`${this.prefix} [ERROR] ${message}`, errorDetails, ...args);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
setLevel(level: LogLevel): void {
|
|
49
|
+
this.level = level;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const createLogger = (config?: LoggerConfig): Logger => {
|
|
54
|
+
return new Logger(config);
|
|
55
|
+
};
|
|
56
|
+
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import {
|
|
2
|
+
REST,
|
|
3
|
+
Routes,
|
|
4
|
+
EmbedBuilder,
|
|
5
|
+
ActionRowBuilder,
|
|
6
|
+
type RESTPostAPIChannelMessageJSONBody,
|
|
7
|
+
type APIEmbed,
|
|
8
|
+
} from "discord.js";
|
|
9
|
+
|
|
10
|
+
type MessageContent =
|
|
11
|
+
| string
|
|
12
|
+
| EmbedBuilder
|
|
13
|
+
| ActionRowBuilder<never>
|
|
14
|
+
| RESTPostAPIChannelMessageJSONBody
|
|
15
|
+
| (EmbedBuilder | ActionRowBuilder<never>)[];
|
|
16
|
+
|
|
17
|
+
interface NormalizedMessageBody {
|
|
18
|
+
content?: string;
|
|
19
|
+
embeds?: APIEmbed[];
|
|
20
|
+
components?: unknown[];
|
|
21
|
+
message_reference?: {
|
|
22
|
+
message_id: string;
|
|
23
|
+
channel_id: string;
|
|
24
|
+
guild_id?: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class MessageHelper {
|
|
29
|
+
private rest: REST;
|
|
30
|
+
|
|
31
|
+
constructor(token: string) {
|
|
32
|
+
this.rest = new REST().setToken(token);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async send(channelId: string, content: MessageContent): Promise<unknown> {
|
|
36
|
+
const body = this.normalizeContent(content);
|
|
37
|
+
return this.rest.post(Routes.channelMessages(channelId), { body });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async reply(
|
|
41
|
+
channelId: string,
|
|
42
|
+
messageId: string,
|
|
43
|
+
guildId: string | undefined,
|
|
44
|
+
content: MessageContent
|
|
45
|
+
): Promise<unknown> {
|
|
46
|
+
const body = this.normalizeContent(content);
|
|
47
|
+
const replyBody: RESTPostAPIChannelMessageJSONBody = {
|
|
48
|
+
...body,
|
|
49
|
+
message_reference: {
|
|
50
|
+
message_id: messageId,
|
|
51
|
+
channel_id: channelId,
|
|
52
|
+
...(guildId && { guild_id: guildId }),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
return this.rest.post(Routes.channelMessages(channelId), {
|
|
56
|
+
body: replyBody,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private normalizeContent(content: MessageContent): RESTPostAPIChannelMessageJSONBody {
|
|
61
|
+
if (typeof content === "string") {
|
|
62
|
+
return { content };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (content instanceof EmbedBuilder) {
|
|
66
|
+
return { embeds: [content.toJSON()] };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (content instanceof ActionRowBuilder) {
|
|
70
|
+
const json = content.toJSON();
|
|
71
|
+
return {
|
|
72
|
+
components: [json],
|
|
73
|
+
} as RESTPostAPIChannelMessageJSONBody;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (Array.isArray(content)) {
|
|
77
|
+
const embeds: APIEmbed[] = [];
|
|
78
|
+
const components: unknown[] = [];
|
|
79
|
+
|
|
80
|
+
for (const item of content) {
|
|
81
|
+
if (item instanceof EmbedBuilder) {
|
|
82
|
+
embeds.push(item.toJSON());
|
|
83
|
+
} else if (item instanceof ActionRowBuilder) {
|
|
84
|
+
components.push(item.toJSON());
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const result: NormalizedMessageBody = {};
|
|
89
|
+
if (embeds.length > 0) {
|
|
90
|
+
result.embeds = embeds;
|
|
91
|
+
}
|
|
92
|
+
if (components.length > 0) {
|
|
93
|
+
result.components = components;
|
|
94
|
+
}
|
|
95
|
+
return result as RESTPostAPIChannelMessageJSONBody;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return content;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { DEFAULT_RECONNECT_DELAYS } from "../types/constants";
|
|
2
|
+
import { GatewayState } from "../types";
|
|
3
|
+
import type { DurableObjectState } from "@cloudflare/workers-types";
|
|
4
|
+
|
|
5
|
+
export class ReconnectManager {
|
|
6
|
+
private reconnectAttempts = 0;
|
|
7
|
+
private maxReconnectAttempts: number;
|
|
8
|
+
private isReconnecting = false;
|
|
9
|
+
private reconnectDelays: {
|
|
10
|
+
initial: number;
|
|
11
|
+
max: number;
|
|
12
|
+
backoffMultiplier: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
constructor(
|
|
16
|
+
maxReconnectAttempts: number = 5,
|
|
17
|
+
reconnectDelays = DEFAULT_RECONNECT_DELAYS
|
|
18
|
+
) {
|
|
19
|
+
this.maxReconnectAttempts = maxReconnectAttempts;
|
|
20
|
+
this.reconnectDelays = reconnectDelays;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async scheduleReconnect(connectFn: () => Promise<void>): Promise<void> {
|
|
24
|
+
if (
|
|
25
|
+
this.isReconnecting ||
|
|
26
|
+
this.reconnectAttempts >= this.maxReconnectAttempts
|
|
27
|
+
) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
this.isReconnecting = true;
|
|
32
|
+
const delay = this.calculateDelay();
|
|
33
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
34
|
+
|
|
35
|
+
this.reconnectAttempts++;
|
|
36
|
+
this.isReconnecting = false;
|
|
37
|
+
await connectFn();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async reconnect(
|
|
41
|
+
shouldResume: boolean,
|
|
42
|
+
ctx: DurableObjectState,
|
|
43
|
+
storageKey: string,
|
|
44
|
+
connectFn: () => Promise<void>,
|
|
45
|
+
closeFn: () => void
|
|
46
|
+
): Promise<void> {
|
|
47
|
+
if (this.isReconnecting) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
51
|
+
throw new Error("Max reconnect attempts reached");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.isReconnecting = true;
|
|
55
|
+
await ctx.storage.deleteAlarm().catch(() => {});
|
|
56
|
+
closeFn();
|
|
57
|
+
|
|
58
|
+
const state = await this.getState(ctx, storageKey);
|
|
59
|
+
if (state) {
|
|
60
|
+
await ctx.storage.put(storageKey, {
|
|
61
|
+
...state,
|
|
62
|
+
shouldResume: shouldResume && state.sessionId !== null,
|
|
63
|
+
reconnectAttempts: this.reconnectAttempts + 1,
|
|
64
|
+
} satisfies GatewayState);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const delay = this.calculateDelay();
|
|
68
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
69
|
+
|
|
70
|
+
this.reconnectAttempts++;
|
|
71
|
+
this.isReconnecting = false;
|
|
72
|
+
await connectFn();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
reset(): void {
|
|
76
|
+
this.reconnectAttempts = 0;
|
|
77
|
+
this.isReconnecting = false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private calculateDelay(): number {
|
|
81
|
+
return Math.min(
|
|
82
|
+
this.reconnectDelays.initial *
|
|
83
|
+
Math.pow(
|
|
84
|
+
this.reconnectDelays.backoffMultiplier,
|
|
85
|
+
this.reconnectAttempts
|
|
86
|
+
),
|
|
87
|
+
this.reconnectDelays.max
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private async getState(
|
|
92
|
+
ctx: DurableObjectState,
|
|
93
|
+
storageKey: string
|
|
94
|
+
): Promise<GatewayState | null> {
|
|
95
|
+
const state = await ctx.storage.get(storageKey);
|
|
96
|
+
return this.validateState(state);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private validateState(state: unknown): GatewayState | null {
|
|
100
|
+
if (state === null || state === undefined) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
if (typeof state === "object" && "heartbeatInterval" in state) {
|
|
104
|
+
return state as GatewayState;
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|