forked from hippoz/brainlet
do not allow sending of empty messages in gateway and add interactivity to app
This commit is contained in:
parent
51e7a46821
commit
b039639ff8
5 changed files with 345 additions and 101 deletions
|
@ -1,5 +1,6 @@
|
|||
module.exports = {
|
||||
"env": {
|
||||
"browser": true,
|
||||
"commonjs": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
|
|
|
@ -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) {
|
||||
|
|
185
brainlet/app/gatewaytest/app.js
Normal file
185
brainlet/app/gatewaytest/app.js
Normal file
|
@ -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 => `
|
||||
<button class="sidebar-button" onclick="${ev(() => this.navigateToChannel(channels[k]))}">${channels[k].title}</button>
|
||||
`).join("")}
|
||||
`;
|
||||
|
||||
this.ChannelMessages = (messages) => () => `
|
||||
${Object.keys(messages).map(k => `
|
||||
<div class="message">
|
||||
<strong>${messages[k].author.username}</strong>
|
||||
${messages[k].content}
|
||||
</div>
|
||||
`).join("")}
|
||||
`;
|
||||
|
||||
this.ChannelTopBar = (channel) => () => `
|
||||
<strong class="top-bar-channel-name">${channel.title}</strong>
|
||||
`;
|
||||
|
||||
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"));
|
|
@ -1,106 +1,45 @@
|
|||
<html>
|
||||
<head>
|
||||
<script>
|
||||
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.ws.onopen = () => console.log("gateway: open");
|
||||
this.ws.onclose = (e) => {
|
||||
this.handshakeCompleted = false;
|
||||
console.log(`gateway: close: ${e.code}:${e.reason}`);
|
||||
}
|
||||
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;
|
||||
console.log("gateway: handshake complete");
|
||||
break;
|
||||
}
|
||||
case "EVENT_CREATE_MESSAGE": {
|
||||
// New message
|
||||
console.log("gateway: got new message", packet.data);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.log("gateway: got unknown packet", message.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
return console.error("gateway:", e);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
GatewayConnection.prototype.packet = function(op, data) {
|
||||
if (typeof op === "string") op = getOpcodeByName(op);
|
||||
return `${op}${opcodeSeparator}${JSON.stringify(data)}`;
|
||||
};
|
||||
|
||||
const connection = new GatewayConnection(localStorage.getItem("token"));
|
||||
</script>
|
||||
<script src="https://git.hippoz.xyz/hippoz/tricarbon/raw/branch/master/lib/indexsmall.js"></script>
|
||||
<script src="app.js"></script>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<main>
|
||||
<div class="sidebar" id="channels">
|
||||
<button class="sidebar-button">████</button>
|
||||
<button class="sidebar-button">████</button>
|
||||
<button class="sidebar-button">██████</button>
|
||||
<button class="sidebar-button">███</button>
|
||||
<button class="sidebar-button">██████</button>
|
||||
<button class="sidebar-button">████</button>
|
||||
<button class="sidebar-button">█████</button>
|
||||
</div>
|
||||
<div class="channel-view">
|
||||
<div class="top-bar" id="top-bar">
|
||||
<strong class="top-bar-channel-name">█████</strong>
|
||||
</div>
|
||||
<div class="channel-message-container" id="messages">
|
||||
<div class="message">
|
||||
<strong>█████</strong>
|
||||
██████████
|
||||
</div>
|
||||
<div class="message">
|
||||
<strong>████</strong>
|
||||
██████
|
||||
</div>
|
||||
<div class="message">
|
||||
<strong>█████</strong>
|
||||
████████
|
||||
</div>
|
||||
<div class="message">
|
||||
<strong>███</strong>
|
||||
█████████████
|
||||
</div>
|
||||
</div>
|
||||
<input class="bottom-bar" placeholder="Go on, type something interesting!" id="message-input">
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
119
brainlet/app/gatewaytest/style.css
Normal file
119
brainlet/app/gatewaytest/style.css
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue