748 lines
21 KiB
JavaScript
748 lines
21 KiB
JavaScript
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; |