279 lines
7.1 KiB
JavaScript
279 lines
7.1 KiB
JavaScript
import express from "express";
|
|
import "express-async-errors";
|
|
import { WebSocket, WebSocketServer } from "ws";
|
|
import { access, readdir } from "fs/promises";
|
|
import { spawn } from "child_process";
|
|
|
|
var app = express();
|
|
app.set("env", process.env.NODE_ENV || "production");
|
|
app.set("trust proxy", "loopback");
|
|
var server = app.listen(process.env.PORT || 9245, process.env.ADDR);
|
|
|
|
app.get("/channels", async (req, res) => {
|
|
res.send(await getChannels());
|
|
});
|
|
|
|
app.post("/newchannel", async (req, res, next) => {
|
|
|
|
switch (req.query.from) {
|
|
case "search":
|
|
var {query, number} = req.query;
|
|
if (!number) number = 500;
|
|
if (!query || !(number >= 1 && number <= 1000)) return res.sendStatus(400);
|
|
var url = `ytsearch${number}:${query}`;
|
|
var channel = `${query.replaceAll(' ','-')}~${number}`;
|
|
break;
|
|
case "channel":
|
|
if (!/^[\w\-.]{3,30}$/.test(req.query.handle)) return res.sendStatus(400);
|
|
var url = `https://www.youtube.com/@${req.query.handle}/videos`;
|
|
var channel = `@${req.query.handle}`;
|
|
break;
|
|
case "playlist":
|
|
if (!req.query.playlist) return res.sendStatus(400);
|
|
var url = `https://www.youtube.com/playlist?list=${req.query.playlist}`;
|
|
var channel = `playlist:${req.query.playlist}`; //todo get playlist name
|
|
break;
|
|
default:
|
|
return res.sendStatus(400);
|
|
}
|
|
|
|
if (/[.\/]/.test(channel)) return res.sendStatus(400);
|
|
|
|
var cp = spawn("python3", ["makechannel.py", url, channel]);
|
|
cp.stdout.pipe(process.stdout);
|
|
cp.stderr.pipe(process.stderr);
|
|
cp.on("close", code => {
|
|
console.log("python exit code", code);
|
|
broadcastChannels();
|
|
res.status(200).type("text").send(channel);
|
|
});
|
|
cp.on("error", next);
|
|
});
|
|
|
|
app.get("/:channel", (req, res, next) => {
|
|
var channel = req.params.channel;
|
|
access(`channels/${channel}.json`).then(() => {
|
|
//req.url = "/index.html"; //??
|
|
res.sendFile("index.html", {root: "public"});
|
|
}).catch(() => {
|
|
next();
|
|
});
|
|
});
|
|
|
|
app.use("/channels/", express.static("channels"));
|
|
app.use(express.static("public"));
|
|
|
|
|
|
|
|
async function getChannels() {
|
|
var channels = await readdir("channels");
|
|
channels = channels.map(c => {
|
|
c = c.replace(/\.json$/,'');
|
|
var n = [...wss.clients].reduce((num, ws) => ws.channel == c ? num + 1 : num, 0);
|
|
return [c, n];
|
|
});
|
|
return channels;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var wss = new WebSocketServer({ server, clientTracking: true });
|
|
|
|
|
|
var drawStates = {};
|
|
var txtLogs = {};
|
|
|
|
function clearLines(channel, owner) {
|
|
if (!drawStates[channel]) return;
|
|
var newLines = [];
|
|
for (var line of drawStates[channel]) {
|
|
if (line.owner != owner) newLines.push(line);
|
|
}
|
|
drawStates[channel] = newLines;
|
|
}
|
|
|
|
function broadcastGlobal(data) {
|
|
for (var client of wss.clients) {
|
|
if (client.readyState == WebSocket.OPEN) {
|
|
client.send(data);
|
|
}
|
|
}
|
|
}
|
|
|
|
function broadcastChannels() {
|
|
getChannels().then(channels => broadcastGlobal(JSON.stringify(["channels", channels])));
|
|
}
|
|
|
|
|
|
wss.on('connection', ws => {
|
|
ws.send(new BigUint64Array([BigInt(Date.now())]));
|
|
|
|
ws.id = (function newId(){
|
|
var used_ids = [...wss.clients].map(ws => ws.id);
|
|
for (var id = 0; id < 256; id++) {
|
|
if (!used_ids.includes(id)) return id;
|
|
}
|
|
ws.close();
|
|
})();
|
|
|
|
function broadcast(data) {
|
|
if (!ws.channel) return;
|
|
for (var client of wss.clients) {
|
|
if (client != ws && client.channel == ws.channel && client.readyState == WebSocket.OPEN) {
|
|
client.send(data);
|
|
}
|
|
}
|
|
}
|
|
|
|
ws.on('message', (data, isBinary) => {
|
|
try {
|
|
if (isBinary) {
|
|
switch (data.length) {
|
|
case 4:
|
|
var x = data.readInt16BE(0);
|
|
var y = data.readInt16BE(2);
|
|
var b = Buffer.alloc(5);
|
|
b.writeUInt8(ws.id, 0);
|
|
b.writeInt16BE(x, 1);
|
|
b.writeInt16BE(y, 3);
|
|
broadcast(b);
|
|
if (ws.depressed && ws.m) {
|
|
drawStates[ws.channel] ||= [];
|
|
drawStates[ws.channel].push({
|
|
x1: ws.m.x,
|
|
y1: ws.m.y,
|
|
x2: x,
|
|
y2: y,
|
|
color: ws.color,
|
|
owner: ws.id
|
|
});
|
|
}
|
|
ws.m = {x, y};
|
|
break;
|
|
case 1:
|
|
var c = data.readUint8(0);
|
|
broadcast(Buffer.from([ws.id, c]));
|
|
switch (c){
|
|
case 1: // click
|
|
ws.depressed = true;
|
|
break;
|
|
case 0: // unclick
|
|
ws.depressed = false;
|
|
break;
|
|
case 2: // right click
|
|
clearLines(ws.channel, ws.id);
|
|
break;
|
|
case 5: // blur
|
|
ws.away = true;
|
|
break;
|
|
case 6: // focus
|
|
ws.away = false;
|
|
break;
|
|
}
|
|
break;
|
|
case 3:
|
|
var color = [data.readUInt8(0), data.readUInt8(1), data.readUInt8(2)];
|
|
broadcast(new Uint8Array([ws.id, ...color]));
|
|
ws.color = color;
|
|
break;
|
|
}
|
|
} else {
|
|
var p = JSON.parse(data.toString());
|
|
switch (p[0]) {
|
|
case "txt":
|
|
broadcast(JSON.stringify(["txt", ws.id, p[1]]));
|
|
ws.txt ||= "";
|
|
txtLogs[ws.channel] ||= [];
|
|
switch (p[1]) {
|
|
case "":
|
|
ws.txt = "";
|
|
txtLogs[ws.channel].push(["␛", ws.color]);
|
|
break;
|
|
case -1:
|
|
ws.txt = ws.txt.slice(0, -1);
|
|
txtLogs[ws.channel].push(["⌫", ws.color]);
|
|
break;
|
|
case 1:
|
|
ws.txt = ws.txt.slice(1);
|
|
txtLogs[ws.channel].push(["⌦", ws.color]);
|
|
break;
|
|
default:
|
|
ws.txt += p[1];
|
|
txtLogs[ws.channel].push([p[1] === '\n' ? "⮒" : p[1], ws.color]);
|
|
}
|
|
break;
|
|
case "channel":
|
|
if (ws.channel) broadcast(new Uint8Array([ws.id, 4]));
|
|
clearLines(ws.channel, ws.id);
|
|
ws.channel = p[1];
|
|
broadcastChannels();
|
|
broadcast(new Uint8Array([ws.id, 3]));
|
|
if (ws.color) broadcast(new Uint8Array([ws.id, ...ws.color]));
|
|
if (ws.x && ws.y) broadcast(new Uint8Array([ws.id, ws.x, ws.y]));
|
|
ws.send(JSON.stringify(["channel", ws.channel]));
|
|
for (let w of wss.clients) {
|
|
if (w.id == null || w.id == ws.id || w.channel != ws.channel) continue;
|
|
if (w.m) {
|
|
let b = Buffer.alloc(5);
|
|
b.writeUInt8(w.id, 0);
|
|
b.writeInt16BE(w.m.x, 1);
|
|
b.writeInt16BE(w.m.y, 3);
|
|
ws.send(b);
|
|
}
|
|
if (w.txt) {
|
|
ws.send(JSON.stringify(["txt", w.id, w.txt]));
|
|
}
|
|
if (w.color) {
|
|
ws.send(new Uint8Array([w.id, ...w.color]));
|
|
}
|
|
if (w.away) {
|
|
ws.send(new Uint8Array([w.id, 5]));
|
|
} else if (w.away === false) {
|
|
ws.send(new Uint8Array([w.id, 6]));
|
|
}
|
|
}
|
|
var lines = drawStates[ws.channel];
|
|
if (lines) {
|
|
var b = Buffer.alloc(12*lines.length);
|
|
for (var i = 0, o = 0; i < lines.length; i++, o = i*12) {
|
|
var line = lines[i];
|
|
b.writeInt16BE(line.x1, o);
|
|
b.writeInt16BE(line.y1, o+2);
|
|
b.writeInt16BE(line.x2, o+4);
|
|
b.writeInt16BE(line.y2 ,o+6);
|
|
b.writeUint8(line.owner, o+8);
|
|
b.writeUint8(line.color[0], o+9);
|
|
b.writeUint8(line.color[1], o+10);
|
|
b.writeUint8(line.color[2], o+11);
|
|
}
|
|
ws.send(b);
|
|
}
|
|
if (txtLogs[ws.channel]) {
|
|
ws.send(JSON.stringify(["txtlog", txtLogs[ws.channel]]));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error(error.stack);
|
|
}
|
|
});
|
|
|
|
ws.on("close", () => {
|
|
broadcast(new Uint8Array([ws.id, 4]));
|
|
broadcastChannels();
|
|
clearLines(ws.channel, ws.id);
|
|
});
|
|
|
|
ws.on('error', console.error);
|
|
});
|