MWSE/Source/Services/Room.js

550 lines
14 KiB
JavaScript

const { Client } = require("../Client.js");
let { randomUUID, createHash } = require("crypto");
const joi = require("joi");
const { on, register } = require("../WebSocket");
const { termoutput } = require("../config.js");
let term = require("terminal-kit").terminal;
function Sha256(update) {
return createHash("sha256").update(update).digest("hex");
};
function Room()
{
/**
* @type {string}
*/
this.id = randomUUID();
/**
* @type {string}
*/
this.name = "";
/**
* @type {string}
*/
this.description = "";
/**
* @type {Client}
*/
this.owner = null;
/**
* @type {Date}
*/
this.createdAt = new Date();
/**
* @type {Map<string, Client>}
*/
this.clients = new Map();
/**
* @type {"public"|"private"}
*/
this.accessType = "";
/**
* @type {"free"|"invite"|"password"|"lock"}
*/
this.joinType = "invite";
/**
* @type {boolean}
*/
this.notifyActionInvite = false;
/**
* @type {boolean}
*/
this.notifyActionJoined = true;
/**
* @type {boolean}
*/
this.notifyActionEjected = true;
/**
* @type {string}
*/
this.credential = null;
/**
* @type {string[]}
*/
this.waitingInvited = new Set();
/**
* @type {Map<string,any>}
*/
this.info = new Map();
}
/**
* @param {Room} room
*/
Room.prototype.publish = function(){
Room.rooms.set(this.id, this);
termoutput && term.green("Room Published ").white(this.name," in ").yellow(this.clients.size).white(" clients")('\n');
};
/**
* @return {Client[]}
*/
Room.prototype.filterPeers = function(optiJson){
let peers = [];
this.clients.forEach(client => {
if(client.match(optiJson))
{
peers.push(client);
}
});
return peers;
};
Room.prototype.toJSON = function(detailed){
let obj = {};
obj.id = this.id;
obj.accessType = this.accessType;
obj.createdAt = this.createdAt;
obj.description = this.description;
obj.joinType = this.joinType;
obj.name = this.name;
obj.owner = this.owner.id;
obj.waitingInvited = [...this.waitingInvited];
if(detailed)
{
obj.credential = this.credential;
obj.notifyActionInvite = this.notifyActionInvite;
obj.notifyActionJoined = this.notifyActionJoined;
obj.notifyActionEjected = this.notifyActionEjected;
obj.clients = [...this.clients.keys()];
}
return obj;
};
Room.prototype.getInfo = function(){
let obj = {};
for (const [name, value] of this.info)
{
obj[name] = value;
}
return obj;
};
/**
* @param {Object} data
* @param {Room} room
*/
Room.fromJSON = function(data, room){
room = room || new Room();
let obj = {};
room.id = data.id;
room.accessType = data.accessType;
room.createdAt = data.createdAt;
room.description = data.description;
room.joinType = data.joinType;
room.name = data.name;
if(data.owner && Client.clients.has(data.owner))
{
room.owner = Client.clients.get(data.owner);
}
room.waitingInvited = new Set(data.waitingInvited);
obj.credential = data.credential;
obj.notifyActionInvite = data.notifyActionInvite;
obj.notifyActionJoined = data.notifyActionJoined;
obj.notifyActionEjected = data.notifyActionEjected;
obj.clients = new Map(
data.clients.map(e => ([
e, // map key
Client.clients.get(e) // map value
])
)
)
return room;
};
/**
*
* @param {any} obj
* @param {string} withOut
* @param {(client:Client) => boolean} map
*/
Room.prototype.send = function(obj, withOut, map){
for (const client of this.clients.values()) {
if(client.id != withOut)
{
(
map ? map(client) : 1
) && client.send(obj);
}
}
termoutput && term.green("Room bulk message ").white(this.name," in ").yellow(this.clients.size + "").white(" clients")('\n');
};
/**
* @param {Client} client
*/
Room.prototype.join = function(client){
if(this.notifyActionJoined)
{
this.send(
[
{
id: client.id,
roomid: this.id,
ownerid: this.owner.id
},
'room/joined'
],
void 0,
client => client.peerInfoNotifiable()
);
};
client.rooms.add(this.id);
this.clients.set(client.id, client);
termoutput && term.green("Client Room joined ").white(this.name," in ").yellow(this.clients.size + "").white(" clients")('\n');
};
Room.prototype.down = function(){
termoutput && term.red("Room is downed ").red(this.name," in ").yellow(this.clients.size + "").red(" clients")('\n');
this.send([{
roomid: this.id,
ownerid: this.owner.id
},'room/closed']);
Room.rooms.delete(this.id);
};
/**
* @param {Client} client
*/
Room.prototype.eject = function(client){
if(this.notifyActionEjected)
{
this.send(
[
{
id: client.id,
roomid: this.id,
ownerid: this.owner.id
},
'room/ejected'
],
client.id,
client => client.peerInfoNotifiable()
);
}
client.rooms.delete(this.id);
this.clients.delete(client.id);
if(this.clients.size == 0)
{
this.down();
termoutput && term.red("Client Room closed ").red(this.name," at 0 clients")('\n');
}
termoutput && term.red("Client Room ejected ").red(this.name," in ").yellow(this.clients.size + "").red(" clients")('\n');
};
/**
* @type {Map<string, Room>}
*/
Room.rooms = new Map();
on('connect', (client) => {
let room = new Room();
room.accessType = "private";
room.joinType = "notify";
room.description = 'Private room';
room.id = client.id;
room.name = "Your Room | " + client.id;
room.owner = client;
room.publish();
room.join(client);
});
on('disconnect', (client) => {
const room = Room.rooms.get(client.id);
if (room) room.eject(client);
for (const roomId of client.rooms) {
const r = Room.rooms.get(roomId);
if (r) r.eject(client);
}
});
let CreateRoomVerify = joi.object({
type: joi.any().required(),
accessType: joi.string().pattern(/^public$|private$/).required(),
notifyActionInvite: joi.boolean().required(),
notifyActionJoined: joi.boolean().required(),
notifyActionEjected: joi.boolean().required(),
joinType: joi.string().pattern(/^free$|^invite$|^password$|^lock$/).required(),
description: joi.string().required(),
name: joi.string().required(),
credential: joi.string().optional(),
ifexistsJoin: joi.boolean().optional(),
autoFetchInfo: joi.boolean().optional(),
});
register('myroom-info', (client, msg) => {
let room = Room.rooms.get(client.id);
return { status: "success", room: room.toJSON() };
});
register('room-peers', (client, msg) => {
const { roomId, filter } = msg;
if (!Room.rooms.has(roomId)) {
return { status: 'fail' };
}
const filteredPeers = Room.rooms.get(roomId).filterPeers(filter || {});
return { status: 'success', peers: filteredPeers.map(i => i.id) };
});
register('room/peer-count', (client, msg) => {
const { roomId, filter } = msg;
if (!Room.rooms.has(roomId)) {
return { status: 'fail' };
}
const filteredPeers = Room.rooms.get(roomId).filterPeers(filter || {});
return { status: 'success', count: filteredPeers.length };
});
register('room-info', (client, msg) => {
const { name } = msg;
for (const [roomId, room] of Room.rooms) {
if (name == room.name) {
return { status: "success", room: room.toJSON() };
}
}
return { status: "fail", message: "NOT-FOUND-ROOM" };
});
register('joinedrooms', (client, msg) => {
return [...client.rooms].map(e => Room.rooms.get(e).toJSON());
});
register('closeroom', (client, msg) => {
const { roomId } = msg;
if (!Room.rooms.has(roomId)) {
return { status: 'fail' };
}
const room = Room.rooms.get(roomId);
if (room.owner === client.id) {
room.down();
return { status: 'success' };
}
return { status: 'fail' };
});
register('create-room', (client, msg) => {
const { error } = CreateRoomValidate.validate(msg);
if (error) {
return { status: 'fail', messages: error.message };
}
const { name } = msg;
for (const [, room] of Room.rooms) {
if (name == room.name) {
return { status: "fail", message: "ALREADY-EXISTS" };
}
}
let room = new Room();
room.accessType = msg.accessType;
room.notifyActionInvite = msg.notifyActionInvite;
room.notifyActionJoined = msg.notifyActionJoined;
room.notifyActionEjected = msg.notifyActionEjected;
room.joinType = msg.joinType;
room.description = msg.description;
room.name = msg.name;
room.owner = client;
if (msg.credential) {
room.credential = Sha256(msg.credential + "");
}
room.publish();
room.join(client);
return { status: "success", room: room.toJSON() };
});
register('joinroom', (client, msg) => {
const { name, autoFetchInfo } = msg;
let roomId;
for (const [_roomId, room] of Room.rooms) {
if (name == room.name) {
roomId = _roomId;
break;
}
}
if (!Room.rooms.has(roomId)) {
return { status: "fail", message: "NOT-FOUND-ROOM" };
}
const room = Room.rooms.get(roomId);
if (room.joinType == "lock") {
return { status: "fail", message: "LOCKED-ROOM" };
}
if (room.joinType == "password") {
if (room.credential == Sha256(msg.credential + "")) {
let info = {};
if (autoFetchInfo) {
info.info = room.getInfo();
}
room.join(client);
return { status: "success", room: room.toJSON(), ...info };
}
return { status: "fail", message: "WRONG-PASSWORD", area: "credential" };
}
if (room.joinType == "free") {
let info = {};
if (autoFetchInfo) {
info.info = room.getInfo();
}
room.join(client);
return { status: "success", room: room.toJSON(), ...info };
}
if (room.joinType == "invite") {
room.waitingInvited.add(client.id);
if (room.notifyActionInvite) {
room.send([{ id: client.id }, "room/invite"]);
} else {
room.owner.send([{ id: client.id }, "room/invite"]);
}
}
return { status: "fail", message: "NOT-FOUND-ROOM" };
});
register('ejectroom', (client, msg) => {
const { roomId } = msg;
if (!Room.rooms.has(roomId)) {
return { status: "fail", message: "NOT-FOUND-ROOM" };
}
const room = Room.rooms.get(roomId);
if (!room.clients.has(client.id)) {
return { status: "fail", message: "ALREADY-ROOM-OUT" };
}
room.eject(client);
return { status: "success" };
});
register('accept/invite-room', (client, msg) => {
const { roomId, clientId } = msg;
if (!Room.rooms.has(roomId)) {
return { status: "fail", message: "NOT-FOUND-ROOM" };
}
const room = Room.rooms.get(roomId);
if (!client.rooms.has(room.id)) {
return { status: "fail", message: "FORBIDDEN-INVITE-ACTIONS" };
}
if (room.joinType == 'invite') {
return { status: "fail", message: "INVALID-DATA" };
}
if (!room.waitingInvited.includes(clientId)) {
return { status: "fail", message: "NO-WAITING-INVITED" };
}
if (!Client.clients.has(clientId)) {
return { status: "fail", message: "NO-CLIENT" };
}
const JoinClient = Client.clients.get(clientId);
room.join(JoinClient);
JoinClient.send([{ status: "accepted" }, 'room/invite/status']);
return { status: "success" };
});
register('reject/invite-room', (client, msg) => {
const { roomId, clientId } = msg;
if (!Room.rooms.has(roomId)) {
return { status: "fail", message: "NOT-FOUND-ROOM" };
}
const room = Room.rooms.get(roomId);
if (!client.rooms.has(room.id)) {
return { status: "fail", message: "FORBIDDEN-INVITE-ACTIONS" };
}
if (room.joinType == 'invite') {
return { status: "fail", message: "INVALID-DATA" };
}
if (!room.waitingInvited.includes(clientId)) {
return { status: "fail", message: "NO-WAITING-INVITED" };
}
if (!Client.clients.has(clientId)) {
return { status: "fail", message: "NO-CLIENT" };
}
const JoinClient = Client.clients.get(clientId);
room.waitingInvited = room.waitingInvited.filter(e => e != clientId);
room.send([{ id: clientId, roomId: room.id }, 'room/invite/status']);
JoinClient.send([{ status: "rejected" }, 'room/invite/status']);
return { status: "success" };
});
register('room/list', (client, msg) => {
const rooms = [];
for (const [id, room] of Room.rooms) {
if (room.accessType == "public") {
rooms.push({
name: room.name,
joinType: room.joinType,
description: room.description,
id
});
}
}
return { type: 'public/rooms', rooms };
});
register('room/info', (client, msg) => {
const { roomId, name } = msg;
if (!Room.rooms.has(roomId)) {
return { status: "fail", message: "NOT-FOUND-ROOM" };
}
const room = Room.rooms.get(roomId);
if (!client.rooms.has(room.id)) {
return { status: "fail", message: "NO-JOINED-ROOM" };
}
if (name) {
return { status: "success", value: room.info.get(name) };
}
return { status: "success", value: room.getInfo() };
});
register('room/setinfo', (client, msg) => {
const { roomId, name, value } = msg;
if (!Room.rooms.has(roomId)) {
return { status: "fail", message: "NOT-FOUND-ROOM" };
}
const room = Room.rooms.get(roomId);
if (!client.rooms.has(room.id)) {
return { status: "fail", message: "NO-JOINED-ROOM" };
}
room.info.set(name, value);
room.send(
[{ name, value, roomId: room.id }, "room/info"],
client.id,
c => c.roomInfoNotifiable()
);
return { status: "success" };
});
exports.Room = Room;