1
Fork 0
This repository has been archived on 2022-02-28. You can view files and clone it, but cannot push or open issues or pull requests.
web-drs-backend/server.node.ts

242 lines
6.1 KiB
TypeScript

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<string>();
const currentlyTyping = new Set<string>();
const currentlyTypingTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
const desiredNames = new Map<string, string>();
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;
}
console.debug(`${from} set desiredName to ${message.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);