diff --git a/.eslintrc.js b/.eslintrc.js index 677ccea..6f8e58a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,5 +1,6 @@ module.exports = { "env": { + "browser": true, "commonjs": true, "es2021": true, "node": true diff --git a/brainlet/api/v2/gateway/index.js b/brainlet/api/v2/gateway/index.js index 1db7270..878ae4a 100644 --- a/brainlet/api/v2/gateway/index.js +++ b/brainlet/api/v2/gateway/index.js @@ -17,7 +17,6 @@ class GatewayServer extends EventEmitter { this.pingInterval = setInterval(() => { this.wss.clients.forEach((client) => { if (!client.alive) { - console.log("gateway: terminating client due to ping timeout"); client.terminate(); } client.alive = false; @@ -78,6 +77,7 @@ class GatewayServer extends EventEmitter { const messageContent = message.data.content.trim(); if (messageContent.length > 2000) return; + if (messageContent === "") return; if (message.data.channel._id.length !== 24) throw new Error("msg: payload has invalid id"); // MONGODB ONLY!! // Check if the user is in that channel before broadcasting the message @@ -113,7 +113,7 @@ GatewayServer.prototype.broadcast = function(channelId, data) { }; GatewayServer.prototype.clientReady = function(ws) { - return ws.readyState === WebSocket.OPEN && ws.session && ws.session.authenticated; + return ws.readyState === websockets.OPEN && ws.session && ws.session.authenticated; }; GatewayServer.prototype.authMessage = function(ws) { diff --git a/brainlet/app/gatewaytest/app.js b/brainlet/app/gatewaytest/app.js new file mode 100644 index 0000000..ef2d8f5 --- /dev/null +++ b/brainlet/app/gatewaytest/app.js @@ -0,0 +1,185 @@ +const opcodes = { + 0: { name: "HELLO", data: "JSON" }, + 1: { name: "YOO", data: "JSON" }, + 2: { name: "YOO_ACK", data: "JSON" }, + 3: { name: "ACTION_CREATE_MESSAGE", data: "JSON" }, + 4: { name: "EVENT_CREATE_MESSAGE", data: "JSON" } +}; + +const opcodeSeparator = "@"; + +const parseMessage = (message) => { + if (typeof message !== "string") throw new Error("msg: message not a string"); + const stringParts = message.split(opcodeSeparator); + if (stringParts < 2) throw new Error("msg: message does not split into more than 2 parts"); + const components = [ stringParts.shift(), stringParts.join(opcodeSeparator) ]; + const op = parseInt(components[0]); + if (isNaN(op)) throw new Error(`msg: message does not contain valid opcode: ${op}`); + + const opcodeData = opcodes[op]; + let data = components[1]; + if (!opcodeData) throw new Error(`msg: message contains unknown opcode ${op}`); + if (opcodeData.data === "JSON") { + data = JSON.parse(data); + } else if (opcodeData.data === "string") { + data = data.toString(); // NOTE: This isnt needed lol + } else { + throw new Error(`msg: invalid data type on opcode ${op}`); + } + + return { + opcode: op, + data: data, + dataType: opcodeData.data, + opcodeType: opcodeData.name || null + }; +}; + +const getOpcodeByName = (name) => { + for (const [key, value] of Object.entries(opcodes)) if (value.name === name) return key; +}; + + +class GatewayConnection { + constructor(token) { + this.ws = new WebSocket("ws://localhost:3005/gateway?v=2"); + + this.handshakeCompleted = false; + this.sessionInformation = null; + + this.ws.onopen = () => console.log("gateway: open"); + this.ws.onclose = (e) => { + this.handshakeCompleted = false; + console.log(`gateway: close: ${e.code}:${e.reason}`); + this.fire("onclose", e); + }; + this.ws.onmessage = (message) => { + try { + const packet = parseMessage(message.data); + if (!packet) return console.error("gateway: invalid packet from server"); + + switch (packet.opcodeType) { + case "HELLO": { + // Got HELLO from server, send YOO as soon as possible + console.log("gateway: got HELLO", packet.data); + console.log("gateway: sending YOO"); + this.ws.send(this.packet("YOO", { token })); + break; + } + case "YOO_ACK": { + // Server accepted connection + console.log("gateway: got YOO_ACK", packet.data); + this.handshakeCompleted = true; + this.sessionInformation = packet.data; + console.log("gateway: handshake complete"); + this.fire("onopen", packet.data); + break; + } + case "EVENT_CREATE_MESSAGE": { + // New message + // console.log("gateway: got new message", packet.data); + this.fire("onmessage", packet.data); + break; + } + default: { + console.log("gateway: got unknown packet", message.data); + break; + } + } + } catch(e) { + return console.error("gateway:", e); + } + }; + } +} + +GatewayConnection.prototype.sendMessage = function(content, channelId) { + if (!this.sessionInformation) throw new Error("gateway: tried to send message before handshake completion"); + + this.ws.send(this.packet("ACTION_CREATE_MESSAGE", { + content, + channel: { + _id: channelId + } + })); +}; + + +GatewayConnection.prototype.packet = function(op, data) { + if (typeof op === "string") op = getOpcodeByName(op); + return `${op}${opcodeSeparator}${JSON.stringify(data)}`; +}; + +GatewayConnection.prototype.fire = function(eventName, ...args) { + if (this[eventName]) return this[eventName](...args); +}; + + +class AppState { + constructor(token) { + this.connection = new GatewayConnection(token); + this.tc = new Tricarbon(); + this.tcEvents = this.tc.useEvents(); + + this.messageStore = {}; + this.selectedChannelId = null; + + this.Sidebar = (channels) => (ev) => ` + ${Object.keys(channels).map(k => ` + + `).join("")} + `; + + this.ChannelMessages = (messages) => () => ` + ${Object.keys(messages).map(k => ` +
+ ${messages[k].author.username} + ${messages[k].content} +
+ `).join("")} + `; + + this.ChannelTopBar = (channel) => () => ` + ${channel.title} + `; + + this.connection.onopen = (sessionInfo) => { + this.renderSidebar(sessionInfo.channels); + this.tc.A("#message-input").addEventListener("keydown", (e) => { + if (e.code === "Enter") { + if (!this.selectedChannelId) return; + const messageContent = this.tc.A("#message-input").value; + if (!messageContent) return; + this.connection.sendMessage(messageContent, this.selectedChannelId); + this.tc.A("#message-input").value = ""; + } + }); + }; + this.connection.onmessage = (message) => { + this.appendMessage(message); + }; + } +} + +AppState.prototype.appendMessage = function(message) { + if (!this.messageStore[message.channel._id]) this.messageStore[message.channel._id] = []; + this.messageStore[message.channel._id].push({ content: message.content, author: message.author }); + if (this.selectedChannelId === message.channel._id) { + this.tc.push(this.ChannelMessages(this.messageStore[message.channel._id] || []), "#messages", false, this.tcEvents("#messages")); + } +}; + +AppState.prototype.navigateToChannel = function(channel) { + console.log("app: navigating to channel", channel); + if (this.selectedChannelId !== channel._id) { + this.selectedChannelId = channel._id; + this.tc.push(this.ChannelTopBar(channel), "#top-bar", false); + this.tc.push(this.ChannelMessages(this.messageStore[channel._id] || []), "#messages", false, this.tcEvents("#messages")); + } +}; + +AppState.prototype.renderSidebar = function(channels) { + this.tc.push(this.Sidebar(channels), "#channels", false, this.tcEvents("#channels")); +}; + +const app = new AppState(localStorage.getItem("token")); \ No newline at end of file diff --git a/brainlet/app/gatewaytest/index.html b/brainlet/app/gatewaytest/index.html index 8771061..4238211 100644 --- a/brainlet/app/gatewaytest/index.html +++ b/brainlet/app/gatewaytest/index.html @@ -1,106 +1,45 @@ - + + + - +
+ +
+
+ █████ +
+
+
+ █████ + ██████████ +
+
+ ████ + ██████ +
+
+ █████ + ████████ +
+
+ ███ + █████████████ +
+
+ +
+
diff --git a/brainlet/app/gatewaytest/style.css b/brainlet/app/gatewaytest/style.css new file mode 100644 index 0000000..98407de --- /dev/null +++ b/brainlet/app/gatewaytest/style.css @@ -0,0 +1,119 @@ +/* This CSS is very hacky and extremely inefficient. No human being, alive or dead, shall go through the pain of reading or maintaining this code */ + +:root { + --bg: #000000; + --fg: #ffffff; +} + +body { + align-items: center; + justify-content: center; + display: flex; + margin: 0; + height: 100%; + font-family: Verdana, Geneva, Tahoma, sans-serif; + + background-color: var(--bg); + color: var(--fg); +} + +main { + display: flex; + flex-direction: row; + flex-wrap: wrap; + flex-grow: 0; + width: 800px; + height: 600px; + margin: 10px; + + background-color: var(--bg); + color: var(--fg); + border: 1px solid var(--fg); +} + +.channel-view { + display: flex; + flex-direction: column; + flex: 10; +} + +.sidebar { + min-width: 150px; + max-width: 150px; + max-height: 100%; + display: flex; + flex-direction: column; + overflow-y: auto; + + border-right: 1px solid var(--fg); +} + +.sidebar-button { + max-width: inherit; + border: 0; + padding: 8px; + margin-right: 0; + margin-left: 0; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + text-align: left; + min-height: 34px; + max-height: 34px; + + border-bottom: 1px solid var(--fg); + background-color: var(--bg); + color: var(--fg); +} + +.sidebar-button:hover, .sidebar-button.selected { + font-weight: bold; +} + +.top-bar { + display: flex; + min-height: 33px; + max-height: 33px; + flex: 1; + align-items: center; + justify-content: center; + + background-color: var(--bg); + border-bottom: 1px solid var(--fg); +} + +.bottom-bar { + min-height: 33px; + max-height: 33px; + flex: 1; + flex-wrap: wrap; + + background-color: var(--bg); + border-top: 1px solid var(--fg); +} + +.channel-message-container { + padding: 15px; + flex: 1; + flex-wrap: wrap; + overflow-y: auto; + + background-color: var(--bg); +} + +input { + background-color: var(--bg); + color: var(--fg); + border: none; +} + +@media only screen and (max-width: 800px) { + main { + width: 95%; + height: 95%; + } + .sidebar { + min-width: 100px; + max-width: 100px; + } +} \ No newline at end of file