import * as fs from "fs"; import * as https from "https"; import { WebSocketServer } from "ws"; import * as crypto from "crypto"; import type { AckMessage, IdResponseMessage, TextMessage, TypingMessage, ConnectedUser, ConnectedUsersMessage, DesiredNameMessage, } from "./lib/ServerMessage"; import { isServerMessage, isTextMessage, isTypingMessage, isDesiredNameMessage, MessageType, } from "./lib/ServerMessage"; const port = 8085; const timeout = 5000; const typingTimeout = 2000; const serverId = "00000000-0000-0000-0000-000000000000"; const httpsServer = https.createServer({ key: fs.readFileSync("./key.pem"), cert: fs.readFileSync("./cert.pem"), }); const webSocketServer = new WebSocketServer({ server: httpsServer }); /* If not using SSL/TLS const webSocketServer = new WebSocketServer({ port }); */ console.log("listening on port: " + port); async function handleTextMessage(message: TextMessage, from: string) { for (const to of webSocketServer.clients) { to.send( JSON.stringify( Object.assign({}, message, { author: from, }) ) ); } } const activeConnections = new Set(); const currentlyTyping = new Set(); const currentlyTypingTimeouts = new Map>(); const desiredNames = new Map(); activeConnections.add(serverId); desiredNames.set(serverId, "[SYSTEM]"); async function handleTypingMessage(_message: TypingMessage, from: string) { currentlyTyping.add(from); if(currentlyTypingTimeouts.has(from)) { clearTimeout(currentlyTypingTimeouts.get(from)!); } currentlyTypingTimeouts.set(from, setTimeout(() => { currentlyTyping.delete(from); currentlyTypingTimeouts.delete(from); sendCurrentlyTypingMessage(from, true); }, typingTimeout)); sendCurrentlyTypingMessage(from); } async function sendCurrentlyTypingMessage(from?: string, stopped = false) { for (const to of webSocketServer.clients) { to.send( JSON.stringify( Object.assign({ type: MessageType.CURRENTLY_TYPING, date: Date.now(), }, { currently: Array.from(currentlyTyping.values()), }, from === undefined ? { __ctx: "regular update", } : { __ctx: from + (stopped ? " stopped typing" : " started typing"), }) ) ) } } async function handleCloseConnection(id: string) { activeConnections.delete(id); if (desiredNames.has(id)) { desiredNames.delete(id); } const connected = Array.from(activeConnections, (id) => { return { id, desiredName: desiredNames.get(id), } as ConnectedUser; }); for (const to of webSocketServer.clients) { to.send( JSON.stringify({ type: MessageType.CONNECTED_USERS, __ctx: `${id} left`, date: Date.now(), connected, } as ConnectedUsersMessage) ); } } function handleNewConnection(id: string) { activeConnections.add(id); const connected = Array.from(activeConnections, (id) => { return { id, desiredName: desiredNames.get(id), } as ConnectedUser; }); for (const to of webSocketServer.clients) { to.send( JSON.stringify({ type: MessageType.CONNECTED_USERS, __ctx: `${id} joined`, date: Date.now(), connected, } as ConnectedUsersMessage) ); } } async function handleDesiredNameMessage(message: DesiredNameMessage, from: string) { if (!activeConnections.has(from) && desiredNames.has(from)) { desiredNames.delete(from); return; } if (message.desiredName) { console.debug(`${from} set desiredName to ${message.desiredName}`); } else { console.debug(`${from} unset desiredName`); } if (message.desiredName === undefined) { desiredNames.delete(from); } else { desiredNames.set(from, message.desiredName); } const connected = Array.from(activeConnections, (id) => { return { id, desiredName: desiredNames.get(id), } as ConnectedUser; }); for (const to of webSocketServer.clients) { to.send( JSON.stringify({ type: MessageType.CONNECTED_USERS, __ctx: `${from} changed desiredName to ${message.desiredName}`, date: Date.now(), connected, } as ConnectedUsersMessage) ); } } webSocketServer.on("connection", function connection(socket) { const close = (reason: string, code: number = 1000) => { socket.send( JSON.stringify({ type: MessageType.ACK, date: Date.now(), __ctx: `closing connection. reason: ${reason}`, } as AckMessage) ); socket.close(code, `closing connection. reason: ${reason}`); handleCloseConnection(authorId); }; const authorId: string = crypto.randomUUID(); socket.on("message", function (rawMessage: string) { const message = JSON.parse(rawMessage); if (!isServerMessage(message)) { console.error(`Unexpected message received from client "${authorId}": \`${message}\``); return; } else { clearTimeout(closeTimeout); closeTimeout = setTimeout(close, timeout); } if (isTextMessage(message)) { if (message.author === authorId) { handleTextMessage(message, authorId); } } else if (isTypingMessage(message)) { handleTypingMessage(message, authorId); } else if (isDesiredNameMessage(message)) { handleDesiredNameMessage(message, authorId); } }); socket.on("close", function close() { console.log("closed a connection"); }); console.log("new client connected! ID:", authorId); socket.send( JSON.stringify({ type: MessageType.ID_RESPONSE, __ctx: "connected successfully", date: Date.now(), authorId, } as IdResponseMessage) ); handleNewConnection(authorId); socket.send( JSON.stringify({ type: MessageType.TEXT, __ctx: "server welcome message", date: Date.now(), author: serverId, content: "Successfully connected. Welcome!\nSend a message to talk to other connected users.", } as TextMessage) ); let closeTimeout = setTimeout(close, timeout); }); httpsServer.listen(port);