diff --git a/.gitignore b/.gitignore index 9abd735..83ff930 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ vanilla/ server/ # Config file for MCGRIZZ -mcgrizz.json \ No newline at end of file +mcgrizz.json +players.cache.json \ No newline at end of file diff --git a/components/Button.tsx b/components/Button.tsx index 1492ab3..b79c06d 100644 --- a/components/Button.tsx +++ b/components/Button.tsx @@ -18,8 +18,13 @@ const colors = { text: "text-white", }, fire: { - bg: "bg-grape", - border: "border-grape-800", + bg: "bg-fire", + border: "border-fire-800", + text: "text-white", + }, + wasabi: { + bg: "bg-wasabi-600", + border: "border-wasabi-800", text: "text-white", }, }; @@ -38,6 +43,7 @@ export function Button( ${colors[color].text} hover:bg-smoke-500 transition-colors + disabled:opacity-30 ${props.class || ""}`; return ( + + + + + ); +} diff --git a/fresh.gen.ts b/fresh.gen.ts index 1012fb8..f52716f 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -5,30 +5,40 @@ import * as $0 from "./routes/_404.tsx"; import * as $1 from "./routes/_app.tsx"; import * as $2 from "./routes/api/fabric/index.ts"; -import * as $3 from "./routes/api/terminal.ts"; -import * as $4 from "./routes/index.tsx"; -import * as $5 from "./routes/setup/eula.tsx"; -import * as $6 from "./routes/setup/fabric.tsx"; -import * as $7 from "./routes/setup/index.tsx"; -import * as $8 from "./routes/setup/start.tsx"; +import * as $3 from "./routes/api/manage.ts"; +import * as $4 from "./routes/api/players.ts"; +import * as $5 from "./routes/index.tsx"; +import * as $6 from "./routes/players.tsx"; +import * as $7 from "./routes/properties.tsx"; +import * as $8 from "./routes/setup/eula.tsx"; +import * as $9 from "./routes/setup/fabric.tsx"; +import * as $10 from "./routes/setup/index.tsx"; +import * as $11 from "./routes/terminal.tsx"; import * as $$0 from "./islands/fabricVersions.tsx"; -import * as $$1 from "./islands/terminal.tsx"; +import * as $$1 from "./islands/players.tsx"; +import * as $$2 from "./islands/statusManager.tsx"; +import * as $$3 from "./islands/terminal.tsx"; const manifest = { routes: { "./routes/_404.tsx": $0, "./routes/_app.tsx": $1, "./routes/api/fabric/index.ts": $2, - "./routes/api/terminal.ts": $3, - "./routes/index.tsx": $4, - "./routes/setup/eula.tsx": $5, - "./routes/setup/fabric.tsx": $6, - "./routes/setup/index.tsx": $7, - "./routes/setup/start.tsx": $8, + "./routes/api/manage.ts": $3, + "./routes/api/players.ts": $4, + "./routes/index.tsx": $5, + "./routes/players.tsx": $6, + "./routes/properties.tsx": $7, + "./routes/setup/eula.tsx": $8, + "./routes/setup/fabric.tsx": $9, + "./routes/setup/index.tsx": $10, + "./routes/terminal.tsx": $11, }, islands: { "./islands/fabricVersions.tsx": $$0, - "./islands/terminal.tsx": $$1, + "./islands/players.tsx": $$1, + "./islands/statusManager.tsx": $$2, + "./islands/terminal.tsx": $$3, }, baseUrl: import.meta.url, }; diff --git a/islands/players.tsx b/islands/players.tsx new file mode 100644 index 0000000..eab5485 --- /dev/null +++ b/islands/players.tsx @@ -0,0 +1,55 @@ +import { useEffect, useRef, useState } from "preact/hooks"; +import { StatusManager } from "./statusManager.tsx"; +import { IS_BROWSER } from "$fresh/runtime.ts"; +import { PlayerData } from "../util/players.ts"; +import { Button } from "../components/Button.tsx"; + +export function ActivePlayerList() { + const [players, setPlayers] = useState([]); + + useEffect(() => { + if (!IS_BROWSER) return; + configureEventSource(); + }, []); + + const eventSource = useRef(); + + const configureEventSource = () => { + if (eventSource.current?.OPEN) eventSource.current.close(); + eventSource.current = new EventSource("/api/players"); + eventSource.current.addEventListener("players", (e) => { + setPlayers(JSON.parse(e.data)); + }); + }; + + const followStatusManager = (action: string) => { + if (action === "started" || action === "restarted") { + configureEventSource(); + } + }; + + return ( +
+ +
+

Active Players

