1665 lines
44 KiB
JavaScript
Executable File
1665 lines
44 KiB
JavaScript
Executable File
"use strict";
|
|
|
|
const express = require("express"),
|
|
router = express.Router(),
|
|
crypto = require("crypto"),
|
|
{ readFile, writeFile } = require("fs").promises,
|
|
fs = require("fs"),
|
|
accessSync = fs.accessSync,
|
|
randomWords = require("random-words"),
|
|
equal = require("fast-deep-equal");
|
|
|
|
const debug = {
|
|
audio: false,
|
|
get: true,
|
|
set: true,
|
|
update: false
|
|
};
|
|
|
|
let chatDB;
|
|
|
|
const chats = {};
|
|
const audio = {};
|
|
|
|
require("../db/chats").then(function(db) {
|
|
chatDB = db;
|
|
});
|
|
|
|
const sessionFromColor = (room, color) => {
|
|
for (let key in room.sessions) {
|
|
if (room.sessions[key].color === color) {
|
|
return room.sessions[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
const newPerson = (color) => {
|
|
return {
|
|
status: "Not active",
|
|
lastActive: 0,
|
|
color: color,
|
|
name: "",
|
|
};
|
|
}
|
|
|
|
const getSession = (room, id) => {
|
|
if (!room.sessions) {
|
|
room.sessions = {};
|
|
}
|
|
|
|
/* If this session is not yet in the chat, add it and set the person's name */
|
|
if (!(id in room.sessions)) {
|
|
room.sessions[id] = {
|
|
id: `[${id.substring(0, 8)}]`,
|
|
name: '',
|
|
color: '',
|
|
person: undefined,
|
|
lastActive: Date.now(),
|
|
live: true
|
|
};
|
|
}
|
|
|
|
const session = room.sessions[id];
|
|
session.lastActive = Date.now();
|
|
session.live = true;
|
|
if (session.person) {
|
|
session.person.live = true;
|
|
session.person.lastActive = session.lastActive;
|
|
}
|
|
|
|
/* Expire old unused sessions */
|
|
for (let _id in room.sessions) {
|
|
const _session = room.sessions[_id];
|
|
if (_session.color || _session.name || _session.person) {
|
|
continue;
|
|
}
|
|
if (_id === id) {
|
|
continue;
|
|
}
|
|
/* 60 minutes */
|
|
const age = Date.now() - _session.lastActive;
|
|
if (age > 60 * 60 * 1000) {
|
|
console.log(`${_session.id}: Expiring old session ${_id}: ${age/(60 * 1000)} minutes`);
|
|
delete room.sessions[_id];
|
|
if (_id in room.sessions) {
|
|
console.log('delete DID NOT WORK!');
|
|
}
|
|
}
|
|
}
|
|
|
|
return room.sessions[id];
|
|
};
|
|
|
|
const loadChat = async (id) => {
|
|
if (/^\.|\//.exec(id)) {
|
|
return undefined;
|
|
}
|
|
|
|
if (id in chats) {
|
|
return chats[id];
|
|
}
|
|
|
|
let room = await readFile(`chats/${id}`)
|
|
.catch(() => {
|
|
return;
|
|
});
|
|
|
|
if (room) {
|
|
try {
|
|
room = JSON.parse(room);
|
|
console.log(`${info}: Creating backup of chats/${id}`);
|
|
await writeFile(`chats/${id}.bk`, JSON.stringify(room));
|
|
} catch (error) {
|
|
console.log(`Load or parse error from chats/${id}:`, error);
|
|
console.log(`Attempting to load backup from chats/${id}.bk`);
|
|
room = await readFile(`chats/${id}.bk`)
|
|
.catch(() => {
|
|
console.error(error, room);
|
|
});
|
|
if (room) {
|
|
try {
|
|
room = JSON.parse(room);
|
|
console.log(`Saving backup to chats/${id}`);
|
|
await writeFile(`chats/${id}`, JSON.stringify(room, null, 2));
|
|
} catch (error) {
|
|
console.error(error);
|
|
room = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!room) {
|
|
room = createChat(id);
|
|
}
|
|
|
|
/* Clear out cached names from person colors and rebuild them
|
|
* from the information in the saved chat sessions */
|
|
for (let color in room.persons) {
|
|
delete room.persons[color].name;
|
|
room.persons[color].status = 'Not active';
|
|
}
|
|
|
|
/* Reconnect session person colors to the person objects */
|
|
room.unselected = [];
|
|
for (let id in room.sessions) {
|
|
const session = room.sessions[id];
|
|
if (session.name && session.color && session.color in room.persons) {
|
|
session.person = room.persons[session.color];
|
|
session.person.name = session.name;
|
|
session.person.status = 'Active';
|
|
session.person.live = false;
|
|
} else {
|
|
session.color = '';
|
|
session.person = undefined;
|
|
}
|
|
|
|
session.live = false;
|
|
|
|
/* Populate the 'unselected' list from the session table */
|
|
if (!room.sessions[id].color && room.sessions[id].name) {
|
|
room.unselected.push(room.sessions[id]);
|
|
}
|
|
}
|
|
|
|
chats[id] = room;
|
|
return room;
|
|
};
|
|
|
|
const clearPerson = (person) => {
|
|
const color = person.color;
|
|
for (let key in person) {
|
|
delete person[key];
|
|
}
|
|
Object.assign(person, newPerson(color));
|
|
}
|
|
|
|
const setPersonName = (room, session, name) => {
|
|
if (session.name === name) {
|
|
return; /* no-op */
|
|
}
|
|
if (session.color) {
|
|
return `You cannot change your name while you have a color selected.`;
|
|
}
|
|
|
|
if (!name) {
|
|
return `You can not set your name to nothing!`;
|
|
}
|
|
|
|
/* Check to ensure name is not already in use */
|
|
let rejoin = false;
|
|
for (let id in room.sessions) {
|
|
const tmp = room.sessions[id];
|
|
if (tmp === session || !tmp.name) {
|
|
continue;
|
|
}
|
|
if (tmp.name.toLowerCase() === name.toLowerCase()) {
|
|
if (!tmp.person || (Date.now() - tmp.person.lastActive) > 60000) {
|
|
rejoin = true;
|
|
/* Update the session object from tmp, but retain websocket
|
|
* from active session */
|
|
Object.assign(session, tmp, { ws: session.ws, id: session.id });
|
|
console.log(`${info}: ${name} has been reallocated to a new session.`);
|
|
delete room.sessions[id];
|
|
} else {
|
|
return `${name} is already taken and has been active in the last minute.`;
|
|
}
|
|
}
|
|
}
|
|
|
|
let message;
|
|
|
|
if (!session.name) {
|
|
message = `A new person has entered the lobby as ${name}.`;
|
|
} else {
|
|
if (rejoin) {
|
|
if (session.color) {
|
|
message = `${name} has reconnected to the room.`;
|
|
} else {
|
|
message = `${name} has rejoined the lobby.`;
|
|
}
|
|
session.name = name;
|
|
if (session.ws && (room.id in audio)
|
|
&& session.name in audio[room.id]) {
|
|
part(audio[room.id], session);
|
|
}
|
|
} else {
|
|
message = `${session.name} has changed their name to ${name}.`;
|
|
if (session.ws && room.id in audio) {
|
|
part(audio[room.id], session);
|
|
}
|
|
}
|
|
}
|
|
|
|
session.name = name;
|
|
session.live = true;
|
|
if (session.person) {
|
|
session.color = session.person.color;
|
|
session.person.name = session.name;
|
|
session.person.status = `Active`;
|
|
session.person.lastActive = Date.now();
|
|
session.person.name = name;
|
|
session.person.live = true;
|
|
}
|
|
|
|
if (session.ws && session.hasAudio) {
|
|
join(audio[room.id], session, {
|
|
hasVideo: session.video ? true : false,
|
|
hasAudio: session.audio ? true : false
|
|
});
|
|
}
|
|
console.log(`${info}: ${message}`);
|
|
addChatMessage(room, null, message);
|
|
|
|
/* Rebuild the unselected list */
|
|
if (!session.color) {
|
|
console.log(`${info}: Adding ${session.name} to the unselected`);
|
|
}
|
|
room.unselected = [];
|
|
for (let id in room.sessions) {
|
|
if (!room.sessions[id].color && room.sessions[id].name) {
|
|
room.unselected.push(room.sessions[id]);
|
|
}
|
|
}
|
|
|
|
sendUpdateToPerson(room, session, {
|
|
name: session.name,
|
|
color: session.color,
|
|
live: session.live,
|
|
private: session.person
|
|
});
|
|
sendUpdateToPersons(room, {
|
|
persons: getFilteredPersons(room),
|
|
unselected: getFilteredUnselected(room),
|
|
chat: room.chat
|
|
});
|
|
/* Now that a name is set, send the full chat to the person */
|
|
sendChatToPerson(room, session);
|
|
}
|
|
|
|
const colorToWord = (color) => {
|
|
switch (color) {
|
|
case 'O': return 'orange';
|
|
case 'W': return 'white';
|
|
case 'B': return 'blue';
|
|
case 'R': return 'red';
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
|
|
const getActiveCount = (room) => {
|
|
let active = 0;
|
|
for (let color in room.persons) {
|
|
if (!room.persons[color].name) {
|
|
continue;
|
|
}
|
|
active++;
|
|
}
|
|
return active;
|
|
}
|
|
|
|
const setPersonColor = (room, session, color) => {
|
|
/* Selecting the same color is a NO-OP */
|
|
if (session.color === color) {
|
|
return;
|
|
}
|
|
|
|
/* Verify the person has a name set */
|
|
if (!session.name) {
|
|
return `You may only select a person when you have set your name.`;
|
|
}
|
|
|
|
if (room.state !== 'lobby') {
|
|
return `You may only select a person when the chat is in the lobby.`;
|
|
}
|
|
|
|
/* Verify selection is valid */
|
|
if (color && !(color in room.persons)) {
|
|
return `An invalid person selection was attempted.`;
|
|
}
|
|
|
|
/* Verify selection is not already taken */
|
|
if (color && room.persons[color].status !== 'Not active') {
|
|
return `${room.persons[color].name} already has ${colorToWord(color)}`;
|
|
}
|
|
|
|
let active = getActiveCount(room);
|
|
|
|
if (session.person) {
|
|
/* Deselect currently active person for this session */
|
|
clearPerson(session.person);
|
|
session.person = undefined;
|
|
session.color = '';
|
|
active--;
|
|
|
|
/* If the person is not selecting a color, then return */
|
|
if (!color) {
|
|
addChatMessage(room, null,
|
|
`${session.name} is no longer ${colorToWord(session.color)}.`);
|
|
room.unselected.push(session);
|
|
room.active = active;
|
|
if (active === 1) {
|
|
addChatMessage(room, null,
|
|
`There are no longer enough persons to start a room.`);
|
|
}
|
|
sendUpdateToPerson(room, session, {
|
|
name: session.name,
|
|
color: '',
|
|
live: session.live,
|
|
private: session.person
|
|
});
|
|
sendUpdateToPersons(room, {
|
|
active: room.active,
|
|
unselected: getFilteredUnselected(room),
|
|
persons: getFilteredPersons(room),
|
|
chat: room.chat
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* All good -- set this person to requested selection */
|
|
active++;
|
|
session.color = color;
|
|
session.live = true;
|
|
session.person = room.persons[color];
|
|
session.person.name = session.name;
|
|
session.person.status = `Active`;
|
|
session.person.lastActive = Date.now();
|
|
session.person.live = true;
|
|
addChatMessage(room, session, `${session.name} has chosen to play as ${colorToWord(color)}.`);
|
|
|
|
const update = {
|
|
persons: getFilteredPersons(room),
|
|
chat: room.chat
|
|
};
|
|
|
|
/* Rebuild the unselected list */
|
|
const unselected = [];
|
|
for (let id in room.sessions) {
|
|
if (!room.sessions[id].color && room.sessions[id].name) {
|
|
unselected.push(room.sessions[id]);
|
|
}
|
|
}
|
|
if (unselected.length !== room.unselected.length) {
|
|
room.unselected = unselected;
|
|
update.unselected = getFilteredUnselected(room);
|
|
}
|
|
|
|
if (room.active !== active) {
|
|
if (room.active < 2 && active >= 2) {
|
|
addChatMessage(room, null,
|
|
`There are now enough persons to start the room.`);
|
|
}
|
|
room.active = active;
|
|
update.active = room.active;
|
|
}
|
|
|
|
sendUpdateToPerson(room, session, {
|
|
name: session.name,
|
|
color: session.color,
|
|
live: session.live,
|
|
private: session.person,
|
|
});
|
|
sendUpdateToPersons(room, update);
|
|
};
|
|
|
|
const addActivity = (room, session, message) => {
|
|
let date = Date.now();
|
|
if (room.activities.length && room.activities[room.activities.length - 1].date === date) {
|
|
date++;
|
|
}
|
|
room.activities.push({ color: session ? session.color : '', message, date });
|
|
if (room.activities.length > 30) {
|
|
room.activities.splice(0, room.activities.length - 30);
|
|
}
|
|
}
|
|
|
|
const addChatMessage = (room, session, message, isNormalChat) => {
|
|
let now = Date.now();
|
|
let lastTime = 0;
|
|
if (room.chat.length) {
|
|
lastTime = room.chat[room.chat.length - 1].date;
|
|
}
|
|
if (now <= lastTime) {
|
|
now = lastTime + 1;
|
|
}
|
|
|
|
const entry = {
|
|
date: now,
|
|
message: message
|
|
};
|
|
if (isNormalChat) {
|
|
entry.normalChat = true;
|
|
}
|
|
if (session && session.name) {
|
|
entry.from = session.name;
|
|
}
|
|
if (session && session.color) {
|
|
entry.color = session.color;
|
|
}
|
|
room.chat.push(entry);
|
|
if (room.chat.length > 50) {
|
|
room.chat.splice(0, room.chat.length - 50);
|
|
}
|
|
};
|
|
|
|
const getColorFromName = (room, name) => {
|
|
for (let id in room.sessions) {
|
|
if (room.sessions[id].name === name) {
|
|
return room.sessions[id].color;
|
|
}
|
|
}
|
|
return '';
|
|
};
|
|
|
|
const getLastPersonName = (room) => {
|
|
let index = room.personOrder.length - 1;
|
|
for (let id in room.sessions) {
|
|
if (room.sessions[id].color === room.personOrder[index]) {
|
|
return room.sessions[id].name;
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
const getFirstPersonName = (room) => {
|
|
let index = 0;
|
|
for (let id in room.sessions) {
|
|
if (room.sessions[id].color === room.personOrder[index]) {
|
|
return room.sessions[id].name;
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
router.put("/:id/:action/:value?", async (req, res) => {
|
|
const { action, id } = req.params,
|
|
value = req.params.value ? req.params.value : "";
|
|
console.log(`PUT chats/${id}/${action}/${value}`);
|
|
|
|
const chat = await loadChat(id);
|
|
if (!chat) {
|
|
const error = `Chat not found and cannot be created: ${id}`;
|
|
return res.status(404).send(error);
|
|
}
|
|
|
|
let error = 'Invalid request';
|
|
|
|
if ('private-token' in req.headers) {
|
|
if (req.headers['private-token'] !== req.app.get('admin')) {
|
|
error = `Invalid admin credentials.`;
|
|
} else {
|
|
error = adminCommands(room, action, value, req.query);
|
|
}
|
|
if (!error) {
|
|
sendChatToPersons(room);
|
|
} else {
|
|
console.log(`admin-action error: ${error}`);
|
|
}
|
|
}
|
|
|
|
return res.status(400).send(error);
|
|
});
|
|
|
|
const clearTimeNotice= (room, session) => {
|
|
if (!session.person.turnNotice) {
|
|
/* benign state; don't alert the user */
|
|
//return `You have not been idle.`;
|
|
}
|
|
session.person.turnNotice = "";
|
|
sendUpdateToPerson(room, session, {
|
|
private: session.person
|
|
});
|
|
};
|
|
|
|
const startTurnTimer = (room, session) => {
|
|
const timeout = 90;
|
|
if (!session.ws) {
|
|
console.log(`${session.id}: Aborting turn timer as ${session.name} is disconnected.`);
|
|
} else {
|
|
console.log(`${session.id}: (Re)setting turn timer for ${session.name} to ${timeout} seconds.`);
|
|
}
|
|
if (room.turnTimer) {
|
|
clearTimeout(room.turnTimer);
|
|
}
|
|
if (!session.connected) {
|
|
room.turnTimer = 0;
|
|
return;
|
|
}
|
|
room.turnTimer = setTimeout(() => {
|
|
console.log(`${session.id}: Turn timer expired for ${session.name}`);
|
|
session.person.turnNotice = 'It is still your turn.';
|
|
sendUpdateToPerson(room, session, {
|
|
private: session.person
|
|
});
|
|
resetTurnTimer(room, session);
|
|
}, timeout * 1000);
|
|
}
|
|
|
|
const resetTurnTimer = (room, session) => {
|
|
startTurnTimer(room, session);
|
|
}
|
|
|
|
const stopTurnTimer = (room) => {
|
|
if (room.turnTimer) {
|
|
console.log(`${info}: Stopping turn timer.`);
|
|
clearTimeout(room.turnTimer);
|
|
room.turnTimer = 0;
|
|
}
|
|
}
|
|
|
|
const ping = (session) => {
|
|
if (!session.ws) {
|
|
console.log(`[no socket] Not sending ping to ${session.name} -- connection does not exist.`);
|
|
return;
|
|
}
|
|
|
|
session.ping = Date.now();
|
|
// console.log(`Sending ping to ${session.name}`);
|
|
session.ws.send(JSON.stringify({ type: 'ping', ping: session.ping }));
|
|
if (session.keepAlive) {
|
|
clearTimeout(session.keepAlive);
|
|
}
|
|
session.keepAlive = setTimeout(() => { ping(session); }, 2500);
|
|
}
|
|
|
|
const wsInactive = (room, req) => {
|
|
const session = getSession(room, req.cookies.person);
|
|
|
|
if (session && session.ws) {
|
|
console.log(`Closing WebSocket to ${session.name} due to inactivity.`);
|
|
session.ws.close();
|
|
session.ws = undefined;
|
|
}
|
|
|
|
/* Prevent future pings */
|
|
if (req.keepAlive) {
|
|
clearTimeout(req.keepAlive);
|
|
}
|
|
}
|
|
|
|
const setChatState = (room, session, state) => {
|
|
if (!state) {
|
|
return `Invalid state.`;
|
|
}
|
|
|
|
if (!session.color) {
|
|
return `You must have an active person to start the room.`;
|
|
}
|
|
|
|
if (state === room.state) {
|
|
return;
|
|
}
|
|
|
|
switch (state) {
|
|
case "chat-order":
|
|
if (room.state !== 'lobby') {
|
|
return `You can only start the chat from the lobby.`;
|
|
}
|
|
const active = getActiveCount(room);
|
|
if (active < 2) {
|
|
return `You need at least two persons to start the room.`;
|
|
}
|
|
/* Delete any non-played colors from the person map; reduces all
|
|
* code that would otherwise have to filter out persons by checking
|
|
* the 'Not active' state of person.status */
|
|
for (let key in room.persons) {
|
|
if (room.persons[key].status !== 'Active') {
|
|
delete room.persons[key];
|
|
}
|
|
}
|
|
addChatMessage(room, null, `${session.name} requested to start the room.`);
|
|
room.state = state;
|
|
|
|
sendUpdateToPersons(room, {
|
|
state: room.state,
|
|
chat: room.chat
|
|
});
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
const resetDisconnectCheck = (room, req) => {
|
|
if (req.disconnectCheck) {
|
|
clearTimeout(req.disconnectCheck);
|
|
}
|
|
//req.disconnectCheck = setTimeout(() => { wsInactive(room, req) }, 20000);
|
|
}
|
|
|
|
const join = (peers, session, { hasVideo, hasAudio }) => {
|
|
const ws = session.ws;
|
|
|
|
if (!session.name) {
|
|
console.error(`${session.id}: <- join - No name set yet. Audio not available.`);
|
|
return;
|
|
}
|
|
|
|
console.log(`${session.id}: <- join - ${session.name}`);
|
|
console.log(`${all}: -> addPeer - ${session.name}`);
|
|
|
|
if (session.name in peers) {
|
|
console.log(`${session.id}:${session.name} - Already joined to Audio.`);
|
|
return;
|
|
}
|
|
|
|
for (let peer in peers) {
|
|
/* Add this caller to all peers */
|
|
peers[peer].ws.send(JSON.stringify({
|
|
type: 'addPeer',
|
|
data: {
|
|
peer_id: session.name,
|
|
should_create_offer: false,
|
|
hasAudio, hasVideo
|
|
}
|
|
}));
|
|
|
|
/* Add each other peer to the caller */
|
|
ws.send(JSON.stringify({
|
|
type: 'addPeer',
|
|
data: {
|
|
peer_id: peer,
|
|
should_create_offer: true,
|
|
hasAudio: peers[peer].hasAudio,
|
|
hasVideo: peers[peer].hasVideo
|
|
}
|
|
}));
|
|
}
|
|
|
|
/* Add this user as a peer connected to this WebSocket */
|
|
peers[session.name] = {
|
|
ws,
|
|
hasAudio,
|
|
hasVideo
|
|
};
|
|
};
|
|
|
|
const part = (peers, session) => {
|
|
const ws = session.ws;
|
|
|
|
if (!session.name) {
|
|
console.error(`${session.id}: <- part - No name set yet. Audio not available.`);
|
|
return;
|
|
}
|
|
|
|
if (!(session.name in peers)) {
|
|
console.log(`${session.id}: <- ${session.name} - Does not exist in chat audio.`);
|
|
return;
|
|
}
|
|
|
|
console.log(`${session.id}: <- ${session.name} - Audio part.`);
|
|
console.log(`${all}: -> removePeer - ${session.name}`);
|
|
|
|
delete peers[session.name];
|
|
|
|
/* Remove this peer from all other peers, and remove each
|
|
* peer from this peer */
|
|
for (let peer in peers) {
|
|
peers[peer].ws.send(JSON.stringify({
|
|
type: 'removePeer',
|
|
data: {'peer_id': session.name}
|
|
}));
|
|
ws.send(JSON.stringify({
|
|
type: 'removePeer',
|
|
data: {'peer_id': session.name}
|
|
}));
|
|
}
|
|
};
|
|
|
|
|
|
const getName = (session) => {
|
|
return session ? (session.name ? session.name : session.id) : 'Admin';
|
|
}
|
|
|
|
const saveChat = async (room) => {
|
|
/* Shallow copy chat, filling its sessions with a shallow copy of sessions so we can then
|
|
* delete the person field from them */
|
|
const reducedChat = Object.assign({}, chat, { sessions: {} }),
|
|
reducedSessions = [];
|
|
|
|
for (let id in room.sessions) {
|
|
const reduced = Object.assign({}, room.sessions[id]);
|
|
if (reduced.person) {
|
|
delete reduced.person;
|
|
}
|
|
if (reduced.ws) {
|
|
delete reduced.ws;
|
|
}
|
|
if (reduced.keepAlive) {
|
|
delete reduced.keepAlive;
|
|
}
|
|
|
|
reducedroom.sessions[id] = reduced;
|
|
|
|
/* Do not send session-id as those are secrets */
|
|
reducedSessions.push(reduced);
|
|
}
|
|
|
|
delete reducedroom.turnTimer;
|
|
delete reducedroom.unselected;
|
|
|
|
/* Save per turn while debugging... */
|
|
room.step = room.step ? room.step : 0;
|
|
/*
|
|
await writeFile(`chats/${room.id}.${room.step++}`, JSON.stringify(reducedChat, null, 2))
|
|
.catch((error) => {
|
|
console.error(`${session.id} Unable to write to chats/${room.id}`);
|
|
console.error(error);
|
|
});
|
|
*/
|
|
await writeFile(`chats/${room.id}`, JSON.stringify(reducedChat, null, 2))
|
|
.catch((error) => {
|
|
console.error(`${session.id} Unable to write to chats/${room.id}`);
|
|
console.error(error);
|
|
});
|
|
}
|
|
|
|
const departLobby = (room, session, color) => {
|
|
const update = {};
|
|
update.unselected = getFilteredUnselected(room);
|
|
|
|
if (session.person) {
|
|
session.person.live = false;
|
|
update.persons = room.persons;
|
|
}
|
|
|
|
if (session.name) {
|
|
if (session.color) {
|
|
addChatMessage(room, null, `${session.name} has disconnected ` +
|
|
`from the room.`);
|
|
} else {
|
|
addChatMessage(room, null, `${session.name} has left the lobby.`);
|
|
}
|
|
update.chat = room.chat;
|
|
} else {
|
|
console.log(`${session.id}: departLobby - ${getName(session)} is ` +
|
|
`being removed from ${room.id}'s sessions.`);
|
|
for (let id in room.sessions) {
|
|
if (room.sessions[id] === session) {
|
|
delete room.sessions[id];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
sendUpdateToPersons(room, update);
|
|
}
|
|
|
|
const all = `[ all ]`;
|
|
const info = `[ info ]`;
|
|
const todo = `[ todo ]`;
|
|
|
|
const sendChatToPerson = (room, session) => {
|
|
console.log(`${session.id}: -> sendChatPerson:${getName(session)} - full chat`);
|
|
if (!session.ws) {
|
|
console.log(`${session.id}: -> sendChatPerson:: Currently no connection`);
|
|
return;
|
|
}
|
|
|
|
let update;
|
|
|
|
/* Only send empty name data to unnamed persons */
|
|
if (!session.name) {
|
|
console.log(`${session.id}: -> sendChatPerson:${getName(session)} - only sending empty name`);
|
|
update = { name: "" };
|
|
} else {
|
|
update = getFilteredChatForPerson(room, session);
|
|
}
|
|
|
|
session.ws.send(JSON.stringify({
|
|
type: 'chat-update',
|
|
update: update
|
|
}));
|
|
};
|
|
|
|
const sendChatToPersons = (room) => {
|
|
console.log(`${all}: -> sendChatPersons - full chat`);
|
|
|
|
for (let key in room.sessions) {
|
|
sendChatToPerson(room, room.sessions[key]);
|
|
}
|
|
};
|
|
|
|
const sendUpdateToPersons = async (room, update) => {
|
|
/* Ensure clearing of a field actually gets sent by setting
|
|
* undefined to 'false'
|
|
*/
|
|
for (let key in update) {
|
|
if (update[key] === undefined) {
|
|
update[key] = false;
|
|
}
|
|
}
|
|
|
|
calculatePoints(room, update);
|
|
|
|
if (debug.update) {
|
|
console.log(`[ all ]: -> sendUpdateToPersons - `, update);
|
|
} else {
|
|
const keys = Object.getOwnPropertyNames(update);
|
|
console.log(`[ all ]: -> sendUpdateToPersons - ${keys.join(',')}`);
|
|
}
|
|
|
|
const message = JSON.stringify({
|
|
type: 'chat-update',
|
|
update
|
|
});
|
|
for (let key in room.sessions) {
|
|
const session = room.sessions[key];
|
|
/* Only send person and chat data to named persons */
|
|
if (!session.name) {
|
|
console.log(`${session.id}: -> sendUpdateToPersons:` +
|
|
`${getName(session)} - only sending empty name`);
|
|
if (session.ws) {
|
|
session.ws.send(JSON.stringify({
|
|
type: 'chat-update',
|
|
update: { name: "" }
|
|
}));
|
|
}
|
|
continue;
|
|
}
|
|
if (!session.ws) {
|
|
console.log(`${session.id}: -> sendUpdateToPersons: ` +
|
|
`Currently no connection.`);
|
|
} else {
|
|
session.ws.send(message);
|
|
}
|
|
}
|
|
}
|
|
|
|
const sendUpdateToPerson = async (room, session, update) => {
|
|
/* If this person does not have a name, *ONLY* send the name, regardless
|
|
* of what is requested */
|
|
if (!session.name) {
|
|
console.log(`${session.id}: -> sendUpdateToPerson:${getName(session)} - only sending empty name`);
|
|
update = { name: "" };
|
|
}
|
|
|
|
/* Ensure clearing of a field actually gets sent by setting
|
|
* undefined to 'false'
|
|
*/
|
|
for (let key in update) {
|
|
if (update[key] === undefined) {
|
|
update[key] = false;
|
|
}
|
|
}
|
|
|
|
calculatePoints(room, update);
|
|
|
|
if (debug.update) {
|
|
console.log(`${session.id}: -> sendUpdateToPerson:${getName(session)} - `, update);
|
|
} else {
|
|
const keys = Object.getOwnPropertyNames(update);
|
|
console.log(`${session.id}: -> sendUpdateToPerson:${getName(session)} - ${keys.join(',')}`);
|
|
}
|
|
|
|
const message = JSON.stringify({
|
|
type: 'chat-update',
|
|
update
|
|
});
|
|
|
|
if (!session.ws) {
|
|
console.log(`${session.id}: -> sendUpdateToPerson: ` +
|
|
`Currently no connection.`);
|
|
} else {
|
|
session.ws.send(message);
|
|
}
|
|
}
|
|
|
|
const getFilteredUnselected = (room) => {
|
|
if (!room.unselected) {
|
|
return [];
|
|
}
|
|
return room.unselected
|
|
.filter(session => session.live)
|
|
.map(session => session.name);
|
|
}
|
|
|
|
const parseChatCommands = (room, message) => {
|
|
/* Chat messages can set chat flags and fields */
|
|
const parts = message.match(/^set +([^ ]*) +(.*)$/i);
|
|
if (!parts || parts.length !== 3) {
|
|
return;
|
|
}
|
|
switch (parts[1].toLowerCase()) {
|
|
case 'chat':
|
|
if (parts[2].trim().match(/^beginner('?s)?( +layout)?/i)) {
|
|
setBeginnerChat(room);
|
|
addChatMessage(room, session, `${session.name} set chat board to the Beginner's Layout.`);
|
|
break;
|
|
}
|
|
const signature = parts[2].match(/^([0-9a-f]{12})-([0-9a-f]{38})-([0-9a-f]{38})/i);
|
|
if (signature) {
|
|
if (setChatFromSignature(room, signature[1], signature[2], signature[3])) {
|
|
room.signature = parts[2];
|
|
addChatMessage(room, session, `${session.name} set chat board to ${parts[2]}.`);
|
|
} else {
|
|
addChatMessage(room, session, `${session.name} requested an invalid chat board.`);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
};
|
|
|
|
const sendError = (session, error) => {
|
|
session.ws.send(JSON.stringify({ type: 'error', error }));
|
|
}
|
|
|
|
const sendWarning = (session, warning) => {
|
|
session.ws.send(JSON.stringify({ type: 'warning', warning }));
|
|
}
|
|
|
|
const getFilteredPersons = (room) => {
|
|
const filtered = {};
|
|
for (let color in room.persons) {
|
|
const person = Object.assign({}, room.persons[color]);
|
|
filtered[color] = person;
|
|
if (person.status === 'Not active') {
|
|
if (room.state !== 'lobby') {
|
|
delete filtered[color];
|
|
}
|
|
continue;
|
|
}
|
|
person.resources = 0;
|
|
[ 'wheat', 'brick', 'sheep', 'stone', 'wood' ].forEach(resource => {
|
|
person.resources += person[resource];
|
|
delete person[resource];
|
|
});
|
|
delete person.development;
|
|
}
|
|
return filtered;
|
|
};
|
|
|
|
const calculatePoints = (room, update) => {
|
|
if (room.state === 'winner') {
|
|
return;
|
|
}
|
|
/* Calculate points and determine if there is a winner */
|
|
for (let key in room.persons) {
|
|
const person = room.persons[key];
|
|
if (person.status === 'Not active') {
|
|
continue;
|
|
}
|
|
const currentPoints = person.points;
|
|
|
|
person.points = 0;
|
|
if (key === room.longestRoad) {
|
|
person.points += 2;
|
|
}
|
|
if (key === room.largestArmy) {
|
|
person.points += 2;
|
|
}
|
|
if (key === room.mostPorts) {
|
|
person.points += 2;
|
|
}
|
|
if (key === room.mostDeveloped) {
|
|
person.points += 2;
|
|
}
|
|
person.points += MAX_SETTLEMENTS - person.settlements;
|
|
person.points += 2 * (MAX_CITIES - person.cities);
|
|
|
|
person.unplayed = 0;
|
|
person.potential = 0;
|
|
person.development.forEach(card => {
|
|
if (card.type === 'vp') {
|
|
if (card.played) {
|
|
person.points++;
|
|
} else {
|
|
person.potential++;
|
|
}
|
|
}
|
|
if (!card.played) {
|
|
person.unplayed++;
|
|
}
|
|
});
|
|
|
|
if (person.points === currentPoints) {
|
|
continue;
|
|
}
|
|
|
|
if (person.points < getVictoryPointRule(room)) {
|
|
update.persons = getFilteredPersons(room);
|
|
continue;
|
|
}
|
|
|
|
/* This person has enough points! Check if they are the current
|
|
* person and if so, declare victory! */
|
|
console.log(`${info}: Whoa! ${person.name} has ${person.points}!`);
|
|
for (let key in room.sessions) {
|
|
if (room.sessions[key].color !== person.color
|
|
|| room.sessions[key].status === 'Not active') {
|
|
continue;
|
|
}
|
|
const message = `Wahoo! ${person.name} has ${person.points} ` +
|
|
`points on their turn and has won!`;
|
|
addChatMessage(room, null, message)
|
|
console.log(`${info}: ${message}`);
|
|
update.winner = Object.assign({}, person, {
|
|
state: 'winner',
|
|
stolen: room.stolen,
|
|
chat: room.chat,
|
|
turns: room.turns,
|
|
persons: room.persons,
|
|
elapsedTime: Date.now() - room.startTime
|
|
});
|
|
room.winner = update.winner;
|
|
room.state = 'winner';
|
|
room.waiting = [];
|
|
stopTurnTimer(room);
|
|
sendUpdateToPersons(room, {
|
|
state: room.state,
|
|
winner: room.winner,
|
|
persons: room.persons /* unfiltered */
|
|
});
|
|
}
|
|
}
|
|
|
|
/* If the chat isn't in a win state, do not share development card information
|
|
* with other persons */
|
|
if (room.state !== 'winner') {
|
|
for (let key in room.persons) {
|
|
const person = room.persons[key];
|
|
if (person.status === 'Not active') {
|
|
continue;
|
|
}
|
|
delete person.potential;
|
|
}
|
|
}
|
|
}
|
|
|
|
const clearChat = (room, session) => {
|
|
resetChat(room);
|
|
addChatMessage(room, null,
|
|
`The chat has been reset. You can play again with this board, or ` +
|
|
`click 'New board' to mix things up a bit.`);
|
|
sendChatToPersons(room);
|
|
};
|
|
|
|
const gotoLobby = (room, session) => {
|
|
if (!room.waiting) {
|
|
room.waiting = [];
|
|
}
|
|
const already = room.waiting.indexOf(session.name) !== -1;
|
|
|
|
const waitingFor = [];
|
|
for (let key in room.sessions) {
|
|
if (room.sessions[key] === session) {
|
|
continue;
|
|
}
|
|
|
|
if (room.sessions[key].person && room.waiting.indexOf(room.sessions[key].name) == -1) {
|
|
waitingFor.push(room.sessions[key].name);
|
|
}
|
|
}
|
|
|
|
if (!already) {
|
|
room.waiting.push(session.name);
|
|
addChatMessage(room, null, `${session.name} has gone to the lobby.`);
|
|
} else if (waitingFor.length !== 0) {
|
|
return `You are already waiting in the lobby. ` +
|
|
`${waitingFor.join(',')} still needs to go to the lobby.`;
|
|
}
|
|
|
|
if (waitingFor.length === 0) {
|
|
resetChat(room);
|
|
addChatMessage(room, null, `All persons are back to the lobby.`);
|
|
addChatMessage(room, null,
|
|
`The chat has been reset. You can play again with this board, or `+
|
|
`click 'New board' to mix things up a bit.`);
|
|
sendChatToPersons(room);
|
|
return;
|
|
}
|
|
|
|
addChatMessage(room, null, `Waiting for ${waitingFor.join(',')} to go to lobby.`);
|
|
sendUpdateToPersons(room, {
|
|
chat: room.chat
|
|
});
|
|
}
|
|
|
|
router.ws("/ws/:id", async (ws, req) => {
|
|
console.log(`WebSocket`);
|
|
if (!req.cookies || !req.cookies.person) {
|
|
ws.send(JSON.stringify({ type: 'error', error: `Unable to find session cookie` }));
|
|
return;
|
|
}
|
|
|
|
const { id } = req.params;
|
|
const roomId = id;
|
|
|
|
const short = `[${req.cookies.person.substring(0, 8)}]`;
|
|
ws.id = short;
|
|
|
|
console.log(`${short}: Chat ${roomId} - New connection from client.`);
|
|
if (!(id in audio)) {
|
|
audio[id] = {}; /* List of peer sockets using session.name as index. */
|
|
console.log(`${short}: Chat ${id} - New Chat Audio`);
|
|
} else {
|
|
console.log(`${short}: Chat ${id} - Already has Audio`);
|
|
}
|
|
|
|
/* Setup WebSocket event handlers prior to performing any async calls or
|
|
* we may miss the first messages from clients */
|
|
ws.on('error', async (event) => {
|
|
console.error(`WebSocket error: `, event.message);
|
|
const chat = await loadChat(roomId);
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
const session = getSession(room, req.cookies.person);
|
|
session.live = false;
|
|
if (session.ws) {
|
|
session.ws.close();
|
|
session.ws = undefined;
|
|
}
|
|
|
|
departLobby(room, session);
|
|
});
|
|
|
|
ws.on('close', async (event) => {
|
|
console.log(`${short} - closed connection`);
|
|
|
|
const chat = await loadChat(roomId);
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
const session = getSession(room, req.cookies.person);
|
|
if (session.person) {
|
|
session.person.live = false;
|
|
}
|
|
session.live = false;
|
|
if (session.ws) {
|
|
/* Cleanup any voice channels */
|
|
if (id in audio) {
|
|
part(audio[id], session);
|
|
}
|
|
session.ws.close();
|
|
session.ws = undefined;
|
|
console.log(`${short}:WebSocket closed for ${getName(session)}`);
|
|
}
|
|
|
|
departLobby(room, session);
|
|
|
|
/* Check for a chat in the Winner state with no more connections
|
|
* and remove it */
|
|
if (room.state === 'lobby') {
|
|
let dead = true;
|
|
for (let id in room.sessions) {
|
|
if (room.sessions[id].live && room.sessions[id].name) {
|
|
dead = false;
|
|
}
|
|
}
|
|
if (dead) {
|
|
console.log(`${session.id}: No more persons in ${room.id}. ` +
|
|
`Removing.`);
|
|
addChatMessage(room, null, `No more active persons in room. ` +
|
|
`It is being removed from the server.`);
|
|
sendUpdateToPersons(room, {
|
|
chat: room.chat
|
|
});
|
|
for (let id in room.sessions) {
|
|
if (room.sessions[id].ws) {
|
|
room.sessions[id].ws.close();
|
|
delete room.sessions[id];
|
|
}
|
|
}
|
|
delete audio[id];
|
|
delete chats[id];
|
|
try {
|
|
fs.unlinkSync(`chats/${id}`);
|
|
} catch (error) {
|
|
console.error(`${session.id}: Unable to remove chats/${id}`);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
ws.on('message', async (message) => {
|
|
let data;
|
|
try {
|
|
data = JSON.parse(message);
|
|
} catch (error) {
|
|
console.error(`${all}: parse error`, message);
|
|
return;
|
|
}
|
|
const chat = await loadChat(roomId);
|
|
const session = getSession(room, req.cookies.person);
|
|
if (!session.ws) {
|
|
session.ws = ws;
|
|
}
|
|
if (session.person) {
|
|
session.person.live = true;
|
|
}
|
|
session.live = true;
|
|
session.lastActive = Date.now();
|
|
|
|
let error, warning, update, processed = true;
|
|
|
|
switch (data.type) {
|
|
case 'join':
|
|
join(audio[id], session, data.config);
|
|
break;
|
|
|
|
case 'part':
|
|
part(audio[id], session);
|
|
break;
|
|
|
|
case 'relayICECandidate': {
|
|
if (!(id in audio)) {
|
|
console.error(`${session.id}:${id} <- relayICECandidate - Does not have Audio`);
|
|
return;
|
|
}
|
|
|
|
const { peer_id, candidate } = data.config;
|
|
if (debug.audio) console.log(`${short}:${id} <- relayICECandidate ${getName(session)} to ${peer_id}`,
|
|
candidate);
|
|
|
|
message = JSON.stringify({
|
|
type: 'iceCandidate',
|
|
data: {'peer_id': getName(session), 'candidate': candidate }
|
|
});
|
|
|
|
if (peer_id in audio[id]) {
|
|
audio[id][peer_id].ws.send(message);
|
|
}
|
|
} break;
|
|
|
|
case 'relaySessionDescription': {
|
|
if (!(id in audio)) {
|
|
console.error(`${id} - relaySessionDescription - Does not have Audio`);
|
|
return;
|
|
}
|
|
const { peer_id, session_description } = data.config;
|
|
if (debug.audio) console.log(`${short}:${id} - relaySessionDescription ${getName(session)} to ${peer_id}`,
|
|
session_description);
|
|
message = JSON.stringify({
|
|
type: 'sessionDescription',
|
|
data: {'peer_id': getName(session), 'session_description': session_description }
|
|
});
|
|
if (peer_id in audio[id]) {
|
|
audio[id][peer_id].ws.send(message);
|
|
}
|
|
} break;
|
|
|
|
case 'pong':
|
|
resetDisconnectCheck(room, req);
|
|
break;
|
|
|
|
case 'chat-update':
|
|
console.log(`${short}: <- chat-update ${getName(session)} - full chat update.`);
|
|
sendChatToPerson(room, session);
|
|
break;
|
|
|
|
case 'person-name':
|
|
console.log(`${short}: <- person-name:${getName(session)} - setPersonName - ${data.name}`)
|
|
error = setPersonName(room, session, data.name);
|
|
if (error) {
|
|
sendError(session, error);
|
|
}else {
|
|
saveChat(room);
|
|
}
|
|
break;
|
|
|
|
case 'set':
|
|
console.log(`${short}: <- set:${getName(session)} ${data.field} = ${data.value}`);
|
|
switch (data.field) {
|
|
case 'state':
|
|
warning = setChatState(room, session, data.value);
|
|
if (warning) {
|
|
sendWarning(session, warning);
|
|
} else {
|
|
saveChat(room);
|
|
}
|
|
break;
|
|
|
|
case 'color':
|
|
warning = setPersonColor(room, session, data.value);
|
|
if (warning) {
|
|
sendWarning(session, warning);
|
|
} else {
|
|
saveChat(room);
|
|
}
|
|
break;
|
|
default:
|
|
console.warn(`WARNING: Requested SET unsupported field: ${data.field}`);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case 'get':
|
|
console.log(`${short}: <- get:${getName(session)} ${data.fields.join(',')}`);
|
|
update = {};
|
|
data.fields.forEach((field) => {
|
|
switch (field) {
|
|
case 'person':
|
|
sendWarning(session, `'person' is not a valid item. use 'private' instead`);
|
|
update.person = undefined;
|
|
break;
|
|
case 'id':
|
|
case 'chat':
|
|
case 'activities':
|
|
update[field] = room[field];
|
|
break;
|
|
case 'name':
|
|
update.name = session.name;
|
|
break;
|
|
case 'unselected':
|
|
update.unselected = getFilteredUnselected(room);
|
|
break;
|
|
case 'private':
|
|
update.private = session.person;
|
|
break;
|
|
case 'persons':
|
|
update.persons = getFilteredPersons(room);
|
|
break;
|
|
case 'color':
|
|
console.log(`${session.id}: -> Returning color as ${session.color} for ${getName(session)}`);
|
|
update.color = session.color;
|
|
break;
|
|
case 'timestamp':
|
|
update.timestamp = Date.now();
|
|
break;
|
|
default:
|
|
if (field in room) {
|
|
console.warn(`${short}: WARNING: Requested GET not-privatized/sanitized field: ${field}`);
|
|
update[field] = room[field];
|
|
} else {
|
|
if (field in session) {
|
|
console.warn(`${short}: WARNING: Requested GET not-sanitized session field: ${field}`);
|
|
update[field] = session[field];
|
|
} else {
|
|
console.warn(`${short}: WARNING: Requested GET unsupported field: ${field}`);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
sendUpdateToPerson(room, session, update);
|
|
break;
|
|
|
|
case 'chat':
|
|
console.log(`${short}:${id} - ${data.type} - ${data.message}`)
|
|
addChatMessage(room, session, `${session.name}: ${data.message}`, true);
|
|
parseChatCommands(room, data.message);
|
|
sendUpdateToPersons(room, { chat: room.chat });
|
|
saveChat(room);
|
|
break;
|
|
|
|
case 'media-status':
|
|
console.log(`${short}: <- media-status - `, data.audio, data.video);
|
|
session.video = data.video;
|
|
session.audio = data.audio;
|
|
break;
|
|
|
|
default:
|
|
processed = false;
|
|
break;
|
|
}
|
|
|
|
if (processed) {
|
|
/* saveChat(room); -- do not save here; only save on changes */
|
|
return;
|
|
}
|
|
|
|
/* The rest of the actions and commands require an active chat
|
|
* participant */
|
|
|
|
if (!session.person) {
|
|
error = `Person must have an active color.`;
|
|
sendError(session, error);
|
|
return;
|
|
}
|
|
|
|
processed = true;
|
|
const priorSession = session;
|
|
|
|
switch (data.type) {
|
|
case 'clear-chat':
|
|
console.log(`${short}: <- clear-chat:${getName(session)}`);
|
|
warning = clearChat(room, session);
|
|
if (warning) {
|
|
sendWarning(session, warning);
|
|
}
|
|
break;
|
|
case 'goto-lobby':
|
|
console.log(`${short}: <- goto-lobby:${getName(session)}`);
|
|
warning = gotoLobby(room, session);
|
|
if (warning) {
|
|
sendWarning(session, warning);
|
|
}
|
|
break;
|
|
default:
|
|
console.warn(`Unsupported request: ${data.type}`);
|
|
processed = false;
|
|
break;
|
|
}
|
|
|
|
/* If action was taken, persist the chat */
|
|
if (processed) {
|
|
saveChat(room);
|
|
}
|
|
|
|
/* If the current person took an action, reset the session timer */
|
|
if (processed && session.color === room.turn.color && room.state !== 'winner') {
|
|
resetTurnTimer(room, session);
|
|
}
|
|
});
|
|
|
|
|
|
/* This will result in the node tick moving forward; if we haven't already
|
|
* setup the event handlers, a 'message' could come through prior to this
|
|
* completing */
|
|
const room = await loadChat(roomId);
|
|
if (!room) {
|
|
console.error(`Unable to load/create new chat for WS request.`);
|
|
return;
|
|
}
|
|
|
|
const session = getSession(room, req.cookies.person);
|
|
session.ws = ws;
|
|
if (session.person) {
|
|
session.person.live = true;
|
|
}
|
|
session.live = true;
|
|
session.lastActive = Date.now();
|
|
if (session.name) {
|
|
sendUpdateToPersons(room, {
|
|
persons: getFilteredPersons(room),
|
|
unselected: getFilteredUnselected(room)
|
|
});
|
|
}
|
|
|
|
/* If the current turn person just rejoined, set their turn timer */
|
|
if (room.turn && room.turn.color === session.color && room.state !== 'winner') {
|
|
resetTurnTimer(room, session);
|
|
}
|
|
|
|
if (session.name) {
|
|
if (session.color) {
|
|
addChatMessage(room, null, `${session.name} has reconnected to the room.`);
|
|
} else {
|
|
addChatMessage(room, null, `${session.name} has rejoined the lobby.`);
|
|
}
|
|
sendUpdateToPersons(room, { chat: room.chat });
|
|
}
|
|
|
|
resetDisconnectCheck(room, req);
|
|
console.log(`${short}: Chat ${id} - WebSocket connect from ${getName(session)}`);
|
|
|
|
/* Send initial ping to initiate communication with client */
|
|
if (!session.keepAlive) {
|
|
console.log(`${short}: Sending initial ping`);
|
|
ping(session);
|
|
} else {
|
|
clearTimeout(session.keepAlive);
|
|
session.keepAlive = setTimeout(() => { ping(session); }, 2500);
|
|
}
|
|
});
|
|
|
|
const getFilteredChatForPerson = (room, session) => {
|
|
/* Shallow copy chat, filling its sessions with a shallow copy of
|
|
* sessions so we can then delete the person field from them */
|
|
const reducedChat = Object.assign({}, chat, { sessions: {} }),
|
|
reducedSessions = [];
|
|
|
|
for (let id in room.sessions) {
|
|
const reduced = Object.assign({}, room.sessions[id]);
|
|
if (reduced.person) {
|
|
delete reduced.person;
|
|
}
|
|
if (reduced.ws) {
|
|
delete reduced.ws;
|
|
}
|
|
if (reduced.keepAlive) {
|
|
delete reduced.keepAlive;
|
|
}
|
|
reducedroom.sessions[id] = reduced;
|
|
|
|
/* Do not send session-id as those are secrets */
|
|
reducedSessions.push(reduced);
|
|
}
|
|
|
|
const person = session.person ? session.person : undefined;
|
|
|
|
/* Strip out data that should not be shared with persons */
|
|
delete reducedroom.developmentCards;
|
|
|
|
/* Delete the chat timer */
|
|
delete reducedroom.turnTimer;
|
|
|
|
reducedroom.unselected = getFilteredUnselected(room);
|
|
|
|
return Object.assign(reducedChat, {
|
|
live: true,
|
|
timestamp: Date.now(),
|
|
status: session.error ? session.error : "success",
|
|
name: session.name,
|
|
color: session.color,
|
|
order: (session.color in room.persons) ? room.persons[session.color].order : 0,
|
|
private: person,
|
|
sessions: reducedSessions,
|
|
layout: layout,
|
|
persons: getFilteredPersons(room),
|
|
});
|
|
}
|
|
|
|
const resetChat = (room) => {
|
|
Object.assign(room, {
|
|
startTime: Date.now(),
|
|
state: 'lobby',
|
|
chat: [],
|
|
activities: [],
|
|
persons: room.persons,
|
|
active: 0
|
|
});
|
|
|
|
stopTurnTimer(room);
|
|
|
|
/* Populate the chat corner and road placement data as cleared */
|
|
for (let i = 0; i < layout.corners.length; i++) {
|
|
room.placements.corners[i] = {
|
|
color: undefined,
|
|
type: undefined
|
|
};
|
|
}
|
|
|
|
/* Reset all person data, and add in any missing colors */
|
|
[ 'R', 'B', 'W', 'O' ].forEach(color => {
|
|
if (color in room.persons) {
|
|
clearPerson(room.persons[color]);
|
|
} else {
|
|
room.persons[color] = newPerson(color);
|
|
}
|
|
});
|
|
|
|
/* Ensure sessions are connected to person objects */
|
|
for (let key in room.sessions) {
|
|
const session = room.sessions[key];
|
|
if (session.color) {
|
|
room.active++;
|
|
session.person = room.persons[session.color];
|
|
session.person.status = 'Active';
|
|
session.person.lastActive = Date.now();
|
|
session.person.live = session.live;
|
|
session.person.name = session.name;
|
|
session.person.color = session.color;
|
|
}
|
|
}
|
|
}
|
|
|
|
const createChat = (id) => {
|
|
/* Look for a new chat with random words that does not already exist */
|
|
while (!id) {
|
|
id = randomWords(4).join('-');
|
|
try {
|
|
/* If file can be read, it already exists so look for a new name */
|
|
accessSync(`chats/${id}`, fs.F_OK);
|
|
id = '';
|
|
} catch (error) {
|
|
break;
|
|
}
|
|
}
|
|
console.log(`${info}: creating ${id}`);
|
|
|
|
const room = {
|
|
id: id,
|
|
persons: {
|
|
O: newPerson('O'),
|
|
R: newPerson('R'),
|
|
B: newPerson('B'),
|
|
W: newPerson('W')
|
|
},
|
|
sessions: {},
|
|
unselected: [],
|
|
chat: [],
|
|
active: 0,
|
|
};
|
|
|
|
addChatMessage(room, null, `New chat created: ${room.id}`);
|
|
|
|
chats[room.id] = room;
|
|
audio[room.id] = {};
|
|
return room;
|
|
};
|
|
|
|
/* Simple NO-OP to set session cookie so person-id can use it as the
|
|
* index */
|
|
router.get("/", (req, res/*, next*/) => {
|
|
let personId;
|
|
if (!req.cookies.person) {
|
|
personId = crypto.randomBytes(16).toString('hex');
|
|
res.cookie('person', personId);
|
|
} else {
|
|
personId = req.cookies.person;
|
|
}
|
|
|
|
console.log(`[${personId.substring(0, 8)}]: Browser hand-shake achieved.`);
|
|
|
|
return res.status(200).send({ person: personId });
|
|
});
|
|
|
|
router.post("/:id?", async (req, res/*, next*/) => {
|
|
const { id } = req.params;
|
|
|
|
let personId;
|
|
if (!req.cookies.person) {
|
|
personId = crypto.randomBytes(16).toString('hex');
|
|
res.cookie('person', personId);
|
|
} else {
|
|
personId = req.cookies.person;
|
|
}
|
|
|
|
if (id) {
|
|
console.log(`[${personId.substring(0,8)}]: Attempting load of ${id}`);
|
|
} else {
|
|
console.log(`[${personId.substring(0,8)}]: Creating new room.`);
|
|
}
|
|
const room = await loadChat(id); /* will create chat if it doesn't exist */
|
|
console.log(`[${personId.substring(0,8)}]: ${room.id} loaded.`);
|
|
|
|
return res.status(200).send({ id: room.id });
|
|
});
|
|
|
|
|
|
module.exports = router;
|