-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
406 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
Oops, something went wrong.