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} */ 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} */ 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} */ 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;