550 lines
14 KiB
JavaScript
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;
|