do not allow sending of empty messages in gateway and add interactivity to app

This commit is contained in:
hippoz 2021-03-22 04:27:59 +02:00
parent 51e7a46821
commit b039639ff8
No known key found for this signature in database
GPG key ID: 7C52899193467641
5 changed files with 345 additions and 101 deletions

View file

@ -1,5 +1,6 @@
module.exports = { module.exports = {
"env": { "env": {
"browser": true,
"commonjs": true, "commonjs": true,
"es2021": true, "es2021": true,
"node": true "node": true

View file

@ -17,7 +17,6 @@ class GatewayServer extends EventEmitter {
this.pingInterval = setInterval(() => { this.pingInterval = setInterval(() => {
this.wss.clients.forEach((client) => { this.wss.clients.forEach((client) => {
if (!client.alive) { if (!client.alive) {
console.log("gateway: terminating client due to ping timeout");
client.terminate(); client.terminate();
} }
client.alive = false; client.alive = false;
@ -78,6 +77,7 @@ class GatewayServer extends EventEmitter {
const messageContent = message.data.content.trim(); const messageContent = message.data.content.trim();
if (messageContent.length > 2000) return; 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!! 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 // 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) { 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) { GatewayServer.prototype.authMessage = function(ws) {

View 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"));

View file

@ -1,106 +1,45 @@
<html> <html>
<head> <head>
<script> <script src="https://git.hippoz.xyz/hippoz/tricarbon/raw/branch/master/lib/indexsmall.js"></script>
const opcodes = { <script src="app.js"></script>
0: { name: "HELLO", data: "JSON" }, <link rel="stylesheet" href="style.css">
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>
</head> </head>
<body> <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> </body>
</html> </html>

View 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;
}
}