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 = {
|
module.exports = {
|
||||||
"env": {
|
"env": {
|
||||||
|
"browser": true,
|
||||||
"commonjs": true,
|
"commonjs": true,
|
||||||
"es2021": true,
|
"es2021": true,
|
||||||
"node": true
|
"node": true
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
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>
|
<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>
|
||||||
|
|
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