import { base64 } from "@scure/base"; import { StreamProvider, StreamProviderEndpoint, StreamProviderInfo, StreamProviderStreamInfo, StreamProviders, } from "."; import { EventKind, EventPublisher, NostrEvent, SystemInterface } from "@snort/system"; import { Login } from "@/index"; import { getPublisher } from "@/login"; import { extractStreamInfo } from "@/utils"; import { StreamState } from "@/const"; export class NostrStreamProvider implements StreamProvider { #publisher?: EventPublisher; constructor(readonly name: string, readonly url: string, pub?: EventPublisher) { if (!url.endsWith("/")) { this.url = `${url}/`; } this.#publisher = pub; } get type() { return StreamProviders.NostrType; } async info() { const rsp = await this.#getJson("GET", "account"); return { type: StreamProviders.NostrType, name: this.name, state: StreamState.Planned, viewers: 0, streamInfo: rsp.event, balance: rsp.balance, tosAccepted: rsp.tos?.accepted, tosLink: rsp.tos?.link, endpoints: rsp.endpoints.map(a => { return { name: a.name, url: a.url, key: a.key, rate: a.cost.rate, unit: a.cost.unit, capabilities: a.capabilities, } as StreamProviderEndpoint; }), forwards: rsp.forwards, } as StreamProviderInfo; } createConfig() { return { type: StreamProviders.NostrType, url: this.url, }; } async updateStreamInfo(_: SystemInterface, ev: NostrEvent): Promise { const { title, summary, image, tags, contentWarning, goal } = extractStreamInfo(ev); await this.#getJson("PATCH", "event", { title, summary, image, tags, content_warning: contentWarning, goal, }); } async topup(amount: number): Promise { const rsp = await this.#getJson("GET", `topup?amount=${amount}`); return rsp.pr; } async acceptTos(): Promise { await this.#getJson("PATCH", "account", { accept_tos: true, }); } async addForward(name: string, target: string): Promise { await this.#getJson("POST", "account/forward", { name, target, }); } async removeForward(id: string): Promise { await this.#getJson("DELETE", `account/forward/${id}`); } async prepareClip(id: string) { return await this.#getJson<{ id: string; length: number }>("GET", `clip/${id}`); } async createClip(id: string, clipId: string, start: number, length: number) { return await this.#getJson<{ url: string }>("POST", `clip/${id}/${clipId}?start=${start}&length=${length}`); } async getNotificationsInfo() { return await this.#getJson<{ publicKey: string }>("GET", "notifications/info"); } async subscribeNotifications(req: { endpoint: string; key: string; auth: string; scope: string }) { return await this.#getJson<{ id: string }>("POST", "notifications/register", req); } async listStreamerSubscriptions(auth: string) { return await this.#getJson>("GET", `notifications?auth=${auth}`); } async addStreamerSubscription(pubkey: string) { return await this.#getJson("PATCH", `notifications?pubkey=${pubkey}`); } async removeStreamerSubscription(pubkey: string) { return await this.#getJson("DELETE", `notifications?pubkey=${pubkey}`); } getTempClipUrl(id: string, clipId: string) { return `${this.url}clip/${id}/${clipId}`; } async #getJson(method: "GET" | "POST" | "PATCH" | "DELETE", path: string, body?: unknown): Promise { const pub = (() => { if (this.#publisher) { return this.#publisher; } else { const login = Login.snapshot(); return login && getPublisher(login); } })(); if (!pub) throw new Error("No signer"); const u = `${this.url}${path}`; const token = await pub.generic(eb => { return eb.kind(EventKind.HttpAuthentication).content("").tag(["u", u]).tag(["method", method]); }); const rsp = await fetch(u, { method, body: body ? JSON.stringify(body) : undefined, headers: { "content-type": "application/json", authorization: `Nostr ${base64.encode(new TextEncoder().encode(JSON.stringify(token)))}`, }, }); const json = await rsp.text(); if (!rsp.ok) { throw new Error(json); } return json.length > 0 ? (JSON.parse(json) as T) : ({} as T); } } interface AccountResponse { balance: number; event?: StreamProviderStreamInfo; endpoints: Array; tos?: { accepted: boolean; link: string; }; forwards: Array; } interface ForwardDest { id: string; name: string; } interface IngestEndpoint { name: string; url: string; key: string; cost: { unit: string; rate: number; }; capabilities: Array; } interface TopUpResponse { pr: string; }