diff --git a/Quotas.js b/Quotas.js index f706b51..7edc660 100644 --- a/Quotas.js +++ b/Quotas.js @@ -1,21 +1,4 @@ module.exports = Object.seal({ - note: { - lobby: { - allowance: 200, - max: 600, - maxHistLen: 3 - }, - normal: { - allowance: 400, - max: 1200, - maxHistLen: 3 - }, - insane: { - allowance: 600, - max: 1800, - maxHistLen: 3 - } - }, chat: { lobby: { amount: 4, @@ -34,7 +17,7 @@ module.exports = Object.seal({ amount: 10, time: 5000 }, - name: { + userset: { amount: 30, time: 30 * 60000 }, diff --git a/TODO.md b/TODO.md deleted file mode 100644 index da4291c..0000000 --- a/TODO.md +++ /dev/null @@ -1,3 +0,0 @@ -1. Send noteQuota on room owner change. -2. Add noteQuota server side. -3. Room.js make color verifier diff --git a/index.js b/index.js index 8442191..3f047d8 100644 --- a/index.js +++ b/index.js @@ -15,7 +15,7 @@ global.isObj = function(a){ return typeof a === "object" && !Array.isArray(a) && a !== null; } -let Server = require("./src/Server"); +let Server = require("./src/Server.js"); let config = require('./config'); global.SERVER = new Server(config); let console = process.platform == 'win32' ? new AsyncConsole("", input => { diff --git a/src/Client.js b/src/Client.js index c762e68..e02a7c3 100644 --- a/src/Client.js +++ b/src/Client.js @@ -1,4 +1,8 @@ const Room = require("./Room.js"); +const Quota = require ("./Quota.js"); +const quotas = require('../Quotas'); +const RateLimit = require('./RateLimit.js').RateLimit; +const RateLimitChain = require('./RateLimit.js').RateLimitChain; require('node-json-color-stringify'); class Client extends EventEmitter { constructor(ws, req, server) { @@ -9,6 +13,10 @@ class Client extends EventEmitter { this.server = server; this.participantId; this.channel; + this.staticQuotas = { + room: new RateLimit(quotas.room.time) + }; + this.quotas = {}; this.ws = ws; this.req = req; this.ip = (req.connection.remoteAddress).replace("::ffff:", ""); @@ -62,6 +70,25 @@ class Client extends EventEmitter { this.ws.send(JSON.stringify(arr)); } } + initParticipantQuotas() { + this.quotas = { + //"chat": new Quota(Quota.PARAMS_A_NORMAL), + chat: { + lobby: new RateLimitChain(quotas.chat.lobby.amount, quotas.chat.lobby.time), + normal: new RateLimitChain(quotas.chat.normal.amount, quotas.chat.normal.time), + insane: new RateLimitChain(quotas.chat.insane.amount, quotas.chat.insane.time) + }, + cursor: new RateLimit(quotas.cursor.time), + chown: new RateLimitChain(quotas.chown.amount, quotas.chown.time), + userset: new RateLimitChain(quotas.userset.amount, quotas.userset.time), + kickban: new RateLimitChain(quotas.kickban.amount, quotas.kickban.time), + + note: new Quota(Quota.PARAMS_LOBBY), + chset: new Quota(Quota.PARAMS_USED_A_LOT), + "+ls": new Quota(Quota.PARAMS_USED_A_LOT), + "-ls": new Quota(Quota.PARAMS_USED_A_LOT) + } + } destroy() { this.ws.close(); if (this.channel) { diff --git a/src/Message.js b/src/Message.js index 5a26e5b..157a858 100644 --- a/src/Message.js +++ b/src/Message.js @@ -1,6 +1,6 @@ -const NoteQuotas = require('../Quotas'); -let quotas; +const Quota = require('./Quota'); const User = require("./User.js"); +const Room = require("./Room.js"); module.exports = (cl) => { cl.once("hi", () => { let user = new User(cl); @@ -13,7 +13,6 @@ module.exports = (cl) => { msg.v = "Beta"; cl.sendArray([msg]) cl.user = data; - quotas = cl.server.connections[cl.user._id].quotas; }) }) cl.on("t", msg => { @@ -25,24 +24,31 @@ module.exports = (cl) => { }]) }) cl.on("ch", msg => { - if (!quotas.room.attempt()) return; if (!msg.hasOwnProperty("set") || !msg.set) msg.set = {}; if (msg.hasOwnProperty("_id") && typeof msg._id == "string") { if (msg._id.length > 512) return; + if (!cl.staticQuotas.room.attempt()) return; cl.setChannel(msg._id, msg.set); + let param; if (cl.channel.isLobby(cl.channel._id)) { - cl.sendArray([{m: 'nq', allowance: NoteQuotas.note.lobby.allowance, max: NoteQuotas.note.lobby.max, maxHistLen: NoteQuotas.note.lobby.maxHistLen}]) + param = Quota.N_PARAMS_LOBBY; + param.m = "nq"; + cl.sendArray([param]) } else { if (!(cl.user._id == cl.channel.crown.userId)) { - cl.sendArray([{m: 'nq', allowance: NoteQuotas.note.normal.allowance, max: NoteQuotas.note.normal.max, maxHistLen: NoteQuotas.note.normal.maxHistLen}]) + param = Quota.N_PARAMS_NORMAL; + param.m = "nq"; + cl.sendArray([param]) } else { - cl.sendArray([{m: 'nq', allowance: NoteQuotas.note.insane.allowance, max: NoteQuotas.note.insane.max, maxHistLen: NoteQuotas.note.insane.maxHistLen}]) + param = Quota.N_PARAMS_RIDICULOUS; + param.m = "nq"; + cl.sendArray([param]) } } } }) - cl.on("m", msg => { - if (!quotas.cursor.attempt()) return; + cl.on("m", (msg, admin) => { + if (!cl.quotas.cursor.attempt() && !admin) return; if (!(cl.channel && cl.participantId)) return; if (!msg.hasOwnProperty("x")) msg.x = null; if (!msg.hasOwnProperty("y")) msg.y = null; @@ -51,8 +57,8 @@ module.exports = (cl) => { cl.channel.emit("m", cl, msg.x, msg.y) }) - cl.on("chown", msg => { - if (!quotas.chown.attempt()) return; + cl.on("chown", (msg, admin) => { + if (!cl.quotas.chown.attempt() && !admin) return; if (!(cl.channel && cl.participantId)) return; //console.log((Date.now() - cl.channel.crown.time)) //console.log(!(cl.channel.crown.userId != cl.user._id), !((Date.now() - cl.channel.crown.time) > 15000)); @@ -61,9 +67,17 @@ module.exports = (cl) => { // console.log(cl.channel.crown) if (cl.user._id == cl.channel.crown.userId || cl.channel.crowndropped) cl.channel.chown(msg.id); + if (msg.id == cl.user.id) { + param = Quota.N_PARAMS_RIDICULOUS; + param.m = "nq"; + cl.sendArray([param]) + } } else { if (cl.user._id == cl.channel.crown.userId || cl.channel.crowndropped) cl.channel.chown(); + param = Quota.N_PARAMS_NORMAL; + param.m = "nq"; + cl.sendArray([param]) } }) cl.on("chset", msg => { @@ -73,19 +87,19 @@ module.exports = (cl) => { cl.channel.settings = msg.set; cl.channel.updateCh(); }) - cl.on("a", msg => { - if (cl.channel.isLobby(cl.channel._id)) { - if (!quotas.chat.lobby.attempt()) return; - } else { - if (!(cl.user._id == cl.channel.crown.userId)) { - if (!quotas.chat.normal.attempt()) return; - } else { - if (!quotas.chat.insane.attempt()) return; - } - } + cl.on("a", (msg, admin) => { if (!(cl.channel && cl.participantId)) return; if (!msg.hasOwnProperty('message')) return; if (cl.channel.settings.chat) { + if (cl.channel.isLobby(cl.channel._id)) { + if (!cl.quotas.chat.lobby.attempt() && !admin) return; + } else { + if (!(cl.user._id == cl.channel.crown.userId)) { + if (!cl.quotas.chat.normal.attempt() && !admin) return; + } else { + if (!cl.quotas.chat.insane.attempt() && !admin) return; + } + } cl.channel.emit('a', cl, msg); } }) @@ -123,11 +137,11 @@ module.exports = (cl) => { cl.server.roomlisteners.delete(cl.connectionid); }) cl.on("userset", msg => { - if (!quotas.name.attempt()) return; if (!(cl.channel && cl.participantId)) return; if (!msg.hasOwnProperty("set") || !msg.set) msg.set = {}; if (msg.set.hasOwnProperty('name') && typeof msg.set.name == "string") { if (msg.set.name.length > 40) return; + if (!cl.quotas.name.attempt()) return; cl.user.name = msg.set.name; let user = new User(cl); user.getUserData().then((usr) => { @@ -145,10 +159,10 @@ module.exports = (cl) => { } }) cl.on('kickban', msg => { - if (!quotas.kickban.attempt()) return; if (!(cl.channel && cl.participantId)) return; if (!(cl.user._id == cl.channel.crown.userId)) return; if (msg.hasOwnProperty('_id') && typeof msg._id == "string") { + if (!cl.quotas.kickban.attempt() && !admin) return; let _id = msg._id; let ms = msg.ms || 0; cl.channel.kickban(_id, ms); @@ -157,7 +171,7 @@ module.exports = (cl) => { cl.on("bye", msg => { cl.destroy(); }) - cl.on("admin message" || "adminmsg" || "admin msg", msg => { + cl.on("admin message", msg => { if (!(cl.channel && cl.participantId)) return; if (!msg.hasOwnProperty('password') || !msg.hasOwnProperty('msg')) return; if (typeof msg.msg != 'object') return; @@ -165,17 +179,7 @@ module.exports = (cl) => { cl.ws.emit("message", JSON.stringify([msg.msg]), true); }) //admin only stuff - /* - - List of admin only stuff - 1. admin_color - 2. admin_noteColor - 3. admin_chown - 4. admin_kickban - 5. admin_chset - - */ - cl.on('admin_color', (msg, admin) => { + cl.on('color', (msg, admin) => { if (!admin) return; if (typeof cl.channel.verifyColor(msg.color) != 'string') return; if (!msg.hasOwnProperty('id') && !msg.hasOwnProperty('_id')) return; @@ -188,12 +192,10 @@ module.exports = (cl) => { let dbentry = user.userdb.get(uSr._id); if (!dbentry) return; dbentry.color = msg.color; - dbentry.noteColor = msg.color; - user.updatedb(); + //user.updatedb(); cl.server.rooms.forEach((room) => { room.updateParticipant(usr.participantId, { - color: msg.color, - noteColor: msg.color + color: msg.color }); }) }) @@ -201,55 +203,5 @@ module.exports = (cl) => { }) }) - cl.on('admin_noteColor', (msg, admin) => { - if (!admin) return; - if (typeof cl.channel.verifyColor(msg.color) != 'string') return; - if (!msg.hasOwnProperty('id') && !msg.hasOwnProperty('_id')) return; - cl.server.connections.forEach((usr) => { - if ((usr.channel && usr.participantId && usr.user) && (usr.user._id == msg._id || (usr.participantId == msg.id))) { - let user = new User(usr); - //user.getUserData().then((uSr) => { - //if (!uSr._id) return; - //let dbentry = user.userdb.get(uSr._id); - //if (!dbentry) return; - //dbentry.color = msg.color; - //user.updatedb(); - cl.server.rooms.forEach((room) => { - room.updateParticipant(usr.participantId, { - noteColor: msg.color - }); - }) - //}) - } - }) - }) - cl.on("admin_chown", (msg, admin) => { - if (!admin) return; - if (msg.hasOwnProperty("id")) { - cl.channel.chown(msg.id); - console.log(msg.id); - } else { - cl.channel.chown(); - } - }) - cl.on('admin_kickban', (msg, admin) => { - if (!admin) return; - if (msg.hasOwnProperty('_id') && typeof msg._id == "string") { - let _id = msg._id; - let ms = msg.ms || 0; - cl.channel.kickban(_id, ms); - } - }) - cl.on("admin_chset", (msg, admin) => { - if (!admin) return; - if (!msg.hasOwnProperty("set") || !msg.set) msg.set = cl.channel.verifySet(cl.channel._id,{}); - cl.channel.settings = msg.set; - cl.channel.updateCh(); - }) - cl.on("admin_notification", (msg, admin) => { - if (!admin) return; - cl.channel.Notification(msg.content); - console.log(msg.content); - }) } \ No newline at end of file diff --git a/src/Quota.js b/src/Quota.js index a3b303a..da24303 100644 --- a/src/Quota.js +++ b/src/Quota.js @@ -1,58 +1,158 @@ -function RateLimit(a,b){ - this.a = b.a || 1; - this.m = b.m || 10; - this.mh = b.mh || 3; - this.setParams(a,{a:this.a,m:this.m,mh:this.mh}); - this.resetPoints(); - if(a !== null){ - var self = this; - this.giveInt = setInterval(()=>{self.give()},a); - }; -}; -RateLimit.prototype.setParams = function(a,b){ - var a = b.a || this.a || 1; - var m = b.m || this.m || 5; - var mh = b.mh || this.mh || 3; - clearInterval(this.giveInt); - this.giveInt = undefined; - if(a !== this.a || m !== this.m || mh !== this.mh){ - this.a = a; - this.m = m; - this.mh = mh; - this.resetPoints(); - if(a !== null){ - var self = this; - this.giveInt = setInterval(()=>{self.give()},a); - }; - return true; - }; - return false; -}; -RateLimit.prototype.resetPoints = function(){ - this.points = this.m; - this.history = []; - for(var i=0; i this.m) this.points = this.m; - }; -}; -RateLimit.prototype.spend = function(needed){ - var sum = 0; - for(var i in this.history){ - sum += this.history[i]; - }; - if(sum <= 0) needed *= this.a; - if(this.points < needed){ - return false; - }else{ - this.points -= needed; - return true; - }; -}; +//Adaptation of https://gist.github.com/brandon-lockaby/7339587 into modern javascript. +/* +class RateLimit { + constructor(interval_ms) { + this._interval_ms = interval_ms || 0; // (0 means no limit) + this._after = 0; + } + attempt(time) { + var time = time || Date.now(); + if(time < this._after) return false; + this._after = time + this._interval_ms; + return true; + }; -module.exports = RateLimit; \ No newline at end of file + interval(interval_ms) { + this._after += interval_ms - this._interval_ms; + this._interval_ms = interval_ms; + }; +} + +class RateLimitChain(num, interval_ms) { + constructor(num, interval_ms) { + this.setNumAndInterval(num, interval_ms); + } + + attempt(time) { + var time = time || Date.now(); + for(var i = 0; i < this._chain.length; i++) { + if(this._chain[i].attempt(time)) return true; + } + return false; + }; + + setNumAndInterval(num, interval_ms) { + this._chain = []; + for(var i = 0; i < num; i++) { + this._chain.push(new RateLimit(interval_ms)); + } + }; +}*/ + +class Quota { + constructor(params, cb) { + this.cb = cb; + this.setParams(params); + this.resetPoints(); + this.interval; + }; + static N_PARAMS_LOBBY = { + allowance: 200, + max: 600, + interval: 2000 + }; + static N_PARAMS_NORMAL = { + allowance: 400, + max: 1200, + interval: 2000 + }; + static N_PARAMS_RIDICULOUS = { + allowance: 600, + max: 1800, + interval: 2000 + }; + static PARAMS_OFFLINE = { + allowance: 8000, + max: 24000, + maxHistLen: 3, + interval: 2000 + }; + static PARAMS_A_NORMAL = { + allowance: 4, + max: 4, + interval: 6000 + }; + static PARAMS_A_CROWNED = { + allowance:10, + max:10, + interval: 2000 + } + static PARAMS_CH = { + allowance: 1, + max: 2, + interval: 1000 + } + static PARAMS_USED_A_LOT = { + allowance:1, + max:1, + interval: 2000 + } + static PARAMS_M = { + allowance:15000, + max:500000, + interval: 2000 + } + getParams() { + return { + m: "nq", + allowance: this.allowance, + max: this.max, + maxHistLen: this.maxHistLen + }; + }; + setParams(params) { + params = params || Quota.PARAMS_OFFLINE; + var allowance = params.allowance || this.allowance || Quota.PARAMS_OFFLINE.allowance; + var max = params.max || this.max || Quota.PARAMS_OFFLINE.max; + var maxHistLen = params.maxHistLen || this.maxHistLen || Quota.PARAMS_OFFLINE.maxHistLen; + let interval = params.interval || 0 + this.inverval = setInterval(() => { + this.tick(); + }, params.interval) + if (allowance !== this.allowance || max !== this.max || maxHistLen !== this.maxHistLen) { + this.allowance = allowance; + this.max = max; + this.maxHistLen = maxHistLen; + this.resetPoints(); + return true; + } + return false; + }; + resetPoints() { + this.points = this.max; + this.history = []; + for (var i = 0; i < this.maxHistLen; i++) + this.history.unshift(this.points); + if (this.cb) this.cb(this.points); + }; + tick() { + // keep a brief history + this.history.unshift(this.points); + this.history.length = this.maxHistLen; + // hook a brother up with some more quota + if (this.points < this.max) { + this.points += this.allowance; + if (this.points > this.max) this.points = this.max; + // fire callback + if (this.cb) this.cb(this.points); + } + }; + spend(needed) { + // check whether aggressive limitation is needed + var sum = 0; + for (var i in this.history) { + sum += this.history[i]; + } + if (sum <= 0) needed *= this.allowance; + // can they afford it? spend + if (this.points < needed) { + return false; + } else { + this.points -= needed; + if (this.cb) this.cb(this.points); // fire callback + return true; + } + }; +} + +module.exports = Quota \ No newline at end of file diff --git a/src/Room.js b/src/Room.js index 5606217..a314a8e 100644 --- a/src/Room.js +++ b/src/Room.js @@ -2,7 +2,7 @@ //room class //room deleter //databases in Map - +const Quota = require("./Quota.js"); class Room extends EventEmitter { constructor(server, _id, settings) { super(); @@ -11,7 +11,9 @@ class Room extends EventEmitter { this.server = server; this.crown = null; this.crowndropped = false; - this.settings = this.verifySet(this._id,{set:settings}); + this.settings = this.verifySet(this._id, { + set: settings + }); this.chatmsgs = []; this.ppl = new Map(); this.connections = []; @@ -25,7 +27,9 @@ class Room extends EventEmitter { let participantId = createKeccakHash('keccak256').update((Math.random().toString() + cl.ip)).digest('hex').substr(0, 24); cl.user.id = participantId; cl.participantId = participantId; + cl.initParticipantQuotas(); if (((this.connections.length == 0 && Array.from(this.ppl.values()).length == 0) && !this.isLobby(this._id)) || this.crown && (this.crown.userId == cl.user._id)) { //user that created the room, give them the crown. + //cl.quotas.a.setParams(Quota.PARAMS_A_CROWNED); this.crown = { participantId: cl.participantId, userId: cl.user._id, @@ -40,8 +44,11 @@ class Room extends EventEmitter { } } this.crowndropped = false; + } else { + //cl.quotas.a.setParams(Quota.PARAMS_A_NORMAL); } this.ppl.set(participantId, cl); + this.connections.push(cl); this.sendArray([{ color: this.ppl.get(cl.participantId).user.color, @@ -60,6 +67,7 @@ class Room extends EventEmitter { } else { cl.user.id = otheruser.participantId; cl.participantId = otheruser.participantId; + cl.quotas = otheruser.quotas; this.connections.push(cl); cl.sendArray([{ m: "c", @@ -173,12 +181,12 @@ class Room extends EventEmitter { } return data; } - verifyColor(strColor){ + verifyColor(strColor) { var test2 = /^#[0-9A-F]{6}$/i.test(strColor); - if(test2 == true){ - return strColor; - } else{ - return false; + if (test2 == true) { + return strColor; + } else { + return false; } } isLobby(_id) { @@ -186,16 +194,16 @@ class Room extends EventEmitter { let lobbynum = _id.split("lobby")[1]; if (_id == "lobby") { return true; - } + } if (!(parseInt(lobbynum).toString() == lobbynum)) return false; - for (let i in lobbynum) { - if (parseInt(lobbynum[i]) >= 0) { - if (parseInt(i) + 1 == lobbynum.length) return true; - - } else { - return false; - } + for (let i in lobbynum) { + if (parseInt(lobbynum[i]) >= 0) { + if (parseInt(i) + 1 == lobbynum.length) return true; + + } else { + return false; } + } } else if (_id.startsWith("test/")) { if (_id == "test/") { return false; @@ -421,4 +429,4 @@ class Room extends EventEmitter { } } -module.exports = Room; +module.exports = Room; \ No newline at end of file diff --git a/src/Server.js b/src/Server.js index 362eca9..afd7ae2 100644 --- a/src/Server.js +++ b/src/Server.js @@ -1,6 +1,5 @@ const Client = require("./Client.js"); const banned = require('../banned.json'); - class Server extends EventEmitter { constructor(config) { super(); @@ -20,30 +19,7 @@ class Server extends EventEmitter { 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", - "admin_color", - "admin_noteColor", - "admin_chset", - "admin_chown", - "admin_kickban", - "admin_notification" - ]; + this.legit_m = ["a", "bye", "hi", "ch", "+ls", "-ls", "m", "n", "devices", "t", "chset", "userset", "chown", "kickban", "admin message", "color"] this.welcome_motd = config.motd || "You agree to read this message."; this._id_Private_Key = config._id_PrivateKey || "boppity"; this.defaultUsername = config.defaultUsername || "Anonymous"; 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 index a1a43fc..f8301dd 100644 --- a/src/User.js +++ b/src/User.js @@ -1,6 +1,3 @@ -const quotas = require('../Quotas'); -const RateLimit = require('./RateLimit.js').RateLimit; -const RateLimitChain = require('./RateLimit.js').RateLimitChain; const ColorEncoder = require("./ColorEncoder.js"); const { promisify } = require('util'); let userdb; @@ -16,26 +13,6 @@ class User { await this.setUpDb(); } let _id = createKeccakHash('keccak256').update((this.cl.server._id_Private_Key + this.cl.ip)).digest('hex').substr(0, 24); - if(this.server.connections[_id]){ // Connection rate quota? - //if(this.connectionsObjects[_id].connections.length < 10) this.connectionsObjects[_id].connections.push({room:undefined,ws:ws,cl:new Connection(ws)}); - }else{ - this.server.connections[_id] = { - quotas:{ - //note: new limiter(2000, { allowance:3000, max:24000, maxHistLen:3}), - chat: { - lobby: new RateLimitChain(quotas.chat.lobby.amount, quotas.chat.lobby.time), - normal: new RateLimitChain(quotas.chat.normal.amount, quotas.chat.normal.time), - insane: new RateLimitChain(quotas.chat.insane.amount, quotas.chat.insane.time) - }, - name: new RateLimitChain(quotas.name.amount, quotas.name.time), - room: new RateLimit(quotas.room.time), - chown: new RateLimitChain(quotas.chown.amount, quotas.chown.time), - cursor: new RateLimit(quotas.cursor.time), - kickban: new RateLimitChain(quotas.kickban.amount, quotas.kickban.time), - } - }; - }; - //console.log("CONNECTED IP: " + this.cl.ip); let usertofind = userdb.get(_id); if (!usertofind) { if (typeof usertofind == 'object' && (usertofind.hasOwnProperty('name') && usertofind.name != this.server.defaultUsername)) return; diff --git a/src/db/users.json b/src/db/users.json index 27c5276..0967ef4 100644 --- a/src/db/users.json +++ b/src/db/users.json @@ -1,37 +1 @@ -{ - "999d1d8bbe8cf06eb15e5d56": { - "color": "#7fd09e", - "noteColor": "#7fd09e", - "name": "Anonymous", - "_id": "999d1d8bbe8cf06eb15e5d56", - "ip": "108.169.248.183" - }, - "a6a35eb1546b541be5f11cc8": { - "color": "#d3de03", - "noteColor": "#d3de03", - "name": "Anonymous", - "_id": "a6a35eb1546b541be5f11cc8", - "ip": "177.18.157.8" - }, - "c6d435dd7fa48be7cea001ba": { - "color": "#FF00FF", - "noteColor": "#FF00FF", - "name": "Wolfy", - "_id": "c6d435dd7fa48be7cea001ba", - "ip": "75.91.45.152" - }, - "931fb786761b12807265771b": { - "color": "#612196", - "noteColor": "#612196", - "name": "Anonymous", - "_id": "931fb786761b12807265771b", - "ip": "185.180.198.66" - }, - "63c4f4f0b079c466e5dd6df6": { - "color": "#b2b055", - "noteColor": "#b2b055", - "name": "Anonymous", - "_id": "63c4f4f0b079c466e5dd6df6", - "ip": "95.110.114.123" - } -} \ No newline at end of file +{}