+ {players.map((p) => )} +
+
+ ); +} + +function PlayerCard({ player }: { player: PlayerData }) { + return ( +
+ {`${player.username}'s +
+

{player.username}

+ UUID: {player.id} +
+ Did you know that bats are the only mammal that can fly? +
+
+
+ ); +} diff --git a/islands/statusManager.tsx b/islands/statusManager.tsx new file mode 100644 index 0000000..b3c0d13 --- /dev/null +++ b/islands/statusManager.tsx @@ -0,0 +1,51 @@ +import { JSX } from "preact"; +import { Button } from "../components/Button.tsx"; +import { ManageAction } from "../routes/api/manage.ts"; +import { useEffect, useState } from "preact/hooks"; +import { IS_BROWSER } from "$fresh/runtime.ts"; + +export function StatusManager( + props: JSX.HTMLAttributes & { + onAction?: (res: string) => void; + }, +) { + const sendCommand = async (action: ManageAction) => { + console.log(action); + const res = await fetch("/api/manage", { + method: "POST", + body: action, + }); + const body = await res.text(); + props.onAction && props.onAction(body); + }; + + const [status, setStatus] = useState(''); + + const getStatus = async () => { + const res = await fetch('/api/manage'); + const body = await res.text(); + + setStatus(body); + } + + useEffect(() => { + if (IS_BROWSER) getStatus(); + },[]) + + return ( +
+ {!!status && Server is {status}} +
+ + + +
+
+ ); +} diff --git a/islands/terminal.tsx b/islands/terminal.tsx index 73ac6ca..f2cd562 100644 --- a/islands/terminal.tsx +++ b/islands/terminal.tsx @@ -1,19 +1,40 @@ import { IS_BROWSER } from "$fresh/runtime.ts"; import { useEffect, useRef, useState } from "preact/hooks"; +import { JSX } from "preact/jsx-runtime"; import { Sockpuppet } from "puppet/client"; + export function Terminal(props: { channelId: string }) { const puppet = useRef(new Sockpuppet("ws://sockpuppet.cyborggrizzly.com")); const [lines, setLines] = useState([]); const divRef = useRef(null); + const storeKey = 'commandHistory'; + const [commandHistory, setCommandHistory] = useState(JSON.parse(localStorage.getItem(storeKey) || '[]')); + const [historyIndex, setHistoryIndex] = useState(commandHistory.length); const [command, setCommand] = useState(""); + const changeHistoryIndex = (by: number) => setHistoryIndex(i => i + by); + useEffect(() => { if (!IS_BROWSER) return; puppet.current.joinChannel(props.channelId, (line) => { setLines((l) => [...l, line]); }); + + document.addEventListener('keyup', (e) => { + console.log(e.key) + switch (e.key) { + case 'Up': + case 'ArrowUp': + changeHistoryIndex(-1); + break; + case 'Down': + case 'ArrowDown': + changeHistoryIndex(1); + break; + } + }) }, []); useEffect(() => { @@ -24,22 +45,38 @@ export function Terminal(props: { channelId: string }) { const sendCommand = (e: Event) => { e.preventDefault(); - puppet.current.getChannel(props.channelId)?.send(command); + puppet.current.getChannel(props.channelId)?.send(historyIndex === commandHistory.length ? command : commandHistory[historyIndex]); + setCommandHistory(c => [...c, command]); + setHistoryIndex(commandHistory.length + 1) setCommand(""); }; + const handleCommandUpdate = (e: JSX.TargetedEvent) => { + const value = e.currentTarget.value; + + if (historyIndex !== commandHistory.length) { + setHistoryIndex(commandHistory.length); + } + + setCommand(value || '') + } + + useEffect(() => { + localStorage.setItem(storeKey, JSON.stringify(commandHistory.slice(0,100))); + }, [commandHistory]) + return (
- {lines.map((l) =>
{l}
)} + {lines.map((l) =>
{l}
)}
-
+
setCommand((e.target as HTMLInputElement).value)} + value={commandHistory[historyIndex] || command} + onInput={handleCommandUpdate} />
diff --git a/routes/api/manage.ts b/routes/api/manage.ts new file mode 100644 index 0000000..f7503a0 --- /dev/null +++ b/routes/api/manage.ts @@ -0,0 +1,31 @@ +import { Handlers } from "$fresh/server.ts"; +import { SERVER_STATE } from "../../state/serverState.ts"; + +export enum ManageAction { + start = "start", + stop = "stop", + restart = "restart", +} + +export const handler: Handlers = { + async POST(req) { + const body: ManageAction & string = await req.text() as ManageAction; + switch (body) { + case ManageAction.start: + SERVER_STATE.startMCServer(); + return new Response("started"); + case ManageAction.stop: + SERVER_STATE.gracefullyStopMCServer(); + return new Response("stopped"); + case ManageAction.restart: + SERVER_STATE.restartMCServer(); + return new Response("restarted"); + default: + SERVER_STATE.sendStdIn(body); + return new Response("action done"); + } + }, + GET() { + return new Response(SERVER_STATE.status); + } +}; diff --git a/routes/api/players.ts b/routes/api/players.ts new file mode 100644 index 0000000..96c296c --- /dev/null +++ b/routes/api/players.ts @@ -0,0 +1,44 @@ +import { Handlers } from "$fresh/server.ts"; +import { SERVER_STATE } from "../../state/serverState.ts"; +import { getActivePlayers } from "../../util/players.ts"; + +export const handler: Handlers = { + GET(_req, _ctx) { + let listener: (e: CustomEvent) => void; + const body = new ReadableStream({ + async start(controller){ + console.log('did the thing') + if (SERVER_STATE.status !== 'running') return; + const players = await getActivePlayers(); + + const event = `event:players\ndata:${JSON.stringify(players)}\n\n` + + controller.enqueue(event); + console.log('sent the thing') + + listener = async (e: CustomEvent) => { + console.log('message received') + if (e.detail.includes('joined the game') || e.detail.includes('lost connection')) { + console.log('connection change') + const players = await getActivePlayers(); + const event = `event: players\ndata: ${JSON.stringify(players)}\n\n` + + controller.enqueue(event); + } + } + + globalThis.addEventListener('stdoutmsg' as any, listener); + console.log('listened the thing') + }, + cancel() { + console.log('cancelled') + globalThis.removeEventListener('stdoutmsg' as any, listener); + } + }) + + return new Response(body.pipeThrough(new TextEncoderStream()), { headers: { + "Content-Type": "text/event-stream", + "cache-control": "no-cache" + }}); + } +} \ No newline at end of file diff --git a/routes/api/terminal.ts b/routes/api/terminal.ts deleted file mode 100644 index 973f400..0000000 --- a/routes/api/terminal.ts +++ /dev/null @@ -1 +0,0 @@ -// const \ No newline at end of file diff --git a/routes/players.tsx b/routes/players.tsx new file mode 100644 index 0000000..b64d549 --- /dev/null +++ b/routes/players.tsx @@ -0,0 +1,12 @@ +import { Content } from "../components/Content.tsx"; +import { ActivePlayerList } from "../islands/players.tsx"; + +export default function PlayerManger() { + return ( +
+ + + +
+ ) +} \ No newline at end of file diff --git a/routes/properties.tsx b/routes/properties.tsx new file mode 100644 index 0000000..8b92adc --- /dev/null +++ b/routes/properties.tsx @@ -0,0 +1,32 @@ +import { Handlers } from "$fresh/server.ts"; +import { MCProperties } from "../components/properties.tsx"; +import { + deserializeMCProperties, + serializeMCProperties, +} from "../util/mcProperties.ts"; + +export const handler: Handlers = { + async POST(req, ctx) { + const filePath = `./server/server.properties`; + const formData = await req.formData(); + const text = await Deno.readTextFile(filePath); + const properties = deserializeMCProperties(text); + + for (const [key, value] of formData.entries()) { + properties.set(key, value as string); + } + + const serialized = serializeMCProperties(properties); + + await Deno.writeTextFile(filePath, serialized); + + return ctx.render(); + }, +}; + +export default async function Properties() { + const text = await Deno.readTextFile(`./server/server.properties`); + const properties = await deserializeMCProperties(text); + + return ; +} diff --git a/routes/setup/eula.tsx b/routes/setup/eula.tsx index 3776579..ae2d564 100644 --- a/routes/setup/eula.tsx +++ b/routes/setup/eula.tsx @@ -4,7 +4,7 @@ import { Content } from "../../components/Content.tsx"; import { SERVER_STATE } from "../../state/serverState.ts"; export const handler: Handlers = { - async POST(req, _ctx) { + POST(req, _ctx) { const url = new URL(req.url); SERVER_STATE.acceptEULA(); return Response.redirect(url.origin + '/setup/start'); diff --git a/routes/setup/start.tsx b/routes/setup/start.tsx deleted file mode 100644 index 2b9a622..0000000 --- a/routes/setup/start.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Handlers } from "$fresh/server.ts"; -import { Content } from "../../components/Content.tsx"; -import { Terminal } from "../../islands/terminal.tsx"; -import { SERVER_STATE } from "../../state/serverState.ts"; - -export const handler: Handlers = { - async GET(req,ctx) { - if (!SERVER_STATE.eulaAccepted) { - const url = new URL(req.url); - return Response.redirect(`${url.origin}/setup/eula`) - } - - const res = await ctx.render(); - return res; - } -} - -export default function StartFabric() { - SERVER_STATE.startMCServer(); - return ( -
- - - -
- ); -} diff --git a/routes/terminal.tsx b/routes/terminal.tsx new file mode 100644 index 0000000..f151993 --- /dev/null +++ b/routes/terminal.tsx @@ -0,0 +1,18 @@ +import { Content } from "../components/Content.tsx"; +import { StatusManager } from "../islands/statusManager.tsx"; +import { Terminal } from "../islands/terminal.tsx"; +import { SERVER_STATE } from "../state/serverState.ts"; + +export default function TerminalPage() { + const chId = SERVER_STATE.channelId; + return ( +
+ +
+ +
+ +
+
+ ); +} \ No newline at end of file diff --git a/state/serverState.ts b/state/serverState.ts index b3c105e..2f9d1c2 100644 --- a/state/serverState.ts +++ b/state/serverState.ts @@ -1,49 +1,67 @@ -import { Tail } from "npm:tail"; import { Sockpuppet } from "puppet/client"; import { acceptEULA, checkEULA } from "../util/EULA.ts"; +import { Loader } from "../types/mcgrizzconf.ts"; +import { updateConfFile } from "../util/confFile.ts"; +import { IS_BROWSER } from "$fresh/runtime.ts"; + +type MCServerEvent = 'message'; +type MCServerEventCallback = (msg: string) => void; class ServerState { - private status: "running" | "stopped" = "stopped"; + private _status: "running" | "stopped" = "stopped"; + public get status() { + return this._status; + } private command!: Deno.Command; private process!: Deno.ChildProcess; - private tail: Tail; - private _eulaAccepted: boolean; private sockpuppet!: Sockpuppet; private _channelId = "blanaba"; - public get channelId () { + public get channelId() { return this._channelId; } public get eulaAccepted() { return this._eulaAccepted; } + private stdin!: WritableStreamDefaultWriter; + + private _serverType: Loader = 'unset'; + public get serverType(): Loader { + return this.serverType; + } + public set serverType(loader: Loader) { + updateConfFile({loader}); + this._serverType = loader; + } constructor() { this._eulaAccepted = checkEULA(); this.sockpuppet = new Sockpuppet( "ws://sockpuppet.cyborggrizzly.com", () => { - this.sockpuppet.joinChannel(this.channelId, (msg) => {this.sendStdIn(msg)}); + this.sockpuppet.joinChannel(this.channelId, (msg) => { + this.sendStdIn(msg); + }); }, ); } - public get stdin() { - return this.process.stdin; - } - public get channel() { - return this.sockpuppet.getChannel(this.channelId) + return this.sockpuppet.getChannel(this.channelId); } - public sendStdIn(message: string) { - const msg = new TextEncoder().encode(message); - this.process.stdin.getWriter().write(msg); + public async sendStdIn(message: string) { + if (IS_BROWSER || !this.stdin) return; + + const msg = new TextEncoder().encode(message + "\n"); + + await this.stdin.write(msg); } + // "instance" should be moved to a private member once multi-instance support is implemented public startMCServer(instance = "server") { this.command = new Deno.Command("java", { @@ -60,6 +78,11 @@ class ServerState { this.process = this.command.spawn(); + const {readable, writable} = new TransformStream(); + readable.pipeTo(this.process.stdin); + this.stdin = writable.getWriter(); + + this._status = "running"; this.startStream(); } @@ -68,14 +91,39 @@ class ServerState { const decoder = new TextDecoder(); while (true) { - const {done, value} = await stream.read(); + const { done, value } = await stream.read(); if (value) { - this.channel?.send(decoder.decode(value)); + const line = decoder.decode(value); + this.channel?.send(line); + const stdoutMsg = new CustomEvent('stdoutmsg', { + detail: line + }) + globalThis.dispatchEvent(stdoutMsg); } if (done) break; } } + public gracefullyStopMCServer() { + this._status = "stopped"; + this.sendStdIn("stop"); + } + + public forceStopMCServer() { + this._status = "stopped"; + this.process.kill(); + } + + public async restartMCServer() { + if (this.status === "running") { + await this.sendStdIn("stop"); + // while (true) { + await this.process.status; + // } + } + this.startMCServer(); + } + public acceptEULA() { this._eulaAccepted = true; acceptEULA(); diff --git a/static/styles/tailwind.css b/static/styles/tailwind.css index 676c97d..4983bf4 100644 --- a/static/styles/tailwind.css +++ b/static/styles/tailwind.css @@ -1,2 +1,2 @@ @font-face{font-family:Minecraft;font-style:normal;font-weight:400;src:url(/fonts/minecraft/MinecraftRegular.otf)}@font-face{font-family:Minecraft;font-style:normal;font-weight:700;src:url(/fonts/minecraft/MinecraftBold.otf)}@font-face{font-family:Minecraft;font-style:italic;font-weight:700;src:url(/fonts/minecraft/MinecraftBoldItalic.otf)}@font-face{font-family:Minecraft;font-style:italic;font-weight:400;src:url(/fonts/minecraft/MinecraftItalic.otf)} -/*! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-feature-settings:normal;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{margin-left:auto;margin-right:auto;width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}input,select{--tw-text-opacity:1;border-radius:.75rem;color:rgb(0 0 0/var(--tw-text-opacity));padding:.5rem}.absolute{position:absolute}.relative{position:relative}.bottom-4{bottom:1rem}.left-0{left:0}.-bottom-1{bottom:-.25rem}.right-0{right:0}.m-auto{margin:auto}.mx-auto{margin-left:auto;margin-right:auto}.my-6{margin-bottom:1.5rem;margin-top:1.5rem}.my-4{margin-bottom:1rem;margin-top:1rem}.mb-8{margin-bottom:2rem}.ml-auto{margin-left:auto}.mt-8{margin-top:2rem}.mt-4{margin-top:1rem}.mt-16{margin-top:4rem}.block{display:block}.flex{display:flex}.grid{display:grid}.contents{display:contents}.h-\[100vh\]{height:100vh}.h-full{height:100%}.h-\[600px\]{height:600px}.h-20{height:5rem}.h-5{height:1.25rem}.h-16{height:4rem}.max-h-full{max-height:100%}.w-full{width:100%}.w-20{width:5rem}.w-5{width:1.25rem}.w-16{width:4rem}.min-w-\[400px\]{min-width:400px}.max-w-screen-md{max-width:768px}.flex-1{flex:1 1 0%}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.gap-4{gap:1rem}.gap-8{gap:2rem}.overflow-auto{overflow:auto}.overflow-y-auto{overflow-y:auto}.whitespace-nowrap{white-space:nowrap}.rounded-3xl{border-radius:1.5rem}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.rounded-full{border-radius:9999px}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-2{border-width:2px}.border-4{border-width:4px}.border-r-2{border-right-width:2px}.border-sky{--tw-border-opacity:1;border-color:rgb(49 167 230/var(--tw-border-opacity))}.border-sky-950{--tw-border-opacity:1;border-color:rgb(13 44 68/var(--tw-border-opacity))}.border-licorice-800{--tw-border-opacity:1;border-color:rgb(150 60 151/var(--tw-border-opacity))}.border-sky-800{--tw-border-opacity:1;border-color:rgb(17 82 123/var(--tw-border-opacity))}.border-grape-800{--tw-border-opacity:1;border-color:rgb(109 33 135/var(--tw-border-opacity))}.bg-licorice{--tw-bg-opacity:1;background-color:rgb(22 10 22/var(--tw-bg-opacity))}.bg-smoke-900{--tw-bg-opacity:1;background-color:rgb(57 57 65/var(--tw-bg-opacity))}.bg-grape{--tw-bg-opacity:1;background-color:rgb(64 10 80/var(--tw-bg-opacity))}.bg-smoke-500{--tw-bg-opacity:1;background-color:rgb(115 117 132/var(--tw-bg-opacity))}.bg-smoke-600{--tw-bg-opacity:1;background-color:rgb(93 94 108/var(--tw-bg-opacity))}.bg-sky{--tw-bg-opacity:1;background-color:rgb(49 167 230/var(--tw-bg-opacity))}.bg-smoke-200{--tw-bg-opacity:1;background-color:rgb(217 217 222/var(--tw-bg-opacity))}.p-4{padding:1rem}.p-8{padding:2rem}.p-2{padding:.5rem}.p-1{padding:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.text-center{text-align:center}.text-right{text-align:right}.font-pixel{font-family:Minecraft,cursive}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-6xl{font-size:3.75rem;line-height:1}.text-5xl{font-size:3rem;line-height:1}.font-bold{font-weight:700}.text-sky{--tw-text-opacity:1;color:rgb(49 167 230/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-smoke-800\/30{color:#41414b4d}.underline{text-decoration-line:underline}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.loader-ball{animation:color-cycle 3s linear infinite;animation-delay:var(--loader-delay);margin:auto}.loader{animation:spin 5s ease-in-out;animation-direction:alternate;animation-iteration-count:infinite}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(4turn)}}@keyframes color-cycle{0%{--tw-bg-opacity:1;background-color:rgb(230 28 28/var(--tw-bg-opacity))}25%{--tw-bg-opacity:1;background-color:rgb(64 10 80/var(--tw-bg-opacity))}50%{--tw-bg-opacity:1;background-color:rgb(49 167 230/var(--tw-bg-opacity))}75%{--tw-bg-opacity:1;background-color:rgb(128 134 39/var(--tw-bg-opacity))}to{--tw-bg-opacity:1;background-color:rgb(230 28 28/var(--tw-bg-opacity))}}*{font-family:Titillium Web,sans-serif}.even\:bg-black\/10:nth-child(2n){background-color:#0000001a}.hover\:bg-smoke-500:hover{--tw-bg-opacity:1;background-color:rgb(115 117 132/var(--tw-bg-opacity))}@media (prefers-color-scheme:dark){.dark\:border-sky-950{--tw-border-opacity:1;border-color:rgb(13 44 68/var(--tw-border-opacity))}.dark\:bg-smoke{--tw-bg-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity))}.dark\:bg-smoke-900{--tw-bg-opacity:1;background-color:rgb(57 57 65/var(--tw-bg-opacity))}.dark\:text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}} \ No newline at end of file +/*! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-feature-settings:normal;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{margin-left:auto;margin-right:auto;width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}input,select{--tw-text-opacity:1;border-radius:.75rem;color:rgb(0 0 0/var(--tw-text-opacity));padding:.5rem}.absolute{position:absolute}.relative{position:relative}.bottom-4{bottom:1rem}.left-0{left:0}.-bottom-1{bottom:-.25rem}.right-0{right:0}.col-span-2{grid-column:span 2/span 2}.m-auto{margin:auto}.mx-auto{margin-left:auto;margin-right:auto}.my-6{margin-bottom:1.5rem;margin-top:1.5rem}.my-4{margin-bottom:1rem;margin-top:1rem}.mb-8{margin-bottom:2rem}.ml-auto{margin-left:auto}.mt-8{margin-top:2rem}.mt-4{margin-top:1rem}.block{display:block}.flex{display:flex}.grid{display:grid}.contents{display:contents}.h-\[100vh\]{height:100vh}.h-full{height:100%}.h-\[600px\]{height:600px}.h-20{height:5rem}.h-5{height:1.25rem}.h-16{height:4rem}.max-h-full{max-height:100%}.w-full{width:100%}.w-16{width:4rem}.w-20{width:5rem}.w-5{width:1.25rem}.min-w-\[400px\]{min-width:400px}.max-w-screen-md{max-width:768px}.flex-1{flex:1 1 0%}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.gap-4{gap:1rem}.gap-8{gap:2rem}.gap-y-4{row-gap:1rem}.gap-x-16{-moz-column-gap:4rem;column-gap:4rem}.overflow-auto{overflow:auto}.overflow-y-auto{overflow-y:auto}.whitespace-nowrap{white-space:nowrap}.rounded-3xl{border-radius:1.5rem}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.rounded-full{border-radius:9999px}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-2{border-width:2px}.border-4{border-width:4px}.border-r-2{border-right-width:2px}.border-sky{--tw-border-opacity:1;border-color:rgb(49 167 230/var(--tw-border-opacity))}.border-sky-950{--tw-border-opacity:1;border-color:rgb(13 44 68/var(--tw-border-opacity))}.border-licorice-800{--tw-border-opacity:1;border-color:rgb(150 60 151/var(--tw-border-opacity))}.border-sky-800{--tw-border-opacity:1;border-color:rgb(17 82 123/var(--tw-border-opacity))}.border-grape-800{--tw-border-opacity:1;border-color:rgb(109 33 135/var(--tw-border-opacity))}.border-fire-800{--tw-border-opacity:1;border-color:rgb(160 20 20/var(--tw-border-opacity))}.border-wasabi-800{--tw-border-opacity:1;border-color:rgb(79 84 32/var(--tw-border-opacity))}.bg-licorice{--tw-bg-opacity:1;background-color:rgb(22 10 22/var(--tw-bg-opacity))}.bg-smoke-900{--tw-bg-opacity:1;background-color:rgb(57 57 65/var(--tw-bg-opacity))}.bg-grape{--tw-bg-opacity:1;background-color:rgb(64 10 80/var(--tw-bg-opacity))}.bg-smoke-500{--tw-bg-opacity:1;background-color:rgb(115 117 132/var(--tw-bg-opacity))}.bg-smoke-600{--tw-bg-opacity:1;background-color:rgb(93 94 108/var(--tw-bg-opacity))}.bg-sky{--tw-bg-opacity:1;background-color:rgb(49 167 230/var(--tw-bg-opacity))}.bg-fire{--tw-bg-opacity:1;background-color:rgb(230 28 28/var(--tw-bg-opacity))}.bg-wasabi-600{--tw-bg-opacity:1;background-color:rgb(128 134 39/var(--tw-bg-opacity))}.bg-smoke-200{--tw-bg-opacity:1;background-color:rgb(217 217 222/var(--tw-bg-opacity))}.p-4{padding:1rem}.p-8{padding:2rem}.p-2{padding:.5rem}.p-1{padding:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.text-center{text-align:center}.text-right{text-align:right}.font-pixel{font-family:Minecraft,cursive}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-lg{font-size:1.125rem}.text-lg,.text-xl{line-height:1.75rem}.text-xl{font-size:1.25rem}.text-6xl{font-size:3.75rem;line-height:1}.text-5xl{font-size:3rem;line-height:1}.font-bold{font-weight:700}.text-sky{--tw-text-opacity:1;color:rgb(49 167 230/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-smoke-800\/30{color:#41414b4d}.underline{text-decoration-line:underline}.opacity-50{opacity:.5}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.loader-ball{animation:color-cycle 3s linear infinite;animation-delay:var(--loader-delay);margin:auto}.loader{animation:spin 5s ease-in-out;animation-direction:alternate;animation-iteration-count:infinite}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(4turn)}}@keyframes color-cycle{0%{--tw-bg-opacity:1;background-color:rgb(230 28 28/var(--tw-bg-opacity))}25%{--tw-bg-opacity:1;background-color:rgb(64 10 80/var(--tw-bg-opacity))}50%{--tw-bg-opacity:1;background-color:rgb(49 167 230/var(--tw-bg-opacity))}75%{--tw-bg-opacity:1;background-color:rgb(128 134 39/var(--tw-bg-opacity))}to{--tw-bg-opacity:1;background-color:rgb(230 28 28/var(--tw-bg-opacity))}}*{font-family:Titillium Web,sans-serif}.even\:bg-black\/10:nth-child(2n){background-color:#0000001a}.hover\:bg-smoke-500:hover{--tw-bg-opacity:1;background-color:rgb(115 117 132/var(--tw-bg-opacity))}.disabled\:opacity-30:disabled{opacity:.3}@media (prefers-color-scheme:dark){.dark\:border-sky-950{--tw-border-opacity:1;border-color:rgb(13 44 68/var(--tw-border-opacity))}.dark\:bg-smoke{--tw-bg-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity))}.dark\:bg-smoke-900{--tw-bg-opacity:1;background-color:rgb(57 57 65/var(--tw-bg-opacity))}.dark\:text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}} \ No newline at end of file diff --git a/util/EULA.ts b/util/EULA.ts index f08d5d1..4d70e04 100644 --- a/util/EULA.ts +++ b/util/EULA.ts @@ -1,11 +1,13 @@ +import { IS_BROWSER } from "$fresh/runtime.ts"; + const eulaRegex = /(eula=false)/; -export const checkEULA = (instance = 'server') => - !eulaRegex.test(Deno.readTextFileSync(`./${instance}/eula.txt`)); +export const checkEULA = (instance = "server") => + !IS_BROWSER && !eulaRegex.test(Deno.readTextFileSync(`./${instance}/eula.txt`)); + // true; - -export const acceptEULA = (instance = 'server') => { +export const acceptEULA = (instance = "server") => { const eula = Deno.readTextFileSync(`./${instance}/eula.txt`); - const mod = eula.replace(eulaRegex, 'eula=true'); - console.log(mod) - Deno.writeTextFileSync(`./${instance}/eula.txt`, mod); -} \ No newline at end of file + const mod = eula.replace(eulaRegex, "eula=true"); + console.log(mod); + !IS_BROWSER && Deno.writeTextFileSync(`./${instance}/eula.txt`, mod); +}; diff --git a/util/confFile.ts b/util/confFile.ts new file mode 100644 index 0000000..cb4e11d --- /dev/null +++ b/util/confFile.ts @@ -0,0 +1,30 @@ +import { MCGrizzConf } from "../types/mcgrizzconf.ts"; + +const defaultConf: MCGrizzConf = { + loader: 'unset', +} + +const confPath = 'mcgrizz.json' + +export function makeConfFile(): MCGrizzConf { + + Deno.writeTextFileSync(confPath, JSON.stringify(defaultConf, null, 2), {create: true}); + + return defaultConf; +} + +export function getConfFile(): MCGrizzConf { + const conf = JSON.parse(Deno.readTextFileSync(confPath)); + + if (!conf) { + return makeConfFile(); + } + + return conf; +} + +export async function updateConfFile(newConf: Partial) { + const conf = {...getConfFile(), newConf}; + + await Deno.writeTextFile(confPath, JSON.stringify(conf, null, 2)); +} diff --git a/util/filters.ts b/util/filters.ts new file mode 100644 index 0000000..ae34851 --- /dev/null +++ b/util/filters.ts @@ -0,0 +1 @@ +export const filterTruthy = (a: unknown) => !!a \ No newline at end of file diff --git a/util/getNavItems.ts b/util/getNavItems.ts index 8d80a80..87029ae 100644 --- a/util/getNavItems.ts +++ b/util/getNavItems.ts @@ -1,6 +1,6 @@ import { MCGrizzConf } from "../types/mcgrizzconf.ts"; import { NavItem } from "../types/nav.ts"; -import { makeConfFile } from "./makeConfFile.ts"; +import { makeConfFile } from "./confFile.ts"; /** * @description Determines the state of setup and returns the nav items for that state @@ -8,7 +8,7 @@ import { makeConfFile } from "./makeConfFile.ts"; export function getNavItems(): NavItem[] { let conf: MCGrizzConf; try { - conf = JSON.parse(Deno.readTextFileSync('mcgrizz.json')); + conf = JSON.parse(Deno.readTextFileSync("mcgrizz.json")); } catch { conf = makeConfFile(); } @@ -16,12 +16,25 @@ export function getNavItems(): NavItem[] { switch (conf.loader) { case "unset": return [{ - title: 'Setup', - href: '/', - }] + title: "Setup", + href: "/", + }]; case "forge": case "fabric": case "vanilla": - return []; + return [ + { + title: "Server Terminal", + href: "/terminal", + }, + { + title: "Players", + href: "/players", + }, + { + title: "Server Properties", + href: "/properties", + }, + ]; } } diff --git a/util/makeConfFile.ts b/util/makeConfFile.ts deleted file mode 100644 index 092a396..0000000 --- a/util/makeConfFile.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { MCGrizzConf } from "../types/mcgrizzconf.ts"; - -export function makeConfFile(): MCGrizzConf { - const conf: MCGrizzConf = { - loader: 'unset', - } - - Deno.writeTextFileSync('mcgrizz.json', JSON.stringify(conf), {create: true}); - - return conf; -} \ No newline at end of file diff --git a/util/mcProperties.ts b/util/mcProperties.ts new file mode 100644 index 0000000..c438437 --- /dev/null +++ b/util/mcProperties.ts @@ -0,0 +1,24 @@ +import { filterTruthy } from "./filters.ts"; + +export const deserializeMCProperties = (serialized: string) => { + const propertiesMap = new Map(); + + const commentRegex = /#.+\r?\n/g; + serialized.replace(commentRegex, '') + .split(/\r?\n/) + .filter(filterTruthy) + .forEach(prop => { + const [key, value] = prop.split('='); + propertiesMap.set(key, value ?? ''); + }); + + return propertiesMap; +} + +export const serializeMCProperties = (deserialized: Map) => { + let text = ''; + for (const [key,value] of deserialized.entries()) { + text += `${key}=${value}\n`; + } + return text; +} \ No newline at end of file diff --git a/util/players.ts b/util/players.ts new file mode 100644 index 0000000..2b51b48 --- /dev/null +++ b/util/players.ts @@ -0,0 +1,46 @@ +import { SERVER_STATE } from "../state/serverState.ts"; +import { filterTruthy } from "./filters.ts"; + +const playerListRegex= /There are [0-9] of a max of [0-9]+ players online:/ + +export const getActivePlayers = (): Promise => new Promise(res => { + const listener = async (e: CustomEvent) => { + if (playerListRegex.test(e.detail)) { + const players: PlayerData[] = [] + const [_, playerString] = e.detail.split(playerListRegex); + for (const playerName of playerString.split(', ')) { + players.push(await getPlayerData(playerName)); + } + res(players.filter(filterTruthy)); + globalThis.removeEventListener('stdoutmsg' as any, listener); + } + } + globalThis.addEventListener('stdoutmsg' as any, listener); + SERVER_STATE.sendStdIn('list'); +}) + +export type PlayerData = { + username: string; + id: string; + avatar: string; +} + +const getPlayerData = async (username: string) => { + username = username.trim(); + if (!username) return; + const cacheFile = 'players.cache.json' + await Deno.create(cacheFile); + const cache = JSON.parse(await Deno.readTextFile(cacheFile) || '{}'); + if (!cache[username]) { + const req = await fetch('https://playerdb.co/api/player/minecraft/' + username, { + headers: { + "User-Agent": "MCGRIZZ/0.1 emma@cyborggrizzly.com" + } + }); + const json = await req.json(); + cache[username] = json.data.player; + await Deno.writeTextFile(cacheFile, JSON.stringify(cache, null, 2), {create: true}); + } + + return cache[username] +} diff --git a/util/startServer.ts b/util/startServer.ts deleted file mode 100644 index 4ae21a5..0000000 --- a/util/startServer.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { startTail } from "./startTail.ts"; - -export async function startMCServer(instance = "server") { - const start = new Deno.Command("java", { - args: [ - "-Xmx2G", - "-jar", - "./server.jar", - "nogui", - ], - cwd: "./" + instance, - }); - - start.output(); - - return await startTail(instance); -} diff --git a/util/startTail.ts b/util/startTail.ts deleted file mode 100644 index b6fd1a0..0000000 --- a/util/startTail.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Sockpuppet } from "puppet/client"; -import { Tail } from "npm:tail"; - -export function startTail(instance = "server") { - return new Promise<[Tail, string]>((resolve) => { - const puppet = new Sockpuppet("ws://sockpuppet.cyborggrizzly.com", () => { - const channelId = crypto.randomUUID(); - puppet.createChannel(channelId); - puppet.joinChannel(channelId, (line) => { - console.log(line); - }); - - setTimeout(() => { - const path = `./${instance}/logs/latest.log`; - const tail = new Tail(path, { follow: true }); - - tail.on("line", (line: string) => { - puppet.getChannel(channelId)?.send(line); - console.log(line); - }); - resolve([tail, channelId]); - }, 1000); - }); - }); -}