diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6c57f5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.json diff --git a/README.md b/README.md index 5810b0b..33b4471 100644 --- a/README.md +++ b/README.md @@ -1 +1,2 @@ # mpp-server +Attempt at making a MPP Server. \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..755561c --- /dev/null +++ b/index.js @@ -0,0 +1,18 @@ +//call new Server +global.WebSocket = require('ws'); +global.EventEmitter = require('events').EventEmitter; +global.fs = require('fs'); +global.createKeccakHash = require('keccak'); +const AsyncConsole = require('asyncconsole') + +let Server = require("./src/Server.js"); +global.SERVER = new Server({ + port: 8080 +}); +let console = new AsyncConsole("", input => { + try { + console.log(JSON.stringify(eval(input))); + } catch(e) { + console.log(e.toString()); + } +}) diff --git a/src/Client.js b/src/Client.js new file mode 100644 index 0000000..887a629 --- /dev/null +++ b/src/Client.js @@ -0,0 +1,81 @@ +const Room = require("./Room.js"); +require('node-json-color-stringify'); +class Client extends EventEmitter { + constructor(ws, req, server) { + super(); + EventEmitter.call(this); + this.user; + this.connectionid = server.connectionid; + this.server = server; + this.participantId; + this.channel; + this.ws = ws; + this.req = req; + this.ip = (req.connection.remoteAddress).replace("::ffff:",""); + this.bindEventListeners(); + require('./Message.js')(this); + } + isConnected() { + return this.ws && this.ws.readyState === WebSocket.OPEN; + } + isConnecting() { + return this.ws && this.ws.readyState === WebSocket.CONNECTING; + } + setChannel(_id, settings) { + if (this.server.rooms.get(_id)) { + let channel = this.channel; + if (channel) this.channel.emit("bye", this); + if (channel) this.channel.updateCh(); + this.channel = this.server.rooms.get(_id); + this.channel.join(this); + } else { + let room = new Room(this.server, _id, settings); + this.server.rooms.set(_id, room); + if (this.channel) this.channel.emit("bye", this); + this.channel = this.server.rooms.get(_id); + this.channel.join(this); + } + } + sendArray(arr) { + if (this.isConnected()) { + console.log(`SEND: `, JSON.colorStringify(arr)); + this.ws.send(JSON.stringify(arr)); + } + } + destroy() { + this.ws.close(); + this.user; + this.participantId; + this.channel; + this.connectionid; + this.server.connections.delete(this.connectionid); + if (this.channel) { + this.channel.emit("bye", this) + } + console.log(`Removed Connection ${this.connectionid}.`); + } + bindEventListeners() { + this.ws.on("message", (evt) => { + try { + var transmission = JSON.parse(evt); + } catch (e) { + this.destroy(); + } finally { + for (let msg of transmission) { + if (!msg.hasOwnProperty("m")) return; + if (!this.server.legit_m.includes(msg.m)) return; + this.emit(msg.m, msg); + console.log(`RECIEVE: `, JSON.colorStringify(msg)); + } + } + }); + this.ws.on("close", () => { + this.destroy(); + }); + this.ws.addEventListener("error", (err) => { + console.error(err); + this.destroy(); + }); + } +} +module.exports = Client; \ No newline at end of file diff --git a/src/ColorEncoder.js b/src/ColorEncoder.js new file mode 100644 index 0000000..d91f04b --- /dev/null +++ b/src/ColorEncoder.js @@ -0,0 +1,16 @@ +function hashCode(str) { // java String#hashCode + var hash = 0; + for (var i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + return hash; +} + +function intToRGB(i){ + var c = (i & 0x00FFFFFF) + .toString(16) + .toUpperCase(); + + return "00000".substring(0, 6 - c.length) + c; +} +module.exports = {hashCode, intToRGB}; \ No newline at end of file diff --git a/src/Message.js b/src/Message.js new file mode 100644 index 0000000..549f6a8 --- /dev/null +++ b/src/Message.js @@ -0,0 +1,32 @@ +const User = require("./User.js"); +const Room = require("./Room.js"); +module.exports = (cl) => { +cl.once("hi", () => { + let user = new User(cl); + user.getUserData().then((data) => { + let msg = {}; + msg.m = "hi"; + msg.motd = cl.server.welcome_motd; + msg.t = Date.now(); + msg.u = data; + msg.v = "1.0 Alpha"; + cl.sendArray([msg]) + cl.user = data; + }) +}) +cl.on("t", msg => { + if (msg.hasOwnProperty("e") && !isNaN(msg.e)) + cl.sendArray([{ + m: "t", + t: Date.now(), + e: msg.e + }]) +}) +cl.on("ch", msg => { + if (!msg.hasOwnProperty("set")) msg.set = {}; + if (msg.hasOwnProperty("_id") && typeof msg._id == "string") { + if (msg._id.length > 512) return; + cl.setChannel(msg._id, msg.set); + } +}) +} \ No newline at end of file diff --git a/src/Quota.js b/src/Quota.js new file mode 100644 index 0000000..016ce46 --- /dev/null +++ b/src/Quota.js @@ -0,0 +1 @@ +//noteQuota.js \ No newline at end of file diff --git a/src/Room.js b/src/Room.js new file mode 100644 index 0000000..aabb832 --- /dev/null +++ b/src/Room.js @@ -0,0 +1,105 @@ +//array of rooms +//room class +//room deleter +//databases in Map + +class Room extends EventEmitter { //clean? + constructor(server, _id, settings) { + super(); + EventEmitter.call(this); + this._id = _id; + this.server = server; + this.settings = { + lobby: true, + visible: settings.visible || true, + crownsolo: settings.crownsolo || false, + chat: settings.chat || true, + color: this.verifyColor(settings.color) || this.server.defaultRoomColor, + color2: this.verifyColor(settings.color) || this.defaultLobbyColor2 + } + this.ppl = new Map(); + this.bindEventListeners(); + this.server.rooms.set(_id, this); + } + join(cl) { + let participantId = createKeccakHash('keccak256').update((Math.random().toString() + cl.ip)).digest('hex').substr(0, 24); + cl.user.id = participantId; + cl.participantId = participantId; + this.ppl.set(participantId, cl); + this.sendArray([{ + color: this.ppl.get(cl.participantId).user.color, + id: this.ppl.get(cl.participantId).participantId, + m: "p", + name: this.ppl.get(cl.participantId).user.name, + x: this.ppl.get(cl.participantId).x || 200, + y: this.ppl.get(cl.participantId).y || 100, + _id: cl.user._id + }], cl) + this.updateCh(cl); + } + remove(p) { + //this.participants.splice(this.participants.findIndex((cl) => cl.participantId == p.participantId), 1); + this.ppl.delete(p.participantId); + this.sendArray([{ + m: "bye", + p: p.participantId + }]); + this.updateCh(); + + } + updateCh(cl) { + if (this.ppl.keys().next().value.length <= 0) this.destroy(); + this.ppl.forEach((usr) => { + this.server.connections.get(usr.connectionid).sendArray([this.fetchData(usr, cl)]) + }) + } + destroy() { + this._id; + console.log(`Deleted room ${this._id}`); + this.settings = {}; + this.ppl; + this.server.rooms.delete(_id); + } + sendArray(arr, not) { + this.ppl.forEach((usr) => { + if (!not || usr.participantId != not.participantId) { + try { + this.server.connections.get(usr.connectionid).sendArray(arr) + } catch (e) { + console.log(e); + } + } + }) + } + fetchData(usr, cl) { + let chppl = []; + [...this.ppl.values()].forEach((a) => { + chppl.push(a.user); + }) + let data = { + m: "ch", + ch: { + count: chppl.length, + settings: this.settings, + _id: this._id + }, + ppl: chppl + } + if (cl) { + if (usr.user.id == cl.user.id) { + data.p = cl.participantId; + } + } + return data; + } + verifyColor(color) { + return color; //TODO make this + } + bindEventListeners() { + this.on("bye", participant => { + this.remove(participant); + }) + } + +} +module.exports = Room; \ No newline at end of file diff --git a/src/Server.js b/src/Server.js new file mode 100644 index 0000000..48ea9b0 --- /dev/null +++ b/src/Server.js @@ -0,0 +1,29 @@ +const Client = require("./Client.js") +class Server extends EventEmitter { + constructor(config) { + super(); + EventEmitter.call(this); + this.wss = new WebSocket.Server({ + port: config.port, + backlog: 100, + verifyClient: function (info, done) { + done(true) + } + }); + this.connectionid = 0; + this.connections = new Map(); + this.rooms = new Map(); + this.wss.on('connection', (ws, req) => { + this.connections.set(++this.connectionid, new Client(ws, req, this)); + }); + this.legit_m = ["a", "bye", "hi", "ch", "+ls", "-ls", "m", "n", "devices", "t", "chset", "userset", "chown", "kickban", "admin message"] + this.welcome_motd = config.motd || "You agree to read this message."; + this._id_Private_Key = config._id_PrivateKey || "boppity"; + this.defaultUsername = config.defaultUsername || "Anonymous"; + this.defaultRoomColor = config.defaultRoomColor || "#3b5054"; + this.defaultLobbyColor = config.defaultLobbyColor || "#19b4b9"; + this.defaultLobbyColor2 = config.defaultLobbyColor2 || "#801014"; + }; +} + +module.exports = Server; \ No newline at end of file diff --git a/src/TODO.txt b/src/TODO.txt new file mode 100644 index 0000000..f9af52b --- /dev/null +++ b/src/TODO.txt @@ -0,0 +1,2 @@ + +Room.js make color verifier diff --git a/src/User.js b/src/User.js new file mode 100644 index 0000000..2514265 --- /dev/null +++ b/src/User.js @@ -0,0 +1,58 @@ +const ColorEncoder = require("./ColorEncoder.js"); +class User { + constructor(cl) { + this.cl = cl; + this.userdb; + this.server = this.cl.server; + this.default_db = {} + } + async getUserData() { + if (!this.userdb) { + this.setUpDb(); + } + let _id = createKeccakHash('keccak256').update((this.cl.server._id_Private_Key + this.cl.ip)).digest('hex').substr(0, 24); + if (!this.userdb.get(_id)) { + this.userdb.set(_id, { + "color": `#${ColorEncoder.intToRGB(ColorEncoder.hashCode(_id)).toLowerCase()}`, + "name": this.server.defaultUsername, + "_id": _id, + "ip": this.cl.ip + }); + this.updatedb(); + } + let user = this.userdb.get(_id); + return { + "color": user.color, + "name": user.name, + "_id": user._id, + } + } + updatedb() { + fs.writeFileSync('src/db/users.json', JSON.stringify(this.strMapToObj(this.userdb), null, 2), (err) => { + if (err) { + throw err; + } + }); + } + strMapToObj(strMap) { + let obj = Object.create(null); + for (let [k, v] of strMap) { + obj[k] = v; + } + return obj; + } + setUpDb() { + let files = fs.readdirSync("src/db/"); + if (!files.includes("users.json")) { + fs.writeFileSync('src/db/users.json', JSON.stringify(this.default_db, null, 2), (err) => { + if (err) { + throw err; + } + }); + this.userdb = new Map(Object.entries(require("./db/users.json"))); + } else { + this.userdb = new Map(Object.entries(require("./db/users.json"))); + } + } +} +module.exports = User; \ No newline at end of file diff --git a/src/db/textfile.txt b/src/db/textfile.txt new file mode 100644 index 0000000..e69de29