diff --git a/discord-waffle-bridge/package.json b/discord-waffle-bridge/package.json new file mode 100644 index 0000000..9daed39 --- /dev/null +++ b/discord-waffle-bridge/package.json @@ -0,0 +1,12 @@ +{ + "name": "discord-waffle-bridge", + "version": "1.0.0", + "main": "src/index.js", + "license": "MIT", + "type": "module", + "dependencies": { + "dotenv": "^16.0.3", + "node-fetch": "^3.2.10", + "ws": "^8.10.0" + } +} diff --git a/discord-waffle-bridge/src/DiscordClient.js b/discord-waffle-bridge/src/DiscordClient.js new file mode 100644 index 0000000..726306c --- /dev/null +++ b/discord-waffle-bridge/src/DiscordClient.js @@ -0,0 +1,333 @@ +import EventEmitter from "events"; +import { WebSocket } from "ws"; +import fetch from "node-fetch"; +import logger from "./logging.js"; + +const log = logger("DiscordClient"); + +const opcodes = { + EVENT: 0, + CLIENT_HEARTBEAT: 1, + IDENTIFY: 2, + UPDATE_PRESENCE: 3, + RECONNECT: 7, + INVALID_SESSION: 9, + HELLO: 10, + HEARTBEAT_ACK: 11, +}; + +const skipReconnectFor = [ + 4004, 4010, 4011, 4012, 4013, 4014 +]; + +const CLOSE_CONNECTION_ON_NO_ACK = false; + +class DiscordClient extends EventEmitter { + constructor(token, { intents, gatewayUrl="wss://gateway.discord.gg/?v=10&encoding=json", apiBase="https://discord.com/api/v10", presence = { activities: [{name: "the voices", type: 2}], status: "online", afk: false } } = {}) { + super(); + + this.token = token; + this.gatewayUrl = gatewayUrl; + this.apiBase = apiBase; + this.ws = null; + this.intents = intents; + + this.user = null; + this.guilds = []; + this.sessionId = null; + this.seq = null; + this.gotServerHeartbeatACK = true; + this.defaultPresence = presence; + this.connectEnabled = true; + + this.knownWebhooks = new Map(); + } + + _setHeartbeat(interval) { + this._heartbeatIntervalTime = interval; + if (interval < 0 && this._heartbeatInterval) { + clearInterval(this._heartbeatInterval); + return; + } + + this._heartbeatInterval = setInterval(() => { + if (CLOSE_CONNECTION_ON_NO_ACK && !this.gotServerHeartbeatACK) { + log("Closing due to no heartbeat ACK..."); + this.ws.close(1000, "No heartbeat ACK."); + return; + } + this.gotServerHeartbeatACK = false; + + this.ws.send(JSON.stringify({ + op: opcodes.CLIENT_HEARTBEAT, + d: this.seq + })); + }, this._heartbeatIntervalTime); + } + + _getIdentifyPayload() { + return { + token: this.token, + intents: this.intents, + properties: { + "$os": "linux", + "$browser": "generic", + "$device": "generic" + }, + presence: this.defaultPresence + }; + } + + _handleGatewayMessage(ws, message) { + try { + message = JSON.parse(message); + } catch(e) { + log("on 'message': failed to parse incoming message as JSON", e); + return; + } + + if (message.s) { + this.seq = message.s; + } + + const payload = message.d; + + switch (message.op) { + case opcodes.HELLO: { + log(`HELLO; heartbeat_interval=${payload.heartbeat_interval}`); + this._setHeartbeat(payload.heartbeat_interval); + + ws.send(JSON.stringify({ + op: opcodes.IDENTIFY, + d: this._getIdentifyPayload() + })); + break; + } + + case opcodes.EVENT: { + switch (message.t) { + case "READY": { + this.user = payload.user; + this.sessionId = payload.session_id; + this.guilds = payload.guilds; + log(`READY. Connected as '${this.user.username}', got ${this.guilds.length} guilds.`); + break; + } + + case "GUILD_CREATE": { + const targetGuildIndex = this.guilds.findIndex(e => e.id === payload.id); + if (targetGuildIndex < 0) { + this.guilds.push(payload); + break; + } + // The guild already exists in our array. This means that + // this GUILD_CREATE event is completing our `Unavailable Guild` + // objects that we got from the initial READY. + this.guilds[targetGuildIndex] = payload; + break; + } + + case "GUILD_UPDATE": { + const targetGuildIndex = this.guilds.findIndex(e => e.id === payload.id); + if (targetGuildIndex < 0) { + // tried to update a guild that doesn't exist??? + this.emit("warn", "got GUILD_UPDATE for a guild that doesn't exist"); + break; + } + this.guilds[targetGuildIndex] = payload; + break; + } + + case "GUILD_DELETE": { + const targetGuildIndex = this.guilds.findIndex(e => e.id === payload.id); + if (targetGuildIndex < 0) { + // tried to delete a guild that doesn't exist??? + this.emit("warn", "got GUILD_DELETE for a guild that doesn't exist"); + break; + } + this.guilds.splice(targetGuildIndex, 1); + break; + } + + case "CHANNEL_CREATE": { + const parentGuildIndex = this.guilds.findIndex(e => e.id === payload.guild_id); + if (parentGuildIndex < 0) { + this.emit("warn", "got CHANNEL_CREATE for a guild that doesn't exist"); + break; + } + this.guilds[parentGuildIndex].channels.push(payload); + break; + } + + case "CHANNEL_UPDATE": { + const parentGuildIndex = this.guilds.findIndex(e => e.id === payload.guild_id); + if (parentGuildIndex < 0) { + this.emit("warn", "got CHANNEL_CREATE for a guild that doesn't exist"); + break; + } + const relevantChannelIndex = this.guilds[parentGuildIndex].channels.findIndex(e => e.id === payload.id); + this.guilds[parentGuildIndex].channels[relevantChannelIndex] = payload; + break; + } + + case "CHANNEL_DELETE": { + const parentGuildIndex = this.guilds.findIndex(e => e.id === payload.guild_id); + if (parentGuildIndex < 0) { + this.emit("warn", "got CHANNEL_CREATE for a guild that doesn't exist"); + break; + } + const relevantChannelIndex = this.guilds[parentGuildIndex].channels.findIndex(e => e.id === payload.id); + this.guilds[parentGuildIndex].channels.splice(relevantChannelIndex, 1); + break; + } + } + + + this.emit(message.t, payload); + + break; + } + + case opcodes.HEARTBEAT_ACK: { + this.gotServerHeartbeatACK = true; + break; + } + + case opcodes.INVALID_SESSION: { + log("INVALID_SESSION - please check your authentication token"); + log("INVALID_SESSION: will not reconnect"); + break; + } + + case opcodes.RECONNECT: { + log("gateway is requesting reconnect (payload RECONNECT)"); + this.connect(); + break; + } + + default: { + log(`got unhandled opcode "${message.op}"`); + break; + } + } + } + + connect() { + if (!this.connectEnabled) { + return; + } + + log("connecting..."); + if (this.ws) { + log("a websocket connection already exists, killing..."); + this.ws.removeAllListeners(); + this._setHeartbeat(-1); + this.ws.close(); + this.ws = null; + } + + const ws = new WebSocket(this.gatewayUrl); + this.ws = ws; + + ws.on("message", (data, isBinary) => { + if (!isBinary) { + this._handleGatewayMessage(ws, data.toString()); + } + }); + + ws.on("open", () => { + log("WebSocket 'open'"); + }); + + ws.on("close", (code, reason) => { + reason = reason.toString(); + log(`on 'close': disconnected from gateway: code '${code}', reason '${reason}'`); + + this.emit("close", code, reason); + this._setHeartbeat(-1); + if (skipReconnectFor.includes(code)) { + log("on 'close': the exit code above is in skipReconnectFor, and thus the server will not reconnect."); + } else { + log("on 'close': the client will now attempt to reconnect..."); + this.connect(); + } + }); + + ws.on("error", (e) => { + log("on 'error': websocket error:", e); + log("on 'error': reconnecting due to previous websocket error..."); + this._setHeartbeat(-1); + this.connect(); + }); + } + + close() { + this.connectEnabled = false; + this._setHeartbeat(-1); + this.ws.close(); + } + + async api(method, path, body=undefined, throwOnError=true) { + const options = { + method, + headers: { + "authorization": `Bot ${this.token}` + } + }; + + if (method !== "GET" && method !== "HEAD" && typeof body === "object") { + options.headers["content-type"] = "application/json"; + options.body = JSON.stringify(body); + } + + const response = await fetch(`${this.apiBase}${path}`, options); + let json = {}; + + try { + json = await response.json(); + } catch(o_O) {} + + if (!response.ok && throwOnError) { + throw new Error(`API request returned non-success status ${response.status}, with JSON body ${JSON.stringify(json)}`); + } + + return json; + } + + async sendMessageAs(channelId, content, username, avatarUrl=null) { + if (typeof content !== "string") { + return; + } + + content = content.trim(); + + if (content.length < 1 || content.length > 2000) { + return; + } + + let webhook = this.knownWebhooks.get(channelId); + if (!webhook) { + webhook = (await this.api("GET", `/channels/${channelId}/webhooks`)).find(e => e.name === "well_known__bridge"); + + if (!webhook) { + webhook = await this.api("POST", `/channels/${channelId}/webhooks`, { + name: "well_known__bridge" + }); + } + + this.knownWebhooks.set(channelId, webhook); + } + + await this.api("POST", `/webhooks/${webhook.id}/${webhook.token}?wait=false`, { + content, + username, + avatar_url: avatarUrl, + tts: false, + allowed_mentions: { + parse: ["users"] + } + }); + } +} + +export default DiscordClient; diff --git a/discord-waffle-bridge/src/WaffleClient.js b/discord-waffle-bridge/src/WaffleClient.js new file mode 100644 index 0000000..b7ca073 --- /dev/null +++ b/discord-waffle-bridge/src/WaffleClient.js @@ -0,0 +1,241 @@ +import fetch from "node-fetch"; +import { WebSocket } from "ws"; +import { WAFFLE_API_BASE, WAFFLE_GATEWAY_BASE } from "./config.js"; +import logger from "./logging.js"; + +export const GatewayErrors = { + BAD_PAYLOAD: 4001, + BAD_AUTH: 4002, + AUTHENTICATION_TIMEOUT: 4003, + NO_PING: 4004, + FLOODING: 4005, + ALREADY_AUTHENTICATED: 4006, + PAYLOAD_TOO_LARGE: 4007, + TOO_MANY_SESSIONS: 4008, +}; + +export const GatewayPayloadType = { + Hello: 0, + Authenticate: 1, + Ready: 2, + Ping: 3, + + ChannelCreate: 110, + ChannelUpdate: 111, + ChannelDelete: 112, + + MessageCreate: 120, + MessageUpdate: 121, + MessageDelete: 122, + + TypingStart: 130, + + PresenceUpdate: 140 +} + +export const GatewayEventType = { + ...GatewayPayloadType, + + Open: -5, + Close: -4, + BadAuth: -3, +} + +export const GatewayPresenceStatus = { + Offline: 0, + Online: 1 +} + +const log = logger("WaffleClient"); + +export default class { + constructor(token=null, extraAuthParams={}) { + this.ws = null; + this.authenticated = false; + this.open = false; + this.heartbeatInterval = null; + this.user = null; + this.channels = null; + this.reconnectDelay = 400; + this.reconnectTimeout = null; + this.handlers = new Map(); + this.disableReconnect = false; + this.token = token; + this.extraAuthParams = extraAuthParams; + } + connect() { + if (!this.token) { + log("no auth token, skipping connection"); + this.dispatch(GatewayEventType.Close, GatewayErrors.BAD_AUTH); + this.dispatch(GatewayEventType.BadAuth, 0); + return false; + } + log(`connecting to gateway - gatewayBase: ${WAFFLE_GATEWAY_BASE}`); + this.ws = new WebSocket(WAFFLE_GATEWAY_BASE); + this.ws.onopen = () => { + if (this.reconnectTimeout) { + clearTimeout(this.reconnectTimeout); + } + this.disableReconnect = false; + this.open = true; + this.dispatch(GatewayEventType.Open, null); + log("open"); + }; + this.ws.onmessage = (event) => { + const payload = JSON.parse(event.data); + + switch (payload.t) { + case GatewayPayloadType.Hello: { + this.send({ + t: GatewayPayloadType.Authenticate, + d: { + token: this.token, + ...this.extraAuthParams + } + }); + + this.heartbeatInterval = setInterval(() => { + this.send({ + t: GatewayPayloadType.Ping, + d: 0 + }); + }, payload.d.pingInterval); + + log("hello"); + break; + } + case GatewayPayloadType.Ready: { + this.user = payload.d.user; + this.channels = payload.d.channels; + this.authenticated = true; + + this.reconnectDelay = 400; + + log("ready"); + break; + } + } + + this.dispatch(payload.t, payload.d); + }; + this.ws.onclose = ({ code }) => { + this.authenticated = false; + this.user = null; + this.channels = null; + this.open = false; + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval); + } + + if (code === GatewayErrors.BAD_AUTH) { + this.dispatch(GatewayEventType.BadAuth, 1); + if (this.reconnectTimeout) + clearTimeout(this.reconnectTimeout); + } else if (this.disableReconnect) { + if (this.reconnectTimeout) + clearTimeout(this.reconnectTimeout); + } else { + if (this.reconnectDelay < 60000) { + this.reconnectDelay *= 2; + } + this.reconnectTimeout = setTimeout(() => { + this.init(token); + }, this.reconnectDelay); + } + + this.dispatch(GatewayEventType.Close, code); + + log("close"); + }; + this.ws.onerror = (e) => { + log("websocket: onerror", e); + }; + + return true; + } + + send(data) { + return this.ws.send(JSON.stringify(data)); + } + + dispatch(event, payload) { + const eventHandlers = this.handlers.get(event); + if (!eventHandlers) + return; + + eventHandlers.forEach((e) => { + e(payload); + }); + } + + subscribe(event, handler) { + if (!this.handlers.get(event)) { + this.handlers.set(event, new Set()); + } + + this.handlers.get(event).add(handler); + return handler; // can later be used for unsubscribe() + } + + unsubscribe(event, handler) { + const eventHandlers = this.handlers.get(event); + if (!eventHandlers) + return; + + eventHandlers.delete(handler); + + if (eventHandlers.size < 1) { + this.handlers.delete(event); + } + } + + close() { + this.disableReconnect = true; + if (this.ws) + this.ws.close(); + } + + async api(method, path, body=undefined, throwOnError=true) { + const options = { + method, + headers: { + "authorization": `Bearer ${this.token}` + } + }; + + if (method !== "GET" && method !== "HEAD" && typeof body === "object") { + options.headers["content-type"] = "application/json"; + options.body = JSON.stringify(body); + } + + const response = await fetch(`${WAFFLE_API_BASE}${path}`, options); + let json = {}; + + try { + json = await response.json(); + } catch(o_O) {} + + if (!response.ok && throwOnError) { + throw new Error(`API request returned non-success status ${response.status}, with JSON body ${JSON.stringify(json)}`); + } + + return json; + } + + async sendMessageAs(channelId, content, username) { + if (typeof content !== "string") { + return; + } + + content = content.trim(); + + if (content.length < 1 || content.length > 2000) { + return; + } + + await this.api("POST", `/channels/${channelId}/messages`, { + content, + nick_username: username + }) + } +}; \ No newline at end of file diff --git a/discord-waffle-bridge/src/config.js b/discord-waffle-bridge/src/config.js new file mode 100644 index 0000000..b77d790 --- /dev/null +++ b/discord-waffle-bridge/src/config.js @@ -0,0 +1,8 @@ +import * as dotenv from "dotenv"; +dotenv.config(); + +export const WAFFLE_API_BASE = process.env.WAFFLE_BASE ?? "http://localhost:3000/api/v1"; +export const WAFFLE_GATEWAY_BASE = process.env.WAFFLE_GATEWAY_BASE ?? "ws://localhost:3000/gateway"; +export const WAFFLE_TOKEN = process.env.WAFFLE_TOKEN; +export const DISCORD_TOKEN = process.env.DISCORD_TOKEN; +export const DISCORD_GUILD = process.env.DISCORD_GUILD; diff --git a/discord-waffle-bridge/src/index.js b/discord-waffle-bridge/src/index.js new file mode 100644 index 0000000..4465639 --- /dev/null +++ b/discord-waffle-bridge/src/index.js @@ -0,0 +1,140 @@ +import { GatewayEventType } from "./WaffleClient.js"; +import { DISCORD_GUILD, DISCORD_TOKEN, WAFFLE_TOKEN } from "./config.js"; +import DiscordClient from "./DiscordClient.js"; +import WaffleClient from "./WaffleClient.js"; + +class Bridge { + constructor() { + this.discordClient = new DiscordClient( + DISCORD_TOKEN, + { + intents: 0 | (1 << 0) | (1 << 9) | (1 << 15), // GUILDS | GUILD_MESSAGES | MESSAGE_CONTENT + } + ); + this.waffleClient = new WaffleClient(WAFFLE_TOKEN, { + bridgesTo: "Discord Inc. (not affiliated)", + privacy: "https://discord.com/privacy", + terms: "https://discord.com/terms" + }); + this.waffleChannelIdToDiscordChannelIdMap = new Map(); + this.discordChannelIdToWaffleChannelIdMap = new Map(); + + this.waffleClient.subscribe(GatewayEventType.MessageCreate, async (message) => { + if (this.waffleClient.user && message.author_id === this.waffleClient.user.id) { + return; + } + + const channel = this.waffleChannelIdToDiscordChannelId(message.channel_id); + if (channel === -1) { + return; + } + + try { + await this.discordClient.sendMessageAs(channel, message.content, message.author_username); + } catch (o_0) { + console.error("Failed to send message to Discord", o_0); + } + }); + + this.discordClient.on("MESSAGE_CREATE", async (message) => { + if (message.guild_id !== DISCORD_GUILD || message.application_id) { + return; + } + + const channel = this.discordChannelIdToWaffleChannelId(message.channel_id); + if (channel === -1) { + return; + } + + let content = ""; + + message.attachments.forEach(e => { + content += ` `; + }); + + if (message.referenced_message) { + let trimmedContent = message.referenced_message.content.substring(0, 300); + if (trimmedContent !== message.referenced_message.content) { + trimmedContent += "..."; + } + if (content !== "") { + // Add 2 newlines after the attachments area + content += "\n\n"; + } + content += `> ${trimmedContent}\n@Discord/${message.referenced_message.author.username}: `; + } + + content += message.content; + + try { + await this.waffleClient.sendMessageAs(channel, content, message.author.username); + } catch (o_0) { + console.error("Failed to send message to Waffle", o_0); + } + }); + } + + discordChannelIdToWaffleChannelId(discordChannelId) { + if (this.discordChannelIdToWaffleChannelIdMap.get(discordChannelId)) { + return this.discordChannelIdToWaffleChannelIdMap.get(discordChannelId); + } + + const guild = this.discordClient.guilds.find(e => e.id === DISCORD_GUILD); + + if (!this.waffleClient.authenticated || !this.waffleClient.channels || !guild) { + return -1; + } + + const discordChannel = guild.channels.find(e => e.id === discordChannelId); + if (!discordChannel) { + return -1; + } + + const waffleChannel = this.waffleClient.channels.find(e => e.name === discordChannel.name); + if (!waffleChannel) { + return -1; + } + + this.discordChannelIdToWaffleChannelIdMap.set(discordChannelId, waffleChannel.id); + + return waffleChannel.id; + } + + waffleChannelIdToDiscordChannelId(waffleChannelId) { + if (this.waffleChannelIdToDiscordChannelIdMap.get(waffleChannelId)) { + return this.waffleChannelIdToDiscordChannelIdMap.get(waffleChannelId); + } + + const guild = this.discordClient.guilds.find(e => e.id === DISCORD_GUILD); + + if (!this.waffleClient.authenticated || !this.waffleClient.channels || !guild) { + return -1; + } + + const waffleChannel = this.waffleClient.channels.find(a => a.id === waffleChannelId); + if (!waffleChannel) { + return -1; + } + + const discordChannel = guild.channels.find(e => e.name === waffleChannel.name); + if (!discordChannel) { + return -1; + } + + this.waffleChannelIdToDiscordChannelIdMap.set(waffleChannelId, discordChannel.id); + + return discordChannel.id; + } + + connect() { + this.discordClient.connect(); + this.waffleClient.connect(); + } +} + +async function main() { + const bridge = new Bridge(); + bridge.connect(); +} + +main(); \ No newline at end of file diff --git a/discord-waffle-bridge/src/logging.js b/discord-waffle-bridge/src/logging.js new file mode 100644 index 0000000..2afa248 --- /dev/null +++ b/discord-waffle-bridge/src/logging.js @@ -0,0 +1,8 @@ +export default function logger(sink) { + return (...args) => { + console.log( + `[${sink}]`, + ...args + ); + }; +} diff --git a/discord-waffle-bridge/yarn.lock b/discord-waffle-bridge/yarn.lock new file mode 100644 index 0000000..3dac96d --- /dev/null +++ b/discord-waffle-bridge/yarn.lock @@ -0,0 +1,52 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +data-uri-to-buffer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz#b5db46aea50f6176428ac05b73be39a57701a64b" + integrity sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA== + +dotenv@^16.0.3: + version "16.0.3" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" + integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== + +fetch-blob@^3.1.2, fetch-blob@^3.1.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" + integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== + dependencies: + node-domexception "^1.0.0" + web-streams-polyfill "^3.0.3" + +formdata-polyfill@^4.0.10: + version "4.0.10" + resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" + integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== + dependencies: + fetch-blob "^3.1.2" + +node-domexception@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + +node-fetch@^3.2.10: + version "3.2.10" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.10.tgz#e8347f94b54ae18b57c9c049ef641cef398a85c8" + integrity sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA== + dependencies: + data-uri-to-buffer "^4.0.0" + fetch-blob "^3.1.4" + formdata-polyfill "^4.0.10" + +web-streams-polyfill@^3.0.3: + version "3.2.1" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" + integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== + +ws@^8.10.0: + version "8.10.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.10.0.tgz#00a28c09dfb76eae4eb45c3b565f771d6951aa51" + integrity sha512-+s49uSmZpvtAsd2h37vIPy1RBusaLawVe8of+GyEPsaJTCMpj/2v8NpeK1SHXjBlQ95lQTmQofOJnFiLoaN3yw== diff --git a/src/gateway/index.ts b/src/gateway/index.ts index 5c62f22..a9121ab 100644 --- a/src/gateway/index.ts +++ b/src/gateway/index.ts @@ -183,7 +183,7 @@ function sendPayload(ws: WebSocket, payload: GatewayPayload) { } function getPresenceEntryForConnection(ws: WebSocket, status: GatewayPresenceStatus): GatewayPresenceEntry | null { - if (!ws.state || !ws.state.ready || !ws.state.user) { + if (!ws.state || !ws.state.user) { return null; }