nonstopmmd/index.js

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);
});