import { NostrEvent } from "@snort/system"; import { useContext, useEffect, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { SnortContext } from "@snort/system-react"; import { NostrStreamProvider, StreamProviderEndpoint, StreamProviderInfo } from "@/providers"; import { SendZaps } from "./send-zap"; import { StreamEditor, StreamEditorProps } from "./stream-editor"; import Spinner from "./spinner"; import { unwrap } from "@snort/shared"; import { useRates } from "@/hooks/rates"; import { DefaultButton } from "./buttons"; import Pill from "./pill"; export function NostrProviderDialog({ provider, showEndpoints, showEditor, showForwards, ...others }: { provider: NostrStreamProvider; showEndpoints: boolean; showEditor: boolean; showForwards: boolean; } & StreamEditorProps) { const system = useContext(SnortContext); const [topup, setTopup] = useState(false); const [info, setInfo] = useState(); const [ep, setEndpoint] = useState(); const [hrs, setHrs] = useState(25); const [tos, setTos] = useState(false); const rate = useRates("BTCUSD"); function sortEndpoints(arr: Array) { return arr.sort((a, b) => ((a.rate ?? 0) > (b.rate ?? 0) ? -1 : 1)); } async function loadInfo() { const info = await provider.info(); setInfo(info); setTos(info.tosAccepted ?? true); setEndpoint(sortEndpoints(info.endpoints)[0]); } useEffect(() => { loadInfo(); }, [provider]); if (!info) { return ; } if (topup) { return ( { const pr = await provider.topup(amount); return { pr }; }, }} onFinish={() => { provider.info().then(v => { setInfo(v); setTopup(false); }); }} /> ); } function calcEstimate() { if (!ep?.rate || !ep?.unit || !info?.balance || !info.balance) return; const raw = Math.max(0, info.balance / ep.rate); if (ep.unit === "min" && raw > 60) { const pm = hrs * 60 * ep.rate; return ( <> {`${(raw / 60).toFixed(0)} hour @ ${ep.rate} sats/${ep.unit}`}   or
{`${pm.toLocaleString()} sats/month ($${(rate.ask * pm * 1e-8).toFixed(2)}/mo) streaming ${hrs} hrs/month`}
Hrs setHrs(e.target.valueAsNumber)} />
); } return `${raw.toFixed(0)} ${ep.unit} @ ${ep.rate} sats/${ep.unit}`; } function parseCapability(cap: string) { const [tag, ...others] = cap.split(":"); if (tag === "variant") { const [height] = others; return height === "source" ? "source" : `${height.slice(0, -1)}p`; } if (tag === "output") { return others[0]; } return cap; } async function acceptTos() { await provider.acceptTos(); const i = await provider.info(); setInfo(i); } function tosInput() { if (!info) return; return ( <>
setTos(e.target.checked)} />

window.open(info.tosLink, "popup", "width=400,height=800")}> ), }} />

); } function streamEndpoints() { if (!info) return; return ( <> {info.endpoints.length > 1 && (

{sortEndpoints(info.endpoints).map(a => ( setEndpoint(a)}> {a.name} ))}
)}

window.navigator.clipboard.writeText(ep?.key ?? "")}>

setTopup(true)}>

{ep?.capabilities?.map(a => ( {parseCapability(a)} ))}
); } function streamEditor() { if (!info || !showEditor) return; if (info.tosAccepted === false) { return tosInput(); } return ( { provider.updateStreamInfo(system, ex); others.onFinish?.(ex); }} ev={ { tags: [ ["title", info.streamInfo?.title ?? ""], ["summary", info.streamInfo?.summary ?? ""], ["image", info.streamInfo?.image ?? ""], ...(info.streamInfo?.goal ? [["goal", info.streamInfo.goal]] : []), ...(info.streamInfo?.content_warning ? [["content-warning", info.streamInfo?.content_warning]] : []), ...(info.streamInfo?.tags?.map(a => ["t", a]) ?? []), ], } as NostrEvent } options={{ canSetStream: false, canSetStatus: false, }} /> ); } function forwardInputs() { if (!info || !showForwards) return; return (

{info.forwards?.map(a => ( <>
{a.name}
{ await provider.removeForward(a.id); await loadInfo(); }}> ))}
); } return ( <> {showEndpoints && streamEndpoints()} {streamEditor()} {forwardInputs()} ); } enum ForwardService { Custom = "custom", Twitch = "twitch", Youtube = "youtube", Facebook = "facebook", Kick = "kick", Trovo = "trovo", } function AddForwardInputs({ provider, onAdd, }: { provider: NostrStreamProvider; onAdd: (name: string, target: string) => void; }) { const [name, setName] = useState(""); const [target, setTarget] = useState(""); const [svc, setService] = useState(ForwardService.Twitch); const [error, setError] = useState(""); const { formatMessage } = useIntl(); async function getTargetFull() { if (svc === ForwardService.Custom) { return target; } if (svc === ForwardService.Twitch) { const urls = (await (await fetch("https://ingest.twitch.tv/ingests")).json()) as { ingests: Array<{ availability: number; name: string; url_template: string; }>; }; const ingestsEurope = urls.ingests.filter( a => a.name.toLowerCase().startsWith("europe:") && a.availability === 1 ); const random = ingestsEurope.at(ingestsEurope.length * Math.random()); return unwrap(random).url_template.replace("{stream_key}", target); } if (svc === ForwardService.Youtube) { return `rtmp://a.rtmp.youtube.com:1935/live2/${target}`; } if (svc === ForwardService.Facebook) { return `rtmps://live-api-s.facebook.com:443/rtmp/${target}`; } if (svc === ForwardService.Trovo) { return `rtmp://livepush.trovo.live:1935/live/${target}`; } if (svc === ForwardService.Kick) { return `rtmps://fa723fc1b171.global-contribute.live-video.net:443/app/${target}`; } } async function doAdd() { if (svc === ForwardService.Custom) { if (!target.startsWith("rtmp://")) { setError( formatMessage({ defaultMessage: "Stream url must start with rtmp://", id: "7+bCC1", }) ); return; } try { // stupid URL parser doesnt work for non-http protocols const u = new URL(target.replace("rtmp://", "http://")); console.debug(u); if (u.host.length < 1) { throw new Error(); } if (u.pathname === "/") { throw new Error(); } } catch { setError( formatMessage({ defaultMessage: "Not a valid URL", id: "1q4BO/", }) ); return; } } else { if (target.length < 2) { setError( formatMessage({ defaultMessage: "Stream Key is required", id: "50+/JW", }) ); return; } } if (name.length < 2) { setError( formatMessage({ defaultMessage: "Name is required", id: "Gvxoji", }) ); return; } try { const t = await getTargetFull(); if (!t) throw new Error( formatMessage({ defaultMessage: "Could not create stream URL", id: "E9APoR", }) ); await provider.addForward(name, t); } catch (e) { setError((e as Error).message); } setName(""); setTarget(""); onAdd(name, target); } return (
setName(e.target.value)} />
setTarget(e.target.value)} /> {error && {error}}
); }