MWSE/Source/Services/Room.js

748 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const { Client } = require("../Client.js");
let {randomUUID,createHash} = require("crypto");
const joi = require("joi");
let {addService,addListener} = require("../WebSocket.js");
const { termoutput } = require("../config.js");
const { ROOM_CREATED, ROOM_DESTROY, ROOM_UPDATE_PROP, ROOM_JOIN_CLIENT, ROOM_EJECT_CLIENT } = require("../IPC.js");
let term = require("terminal-kit").terminal;
const stats = require("../stats");
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();
this.sync = function(...args){
process.nextTick(()=>{
for (const name of args) {
ROOM_UPDATE_PROP(this.id, name, this[name]);
}
})
};
}
/**
* @param {Room} room
*/
Room.prototype.publish = function(){
stats.mwse_rooms++;
Room.rooms.set(this.id, this);
ROOM_CREATED(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);
ROOM_JOIN_CLIENT(this.id, client.id);
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);
ROOM_DESTROY(this)
stats.mwse_rooms--;
};
/**
* @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);
ROOM_EJECT_CLIENT(this.id, 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();
addListener('connect',(global, 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);
});
addListener('disconnect',(global, client)=>{
Room.rooms.get(client.id).eject(client);
for (const roomId of client.rooms) {
Room.rooms.get(roomId).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(),
});
addService(({
client,
end,
global,
message,
next,
response
})=>{
let {type} = message;
switch(type)
{
case 'myroom-info':{
let room = Room.rooms.get(client.id);
end({
status: "success",
room: room.toJSON()
})
break;
}
case 'room-peers':{
let {roomId,filter} = message;
if(Room.rooms.has(roomId))
{
let filteredPeers = Room.rooms.get(roomId).filterPeers(filter || {});
end({
status: 'success',
peers: filteredPeers.map(i => i.id)
});
}else{
end({
status: 'fail'
})
}
break;
}
case 'room/peer-count':{
let {roomId,filter} = message;
if(Room.rooms.has(roomId))
{
let filteredPeers = Room.rooms.get(roomId).filterPeers(filter || {});
end({
status: 'success',
count: filteredPeers.length
});
}else{
end({
status: 'fail'
})
}
break;
}
case 'room-info':{
let {name} = message;
for (const [roomId,{name:RoomName}] of Room.rooms) {
if(name == RoomName)
{
return end({
status : "success",
room : Room.rooms.get(roomId).toJSON()
})
}
};
end({
status : "fail",
message : "NOT-FOUND-ROOM"
})
break;
}
case 'joinedrooms':{
let data = [
...client.rooms
].map(e => {
return Room.rooms.get(e).toJSON()
});
end(data)
break;
}
case 'closeroom':{
let {roomId} = message;
if(Room.rooms.has(roomId))
{
let room = Room.rooms.get(roomId);
if(room.owner === client.id)
{
room.down();
end({
status: 'success'
});
}else{
end({
status: 'fail'
});
}
}else{
end({
status: 'fail'
})
}
break;
}
case 'create-room':{
let {error} = CreateRoomVerify.validate(message);
if(error)
{
return end({
status: 'fail',
messages: error.message
})
}else{
let {name} = message;
for (const [,{name:RoomName}] of Room.rooms) {
if(name == RoomName)
{
return end({
status : "fail",
message : "ALREADY-EXISTS"
})
}
}
let room = new Room();
room.accessType = message.accessType;
room.notifyActionInvite = message.notifyActionInvite;
room.notifyActionJoined = message.notifyActionJoined;
room.notifyActionEjected = message.notifyActionEjected;
room.joinType = message.joinType;
room.description = message.description;
room.name = message.name;
room.owner = client;
if(message.credential)
{
room.credential = Sha256(message.credential + "");
}
room.publish();
room.join(client);
end({
status: "success",
room: room.toJSON()
});
}
break;
}
case 'joinroom':{
let {name,autoFetchInfo} = message;
let roomId;
for (const [_roomId,{name:RoomName}] of Room.rooms) {
if(name == RoomName)
{
roomId = _roomId
break;
}
}
let isRoom = Room.rooms.has(roomId);
if(isRoom)
{
let room = Room.rooms.get(roomId);
if(room.joinType == "lock")
{
return end({
status : "fail",
message : "LOCKED-ROOM"
})
}else if(room.joinType == "password")
{
if(room.credential == Sha256(message.credential + ""))
{
let info = {};
if(autoFetchInfo)
{
info.info = room.getInfo();
};
room.join(client);
return end({
status : "success",
room: room.toJSON()
})
}else return end({
status : "fail",
message : "WRONG-PASSWORD",
area: "credential"
})
}else if(room.joinType == "free"){
let info = {};
if(autoFetchInfo)
{
info.info = room.getInfo();
};
room.join(client);
return end({
status : "success",
room: room.toJSON(),
...info
})
}else 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"]);
}
};
}else{
return end({
status : "fail",
message : "NOT-FOUND-ROOM"
})
}
break;
}
case 'ejectroom':{
let {roomId} = message;
let isRoom = Room.rooms.has(roomId);
if(isRoom)
{
let room = Room.rooms.get(roomId);
if(room.clients.has(client.id))
{
room.eject(client)
return end({
status : "success"
})
}else{
return end({
status : "fail",
message : "ALREADY-ROOM-OUT"
})
}
}else{
return end({
status : "fail",
message : "NOT-FOUND-ROOM"
})
}
break;
}
case 'accept/invite-room':{
let {roomId, clientId} = message;
// Odanın varlığının kontrolü
if(!Room.rooms.has(roomId))
{
end({
status : "fail",
message : "NOT-FOUND-ROOM"
})
};
let room = Room.rooms.get(roomId);
// erişim kontrolü
if(!client.rooms.has(room.id))
{
return end({
status : "fail",
message : "FORBIDDEN-INVITE-ACTIONS"
})
}
// Odaya katılma şeklinin doğruluğu
if(room.joinType == 'invite')
{
return end({
status : "fail",
message : "INVALID-DATA"
})
};
// Odaya katılma talebinin doğruluğu
if(!room.waitingInvited.includes(clientId))
{
return end({
status : "fail",
message : "NO-WAITING-INVITED"
})
};
// Odaya katılan kişinin varlığı
if(!Client.clients.has(clientId))
{
return end({
status : "fail",
message : "NO-CLIENT"
})
};
// Odaya kişiyi kabul etme
let JoinClient = Client.clients.get(clientId)
room.join(JoinClient);
JoinClient.send([{
status:"accepted"
},'room/invite/status']);
return end({
status : "success"
});
}
case 'reject/invite-room':{
let {roomId, clientId} = message;
// Odanın varlığının kontrolü
if(!Room.rooms.has(roomId))
{
end({
status : "fail",
message : "NOT-FOUND-ROOM"
})
};
let room = Room.rooms.get(roomId);
// erişim kontrolü
if(!client.rooms.has(room.id))
{
return end({
status : "fail",
message : "FORBIDDEN-INVITE-ACTIONS"
})
}
// Odaya katılma şeklinin doğruluğu
if(room.joinType == 'invite')
{
return end({
status : "fail",
message : "INVALID-DATA"
})
};
// Odaya katılma talebinin doğruluğu
if(!room.waitingInvited.includes(clientId))
{
return end({
status : "fail",
message : "NO-WAITING-INVITED"
})
};
// Odaya katılan kişinin varlığı
if(!Client.clients.has(clientId))
{
return end({
status : "fail",
message : "NO-CLIENT"
})
};
// Odaya davet edilen kişiyi reddetme
let 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 end({
status : "success"
});
}
case 'room/list':{
let rooms = [];
for (const [id, {accessType,name,joinType,description}] of Room.rooms)
{
if(accessType == "public")
{
rooms.push({
name,
joinType,
description,
id
})
}
}
end({
type:'public/rooms',
rooms
});
}
case 'room/info':{
let {roomId,name} = message;
// Odanın varlığının kontrolü
if(!Room.rooms.has(roomId))
{
end({
status : "fail",
message : "NOT-FOUND-ROOM"
})
};
let room = Room.rooms.get(roomId);
// erişim kontrolü
if(!client.rooms.has(room.id))
{
return end({
status : "fail",
message : "NO-jıoned-ROOM"
})
}
if(name)
{
return end({
status: "success",
value: room.info.get(name)
});
}else{
return end({
status: "success",
value: room.getInfo()
});
}
}
case 'room/setinfo':{
let {roomId,name,value} = message;
// Odanın varlığının kontrolü
if(!Room.rooms.has(roomId))
{
return end({
status : "fail",
message : "NOT-FOUND-ROOM"
})
};
let room = Room.rooms.get(roomId);
// erişim kontrolü
if(!client.rooms.has(room.id))
{
return end({
status : "fail",
message : "NO-JOINED-ROOM"
})
}
room.info.set(name, value);
room.send(
[
{
name,
value,
roomId:room.id
},
"room/info"
],
client.id,
client => {
return client.roomInfoNotifiable()
}
);
return end({
status: "success"
});
}
default:{
next();
}
}
});
exports.Room = Room;