diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..3ce419a --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +HOSTNAME=localhost +PORT=2056 \ No newline at end of file diff --git a/index.ts b/index.ts deleted file mode 100644 index e4b48b7..0000000 --- a/index.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { file, gc, serve } from "bun"; - -import Backend from "./src/back"; - -import pkg from "./package.json"; - -let heartrate = 0; -let lanyard = {}; - -require("node:fs/promises") - .rm("./dist", { recursive: true, force: true }) - .catch(() => { - // ignore - }); - -if (!Backend.development) { - await Backend.build(); -} - -const server = serve({ - routes: { - "/": async (req: Bun.BunRequest, server: Bun.Server) => { - await Backend.postAnalytics(req, server); - - if (Backend.development) { - await Backend.build(); - } - - return await Backend.Responses.file(file("./dist/index.html")); - }, - - "/assets/:file": async (req: Bun.BunRequest<"/assets/:file">) => { - return await Backend.Responses.file(file(`./dist/${req.params.file}`)); - }, - - "/public/:file": async (req: Bun.BunRequest<"/public/:file">) => { - return await Backend.Responses.file(file(`./public/${req.params.file}`)); - }, - - "/api/server": () => { - const string = JSON.stringify(process); - const data = JSON.parse(string); - - // clear possibly data that could be sensitive - data.env = {}; - - data.availableMemory = process.availableMemory(); - data.constrainedMemory = process.constrainedMemory(); - data.cpuUsage = process.cpuUsage(); - data.memoryUsage = process.memoryUsage(); - data.uptime = process.uptime(); - data.package = pkg; - - return Backend.Responses.json({ data }); - }, - "/api/health": () => { - return Backend.Responses.ok(); - }, - "/api/ws": async (req, server) => { - if (server.upgrade(req)) { - return; - } - - await Backend.postAnalytics(req, server); - return Response.redirect("/"); - }, - "/api/gc": async () => { - gc(true); - - return Backend.Responses.ok(); - }, - "/api/headers": async (req) => { - return Backend.Responses.json({ ...req.headers.toJSON() }); - }, - }, - - fetch: async (request, server) => { - await Backend.postAnalytics(request, server); - - return Response.redirect("/"); - }, - - websocket: { - idleTimeout: 1, - open: async (ws) => { - ws.subscribe("lanyard"); - ws.send(JSON.stringify({ type: "lanyard", data: lanyard }), true); - - ws.subscribe("hyperate"); - ws.send( - JSON.stringify({ type: "hyperate", data: { hr: heartrate } }), - true, - ); - }, - message: async (ws, message) => { - ws.send(JSON.stringify({ type: "echo", data: message }), true); - }, - }, - development: Backend.development, - port: 2056, -}); - -new Backend.Sockets.Hyperate((data) => { - heartrate = data; - server.publish( - "hyperate", - JSON.stringify({ type: "hyperate", data: { hr: heartrate } }), - true, - ); -}); - -new Backend.Sockets.Lanyard((data) => { - lanyard = data; - server.publish( - "lanyard", - JSON.stringify({ type: "lanyard", data: lanyard }), - true, - ); -}); diff --git a/package.json b/package.json index 8cc2293..10fdfd5 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,8 @@ "name": "ipv4.army", "module": "index.ts", "scripts": { - "dev": "NODE_ENV=development bun run --hot . --watch", - "start": "bun run .", + "dev": "NODE_ENV=development bun run --hot src/index.ts", + "start": "bun run src/index.ts", "lint": "bunx biome ci . --verbose", "lint:fix": "bunx biome check --fix" }, diff --git a/src/back/Sockets/Lanyard.ts b/src/back/Sockets/Lanyard.ts index 04f9b9d..7d92430 100644 --- a/src/back/Sockets/Lanyard.ts +++ b/src/back/Sockets/Lanyard.ts @@ -3,9 +3,9 @@ import ReconnectingWebSocket from "reconnecting-websocket"; export default class { private _socket: ReconnectingWebSocket; private _keepAlive: NodeJS.Timeout | null; - private _callback: (data: { [key: string]: string }) => void; + private _callback: (data: LanyardData) => void; - constructor(callback: (data: { [key: string]: string }) => void) { + constructor(callback: (data: LanyardData) => void) { this._socket = new ReconnectingWebSocket( "wss://lanyard.creations.works/socket", ); diff --git a/src/front/App.tsx b/src/front/App.tsx index 40dd719..7e0a8cb 100644 --- a/src/front/App.tsx +++ b/src/front/App.tsx @@ -1,27 +1,145 @@ import Hyperate from "./components/Hyperate"; import Lanyard from "./components/Lanyard"; +let latestLanyard: LanyardData | null = null; + +window.addEventListener("lanyard-update", (e) => { + latestLanyard = (e as CustomEvent).detail; +}); + export default () => { - return ( -
-

seth> cat ./about.txt

-

- A Dedicated Backend Developer, -
- with a passion for high-fidelity audio, -
- gaming, and web development. -

+ const container = document.createElement("div"); + container.className = "app terminal"; -

seth> curl /tmp/discord-ipc

-

- -

+ const renderElement = (content: string | Node) => { + const p = document.createElement("p"); + if (typeof content === "string") { + p.textContent = content; + } else { + p.appendChild(content); + } + return p; + }; -

seth> cat /tmp/heartrate

-

- -

-
- ); + const prompt = "[seth@ipv4 ~]$"; + + const staticLines: (string | (() => Node))[] = [ + `${prompt} cat ./about.txt`, + () => + document + .createRange() + .createContextualFragment( + "A Dedicated Backend Developer,
with a passion for high-fidelity audio,
gaming, and web development.", + ), + `${prompt} cat /tmp/discord-ipc`, + () => Lanyard(), + `${prompt} cat /tmp/heartrate`, + () => Hyperate(), + ]; + + const renderStatic = () => { + for (const line of staticLines) { + const content = typeof line === "function" ? line() : line; + container.appendChild(renderElement(content)); + } + }; + + renderStatic(); + + const lanyardInstance = Lanyard(); + const files: Record Node> = { + "./about.txt": () => + document + .createRange() + .createContextualFragment( + "A Dedicated Backend Developer,
with a passion for high-fidelity audio,
gaming, and web development.", + ), + "/tmp/discord-ipc": () => lanyardInstance, + "/tmp/heartrate": () => Hyperate(), + }; + + const history: string[] = []; + let historyIndex = -1; + + const inputBox = document.createElement("input"); + inputBox.className = "terminal-input"; + inputBox.autofocus = true; + + const inputLine = document.createElement("div"); + inputLine.className = "terminal-line"; + + const promptSpan = document.createElement("span"); + promptSpan.textContent = `${prompt} `; + + inputLine.appendChild(promptSpan); + inputLine.appendChild(inputBox); + container.appendChild(inputLine); + + const appendLine = (line: string | Node) => { + container.insertBefore(renderElement(line), inputLine); + }; + + inputBox.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + const cmd = inputBox.value.trim(); + if (!cmd) return; + + history.push(cmd); + historyIndex = history.length; + + appendLine(`${prompt} ${cmd}`); + + let out: string | Node; + + if (cmd.startsWith("cat ")) { + const file = cmd.slice(4).trim(); + out = files[file]?.() ?? `cat: ${file}: No such file`; + } else if (cmd === "ls") { + out = Object.keys(files) + .filter((f) => f.startsWith("./")) + .map((f) => f.slice(2)) + .join("\n"); + } else if (cmd.startsWith("ls ")) { + const dir = cmd.slice(3).trim(); + if (dir === "/tmp") { + out = Object.keys(files) + .filter((f) => f.startsWith("/tmp/")) + .map((f) => f.slice("/tmp/".length)) + .join("\n"); + } else { + out = `ls: cannot access '${dir}': No such file or directory`; + } + } else if (cmd === "help") { + out = [ + "Available commands:", + " cat [file] View contents of a file", + " ls List files in current directory", + " ls /tmp List files in /tmp directory", + " help Show this message", + ].join("\n"); + } else { + out = `bash: ${cmd}: command not found`; + } + + appendLine(out); + inputBox.value = ""; + } else if (e.key === "ArrowUp") { + if (historyIndex > 0) { + historyIndex--; + inputBox.value = history[historyIndex] || ""; + } + e.preventDefault(); + } else if (e.key === "ArrowDown") { + if (historyIndex < history.length - 1) { + historyIndex++; + inputBox.value = history[historyIndex] || ""; + } else { + historyIndex = history.length; + inputBox.value = ""; + } + e.preventDefault(); + } + }); + + return container; }; diff --git a/src/front/Socket.ts b/src/front/Socket.ts index 3c271bf..334effc 100644 --- a/src/front/Socket.ts +++ b/src/front/Socket.ts @@ -39,8 +39,11 @@ class Socket extends EventTarget { }, 30 * 1000); } - emitLanyard(lanyard: object) { + emitLanyard(lanyard: LanyardData) { this.dispatchEvent(new CustomEvent("lanyard", { detail: lanyard })); + window.dispatchEvent( + new CustomEvent("lanyard-update", { detail: lanyard }), + ); } emitHyperate(heartRate: number) { this.dispatchEvent(new CustomEvent("hyperate", { detail: heartRate })); diff --git a/src/front/components/Lanyard/index.tsx b/src/front/components/Lanyard/index.tsx index aee6527..9b58d6a 100644 --- a/src/front/components/Lanyard/index.tsx +++ b/src/front/components/Lanyard/index.tsx @@ -1,22 +1,22 @@ -import { highlightAll } from "@speed-highlight/core"; +import { highlightElement } from "@speed-highlight/core"; import { createRef } from "tsx-dom"; - import socket from "../../Socket"; -const statusTypes: { [key: string]: string } = { +const statusTypes = { online: "rgb(0, 150, 0)", idle: "rgb(150, 150, 0)", dnd: "rgb(150, 0, 0)", offline: "rgb(150, 150, 150)", }; -const gradientTypes: { [key: string]: string } = { +const gradientTypes = { online: "rgba(0, 150, 0, 0.1)", idle: "rgba(150, 150, 0, 0.1)", dnd: "rgba(150, 0, 0, 0.1)", offline: "rgba(150, 150, 150, 0.1)", }; -const activityTypes: { [key: number]: string } = { + +const activityTypes: Record = { 0: "Playing", 1: "Streaming", 2: "Listening to", @@ -25,43 +25,37 @@ const activityTypes: { [key: number]: string } = { 5: "Competing in", }; -const stringify = (data: { [key: string]: string }) => { - return JSON.stringify(data, null, 2); -}; - export default () => { - const code = createRef(); + const container = createRef(); socket.addEventListener("lanyard", (event: Event) => { - const lanyard = (event as CustomEvent).detail; + const lanyard = (event as CustomEvent).detail; + document.body.style = `--status-color: ${statusTypes[lanyard.discord_status]}; --gradient-color: ${gradientTypes[lanyard.discord_status]};`; - if (code.current) { - code.current.innerHTML = stringify({ - status: lanyard.discord_status, - activities: lanyard.activities.map( - (act: { - type: number; - name: string; - details: string; - state: string; - }) => { - return [ - ...new Set([ - activityTypes[act.type], - act.name, - act.details, - act.state, - ]), - ].filter((n) => n); - }, - ), - }); + + if (container.current) { + container.current.className = "shj-lang-json"; + container.current.textContent = JSON.stringify( + { + status: lanyard.discord_status, + activities: lanyard.activities.map((act) => { + const type = activityTypes[act.type]; + const parts = [type]; + if (act.name !== type) parts.push(act.name); + if (act.details) parts.push(act.details); + if (act.state) parts.push(act.state); + return parts; + }), + }, + null, + 2 + ); + highlightElement(container.current); } - highlightAll(); }); return ( -
+
{"{}"}
); diff --git a/src/front/index.css b/src/front/index.css index 1b5386d..f52e3e0 100644 --- a/src/front/index.css +++ b/src/front/index.css @@ -1,5 +1,4 @@ @import "../../node_modules/@speed-highlight/core/dist/themes/dark.css"; - @import "./App.css"; html, @@ -21,4 +20,47 @@ body { rgba(0, 0, 0, 1) 100% ); display: flex; + height: 100vh; + width: 100vw; + overflow: hidden; +} + +p { + margin: 0; + padding: 0; + line-height: 1.4em; +} + +.terminal { + white-space: pre-wrap; + font-family: monospace; + width: 100vw; + height: 100vh; + overflow-y: auto; + display: flex; + flex-direction: column; + box-sizing: border-box; + + gap: 0.4em; +} + +.terminal-input { + background: transparent; + border: none; + color: inherit; + font: inherit; + outline: none; + display: inline-block; + width: 100%; +} + +.terminal-line { + display: flex; + align-items: baseline; + flex-direction: row; + width: 100%; +} + +.terminal-line > span { + white-space: pre; } diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..65da033 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,105 @@ +import fs from "node:fs/promises"; +import { file, gc, serve } from "bun"; +import pkg from "../package.json"; +import Backend from "./back"; + +let heartrate = 0; +let lanyard: LanyardData = { + discord_status: "online", + activities: [], +}; + +await fs.rm("./dist", { recursive: true, force: true }).catch(() => {}); + +if (!Backend.development) { + await Backend.build(); +} + +const server = serve({ + port: process.env.PORT || 3000, + hostname: process.env.HOSTNAME || "localhost", + development: Backend.development, + + routes: { + "/": async (req, server) => { + await Backend.postAnalytics(req, server); + if (Backend.development) await Backend.build(); + return Backend.Responses.file(file("./dist/index.html")); + }, + + "/assets/:file": async (req) => + Backend.Responses.file(file(`./dist/${req.params.file}`)), + + "/public/:file": async (req) => + Backend.Responses.file(file(`./public/${req.params.file}`)), + + "/api/server": () => { + const safeProcess = JSON.parse(JSON.stringify(process)); + safeProcess.env = {}; + safeProcess.availableMemory = process.availableMemory(); + safeProcess.constrainedMemory = process.constrainedMemory(); + safeProcess.cpuUsage = process.cpuUsage(); + safeProcess.memoryUsage = process.memoryUsage(); + safeProcess.uptime = process.uptime(); + safeProcess.package = pkg; + + return Backend.Responses.json({ data: safeProcess }); + }, + + "/api/health": () => Backend.Responses.ok(), + + "/api/ws": async (req, server) => { + if (!server.upgrade(req)) { + await Backend.postAnalytics(req, server); + return Response.redirect("/"); + } + }, + + "/api/gc": async () => { + gc(true); + return Backend.Responses.ok(); + }, + + "/api/headers": (req) => Backend.Responses.json(req.headers.toJSON()), + }, + + fetch: async (req, server) => { + await Backend.postAnalytics(req, server); + return Response.redirect("/"); + }, + + websocket: { + idleTimeout: 1, + open: (ws) => { + ws.subscribe("lanyard"); + ws.send(JSON.stringify({ type: "lanyard", data: lanyard }), true); + + ws.subscribe("hyperate"); + ws.send( + JSON.stringify({ type: "hyperate", data: { hr: heartrate } }), + true, + ); + }, + message: (ws, msg) => { + ws.send(JSON.stringify({ type: "echo", data: msg }), true); + }, + }, +}); + +new Backend.Sockets.Hyperate((data) => { + heartrate = data; + server.publish( + "hyperate", + JSON.stringify({ type: "hyperate", data: { hr: heartrate } }), + true, + ); +}); + +new Backend.Sockets.Lanyard((data) => { + lanyard = data; + server.publish( + "lanyard", + JSON.stringify({ type: "lanyard", data: lanyard }), + true, + ); +}); diff --git a/tsconfig.json b/tsconfig.json index ab77b3d..a7c98f5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,7 @@ // Some stricter flags (disabled by default) "noUnusedLocals": false, "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false + "noPropertyAccessFromIndexSignature": false, + "typeRoots": ["./types"] } } diff --git a/types/lanyard.d.ts b/types/lanyard.d.ts new file mode 100644 index 0000000..d0f92b0 --- /dev/null +++ b/types/lanyard.d.ts @@ -0,0 +1,13 @@ +type LanyardActivity = { + type: number; + name: string; + details?: string; + state?: string; + [key: string]: unknown; +}; + +type LanyardData = { + discord_status: "online" | "idle" | "dnd" | "offline"; + activities: LanyardActivity[]; + [key: string]: unknown; +};