/* Spacebar: A FOSS re-implementation and extension of the Discord.com backend. Copyright (C) 2023 Spacebar and Spacebar Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import { CLOSECODES, Payload, WebSocket } from "@spacebar/gateway"; import * as erlpack from "harmony-erlpack"; import fs from "node:fs/promises"; import BigIntJson from "json-bigint"; import path from "node:path"; import WS from "ws"; import OPCodeHandlers from "../opcodes"; import { check } from "../opcodes/instanceOf"; import { PayloadSchema } from "@spacebar/schemas"; const bigIntJson = BigIntJson({ storeAsString: true }); export async function Message(this: WebSocket, buffer: WS.Data) { // TODO: compression let data: Payload; if ( (Buffer.isBuffer(buffer) && buffer[0] === 123) || // ASCII 123 = `{`. Bad check for JSON typeof buffer === "string" ) { data = bigIntJson.parse(buffer.toString()); } else if (this.encoding === "json" && Buffer.isBuffer(buffer)) { if (this.compress === "zlib-stream") { try { buffer = this.inflate!.process(buffer); } catch { buffer = buffer.toString(); } } else if (this.compress === "zstd-stream") { try { buffer = await this.zstdDecoder!.decode(buffer); } catch { buffer = buffer.toString(); } } data = bigIntJson.parse(buffer as string); } else if (this.encoding === "etf" && Buffer.isBuffer(buffer) && erlpack) { try { // cast is ~safe: unpack returns the parsed data in the shape it was provided, @yukikaze-bot/erlpack got around this by returning `any` instead of an actual type union. data = erlpack.unpack(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)) as unknown as Payload; } catch { console.error(`[Gateway/${this.user_id ?? this.ipAddress}] Failed to decode ETF payload`); return this.close(CLOSECODES.Decode_error); } } else { console.error(`[Gateway/${this.user_id ?? this.ipAddress}] Unknown payload format`); return this.close(CLOSECODES.Decode_error); } if (process.env.WS_VERBOSE) console.log(`[Websocket] Incomming message: ${JSON.stringify(data)}`); if (process.env.WS_DUMP) { const id = this.session_id || "unknown"; await fs.mkdir(path.join("dump", id), { recursive: true }); await fs.writeFile(path.join("dump", id, `${Date.now()}.in.json`), JSON.stringify(data, null, 2)); if (!this.session_id) console.log(`[Gateway/${this.user_id ?? this.ipAddress}] Unknown session id, dumping to unknown folder`); } check.call(this, PayloadSchema, data); const OPCodeHandler = OPCodeHandlers[data.op]; if (!OPCodeHandler) { console.error(`[Gateway/${this.user_id ?? this.ipAddress}] Unknown opcode`, data.op); // TODO: if all opcodes are implemented comment this out: // this.close(CLOSECODES.Unknown_opcode); return; } try { return await OPCodeHandler.call(this, data); } catch (error) { console.error(`[Gateway/${this.user_id ?? this.ipAddress}] Error: Op ${data.op}`, error); // if (!this.CLOSED && this.CLOSING) return this.close(CLOSECODES.Unknown_error); } }