Skip to content

Commit

Permalink
add hosting view
Browse files Browse the repository at this point in the history
  • Loading branch information
hzrd149 committed Mar 5, 2024
1 parent 6be5cb4 commit c4631e2
Show file tree
Hide file tree
Showing 8 changed files with 406 additions and 3 deletions.
14 changes: 14 additions & 0 deletions src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import Drive from "./pages/Drive.svelte";
import { Alert, Button, Checkbox } from "flowbite-svelte";
import { InfoCircleSolid } from "flowbite-svelte-icons";
import Hosting from "./pages/Hosting.svelte";
import { lastError } from "./services/error";
let remember = localStorage.getItem("auto-login") === "true";
Expand All @@ -19,6 +21,7 @@
const routes = {
"/files": Files,
"/hosting": Hosting,
"/servers": Servers,
"/misc": Misc,
"/drive/:naddr": Drive,
Expand Down Expand Up @@ -47,3 +50,14 @@
</div>
{/if}
</div>

{#if $lastError}
<Alert class="!items-start" dismissable on:close={() => ($lastError = null)}>
<span slot="icon">
<InfoCircleSolid slot="icon" class="h-4 w-4" />
<span class="sr-only">Error</span>
</span>
<p class="font-medium">{$lastError.message}</p>
<p class="whitespace-pre">{$lastError.stack}</p>
</Alert>
{/if}
87 changes: 87 additions & 0 deletions src/components/HostDriveModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<script lang="ts">
import { Button, Input, Modal, Select, Spinner } from "flowbite-svelte";
import { ndk } from "../services/ndk";
import { getDriveName } from "../helpers/drives";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { drives } from "../services/drives";
import dayjs from "dayjs";
import { lastError } from "../services/error";
import { get } from "svelte/store";
export let open = false;
const format = "YYYY-MM-DD";
let selectedDrive = "";
let name = "";
let start = dayjs().format(format);
let end = dayjs().add(1, "month").format(format);
let loading = false;
async function createHostRequest(e: SubmitEvent) {
loading = true;
e.preventDefault();
try {
const now = dayjs().unix();
if (!name) throw new Error("Name required");
if (dayjs(end).unix() <= now) throw new Error("End date must be in the future");
const drive = get(drives)[selectedDrive];
if (!drive) throw new Error("Must select drive");
const request = new NDKEvent(ndk);
// TODO: find correct kind
request.kind = 5902;
request.content = "";
request.tags.push(["i", drive.encode(), "text"]);
request.tags.push(["param", "name", name]);
request.tags.push(["param", "start", String(dayjs(start, format).unix())]);
request.tags.push(["param", "end", String(dayjs(end, format).unix())]);
await request.sign();
await request.publish();
open = false;
} catch (err) {
if (err instanceof Error) lastError.set(err);
}
loading = false;
}
</script>

<Modal title="Host Drive" bind:open outsideclose size="md">
{#if loading}
<Spinner />
{:else}
<form class="flex flex-col gap-2" on:submit={createHostRequest}>
<div>
<label for="drive">Drive</label>
<Select id="drive" name="drive" placeholder="Select Drive..." bind:value={selectedDrive} required>
{#each Object.entries($drives) as [id, drive]}
<option value={id}>{getDriveName(drive)}</option>
{/each}
</Select>
</div>

<div>
<label for="name">Name</label>
<Input type="text" required bind:value={name} pattern="^[a-zA-Z0-9\-]+$" />
</div>

<div class="flex gap-2">
<div class="flex-1">
<label for="start">Start</label>
<Input type="date" required bind:value={start} />
</div>

<div class="flex-1">
<label for="end">End</label>
<Input type="date" required bind:value={end} />
</div>
</div>

<div class="flex flex-row-reverse gap-2">
<Button type="submit">Get Quotes</Button>
<Button color="alternative" on:click={() => (open = false)}>Cancel</Button>
</div>
</form>
{/if}
</Modal>
94 changes: 94 additions & 0 deletions src/components/HostRequestCard.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<script lang="ts">
import type { NDKEvent } from "@nostr-dev-kit/ndk";
import { A, Button, Card } from "flowbite-svelte";
import {
getHostStatusMetadata,
getJobStatusType,
getRequestInput,
getRequestInputParam,
getStatusType,
} from "../helpers/dvm";
import { nip19 } from "nostr-tools";
import { drives } from "../services/drives";
import { getDriveName } from "../helpers/drives";
import dayjs from "dayjs";
import { ArrowRightOutline } from "flowbite-svelte-icons";
import { onDestroy } from "svelte";
import { ndk } from "../services/ndk";
import { Avatar, Name } from "@nostr-dev-kit/ndk-svelte-components";
export let request: NDKEvent;
$: input = getRequestInput(request.rawEvent());
$: name = getRequestInputParam(request.rawEvent(), "name");
$: start = getRequestInputParam(request.rawEvent(), "start");
$: end = getRequestInputParam(request.rawEvent(), "end");
$: parsedInput = input?.value ? nip19.decode(input.value) : undefined;
$: drive = parsedInput?.type === "naddr" ? $drives[parsedInput.data.identifier] : undefined;
const events = ndk.storeSubscribe({ kinds: [7000, 6902 as number], "#e": [request.id] }, { closeOnEose: false });
$: dvmStatus = $events
.filter((e) => e.kind === 7000)
.reduce<Record<string, NDKEvent>>((dir, event) => {
if (event.kind !== 7000) return dir;
if (!dir[event.pubkey] || event.created_at! > dir[event.pubkey].created_at!) {
dir[event.pubkey] = event;
}
return dir;
}, {});
$: dvmComplete = $events
.filter((e) => e.kind === 6902)
.reduce<Record<string, NDKEvent>>((dir, event) => {
if (!dir[event.pubkey] || event.created_at! > dir[event.pubkey].created_at!) {
dir[event.pubkey] = event;
}
return dir;
}, {});
$: console.log(dvmStatus, dvmComplete);
onDestroy(() => {
events.unsubscribe();
});
</script>

<Card>
<div class="mb-2 flex items-center gap-2">
<h5 class="text-lg font-bold tracking-tight text-gray-900 dark:text-white">
<span>{name}</span>
</h5>
<span> - </span>
<a href="#/drive/{input?.value}" class="text-primary-500 hover:underline">
{drive ? getDriveName(drive) : "Unknown"}
</a>
</div>
<div class="font-normal leading-tight text-gray-700 dark:text-gray-400">
<p>
Start: {dayjs.unix(parseInt(start)).format("ll")}
</p>
<p>End: {dayjs.unix(parseInt(end)).format("ll")}</p>
</div>
<div class="my-2">
<p class="font-bold leading-tight text-gray-700 dark:text-gray-400">Hosting:</p>
{#each Object.values(dvmStatus) as status}
<div class="flex gap-2">
<Name pubkey={status.pubkey} />
<span>{getStatusType(status)} - </span>
<a
class="text-primary-300 hover:underline dark:text-primary-700"
href={getHostStatusMetadata(status)?.url}
target="_blank">{getHostStatusMetadata(status)?.domain}</a
>
</div>
{/each}
</div>
<div class="flex items-center gap-2">
<p class="ml-auto">{dayjs.unix(parseInt(end)).diff(dayjs(), "days")} days left</p>
<Button>
Extend <ArrowRightOutline class="ms-2 h-3.5 w-3.5 text-white" />
</Button>
</div>
</Card>
10 changes: 8 additions & 2 deletions src/components/TopNav.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@
import { CogSolid } from "flowbite-svelte-icons";
</script>

<div class="flex items-center gap-2 bg-gray-50 px-6 py-2 text-gray-700 dark:bg-gray-800 dark:text-gray-200">
<h1 class="mr-auto text-lg font-bold">
<div class="flex flex-wrap items-center gap-2 bg-gray-50 px-6 py-2 text-gray-700 dark:bg-gray-800 dark:text-gray-200">
<h1 class="text-lg font-bold">
<a href="#/">🌸 Blossom Drive</a>
</h1>
{#if $activeUser}
<Button href="#/" size="sm" color="alternative">Drives</Button>
<!-- <Button href="#/hosting" size="sm" color="alternative">Hosting</Button> -->
{/if}

<div class="mx-auto" />

{#if $activeUser}
<Button href="#/servers" size="sm" color="alternative"><CogSolid class="mr-2" /> Servers</Button>
Expand Down
160 changes: 160 additions & 0 deletions src/helpers/dvm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import type { NDKEvent, NostrEvent } from "@nostr-dev-kit/ndk";

type DVMMetadata = {
name?: string;
about?: string;
image?: string;
nip90Params?: Record<string, { required: boolean; values: string[] }>;
};
export function parseDVMMetadata(event: NostrEvent) {
try {
return JSON.parse(event.content) as DVMMetadata;
} catch (e) {
return {};
}
}

export function getRequestInputTag(e: NostrEvent) {
return e.tags.find((t) => t[0] === "i");
}

export function getRequestInput(e: NostrEvent) {
const tag = getRequestInputTag(e);
if (!tag) return null;
const [_, value, type, relay, marker] = tag;
if (!value) throw new Error("Missing input value");
if (!type) throw new Error("Missing input type");
return { value, type, relay, marker };
}
export function getRequestRelays(event: NostrEvent) {
return event.tags.find((t) => t[0] === "relays")?.slice(1) ?? [];
}
export function getRequestOutputType(event: NostrEvent): string | undefined {
return event.tags.find((t) => t[0] === "output")?.[1];
}

export function getRequestInputParams(e: NostrEvent, keys: string) {
return e.tags.filter((t) => t[0] === "param" && t[1] === keys).map((t) => t[2]);
}

export function getRequestInputParam(e: NostrEvent, key: string): string;
export function getRequestInputParam(e: NostrEvent, key: string, required: true): string;
export function getRequestInputParam(e: NostrEvent, key: string, required: false): string | undefined;
export function getRequestInputParam(e: NostrEvent, key: string, required: boolean = true) {
const value = getRequestInputParams(e, key)[0];
if (value === undefined && required) throw new Error(`Missing ${key} param`);
return value;
}

export function getStatusType(e: NDKEvent | NostrEvent) {
return e.tags.find((t) => t[0] === "status")?.[1];
}

export function getHostStatusMetadata(e: NDKEvent | NostrEvent) {
if (e.content.startsWith("{")) {
try {
return JSON.parse(e.content) as { url: string; domain: string; end: number };
} catch (e) {}
}
}

// chaining

export function getResponseFromDVM(job: DVMJob, pubkey: string) {
return job.responses.find((r) => r.pubkey === pubkey);
}
export function getResultEventIds(result: NostrEvent) {
const parsed = JSON.parse(result.content);
if (!Array.isArray(parsed)) return [];
const tags = parsed as string[][];
return tags.filter((t) => t[0] === "e").map((t) => t[1]);
}

export type DVMResponse = { pubkey: string; result?: NostrEvent; status?: NostrEvent };
export type DVMJob = { request: NostrEvent; responses: DVMResponse[] };
export type ChainedDVMJob = DVMJob & { next: ChainedDVMJob[]; prevId?: string; prev?: ChainedDVMJob };

export function getJobStatusType(job: DVMJob, dvm?: string) {
const response = dvm ? job.responses[0] : job.responses.find((r) => r.pubkey === dvm);
return response?.status?.tags.find((t) => t[0] === "status")?.[1];
}

export function groupEventsIntoJobs(events: NostrEvent[]) {
const jobs: Record<string, DVMJob> = {};
for (const event of events) {
if (event.kind! >= 5000 && event.kind! < 6000) jobs[event.id!] = { request: event, responses: [] };
}

for (const event of events) {
// skip requests
if (event.kind! >= 5000 && event.kind! < 6000) continue;

const requestId = event.tags.find((t) => t[0] === "e")?.[1];
if (!requestId) continue;
const job = jobs[requestId];
if (!job) continue;

let response = job.responses.find((r) => r.pubkey === event.pubkey);
if (!response) {
response = { pubkey: event.pubkey };
job.responses.push(response);
}

if (event.kind! >= 6000 && event.kind! < 7000) {
if (!response.result || response.result.created_at < event.created_at) response.result = event;
} else if (event.kind === DVM_STATUS_KIND) {
if (!response.status || response.status.created_at < event.created_at) response.status = event;
}
}

return jobs;
}

export function chainJobs(jobs: DVMJob[]) {
const chainedJobs: Record<string, ChainedDVMJob> = {};
for (const job of jobs) {
const input = getRequestInput(job.request);
const prevId = input?.type === "event" ? input.value : undefined;
chainedJobs[job.request.id!] = { ...job, next: [], prevId };
}

// link jobs
for (const job of Object.values(chainedJobs)) {
if (job.prevId) {
const prev = chainedJobs[job.prevId];
if (prev) {
prev.next.push(job);
job.prev = prev;
}
}
}

const rootJobs: ChainedDVMJob[] = Object.values(chainedJobs)
.filter((job) => !job.prevId)
.sort((a, b) => b.request.created_at - a.request.created_at);

return rootJobs;
}

export function flattenJobChain(jobs: ChainedDVMJob[]) {
const chains = Object.values(jobs)
.filter((page) => !page.prevId)
.map((root) => {
const pages: ChainedDVMJob[] = [];

let i = root;
while (i) {
pages.push(i);
i = i.next[0];
}

return pages;
})
.sort((a, b) => b[0].request.created_at - a[0].request.created_at);

return chains;
}

export function getEventIdsFromJobs(jobs: ChainedDVMJob[]) {
return jobs.map((j) => j.responses?.map((r) => (r.result ? getResultEventIds(r.result) : [])).flat()).flat();
}
Loading

0 comments on commit c4631e2

Please sign in to comment.