|
|
|
// Import dependencies
|
|
|
|
const { WebSocket, WebSocketServer } = require("ws");
|
|
|
|
const express = require("express");
|
|
|
|
const dotenv = require("dotenv");
|
|
|
|
dotenv.config();
|
|
|
|
|
|
|
|
// Setup Environmental Variables
|
|
|
|
const PORT = process.env.PORT || 3000;
|
|
|
|
const PREFIX = process.env.PREFIX || "";
|
|
|
|
const PUBLIC = process.env.PUBLIC || "";
|
|
|
|
|
|
|
|
// Setup Express Router
|
|
|
|
// Create the routes of the application
|
|
|
|
// Here are two pages and a wildcard:
|
|
|
|
// at / there is the index.html page, where to draw
|
|
|
|
// at /destination there is the destination.html page, where to receive the drawings
|
|
|
|
// the wildcard /* serves the static files from the public folder
|
|
|
|
|
|
|
|
const router = express.Router();
|
|
|
|
const routes = (app) => {
|
|
|
|
app.get("/", (req, res) => {
|
|
|
|
res.render("index", {
|
|
|
|
address: PREFIX,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
app.get("/destination", (req, res) => {
|
|
|
|
res.render("destination", {
|
|
|
|
address: PREFIX,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
app.get("/*", (req, res) => {
|
|
|
|
res.sendFile(req.url, { root: "public" });
|
|
|
|
});
|
|
|
|
return app;
|
|
|
|
};
|
|
|
|
|
|
|
|
// Setup the Express server
|
|
|
|
const server = express()
|
|
|
|
.set("view engine", "html")
|
|
|
|
.engine("html", require("hbs").__express)
|
|
|
|
.use(PREFIX, routes(router, {}))
|
|
|
|
.use(express.static("public"))
|
|
|
|
.listen(PORT, () => console.log(`Listening on ${PORT}`));
|
|
|
|
|
|
|
|
// Setup the Websocket server on top of the Express one
|
|
|
|
const wss = new WebSocketServer({ server, clientTracking: true });
|
|
|
|
|
|
|
|
// Global variables to manage the connected Destination and User clients
|
|
|
|
let DESTINATIONS = new Set();
|
|
|
|
let USERS = new Set();
|
|
|
|
var theme = "";
|
|
|
|
|
|
|
|
// The message processor defines which function is associated to every websocket message type.
|
|
|
|
// It map a type to a function, passing some optional parameters such as the message itself and the websocket client that sent it.
|
|
|
|
// for example an incoming message like {type: hello} will trigger the registerDest(ws, msg) function.
|
|
|
|
// In this way to add message types and functionalities gets easier, and avoid long chain of if-else statements.
|
|
|
|
|
|
|
|
const messageProcessor = {
|
|
|
|
default: (ws, msg) => unknownMsg(msg),
|
|
|
|
hello: (ws, msg) => registerDest(ws, msg),
|
|
|
|
drawings: (ws, msg) => toDest(msg),
|
|
|
|
theme: (ws, msg) => ((theme = msg.theme), broadcast(msg)),
|
|
|
|
};
|
|
|
|
|
|
|
|
// Message processor functions
|
|
|
|
|
|
|
|
// Default function, to cactch unkown message types
|
|
|
|
const unknownMsg = (msg) => {
|
|
|
|
console.log("Unknown message type...");
|
|
|
|
console.log(msg);
|
|
|
|
};
|
|
|
|
|
|
|
|
// Add the ws client in the destinations set
|
|
|
|
// Removing it when it disconnets
|
|
|
|
const registerDest = (ws, msg) => {
|
|
|
|
console.log("Destination client connected");
|
|
|
|
DESTINATIONS.add(ws);
|
|
|
|
ws.on("close", () => {
|
|
|
|
DESTINATIONS.delete(ws);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
// Send a message to all the connected Destinations
|
|
|
|
const toDest = (msg) => {
|
|
|
|
let message = JSON.stringify(msg);
|
|
|
|
DESTINATIONS.forEach((DESTINATION) => {
|
|
|
|
if (DESTINATION?.readyState === WebSocket.OPEN) {
|
|
|
|
DESTINATION.send(message);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
// Send a message to all the connected Users
|
|
|
|
const broadcast = (msg) => {
|
|
|
|
let message = JSON.stringify(msg);
|
|
|
|
for (const user of USERS.values()) {
|
|
|
|
if (user?.readyState === WebSocket.OPEN && !DESTINATIONS.has(user)) {
|
|
|
|
user.send(message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Websocket events listener
|
|
|
|
wss.on("connection", (ws) => {
|
|
|
|
USERS.add(ws);
|
|
|
|
ws.send(JSON.stringify({ type: "theme", theme: theme }));
|
|
|
|
|
|
|
|
ws.on("message", (data) => {
|
|
|
|
// Parse the incoming data safely
|
|
|
|
let message;
|
|
|
|
try {
|
|
|
|
message = JSON.parse(data);
|
|
|
|
} catch (e) {}
|
|
|
|
|
|
|
|
// Call the message processor, eventually falling back to use the default function if the type is not defined.
|
|
|
|
if (message) {
|
|
|
|
(messageProcessor[message?.type] || messageProcessor.default)(ws, message);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
ws.on("close", () => {
|
|
|
|
USERS.delete(ws);
|
|
|
|
});
|
|
|
|
});
|