Compare commits

...

22 Commits

Author SHA1 Message Date
lamp 840490b362 ip as default nick 2022-01-17 22:30:17 -06:00
lamp 7c8741eb62 🤦‍♂️ 2022-01-17 22:28:52 -06:00
lamp e99fa4cbab fix 2022-01-13 22:27:50 -06:00
lamp d2e24df6f4 Update 'readme.md' 2021-12-03 15:52:11 -06:00
lamp 3e5296ac25 Merge branch 'master' of gitea.moe:lamp/piano 2021-12-03 01:35:30 -06:00
lamp 8112a65204 fix 2021-12-03 01:34:41 -06:00
lamp 3537634d17 in case more than 256 connections 2021-12-02 23:34:17 -08:00
lamp 42eb01257b Merge branch 'master' of gitea.moe:lamp/piano 2021-12-03 01:23:36 -06:00
lamp 761a33e540 fix id generation 2021-12-02 23:23:26 -08:00
lamp c13ba1839d front not ignore self note 2021-12-03 01:03:59 -06:00
lamp 6b8e0e2171 Add 'readme.md' 2021-12-02 22:59:14 -06:00
lamp cdd0552c78 reorganize files 2021-12-02 20:28:16 -08:00
lamp 171309f42c fix 2021-12-02 20:24:20 -08:00
lamp 24e15b12b5 fix 2021-12-02 20:23:10 -08:00
lamp 470e8e5937 env port & addr 2021-12-02 20:19:24 -08:00
lamp e2e3f1d35e xff 2021-12-02 20:15:12 -08:00
lamp 239a65a241 fix notes, chat log 2021-12-02 19:56:31 -08:00
lamp 33033e150c nick work 2021-12-02 19:21:33 -08:00
lamp a88ba01c6d init working backend
stripped out room and rate limiting from frontend
2021-12-02 19:08:24 -08:00
lamp 75f07f2d82 hmm wip 2021-12-02 17:54:28 -08:00
lamp 8b6f2e2588 init backend 2021-12-02 14:56:12 -08:00
lamp cd9363ea49 mv 2021-12-02 14:40:25 -08:00
13 changed files with 1148 additions and 338 deletions
+1
View File
@@ -0,0 +1 @@
node_modules
@@ -2,12 +2,12 @@
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Piano | DayDun</title> <title>Piano</title>
<link href="https://fonts.googleapis.com/css?family=Roboto:400,700" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Roboto:400,700" rel="stylesheet">
<link href="/piano/style.css" rel="stylesheet"> <link href="style.css" rel="stylesheet">
<script src="/piano/main.js"></script> <script src="main.js"></script>
<script src="/piano/midiParser.js"></script> <script src="midiParser.js"></script>
<script src="/piano/midiPlayer.js"></script> <script src="midiPlayer.js"></script>
</head> </head>
<body> <body>
<div id="playspace"> <div id="playspace">
@@ -29,15 +29,6 @@
<button id="user-kick">Kick</button> <button id="user-kick">Kick</button>
</div> </div>
<div id="dimmer"></div> <div id="dimmer"></div>
<div id="rooms-menu">
<ul id="rooms"></ul>
</div>
<div id="room-settings-menu">
<label title="Change the name of this room">Room name <input id="room-name" type="text"></input></label>
<label title="Change how fast people can mash their piano">Note quota <input id="room-quota-note" type="number"> notes per <input id="room-quota-time" type="number"> milliseconds</label>
<label title="Change the default piano sound that everyone in the room hears">Piano sound <select id="room-sound"></select></label>
<button id="room-settings-save">Save</button>
</div>
<div id="midi-player-menu"> <div id="midi-player-menu">
<input id="midi-upload" type="file" accept="audio/midi"> <input id="midi-upload" type="file" accept="audio/midi">
<button id="midi-play">Play</button> <button id="midi-play">Play</button>
@@ -65,7 +56,6 @@
</div> </div>
</div> </div>
<div id="footer"> <div id="footer">
<button id="rooms-button">Rooms</button>
<button id="room-settings-button">Room settings</button> <button id="room-settings-button">Room settings</button>
<button id="midi-player-button">Midi player</button> <button id="midi-player-button">Midi player</button>
+5 -157
View File
@@ -114,28 +114,6 @@ function loadMppBank(url, format) {
} }
} }
class Bucket {
constructor(rate, time) {
this.lastCheck = Date.now();
this.allowance = rate;
this.rate = rate;
this.time = time;
}
spend(amount) {
this.allowance += (Date.now() - this.lastCheck) * (this.rate / this.time);
this.lastCheck = Date.now();
if (this.allowance > this.rate) {
this.allowance = this.rate;
}
if (this.allowance < amount) {
return false;
}
this.allowance -= amount;
return true;
}
}
class Key { class Key {
constructor(index) { constructor(index) {
this.index = index; this.index = index;
@@ -180,7 +158,6 @@ class Piano {
this.keys.push(new Key(i)); this.keys.push(new Key(i));
} }
this.bucket = new Bucket(Infinity, 0);
this.sustain = false; this.sustain = false;
this.performanceMode = false; this.performanceMode = false;
@@ -319,8 +296,6 @@ class Piano {
} }
localKeyDown(key, velocity) { localKeyDown(key, velocity) {
if (!this.bucket.spend(1)) return;
net.keyDown(key, velocity); net.keyDown(key, velocity);
this.keyDown(key, velocity, net.id); this.keyDown(key, velocity, net.id);
} }
@@ -535,27 +510,6 @@ function openModal(element, dimmer, callback) {
document.addEventListener("click", clickListener, true); document.addEventListener("click", clickListener, true);
} }
class Room {
constructor(room) {
this.name = room.name;
this.keyQuota = room.keyQuota;
this.sound = room.sound;
this.updateRoomSettings();
}
updateRoomSettings() {
piano.bucket = new Bucket(this.keyQuota.rate, this.keyQuota.per);
document.getElementById("room-name").value = this.name;
document.getElementById("room-quota-note").value = this.keyQuota.rate;
document.getElementById("room-quota-time").value = this.keyQuota.per;
document.getElementById("room-sound").value = this.sound;
}
}
class User { class User {
constructor(id, uid, nick, color) { constructor(id, uid, nick, color) {
this.id = id; this.id = id;
@@ -638,12 +592,8 @@ class User {
} }
class Networker { class Networker {
constructor() { constructor(url = location.origin.replace("http", "ws")) {
let url = "wss://daydun.com:5012?"; if (localStorage.nick) url += "?nick=" + encodeURIComponent(localStorage.nick);
url += "room=" + encodeURIComponent(decodeURIComponent(location.pathname.slice(7)));
if (localStorage.nick) {
url += "&nick=" + encodeURIComponent(localStorage.nick);
}
this.ws = new WebSocket(url); this.ws = new WebSocket(url);
this.ws.binaryType = "arraybuffer"; this.ws.binaryType = "arraybuffer";
@@ -661,7 +611,6 @@ class Networker {
}); });
this.users = {}; this.users = {};
this.rooms = [];
} }
message(message) { message(message) {
@@ -672,12 +621,8 @@ class Networker {
switch(message.type) { switch(message.type) {
case "load": case "load":
if (this.room == message.room) {
break;
}
this.id = message.id; this.id = message.id;
this.room = new Room(message.room);
this.users = {}; this.users = {};
for (let i=0; i<message.users.length; i++) { for (let i=0; i<message.users.length; i++) {
@@ -687,52 +632,10 @@ class Networker {
chat.chatLog(message.chatlog); chat.chatLog(message.chatlog);
this.rooms = message.rooms;
for (let i=0; i<this.rooms.length; i++) {
let element = document.createElement("li");
element.addEventListener("click", (function(room) {
return function() {
window.location.pathname = "/piano/" + room;
};
})(this.rooms[i].name));
let roomName = document.createElement("span");
roomName.className = "name";
roomName.innerText = this.rooms[i].name;
element.appendChild(roomName);
let users = document.createElement("span");
users.className = "users";
users.innerText = this.rooms[i].users;
element.appendChild(users);
document.getElementById("rooms").appendChild(element);
}
if (localStorage.adminlogin) { if (localStorage.adminlogin) {
net.chat("/adminlogin " + localStorage.adminlogin); net.chat("/adminlogin " + localStorage.adminlogin);
} }
break; break;
case "rooms":
this.rooms = message.rooms;
document.getElementById("rooms").innerHTML = "";
for (let i=0; i<this.rooms.length; i++) {
let element = document.createElement("li");
element.addEventListener("click", (function(room) {
return function() {
window.location.pathname = "/piano/" + room;
};
})(this.rooms[i].name));
let roomName = document.createElement("span");
roomName.className = "name";
roomName.innerText = this.rooms[i].name;
element.appendChild(roomName);
let users = document.createElement("span");
users.className = "users";
users.innerText = this.rooms[i].users;
element.appendChild(users);
document.getElementById("rooms").appendChild(element);
}
break;
case "join": case "join":
this.users[message.id] = new User(message.id, message.uid, message.nick, message.color); this.users[message.id] = new User(message.id, message.uid, message.nick, message.color);
break; break;
@@ -764,13 +667,6 @@ class Networker {
document.body.className = "admin"; document.body.className = "admin";
} }
break; break;
case "room-settings":
this.room.name = message.name;
this.room.keyQuota.rate = message.keyQuota.rate;
this.room.keyQuota.per = message.keyQuota.per;
this.room.sound = message.sound;
this.room.updateRoomSettings();
break;
} }
return; return;
@@ -782,25 +678,19 @@ class Networker {
let key = dv.getUint8(1); let key = dv.getUint8(1);
let velocity = dv.getUint8(2) / 255; let velocity = dv.getUint8(2) / 255;
let id = dv.getUint8(3); let id = dv.getUint8(3);
if (id != this.id) { piano.keyDown(key, velocity, id);
piano.keyDown(key, velocity, id);
}
break; break;
} }
case 1: { // Key up case 1: { // Key up
let key = dv.getUint8(1); let key = dv.getUint8(1);
let sustain = dv.getUint8(2); let sustain = dv.getUint8(2);
let id = dv.getUint8(3); let id = dv.getUint8(3);
if (id != this.id) { piano.keyUp(key, sustain, id);
piano.keyUp(key, sustain, id);
}
break; break;
} }
case 2: { // Sustain release case 2: { // Sustain release
let id = dv.getUint8(1); let id = dv.getUint8(1);
if (id != this.id) { piano.releaseSustain(id);
piano.releaseSustain(id);
}
} }
} }
} }
@@ -830,18 +720,6 @@ class Networker {
this.ws.send(buffer); this.ws.send(buffer);
} }
roomSettings(data) {
this.ws.send(JSON.stringify({
type: "room-settings",
name: data.name,
keyQuota: {
rate: data.keyQuota.rate,
per: data.keyQuota.per
},
sound: data.sound
}));
}
chat(message) { chat(message) {
this.ws.send(JSON.stringify({ this.ws.send(JSON.stringify({
type: "chat", type: "chat",
@@ -972,14 +850,6 @@ class Chat {
element.className = "nick"; element.className = "nick";
element.textContent = message.id + " changed nick to " + message.nick; element.textContent = message.id + " changed nick to " + message.nick;
break; break;
case "rank":
element.className = "rank";
if (message.rank === 1) {
element.textContent = "You're now the owner of this room.";
} else if (message.rank === 2) {
element.textContent = "You're now logged in as admin.";
}
break;
case "disconnected": case "disconnected":
element.className = "disconnected"; element.className = "disconnected";
element.textContent = "You were disconnected from the server!"; element.textContent = "You were disconnected from the server!";
@@ -1054,14 +924,6 @@ window.addEventListener("load", function() {
}); });
// Footer menus // Footer menus
document.getElementById("rooms-button").addEventListener("click", function(event) {
openModal(document.getElementById("rooms-menu"), true);
event.stopImmediatePropagation();
});
document.getElementById("room-settings-button").addEventListener("click", function(event) {
openModal(document.getElementById("room-settings-menu"), true);
event.stopImmediatePropagation();
});
document.getElementById("midi-player-button").addEventListener("click", function(event) { document.getElementById("midi-player-button").addEventListener("click", function(event) {
openModal(document.getElementById("midi-player-menu"), false); openModal(document.getElementById("midi-player-menu"), false);
event.stopImmediatePropagation(); event.stopImmediatePropagation();
@@ -1071,19 +933,6 @@ window.addEventListener("load", function() {
event.stopImmediatePropagation(); event.stopImmediatePropagation();
}); });
// Room settings
document.getElementById("room-settings-save").addEventListener("click", function() {
net.roomSettings({
name: document.getElementById("room-name").value,
keyQuota: {
rate: parseInt(document.getElementById("room-quota-note").value),
per: parseInt(document.getElementById("room-quota-time").value)
},
sound: document.getElementById("room-sound").value
});
document.getElementById("dimmer").click();
});
// Midi player // Midi player
document.getElementById("midi-upload").addEventListener("change", function() { document.getElementById("midi-upload").addEventListener("change", function() {
let reader = new FileReader(); let reader = new FileReader();
@@ -1167,7 +1016,6 @@ window.addEventListener("load", function() {
} }
document.getElementById("setting-sounds").appendChild(getOption()); document.getElementById("setting-sounds").appendChild(getOption());
document.getElementById("room-sound").appendChild(getOption());
}); });
document.getElementById("setting-sounds").addEventListener("input", function() { document.getElementById("setting-sounds").addEventListener("input", function() {
let sound = this.value; let sound = this.value;
@@ -1,166 +1,166 @@
class MidiTrack { class MidiTrack {
constructor(player, chunk) { constructor(player, chunk) {
this.player = player; this.player = player;
this.events = chunk.events; this.events = chunk.events;
this.length = 0; this.length = 0;
for (let i=0; i< this.events.length; i++) { for (let i=0; i< this.events.length; i++) {
this.length += this.events[i].deltaTime; this.length += this.events[i].deltaTime;
} }
this.index = 0; this.index = 0;
this.lastTime = 0; this.lastTime = 0;
} }
parseEvent(event) { parseEvent(event) {
if (event instanceof MidiVoiceEvent) { if (event instanceof MidiVoiceEvent) {
this.player.parseEvent(event); this.player.parseEvent(event);
} else if (event instanceof MidiSetTempoEvent) { } else if (event instanceof MidiSetTempoEvent) {
this.player.bpm = 60000000 / event.tempo; this.player.bpm = 60000000 / event.tempo;
} }
} }
tick(delta) { tick(delta) {
while(true) { while(true) {
if (this.index >= this.events.length) { if (this.index >= this.events.length) {
break; break;
} }
let event = this.events[this.index]; let event = this.events[this.index];
if (this.lastTime + event.deltaTime - this.player.time <= 0) { if (this.lastTime + event.deltaTime - this.player.time <= 0) {
this.lastTime += event.deltaTime; this.lastTime += event.deltaTime;
this.parseEvent(event); this.parseEvent(event);
this.index++; this.index++;
} else { } else {
break; break;
} }
} }
} }
} }
class MidiPlayer { class MidiPlayer {
constructor() { constructor() {
this.tracks = []; this.tracks = [];
this.playing = false; this.playing = false;
this.speed = 1; this.speed = 1;
this.bpm = 120; this.bpm = 120;
this.time = 0; this.time = 0;
} }
loadMidi(midi) { loadMidi(midi) {
this.tracks = []; this.tracks = [];
this.time = 0; this.time = 0;
this.length = 0; this.length = 0;
this.bpm = 120; this.bpm = 120;
this.midi = midi; this.midi = midi;
if (!(this.midi.chunks[0] instanceof MidiHeaderChunk)) { if (!(this.midi.chunks[0] instanceof MidiHeaderChunk)) {
throw "No header chunk"; throw "No header chunk";
} }
let header = this.midi.chunks[0]; let header = this.midi.chunks[0];
this.format = header.format; this.format = header.format;
if ((header.division & 0x0800) === 1) { if ((header.division & 0x0800) === 1) {
throw "SMPTA time format not yet supported"; throw "SMPTA time format not yet supported";
} else { } else {
this.ticksPerQuarterNote = header.division; this.ticksPerQuarterNote = header.division;
} }
for (let i=1; i<this.midi.chunks.length; i++) { for (let i=1; i<this.midi.chunks.length; i++) {
let chunk = this.midi.chunks[i]; let chunk = this.midi.chunks[i];
if (chunk instanceof MidiTrackChunk) { if (chunk instanceof MidiTrackChunk) {
let track = new MidiTrack(this, chunk); let track = new MidiTrack(this, chunk);
this.length = Math.max(track.length, this.length); this.length = Math.max(track.length, this.length);
this.tracks.push(track); this.tracks.push(track);
} }
} }
} }
parseEvent(event) { parseEvent(event) {
if (event instanceof MidiNoteOnEvent) { if (event instanceof MidiNoteOnEvent) {
if (event.channel === 9) { if (event.channel === 9) {
return; return;
} }
if (event.velocity === 0) { if (event.velocity === 0) {
piano.localKeyUp(event.key, false); piano.localKeyUp(event.key, false);
} else { } else {
piano.localKeyDown(event.key, event.velocity / 127); piano.localKeyDown(event.key, event.velocity / 127);
} }
} else if (event instanceof MidiNoteOffEvent) { } else if (event instanceof MidiNoteOffEvent) {
if (event.channel === 9) { if (event.channel === 9) {
return; return;
} }
piano.localKeyUp(event.key, false); piano.localKeyUp(event.key, false);
} }
} }
tick(delta) { tick(delta) {
let deltaTicks = delta / 1000 * (this.bpm / 60) * this.ticksPerQuarterNote; let deltaTicks = delta / 1000 * (this.bpm / 60) * this.ticksPerQuarterNote;
this.time += deltaTicks; this.time += deltaTicks;
switch(this.format) { switch(this.format) {
case 0: case 0:
this.tracks[0].tick(deltaTicks); this.tracks[0].tick(deltaTicks);
break; break;
case 1: case 1:
for (let i=0; i<this.tracks.length; i++) { for (let i=0; i<this.tracks.length; i++) {
this.tracks[i].tick(deltaTicks); this.tracks[i].tick(deltaTicks);
} }
break; break;
case 2: case 2:
// Todo // Todo
break; break;
} }
if (this.time >= this.length) { if (this.time >= this.length) {
this.time = this.length; this.time = this.length;
this.pause(); this.pause();
} }
} }
setTime(time) { setTime(time) {
// TODO skip notes // TODO skip notes
this.time = time; this.time = time;
} }
play() { play() {
if (this.playing) return; if (this.playing) return;
this.playing = true; this.playing = true;
let lastTime = Date.now(); let lastTime = Date.now();
this.interval = setInterval(() => { this.interval = setInterval(() => {
let delta = Date.now() - lastTime; let delta = Date.now() - lastTime;
lastTime = Date.now(); lastTime = Date.now();
this.tick(delta * this.speed); this.tick(delta * this.speed);
}, 1000 / 60); }, 1000 / 60);
} }
pause() { pause() {
this.playing = false; this.playing = false;
clearInterval(this.interval); clearInterval(this.interval);
} }
} }
let midiPlayer = new MidiPlayer(); let midiPlayer = new MidiPlayer();
function uploadMidi() { function uploadMidi() {
let input = document.createElement("input"); let input = document.createElement("input");
input.type = "file"; input.type = "file";
input.accept = "audio/midi"; input.accept = "audio/midi";
input.addEventListener("change", function() { input.addEventListener("change", function() {
let reader = new FileReader(); let reader = new FileReader();
reader.addEventListener("load", function() { reader.addEventListener("load", function() {
let midiFile = new MidiFile(reader.result); let midiFile = new MidiFile(reader.result);
midiPlayer.loadMidi(midiFile); midiPlayer.loadMidi(midiFile);
console.log(midiFile); console.log(midiFile);
}); });
reader.readAsArrayBuffer(input.files[0]); reader.readAsArrayBuffer(input.files[0]);
}); });
input.click(); input.click();
} }

Before

Width:  |  Height:  |  Size: 226 B

After

Width:  |  Height:  |  Size: 226 B

Before

Width:  |  Height:  |  Size: 160 B

After

Width:  |  Height:  |  Size: 160 B

Before

Width:  |  Height:  |  Size: 325 B

After

Width:  |  Height:  |  Size: 325 B

+882
View File
@@ -0,0 +1,882 @@
{
"name": "server",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"dependencies": {
"express": "^4.17.1",
"ws": "^8.3.0"
}
},
"node_modules/accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"dependencies": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"node_modules/body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"dependencies": {
"bytes": "3.1.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
"dependencies": {
"safe-buffer": "5.1.2"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
"dependencies": {
"accepts": "~1.3.7",
"array-flatten": "1.1.1",
"body-parser": "1.19.0",
"content-disposition": "0.5.3",
"content-type": "~1.0.4",
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.1.2",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.5",
"qs": "6.7.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
"send": "0.17.1",
"serve-static": "1.14.1",
"setprototypeof": "1.1.1",
"statuses": "~1.5.0",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
}
},
"node_modules/finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"statuses": "~1.5.0",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"dependencies": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.51.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz",
"integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.34",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz",
"integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==",
"dependencies": {
"mime-db": "1.51.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"node_modules/negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"dependencies": {
"bytes": "3.1.0",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
"dependencies": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.7.2",
"mime": "1.6.0",
"ms": "2.1.1",
"on-finished": "~2.3.0",
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
},
"node_modules/serve-static": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
"dependencies": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"node_modules/statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/ws": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.3.0.tgz",
"integrity": "sha512-Gs5EZtpqZzLvmIM59w4igITU57lrtYVFneaa434VROv4thzJyV6UjIL3D42lslWlI+D4KzLYnxSwtfuiO79sNw==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
},
"dependencies": {
"accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"requires": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
}
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"requires": {
"bytes": "3.1.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
}
},
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
"content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
"requires": {
"safe-buffer": "5.1.2"
}
},
"content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
},
"cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
},
"cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
},
"destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
},
"express": {
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
"requires": {
"accepts": "~1.3.7",
"array-flatten": "1.1.1",
"body-parser": "1.19.0",
"content-disposition": "0.5.3",
"content-type": "~1.0.4",
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.1.2",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.5",
"qs": "6.7.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
"send": "0.17.1",
"serve-static": "1.14.1",
"setprototypeof": "1.1.1",
"statuses": "~1.5.0",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
}
},
"finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"statuses": "~1.5.0",
"unpipe": "~1.0.0"
}
},
"forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="
},
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
},
"http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
},
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"mime-db": {
"version": "1.51.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz",
"integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g=="
},
"mime-types": {
"version": "2.1.34",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz",
"integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==",
"requires": {
"mime-db": "1.51.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"requires": {
"ee-first": "1.1.1"
}
},
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"requires": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
}
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
"range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"raw-body": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"requires": {
"bytes": "3.1.0",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.7.2",
"mime": "1.6.0",
"ms": "2.1.1",
"on-finished": "~2.3.0",
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
},
"dependencies": {
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
}
}
},
"serve-static": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.1"
}
},
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
"toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
"type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"requires": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
}
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"ws": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.3.0.tgz",
"integrity": "sha512-Gs5EZtpqZzLvmIM59w4igITU57lrtYVFneaa434VROv4thzJyV6UjIL3D42lslWlI+D4KzLYnxSwtfuiO79sNw==",
"requires": {}
}
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"dependencies": {
"express": "^4.17.1",
"ws": "^8.3.0"
}
}
+5
View File
@@ -0,0 +1,5 @@
The frontend was stolen from [DayDun](https://daydun.com/piano/) and the backend re-implemented from scratch. Rooms and rate limit functionality was stripped out, each connection is a different user, and raw IP addresses are used for user ids.
The notable thing about this and DayDun's piano is that note events are simply, individually, immediately broadcasted in a minimal binary format, and clients _instantly_ play notes they receive; unlike Brandon Lockaby's Multiplayer Piano which buffers note events with timing data into JSON that's sent every 200ms and then played exactly _one second_ after they actually happened. (PianoRhythm does something similar.) Doing this preserves the exact note timing regardless of networking quality (unless it takes longer than a second for the data to get through), which is fine for listening to other players, but a problem when playing together. With this piano, notes go directly through as fast as possible, which is perfect for local networks, and reveals the true networking quality and latency over the internet.
It's also super simple to run if you do want to use it on your local network; just download, `npm install`, `node server.js` and connect to it (default port is 924).
+78
View File
@@ -0,0 +1,78 @@
var express = require("express");
var qs = require("qs");
var proxyaddr = require("proxy-addr");
var {WebSocketServer} = require("ws");
var app = express();
app.set("trust proxy", "loopback");
app.use(express.static("frontend"));
var server = app.listen(process.env.PORT || 924, process.env.ADDRESS);
var wss = new WebSocketServer({server, clientTracking: true});
var chatlog = [];
wss.on("connection", (ws, req) => {
req.ip = proxyaddr(req, app.get("trust proxy"));
req.query = qs.parse(req.url.substr(req.url.indexOf('?')+1));
function broadcast(msg, excludeSelf) {
if (typeof msg == "object" && !(msg instanceof Buffer)) msg = JSON.stringify(msg);
for (let ows of wss.clients) if (!(ows == ws && excludeSelf)) ows.send(msg);
}
function broadcastChat(message) {
broadcast({type: "chat", message});
chatlog.push(message);
if (chatlog.length >= 100) chatlog.shift();
}
ws.user = {
uid: req.ip,
nick: req.query.nick || req.ip,
color: [Math.floor(Math.random()*256),Math.floor(Math.random()*256),Math.floor(Math.random()*256)]
}
let t = Array.from(wss.clients).map(ws => ws.user.id);
for (let i = 0; i < 256; i++) if (!t.includes(i)) { ws.user.id = i; break; }
if (ws.user.id == null) return ws.close();
console.log("join", ws.user);
ws.send(JSON.stringify({
type: "load",
id: ws.user.id,
uid: ws.user.uid,
users: Array.from(wss.clients).map(x => x.user),
chatlog
}));
broadcast({type: "join", id: ws.user.id, uid: ws.user.uid, nick: ws.user.nick, color: ws.user.color}, true);
broadcastChat({type: "join", id: ws.user.id, uid: ws.user.uid, nick: ws.user.nick});
ws.on("close", () => {
console.log("leave", ws.user);
broadcast({type: "leave", id: ws.user.id, uid: ws.user.uid, nick: ws.user.nick, color: ws.user.color}, true);
broadcastChat({type: "leave", id: ws.user.id, uid: ws.user.uid, nick: ws.user.nick})
});
ws.on("message", (msg, isBinary) => {
if (isBinary) {
broadcast(Buffer.concat([msg, Buffer.from([ws.user.id])]), true);
} else {
msg = msg.toString();
try {
msg = JSON.parse(msg);
} catch (error) { return }
console.log(msg);
switch (msg.type) {
case "chat":
broadcastChat({type: "message", content: msg.message, user: ws.user});
break;
case "nick":
ws.user.nick = msg.nick;
broadcast({type: "nick", nick: ws.user.nick, id: ws.user.id, uid: ws.user.uid});
broadcastChat({type: "nick", nick: ws.user.nick, id: ws.user.id});
break;
}
}
});
});