Files
discordeno/module/shard.ts
2020-05-18 15:50:19 -04:00

183 lines
5.0 KiB
TypeScript

import {
connectWebSocket,
isWebSocketCloseEvent,
WebSocket,
} from "https://deno.land/std@0.50.0/ws/mod.ts";
import {
GatewayOpcode,
DiscordBotGatewayData,
DiscordHeartbeatPayload,
ReadyPayload,
} from "../types/discord.ts";
import { logRed } from "../utils/logger.ts";
import { FetchMembersOptions } from "../types/guild.ts";
import { delay } from "https://deno.land/std@0.50.0/async/delay.ts";
let shardSocket: WebSocket;
/** The session id is needed for RESUME functionality when discord disconnects randomly. */
let sessionID = "";
// Discord requests null if no number has yet been sent by discord
let previousSequenceNumber: number | null = null;
let needToResume = false;
// TODO: If a client does not receive a heartbeat ack between its attempts at sending heartbeats, it should immediately terminate the connection with a non-1000 close code, reconnect, and attempt to resume.
async function sendConstantHeartbeats(
interval: number,
) {
await delay(interval);
shardSocket.send(
JSON.stringify({ op: GatewayOpcode.Heartbeat, d: previousSequenceNumber }),
);
sendConstantHeartbeats(interval);
}
async function resumeConnection(
botGatewayData: DiscordBotGatewayData,
identifyPayload: object,
) {
// Run it once
createShard(botGatewayData, identifyPayload, true);
// Then retry every 15 seconds
await delay(1000 * 15);
if (needToResume) resumeConnection(botGatewayData, identifyPayload);
}
const createShard = async (
botGatewayData: DiscordBotGatewayData,
identifyPayload: object,
resuming = false,
) => {
shardSocket = await connectWebSocket(botGatewayData.url);
let resumeInterval = 0;
if (!resuming) {
// Intial identify with the gateway
await shardSocket.send(
JSON.stringify({ op: GatewayOpcode.Identify, d: identifyPayload }),
);
} else {
await shardSocket.send(JSON.stringify({
op: GatewayOpcode.Resume,
d: {
...identifyPayload,
session_id: sessionID,
seq: previousSequenceNumber,
},
}));
}
for await (const message of shardSocket) {
if (typeof message === "string") {
const data = JSON.parse(message);
switch (data.op) {
case GatewayOpcode.Hello:
sendConstantHeartbeats(
(data.d as DiscordHeartbeatPayload).heartbeat_interval,
);
break;
case GatewayOpcode.Reconnect:
case GatewayOpcode.InvalidSession:
needToResume = true;
resumeConnection(botGatewayData, identifyPayload);
break;
default:
if (data.t === "RESUMED") {
needToResume = false;
break;
}
// Important for RESUME
if (data.t === "READY") {
sessionID = (data.d as ReadyPayload).session_id;
}
// Update the sequence number if it is present
if (data.s) previousSequenceNumber = data.s;
// @ts-ignore
postMessage(
{
type: "HANDLE_DISCORD_PAYLOAD",
payload: message,
resumeInterval,
},
);
break;
}
} else if (isWebSocketCloseEvent(message)) {
logRed(`Close :( ${JSON.stringify(message)}`);
// These error codes should just crash the projects
if ([4004, 4005, 4012, 4013, 4014].includes(message.code)) {
throw new Error(
"Shard.ts: Error occurred that is not resumeable or able to be reconnected.",
);
}
// These error codes can not be resumed but need to reconnect from start
if ([4003, 4007, 4008, 4009].includes(message.code)) {
createShard(botGatewayData, identifyPayload);
} else {
needToResume = true;
resumeConnection(botGatewayData, identifyPayload);
}
}
}
};
function requestGuildMembers(
guildID: string,
nonce: string,
options?: FetchMembersOptions,
) {
shardSocket.send(JSON.stringify({
op: GatewayOpcode.RequestGuildMembers,
d: {
guild_id: guildID,
query: options?.query || "",
limit: options?.query || 0,
presences: options?.presences || false,
user_ids: options?.userIDs,
nonce,
},
}));
}
// TODO: Errors need to be fixed by VSC plugin
// @ts-ignore
postMessage({ type: "REQUEST_CLIENT_OPTIONS" });
// @ts-ignore
onmessage = (message: MessageEvent) => {
if (message.data.type === "CREATE_SHARD") {
createShard(
message.data.botGatewayData,
message.data.identifyPayload,
);
}
if (message.data.type === "FETCH_MEMBERS") {
requestGuildMembers(
message.data.guildID,
message.data.nonce,
message.data.options,
);
}
if (message.data.type === "EDIT_BOTS_STATUS") {
shardSocket.send(JSON.stringify({
op: GatewayOpcode.StatusUpdate,
d: {
since: null,
game: message.data.game.name
? {
name: message.data.game.name,
type: message.data.game.type,
}
: null,
status: message.data.status,
afk: false,
},
}));
}
};