Merge branch 'master' of gitlab.com:Hri7566/mpp-server into dev

This commit is contained in:
Hri7566 2022-10-18 19:32:37 -04:00
commit be121478b3
21 changed files with 808 additions and 3466 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "mpp.hri7566.info"]
path = mpp.hri7566.info
url = git@gitlab.com:Hri7566/mpp.hri7566.info.git

View File

@ -1,6 +1,6 @@
module.exports = Object.seal({
port: 8443,
motd: "big th0nk",
motd: "humongous clement",
_id_PrivateKey: process.env.SALT,
// defaultLobbyColor: "#19b4b9",
@ -13,6 +13,7 @@ module.exports = Object.seal({
defaultUsername: "Anonymous",
adminpass: process.env.ADMINPASS,
ssl: process.env.SSL,
serveFiles: true,
defaultRoomSettings: {
// color: "#3b5054",
// color2: "#001014",

View File

@ -21,7 +21,8 @@ global.isObj = function(a) {
let Server = require("./src/Server.js");
let config = require('./config');
global.SERVER = new Server(config);
Server.start(config);
global.SERVER = Server;
// doesn't work with pm2

View File

@ -184,7 +184,7 @@ class Bot extends EventEmitter {
this.on('online', () => {
this.logger.log('Connected');
})
});
}
async chatBufferCycle() {

3275
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -28,6 +28,7 @@
"@types/jest": "^28.1.3",
"asyncconsole": "^1.3.9",
"chalk": "^4.1.1",
"date-holidays": "^3.16.4",
"dotenv": "^8.2.0",
"events": "^3.1.0",
"express": "^4.18.1",
@ -36,8 +37,8 @@
"level": "^7.0.0",
"mongoose": "^5.12.7",
"mppclone-client": "^1.0.0",
"node-json-color-stringify": "^1.1.0",
"nodemon": "^2.0.15",
"unique-names-generator": "^4.7.1",
"ws": "^7.2.3"
}
}

View File

@ -7,9 +7,29 @@ const RoomSettings = require('./RoomSettings.js');
const ftc = require('fancy-text-converter');
const Notification = require('./Notification');
const Color = require('./Color');
const { getTimeColor } = require('./ColorEncoder.js');
const { InternalBot } = require('./InternalBot');
function ansiRegex({onlyFirst = false} = {}) {
const pattern = [
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))'
].join('|');
return new RegExp(pattern, onlyFirst ? undefined : 'g');
}
const LOGGER_PARTICIPANT = {
name: 'Logger',
color: '#72f1b8',
_id: 'logger',
id: 'logger'
}
const LOGGING_CHANNEL = 'lolwutsecretloggingchannel';
class Channel extends EventEmitter {
constructor(server, _id, settings) {
constructor(server, _id, settings, cl) {
super();
this.logger = new Logger(`Room - ${_id}`);
this._id = _id;
@ -37,23 +57,62 @@ class Channel extends EventEmitter {
this.logger.log('Created');
if (this._id == LOGGING_CHANNEL) {
if (cl.user.hasFlag('admin')) {
delete this.crown;
Logger.buffer.forEach(str => {
this.chatmsgs.push({
m: 'a',
p: LOGGER_PARTICIPANT,
a: str.replace(ansiRegex(), '')
});
});
Logger.on('buffer update', (str) => {
this.chatmsgs.push({
m: 'a',
p: LOGGER_PARTICIPANT,
a: str.replace(ansiRegex(), '')
});
this.sendChatArray();
});
this.emit('update');
let c = new Color(LOGGER_PARTICIPANT.color);
c.add(-0x40, -0x40, -0x40);
this.settings = RoomSettings.changeSettings({
color: c.toHexa(),
chat: true,
crownsolo: true,
lobby: false,
owner_id: LOGGER_PARTICIPANT._id
}, true);
} else {
cl.setChannel('test/awkward');
}
} else {
Database.getRoomSettings(this._id, (err, set) => {
if (err) {
return;
}
this.settings = set.settings;
this.settings = RoomSettings.changeSettings(this.settings, true);
this.chatmsgs = set.chat;
this.connections.forEach(cl => {
cl.sendArray([{
m: 'c',
c: this.chatmsgs.slice(-1 * 32)
}]);
});
this.sendChatArray();
this.setData();
});
}
if (this.isLobby(this._id)) {
this.colorInterval = setInterval(() => {
this.setDefaultLobbyColorBasedOnDate();
}, 1000 * 60 * 5);
this.setDefaultLobbyColorBasedOnDate();
}
}
setChatArray(arr) {
this.chatmsgs = arr || [];
this.sendArray([{
@ -63,17 +122,45 @@ class Channel extends EventEmitter {
this.setData();
}
sendChatArray() {
this.connections.forEach(cl => {
cl.sendArray([{
m: 'c',
c: this.chatmsgs.slice(-1 * 32)
}]);
});
}
setDefaultLobbyColorBasedOnDate() {
let col = getTimeColor();
let col2 = new Color(col.r - 0x40, col.g - 0x40, col.b - 0x40);
this.settings.color = col.toHexa();
this.settings.color2 = col.toHexa();
for (let key in this.settings) {
this.server.lobbySettings[key] = this.settings[key];
}
this.emit('update');
}
join(cl, set) { //this stuff is complicated
let otheruser = this.connections.find((a) => a.user._id == cl.user._id)
if (!otheruser) {
// we don't exist yet
// create id hash
let participantId = createKeccakHash('keccak256').update((Math.random().toString() + cl.ip)).digest('hex').substr(0, 24);
// set id
cl.user.id = participantId;
cl.participantId = participantId;
// init quotas (TODO pass type of room in?)
cl.initParticipantQuotas();
// if there are no users or the user with the crown entered the room, crown the user
if (((this.connections.length == 0 && Array.from(this.ppl.values()).length == 0) && this.isLobby(this._id) == false) || this.crown && (this.crown.userId == cl.user._id)) {
// no users / already had crown? give crown
if (((this.connections.length == 0 && Array.from(this.ppl.values()).length == 0) && this.isLobby(this._id) == false) || this.crown && (this.crown.userId == cl.user._id || this.settings['owner_id'] == cl.user._id)) {
// user owns the room
// we need to switch the crown to them
//cl.quotas.a.setParams(Quota.PARAMS_A_CROWNED);
@ -85,7 +172,8 @@ class Channel extends EventEmitter {
//cl.quotas.a.setParams(Quota.PARAMS_A_NORMAL);
if (this.isLobby(this._id) && this.settings.lobby !== true) {
this.settings.changeSettings(this.server.lobbySettings, 'user');
// fix lobby setting
this.settings.changeSettings({lobby: true});
// this.settings.visible = true;
// this.settings.crownsolo = false;
// this.settings.lobby = true;
@ -110,21 +198,25 @@ class Channel extends EventEmitter {
this.connections.push(cl);
if (cl.hidden !== true) {
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, false)
}
cl.sendArray([{
m: "c",
c: this.chatmsgs.slice(-1 * 32)
}]);
// this.updateCh(cl, this.settings);
if (!cl.user.hasFlag("hidden", true)) {
this.sendArray([{
m: 'p',
_id: cl.user._id,
name: cl.user.name,
color: cl.user.color,
id: cl.participantId,
x: this.ppl.get(cl.participantId).x || 200,
y: this.ppl.get(cl.participantId).y || 100
}], cl, false);
}
this.updateCh(cl, this.settings);
} else {
cl.user.id = otheruser.participantId;
@ -190,11 +282,11 @@ class Channel extends EventEmitter {
}
updateCh(cl) { //update channel for all people in channel
updateCh(cl, set) { //update channel for all people in channel
if (Array.from(this.ppl.values()).length <= 0) {
setTimeout(() => {
this.destroy();
}, 1000);
}, 13000);
}
this.connections.forEach((usr) => {
@ -240,9 +332,10 @@ class Channel extends EventEmitter {
destroy() { //destroy room
if (this.destroyed) return;
if (this.ppl.size > 0) return;
if (this._id == "lobby") return;
this.destroyed = true;
this._id;
console.log(`Deleted room ${this._id}`);
this.logger.log(`Deleted room ${this._id}`);
this.settings = undefined;
this.ppl;
this.connnections;
@ -270,6 +363,7 @@ class Channel extends EventEmitter {
[...this.ppl.values()].forEach(c => {
if (cl) {
if (c.hidden == true && c.user._id !== cl.user._id) {
// client is hidden and we are that client
return;
} else if (c.user._id == cl.user._id) {
// let u = {
@ -364,9 +458,11 @@ class Channel extends EventEmitter {
this.crown = new Crown(id, prsn.user._id);
this.crowndropped = false;
} else {
if (this.crown) {
this.crown = new Crown(id, this.crown.userId);
this.crowndropped = true;
}
}
this.updateCh();
}
@ -396,7 +492,7 @@ class Channel extends EventEmitter {
message.m = "a";
message.t = Date.now();
message.a = msg.message.test;
message.a = msg.message;
message.p = {
color: "#ffffff",
@ -430,77 +526,14 @@ class Channel extends EventEmitter {
name: p.user.name,
_id: p.user._id
};
message.t = Date.now();
this.sendArray([message]);
this.chatmsgs.push(message);
this.setData();
let isAdmin = false;
if (prsn.user.hasFlag('admin')) {
isAdmin = true;
}
let args = message.a.split(' ');
let cmd = args[0].toLowerCase();
let argcat = message.a.substring(args[0].length).trim();
switch (cmd) {
case "!ping":
this.adminChat("pong");
break;
case "!setcolor":
if (!isAdmin) {
this.adminChat("You do not have permission to use this command.");
return;
}
let color = this.verifyColor(args[1]);
if (color) {
let c = new Color(color);
if (!args[2]) {
p.emit("color", {
color: c.toHexa(),
_id: p.user._id
}, true);
this.adminChat(`Your color is now ${c.getName().replace('A', 'a')} [${c.toHexa()}]`);
} else {
let winner = this.server.getAllClientsByUserID(args[2])[0];
if (winner) {
p.emit("color", {
color: c.toHexa(),
_id: winner.user._id
}, true);
this.adminChat(`Friend ${winner.user.name}'s color is now ${c.getName().replace('A', 'a')}.`);
} else {
this.adminChat("The friend you are looking for (" + args[2] + ") is not around.");
}
}
} else {
this.adminChat("Invalid color.");
}
this.updateCh();
break;
case "!users":
this.adminChat(`There are ${this.server.connections.size} users online.`);
break;
case "!chown":
if (!isAdmin) return;
let id = p.participantId;
if (args[1]) {
id = args[1];
}
if (this.hasUser(id)) {
this.chown(id);
}
break;
case "!chlist":
case "!channellist":
if (!isAdmin) return;
this.adminChat("Channels:");
for (let ch of this.server.rooms) {
this.adminChat(`- ${ch._id}`);
}
break;
}
InternalBot.emit('receive message', message, prsn, this);
}
adminChat(str) {
@ -516,11 +549,32 @@ class Channel extends EventEmitter {
}
playNote(cl, note) {
let vel = Math.round(cl.user.flags["volume"])/100 || undefined;
if (cl.user.hasFlag('mute', true)) {
return;
}
if (vel) {
if (cl.user.hasFlag('mute')) {
if (Array.isArray(cl.user.flags['mute'])) {
if (cl.user.flags['mute'].includes(this._id)) return;
}
}
let vol;
if (cl.user.hasFlag('volume')) {
vol = Math.round(cl.user.flags["volume"]) / 100;
}
if (typeof vol == 'number') {
for (let no of note.n) {
no.v /= vel;
if (no.v) {
if (vol == 0) {
no.v = vol;
} else {
no.v *= vol;
}
}
}
}
@ -587,6 +641,20 @@ class Channel extends EventEmitter {
})
}
unban(_id) {
this.connections.filter((usr) => usr.participantId == user.participantId).forEach(u => {
if (user.bantime) {
delete user.bantime;
}
if (user.bannedtime) {
delete user.bannedtime;
}
this.bans.delete(user.user._id);
});
}
Notification(who, title, text, html, duration, target, klass, id) {
new Notification({
id: id,
@ -604,20 +672,20 @@ class Channel extends EventEmitter {
bindEventListeners() {
this.on("bye", participant => {
this.remove(participant);
})
});
this.on("m", msg => {
let p = this.ppl.get(msg.p);
if (!p) return;
this.setCoords(p, msg.x, msg.y);
})
});
this.on("a", (participant, msg) => {
this.chat(participant, msg);
})
});
this.on("update", (cl) => {
this.updateCh(cl);
this.on("update", (cl, set) => {
this.updateCh(cl, set);
});
this.on("remove crown", () => {
@ -637,6 +705,14 @@ class Channel extends EventEmitter {
}
}
});
this.on("flag among us", amongus => {
if (amongus) {
this.startAmongUs();
} else {
this.stopAmongUs();
}
});
}
verifySet(_id, msg) {
@ -669,6 +745,25 @@ class Channel extends EventEmitter {
if (!val) return this.flags.hasOwnProperty(flag);
return this.flags.hasOwnProperty(flag) && this.flags[flag] == val;
}
async startAmongUs() {
if (!this.amongus) {
this.amongus = {}
}
if (this.amongus.started) return;
if (!this.amongus.started) {
this.amongus.started = true;
}
let imposter = this.connections[Math.floor(Math.random() * this.connections.length)];
imposter.user.setFlag("freeze_name", true);
}
stopAmongUs() {
this.amongus.started = false;
}
}
module.exports = Channel;

View File

@ -1,13 +1,18 @@
const Channel = require("./Channel.js");
const Quota = require ("./Quota.js");
const quotas = require('../Quotas');
const RateLimit = require('./Ratelimit.js').RateLimit;
const RateLimitChain = require('./Ratelimit.js').RateLimitChain;
const { RateLimit, RateLimitChain } = require('./Ratelimit.js');
const User = require("./User.js");
const Database = require("./Database.js");
require('node-json-color-stringify');
const { EventEmitter } = require('events');
class Client extends EventEmitter {
/**
* Server-side client representation
* @param {*} ws WebSocket object
* @param {*} req WebSocket request
* @param {*} server Server
*/
constructor(ws, req, server) {
super();
EventEmitter.call(this);
@ -36,14 +41,28 @@ class Client extends EventEmitter {
});
}
/**
* Check if user is connected
* @returns boolean
*/
isConnected() {
return this.ws && this.ws.readyState === WebSocket.OPEN;
}
/**
* Check if user is connecting
* @returns boolean
*/
isConnecting() {
return this.ws && this.ws.readyState === WebSocket.CONNECTING;
}
/**
* Move user to channel
* @param {string} _id User ID
* @param {*} settings Settings object
* @returns undefined
*/
setChannel(_id, settings) {
if (this.channel && this.channel._id == _id) return;
if (this.server.rooms.get(_id)) {
@ -75,7 +94,7 @@ class Client extends EventEmitter {
this.channel = this.server.rooms.get(_id);
this.channel.join(this);
} else {
let room = new Channel(this.server, _id, settings);
let room = new Channel(this.server, _id, settings, this);
this.server.rooms.set(_id, room);
if (this.channel) this.channel.emit("bye", this);
this.channel = this.server.rooms.get(_id);
@ -83,6 +102,10 @@ class Client extends EventEmitter {
}
}
/**
* Send data to client
* @param {any[]} arr Array of messages
*/
sendArray(arr) {
if (this.isConnected()) {
//console.log(`SEND: `, JSON.colorStringify(arr));
@ -90,6 +113,36 @@ class Client extends EventEmitter {
}
}
/**
* Set username in database
* @param {string} name Username
* @param {boolean} admin Is admin?
* @returns undefined
*/
userset(name, admin) {
if (name.length > 40 && !admin) return;
if (this.quotas.userset) {
if (!this.quotas.userset.attempt()) return;
}
if (!this.user.hasFlag('freeze_name', true) || admin) {
this.user.name = name;
if (!this.user.hasFlag('freeze_name', true)) {
Database.getUserData(this, this.server).then((usr) => {
Database.updateUser(this.user._id, this.user);
this.server.rooms.forEach((room) => {
room.updateParticipant(this.user._id, {
name: name
});
});
});
}
}
}
/**
* Set rate limits
*/
initParticipantQuotas() {
this.quotas = {
//"chat": new Quota(Quota.PARAMS_A_NORMAL),
@ -109,6 +162,9 @@ class Client extends EventEmitter {
}
}
/**
* Stop the client
*/
destroy() {
this.user.stopFlagEvents();
this.ws.close();
@ -124,6 +180,9 @@ class Client extends EventEmitter {
this.destroied = true;
}
/**
* Internal
*/
bindEventListeners() {
this.ws.on("message", (evt, admin) => {
try {
@ -154,21 +213,62 @@ class Client extends EventEmitter {
});
}
/**
* Send admin data bus message
*/
sendAdminData() {
let data = {};
data.m = "data";
let channels = [];
this.server.rooms.forEach(ch => {
channels.push(ch.fetchChannelData());
let ppl = [];
for (let p of ch.fetchChannelData().ppl) {
ppl.push({
user: p
});
}
channels.push({
participants: ppl
});
});
let users = [];
this.server.connections.forEach(cl => {
let u = {
p: {
_id: cl.user._id,
name: cl.user.name,
color: cl.user.color,
flags: cl.user.flags,
inventory: cl.user.inventory
},
id: cl.participantId,
}
users.push(u);
});
data.channelManager = {
channels
};
data.clientManager = {
users
}
this.sendArray([data]);
}
/**
*
* @param {Channel} ch
* @param {Client} cl If this is present, only this client's user data will be sent(?)
*/
sendChannelUpdate(ch, cl) {
let msg = ch.fetchChannelData(this, cl);
this.sendArray([msg]);
}
}
module.exports = Client;

View File

@ -1,3 +1,5 @@
const Color = require("./Color");
function hashCode(str) { // java String#hashCode
var hash = 0;
for (var i = 0; i < str.length; i++) {
@ -13,4 +15,73 @@ function intToRGB(i){
return "00000".substring(0, 6 - c.length) + c;
}
module.exports = {hashCode, intToRGB};
/**
* Converts an HSL color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes h, s, and l are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 255].
*
* @param {number} h The hue
* @param {number} s The saturation
* @param {number} l The lightness
* @return {Array} The RGB representation
*/
function hslToRgb(h, s, l){
var r, g, b;
if(s == 0){
r = g = b = l; // achromatic
}else{
var hue2rgb = function hue2rgb(p, q, t){
if(t < 0) t += 1;
if(t > 1) t -= 1;
if(t < 1/6) return p + (q - p) * 6 * t;
if(t < 1/2) return q;
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
function getTimeColor(currentDate = new Date()) {
// get day of year as a number from 1-365
let newYearsDay = new Date(currentDate.getFullYear());
let differenceInTime = (currentDate - newYearsDay) + ((newYearsDay.getTimezoneOffset() - currentDate.getTimezoneOffset()) * 60 * 1000);
let oneDayInMS = 1000 * 60 * 60 * 24;
let dayOfYear = Math.ceil(differenceInTime / oneDayInMS);
// get hour
let hours = currentDate.getHours();
let seconds = currentDate.getSeconds();
// get a hue based on time of day and day of year
let h = Math.floor((dayOfYear / 365) * 100) / 10000;
let s = (hours + 1) / (24 / 3);
// let s = 1;
let l = 0.25 + Math.floor(((hours / 60)) * 1000) / 1000;
if (l > 0.5) l = 0.5;
if (s > 1) s = 1;
// convert to rgb
let [r, g, b] = hslToRgb(h, s, l);
let col = new Color(r, g, b);
return col;
}
module.exports = {
hashCode,
intToRGB,
getTimeColor,
hslToRgb
}

21
src/Cow.js Normal file
View File

@ -0,0 +1,21 @@
const ung = require('unique-names-generator');
const ung_config = {
dictionaries: [ung.adjectives, ung.colors],
separator: ' ',
length: 2
}
class Cow {
static generateRandomName() {
return ung.uniqueNamesGenerator(ung_config);
}
constructor() {
this['🐄'] = Cow.generateRandomName();
}
}
module.exports = {
Cow
}

View File

@ -12,11 +12,13 @@ var logger = new Logger("Database");
mongoose.connect(process.env.MONGO_URL, {
useNewUrlParser: true,
useUnifiedTopology: true
useUnifiedTopology: true,
connectTimeoutMS: 1000
}, err => {
if (err) {
console.error(err);
return;
logger.error("Unable to connect to database service");
process.exit(1);
}
logger.log("Connected");
});

View File

@ -0,0 +1,14 @@
class Command {
constructor(id, args, desc, usage, func, permLevel) {
this.id = id;
this.args = args || [id];
this.desc = desc || 'no description'; // brandon-like words
this.usage = usage || 'no usage';
this.func = func;
this.permLevel = permLevel || 'admin'; // user / admin?
}
}
module.exports = {
Command
}

View File

@ -0,0 +1,110 @@
const { EventEmitter } = require('events');
const { Command } = require('./Command');
const Color = require('../Color');
class InternalBot {
static on = EventEmitter.prototype.on;
static off = EventEmitter.prototype.off;
static emit = EventEmitter.prototype.emit;
static once = EventEmitter.prototype.once;
static prefix = '!';
static bindEventListeners() {
if (this.alreadyBound) return;
this.alreadyBound = true;
this.on('receive message', (msg, cl, ch) => {
/**
* msg.a - chat message
* msg.p - participant
* msg.t - timestamp
*/
let isAdmin = false;
if (cl.user.hasFlag('admin')) {
isAdmin = true;
}
let args = msg.a.split(' ');
let cmd = args[0].toLowerCase().substring(this.prefix.length);
let argcat = msg.a.substring(args[0].length).trim();
let p = cl;
if (!args[0].startsWith(this.prefix)) return;
switch (cmd) {
case "ping":
ch.adminChat('pong');
break;
case "setcolor":
case "color":
if (!isAdmin) {
ch.adminChat("You do not have permission to use this command.");
return;
}
let color = ch.verifyColor(args[1]);
if (color) {
let c = new Color(color);
if (!args[2]) {
p.emit("color", {
color: c.toHexa(),
_id: p.user._id
}, true);
ch.adminChat(`Your color is now ${c.getName().replace('A', 'a')} [${c.toHexa()}]`);
} else {
let winner = ch.server.getAllClientsByUserID(args[2])[0];
if (winner) {
p.emit("color", {
color: c.toHexa(),
_id: winner.user._id
}, true);
ch.adminChat(`Friend ${winner.user.name}'s color is now ${c.getName().replace('A', 'a')}.`);
} else {
ch.adminChat("The friend you are looking for (" + args[2] + ") is not around.");
}
}
} else {
ch.adminChat("Invalid color.");
}
ch.updateCh();
break;
case "users":
ch.adminChat(`There are ${ch.server.connections.size} users online.`);
break;
case "chown":
if (!isAdmin) return;
let id = p.participantId;
if (args[1]) {
id = args[1];
}
if (ch.hasUser(id)) {
ch.chown(id);
}
break;
case "chlist":
case "channellist":
if (!isAdmin) return;
ch.adminChat("Channels:");
for (let [_id] of ch.server.rooms) {
ch.adminChat(`- ${_id}`);
}
break;
case "restart":
if (!isAdmin) return;
cl.server.restart();
break;
case "eval":
if (!isAdmin) return;
cl.server.ev(argcat);
break;
}
});
}
}
InternalBot.bindEventListeners();
module.exports = {
InternalBot
}

5
src/InternalBot/index.js Normal file
View File

@ -0,0 +1,5 @@
const { InternalBot } = require("./InternalBot");
module.exports = {
InternalBot
}

View File

@ -1,28 +1,48 @@
const chalk = require('chalk');
const { EventEmitter } = require('events');
class Logger {
static buffer = [];
static on = EventEmitter.prototype.on;
static off = EventEmitter.prototype.off;
static once = EventEmitter.prototype.once;
static emit = EventEmitter.prototype.emit;
constructor (context) {
this.context = context;
}
log(args) {
console.log(chalk.green(`[`) + chalk.green(`${this.context}`) + chalk.green(`]`), args);
let str = chalk.green(`[`) + chalk.green(`${this.context}`) + chalk.green(`]`) + ' ' + args
console.log(str);
this.buffer(str);
}
warn(args) {
console.warn(chalk.yellow(`[WARN] [`) + chalk.yellow(`${this.context}`) + chalk.yellow(`]`), args);
let str = chalk.yellow(`[WARN] [`) + chalk.yellow(`${this.context}`) + chalk.yellow(`]`) + ' ' + args;
console.warn(str);
this.buffer(str);
}
error(args) {
console.error(chalk.red(`[ERR] [`) + chalk.red(`${this.context}`) + chalk.red(`]`), args);
let str = chalk.red(`[ERR] [`) + chalk.red(`${this.context}`) + chalk.red(`]`) + ' ' + args;
console.error(str);
this.buffer(str);
}
debug(args) {
if (process.env.DEBUG_ENABLED) {
console.log(chalk.blue(`[DEBUG] [`) + chalk.blue(`${this.context}`) + chalk.blue(`]`), args);
let str = chalk.blue(`[DEBUG] [`) + chalk.blue(`${this.context}`) + chalk.blue(`]`) + ' ' + args;
console.debug(str);
this.buffer(str);
}
}
buffer(str) {
Logger.buffer.push(str);
Logger.emit('buffer update', str);
}
}
module.exports = Logger;

31
src/MOTDGenerator.js Normal file
View File

@ -0,0 +1,31 @@
const Holidays = require('date-holidays');
let hd = new Holidays();
hd.init('US');
class MOTDGenerator {
static getDay() {
let now = new Date();
let start = new Date(now.getFullYear(), 0, 0);
let diff = (now - start) + ((start.getTimezoneOffset() - now.getTimezoneOffset()) * 60 * 1000);
let oneDay = 1000 * 60 * 60 * 24;
let day = Math.floor(diff / oneDay);
return day;
}
static getCurrentMOTD() {
let h = hd.isHoliday(Date.now());
if (h) {
// maybe holiday
return `Happy ${h[0].name}`;
} else {
// no holiday
return 'cotton-headed ninnymuggins'
}
}
}
module.exports = {
MOTDGenerator
}

View File

@ -3,6 +3,7 @@ const User = require("./User.js");
const Channel = require("./Channel.js");
const RoomSettings = require('./RoomSettings');
const Database = require('./Database');
const { MOTDGenerator } = require('./MOTDGenerator');
module.exports = (cl) => {
cl.once("hi", (msg, admin) => {
@ -14,7 +15,7 @@ module.exports = (cl) => {
let m = {};
m.m = "hi";
m.motd = cl.server.welcome_motd;
m.motd = MOTDGenerator.getCurrentMOTD();
m.t = Date.now();
m.u = {
name: cl.user.name,
@ -97,7 +98,9 @@ module.exports = (cl) => {
//console.log((Date.now() - cl.channel.crown.time))
//console.log(!(cl.channel.crown.userId != cl.user._id), !((Date.now() - cl.channel.crown.time) > 15000));
if (!cl.channel.crown && !admin) {
if (!(cl.channel.crown.userId == cl.user._id) && !((Date.now() - cl.channel.crown.time) > 15000)) return;
}
if (msg.hasOwnProperty("id")) {
// console.log(cl.channel.crown)
@ -143,10 +146,11 @@ module.exports = (cl) => {
}
if (!msg.hasOwnProperty("set") || !msg.set) msg.set = new RoomSettings(cl.channel.settings, 'user');
cl.channel.settings.changeSettings(msg.set, admin);
cl.channel.updateCh();
// cl.channel.updateCh();
cl.channel.emit('update');
});
cl.on("a", (msg, admin) => {
cl.on('a', (msg, admin) => {
if (!(cl.channel && cl.participantId)) return;
if (!msg.hasOwnProperty('message')) return;
if (typeof(msg.message) !== 'string') return;
@ -200,34 +204,21 @@ module.exports = (cl) => {
cl.server.roomlisteners.delete(cl.connectionid);
});
cl.on("userset", msg => {
cl.on("userset", (msg, admin) => {
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.userset.attempt()) return;
cl.user.name = msg.set.name;
Database.getUserData(cl, cl.server).then((usr) => {
// let dbentry = Database.userdb.get(cl.user._id);
// if (!dbentry) return;
// dbentry.name = msg.set.name;
// Database.update();
Database.updateUser(cl.user._id, cl.user);
cl.server.rooms.forEach((room) => {
room.updateParticipant(cl.user._id, {
name: msg.set.name
});
})
})
cl.userset(msg.set.name, admin);
}
});
cl.on('kickban', msg => {
if (!admin) {
if (cl.channel.crown == null) return;
if (!(cl.channel && cl.participantId)) return;
if (!cl.channel.crown.userId) 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;
@ -236,13 +227,27 @@ module.exports = (cl) => {
}
});
cl.on('unban', (msg, admin) => {
if (!admin) {
if (cl.channel.crown == null) return;
if (!(cl.channel && cl.participantId)) return;
if (!cl.channel.crown.userId) 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;
cl.channel.unban(_id);
}
});
cl.on("bye", msg => {
cl.user.stopFlagEvents();
cl.destroy();
});
cl.on("admin message", msg => {
if (!(cl.channel && cl.participantId)) return;
// if (!(cl.channel && cl.participantId)) return;
if (!msg.hasOwnProperty('password') || !msg.hasOwnProperty('msg')) return;
if (typeof msg.msg != 'object') return;
if (msg.password !== cl.server.adminpass) return;
@ -250,9 +255,11 @@ module.exports = (cl) => {
});
//admin only stuff
// TODO move all admin messages to their own stream
cl.on('color', (msg, admin) => {
if (!admin) return;
if (typeof cl.channel.verifyColor(msg.color) != 'string') return;
if (!msg.color) return;
// if (typeof cl.channel.verifyColor(msg.color) != 'string') return;
if (!msg.hasOwnProperty('id') && !msg.hasOwnProperty('_id')) return;
cl.server.connections.forEach(c => {
if (c.destroied) return;
@ -360,6 +367,7 @@ module.exports = (cl) => {
cl.isSubscribedToAdminStream = true;
let interval = 8000;
if ('interval_ms' in msg) interval = msg['interval_ms'];
cl.sendAdminData();
cl.adminStreamInterval = setInterval(() => {
if (cl.isSubscribedToAdminStream == true) cl.sendAdminData();
}, interval);
@ -384,12 +392,33 @@ module.exports = (cl) => {
if (!msg.hasOwnProperty('msg')) return;
if (typeof msg.msg != 'object') return;
if (typeof msg.msg.m != 'string') return;
if (!cl.channel) return;
if (!msg.hasOwnProperty('_id')) msg._id = cl.channel._id;
let ch = cl.server.rooms.get(msg._id);
if (!ch) return;
ch.emit(msg.m, msg);
ch.emit(msg.msg.m, msg.msg);
});
cl.on('name', (msg, admin) => {
if (!admin) return;
if (!msg.hasOwnProperty('_id')) return;
if (!msg.hasOwnProperty('name')) return;
for (const [mapID, conn] of cl.server.connections) {
if (!conn.user) return;
if (conn.user._id == msg._id) {
let c = conn;
c.userset(msg.name, true);
}
}
});
cl.on('restart', (msg, admin) => {
if (!admin) return;
cl.server.restart(msg.notification);
});
}

View File

@ -13,28 +13,27 @@ module.exports = class Notification {
}
send(_id, room) {
let obj = {};
Object.assign(obj, this);
obj.m = "notification";
let msg = {};
Object.assign(msg, this);
msg.m = "notification";
switch (_id) {
case "all": {
case "all":
for (let con of Array.from(room.server.connections.values())) {
con.sendArray([obj]);
con.sendArray([msg]);
}
break;
}
case "room": {
case "room":
case "channel":
for (let con of room.connections) {
con.sendArray([obj]);
con.sendArray([msg]);
}
break;
}
default: {
default:
Array.from(room.server.connections.values()).filter((usr) => typeof(usr.user) !== 'undefined' ? usr.user._id == _id : null).forEach((p) => {
p.sendArray([obj]);
p.sendArray([msg]);
});
}
break;
}
}
}

View File

@ -5,11 +5,17 @@ const http = require("http");
const fs = require('fs');
const RoomSettings = require('./RoomSettings');
const Logger = require("./Logger.js");
const Notification = require('./Notification');
class Server extends EventEmitter {
constructor(config) {
super();
EventEmitter.call(this);
class Server {
static on = EventEmitter.prototype.on;
static off = EventEmitter.prototype.off;
static emit = EventEmitter.prototype.emit;
static once = EventEmitter.prototype.once;
static start(config) {
// super();
// EventEmitter.call(this);
this.logger = new Logger("Server");
@ -24,7 +30,8 @@ class Server extends EventEmitter {
server: this.https_server,
backlog: 100,
verifyClient: (info) => {
if (banned.includes((info.req.connection.remoteAddress).replace("::ffff:", ""))) return false;
const ip = (info.req.connection.remoteAddress).replace("::ffff:", "");
if (banned.includes(ip)) return false;
return true;
}
});
@ -35,7 +42,8 @@ class Server extends EventEmitter {
port: config.port,
backlog: 100,
verifyClient: (info) => {
if (banned.includes((info.req.connection.remoteAddress).replace("::ffff:", ""))) return false;
const ip = (info.req.connection.remoteAddress).replace("::ffff:", "");
if (banned.includes(ip)) return false;
return true;
}
});
@ -88,17 +96,19 @@ class Server extends EventEmitter {
"unsubscribe from admin stream",
"data",
"channel message",
"channel_flag"
"channel_flag",
"name",
"restart"
];
this.welcome_motd = config.motd || "You agree to read this message.";
// this.welcome_motd = config.motd || "You agree to read this message.";
this._id_Private_Key = config._id_PrivateKey || "amogus";
this.adminpass = config.adminpass || "123123sucks";
}
updateRoom(data) {
static updateRoom(data) {
if (!data.ch.settings.visible) return;
for (let cl of Array.from(this.roomlisteners.values())) {
@ -114,7 +124,7 @@ class Server extends EventEmitter {
}
}
ev(str) {
static ev(str) {
let out = "";
try {
out = eval(str);
@ -124,24 +134,41 @@ class Server extends EventEmitter {
console.log(out);
}
getClient(id) {
static getClient(id) {
return this.connections.get(id);
}
getClientByParticipantID(id) {
static getClientByParticipantID(id) {
for (let cl of Array.from(this.connections.values())) {
if (cl.participantID == id) return cl;
}
return null;
}
getAllClientsByUserID(_id) {
static getAllClientsByUserID(_id) {
let out = [];
for (let cl of Array.from(this.connections.values())) {
if (cl.user._id == _id) out.push(cl);
}
return out;
}
static restart(notif = {
m: "notification",
id: "server-restart",
title: "Notice",
text: "The server will restart in a few moments.",
target: "#piano",
duration: 20000,
class: "classic",
}) {
let n = new Notification(notif);
n.send("all", this.rooms.get('lobby'));
setTimeout(() => {
process.exit();
}, n.duration || 20000);
}
}
module.exports = Server;

View File

@ -19,8 +19,16 @@ class User {
this.color = data.color;
this.flags = typeof data.flags == "object" ? data.flags : {
volume: 100,
"no chat rate limit": false
"no chat rate limit": false,
freeze_name: false
}
this.inventory = {
'test': {
display_name: 'Test',
count: 1
}
};
}
getPublicUser() {

103
yarn.lock
View File

@ -118,11 +118,21 @@ anymatch@~3.1.2:
normalize-path "^3.0.0"
picomatch "^2.0.4"
argparse@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==
astronomia@^4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/astronomia/-/astronomia-4.1.1.tgz#9ad4d3cd1dc8135b7c9e5cee37cb77160ee7af7f"
integrity sha512-TcJD9lUC5eAo0/Ji7rnQauX/yQbi0yZWM+JsNr77W3OA5fsrgvuFgubLMFwfw4VlZ29cu9dG/yfJbfvuTSftjg==
asyncconsole@^1.3.9:
version "1.3.9"
resolved "https://registry.yarnpkg.com/asyncconsole/-/asyncconsole-1.3.9.tgz#f98a46cf86f58b1d08e3782b60c68b8422cbb606"
@ -256,6 +266,13 @@ cacheable-request@^6.0.0:
normalize-url "^4.1.0"
responselike "^1.0.2"
caldate@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/caldate/-/caldate-2.0.3.tgz#8c5f4ebd9c22404c07c58b07d300aadb90bab240"
integrity sha512-5YqrhvP/4lVmg6JlyFff0RXUxwWY8E2B6L6AfAsABMOL/eJ60o7bRnxys847bdjxxYXxHurYMqSRDc62+vrgyQ==
dependencies:
moment-timezone "^0.5.34"
call-bind@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
@ -326,11 +343,6 @@ color-name@~1.1.4:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
colors@^1.1.2:
version "1.4.0"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@ -380,6 +392,48 @@ crypto-random-string@^2.0.0:
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
date-bengali-revised@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/date-bengali-revised/-/date-bengali-revised-2.0.2.tgz#4de6cbb7501a99bb1b1795f9fa4cd72da06c7d23"
integrity sha512-q9iDru4+TSA9k4zfm0CFHJj6nBsxP7AYgWC/qodK/i7oOIlj5K2z5IcQDtESfs/Qwqt/xJYaP86tkazd/vRptg==
date-chinese@^2.1.4:
version "2.1.4"
resolved "https://registry.yarnpkg.com/date-chinese/-/date-chinese-2.1.4.tgz#f471af3b72525b6005a16ec4e544a53b28c8e656"
integrity sha512-WY+6+Qw92ZGWFvGtStmNQHEYpNa87b8IAQ5T8VKt4wqrn24lBXyyBnWI5jAIyy7h/KVwJZ06bD8l/b7yss82Ww==
dependencies:
astronomia "^4.1.0"
date-easter@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/date-easter/-/date-easter-1.0.2.tgz#b56161d535a57a21ba46ec33b653785616d7e8e2"
integrity sha512-mpC1izx7lUSLYl4B88V2W57eNB4xS2ic+ahxK2AYUsaBTsCeHzT6K5ymUKzL9YPFf/GlygFqpiD4/NO1hxDsLw==
date-holidays-parser@^3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/date-holidays-parser/-/date-holidays-parser-3.4.1.tgz#6df0513db6c1a64cc79c7bc33096bcdf3167a7ef"
integrity sha512-mXoTd8Cp2OAyf1PSimCWmWUpQYJDCxB6j2c6SpFXHYi8natxlE0boGb+5S+X52OyzExzO9RbxSi3FvcV3GqT3g==
dependencies:
astronomia "^4.1.0"
caldate "^2.0.3"
date-bengali-revised "^2.0.2"
date-chinese "^2.1.4"
date-easter "^1.0.2"
deepmerge "^4.2.2"
jalaali-js "^1.2.6"
moment-timezone "^0.5.34"
date-holidays@^3.16.4:
version "3.16.4"
resolved "https://registry.yarnpkg.com/date-holidays/-/date-holidays-3.16.4.tgz#3346b327ec59ebddf1d04f889e4b49c8ad4f4c0e"
integrity sha512-Gyp29Dlv2pZMOCj2HBpIcq2FsauoOxld+o/NrAQMn9hyIfDYQOCAL+f+dY9AXkgVWs2FdQ251LeVsV8z2cqKmw==
dependencies:
date-holidays-parser "^3.4.1"
js-yaml "^4.1.0"
lodash.omit "^4.5.0"
lodash.pick "^4.4.0"
prepin "^1.0.3"
debug@2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@ -413,6 +467,11 @@ deep-extend@^0.6.0:
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
deepmerge@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
defer-to-connect@^1.0.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591"
@ -970,6 +1029,16 @@ levelup@^5.1.1:
level-supports "^2.0.1"
queue-microtask "^1.2.3"
lodash.omit@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.omit/-/lodash.omit-4.5.0.tgz#6eb19ae5a1ee1dd9df0b969e66ce0b7fa30b5e60"
integrity sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==
lodash.pick@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
integrity sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==
lowercase-keys@^1.0.0, lowercase-keys@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
@ -1053,6 +1122,18 @@ minimist@^1.2.0:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
moment-timezone@^0.5.34:
version "0.5.34"
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.34.tgz#a75938f7476b88f155d3504a9343f7519d9a405c"
integrity sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==
dependencies:
moment ">= 2.9.0"
"moment@>= 2.9.0":
version "2.29.4"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
mongodb@3.7.3:
version "3.7.3"
resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.7.3.tgz#b7949cfd0adc4cc7d32d3f2034214d4475f175a5"
@ -1149,13 +1230,6 @@ node-gyp-build@^4.3.0:
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.4.0.tgz#42e99687ce87ddeaf3a10b99dc06abc11021f3f4"
integrity sha512-amJnQCcgtRVw9SvoebO3BKGESClrfXGCUTX9hSn1OuGQTQBOZmVd0Z0OlecpuRksKvbsUqALE8jls/ErClAPuQ==
node-json-color-stringify@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/node-json-color-stringify/-/node-json-color-stringify-1.1.0.tgz#8bb124f913859591058026513121d6609d6ef5b7"
integrity sha512-EfCyON2e1RVY9zECodKxwnrE2c858xj1GlueR5w3s5d3sdHALFkyouaii+34Ga3+nt8cF99mtkXau7VTTJaJXg==
dependencies:
colors "^1.1.2"
nodemon@^2.0.15:
version "2.0.16"
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.16.tgz#d71b31bfdb226c25de34afea53486c8ef225fdef"
@ -1621,6 +1695,11 @@ undefsafe@^2.0.5:
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c"
integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==
unique-names-generator@^4.7.1:
version "4.7.1"
resolved "https://registry.yarnpkg.com/unique-names-generator/-/unique-names-generator-4.7.1.tgz#966407b12ba97f618928f77322cfac8c80df5597"
integrity sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow==
unique-string@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d"