Files
zap.stream/src/utils.ts
2024-05-20 16:45:18 +01:00

231 lines
6.4 KiB
TypeScript

import { CachedMetadata, NostrEvent, NostrLink, TaggedNostrEvent } from "@snort/system";
import type { Tags } from "@/types";
import { LIVE_STREAM, StreamState } from "@/const";
import { GameInfo } from "./service/game-database";
import { AllCategories } from "./pages/category";
import { hexToBech32 } from "@snort/shared";
export function toAddress(e: NostrEvent): string {
if (e.kind && e.kind >= 30000 && e.kind <= 40000) {
const dTag = findTag(e, "d");
return `${e.kind}:${e.pubkey}:${dTag}`;
}
if (e.kind === 0 || e.kind === 3) {
return e.pubkey;
}
return e.id;
}
export function findTag(e: NostrEvent | undefined, tag: string) {
const maybeTag = e?.tags.find(evTag => {
return evTag[0] === tag;
});
return maybeTag && maybeTag[1];
}
export function eventLink(ev: NostrEvent | TaggedNostrEvent) {
return NostrLink.fromEvent(ev).encode();
}
export function getHost(ev?: NostrEvent) {
return ev?.tags.find(a => a[0] === "p" && a[3] === "host")?.[1] ?? ev?.pubkey ?? "";
}
export function profileLink(meta: CachedMetadata | undefined, pubkey: string) {
if (meta && meta.nip05 && meta.nip05.endsWith("@zap.stream") && meta.isNostrAddressValid) {
const [name] = meta.nip05.split("@");
return `/p/${name}`;
}
return `/p/${hexToBech32("npub", pubkey)}`;
}
export function openFile(): Promise<File | undefined> {
return new Promise(resolve => {
const elm = document.createElement("input");
elm.type = "file";
elm.onchange = (e: Event) => {
const elm = e.target as HTMLInputElement;
if (elm.files) {
resolve(elm.files[0]);
} else {
resolve(undefined);
}
};
elm.click();
});
}
export function getTagValues(tags: Tags, tag: string): Array<string> {
return tags
.filter(t => t.at(0) === tag)
.map(t => t.at(1))
.filter(t => t)
.map(t => t as string);
}
export function getEventFromLocationState(state: unknown | undefined | null) {
return state && typeof state === "object" && "kind" in state && state.kind === LIVE_STREAM
? (state as TaggedNostrEvent)
: undefined;
}
export function uniqBy<T>(vals: Array<T>, key: (x: T) => string) {
return Object.values(
vals.reduce(
(acc, v) => {
const k = key(v);
acc[k] ??= v;
return acc;
},
{} as Record<string, T>,
),
);
}
export function getPlaceholder(id: string) {
return `https://nostr.api.v0l.io/api/v1/avatar/robots/${id}.webp`;
}
export function debounce(time: number, fn: () => void): () => void {
const t = setTimeout(fn, time);
return () => clearTimeout(t);
}
export interface StreamInfo {
id?: string;
title?: string;
summary?: string;
image?: string;
status?: StreamState;
stream?: string;
recording?: string;
contentWarning?: string;
tags: Array<string>;
goal?: string;
participants?: string;
starts?: string;
ends?: string;
service?: string;
host?: string;
gameId?: string;
gameInfo?: GameInfo;
}
const gameTagFormat = /^[a-z-]+:[a-z0-9-]+$/i;
export function extractStreamInfo(ev?: NostrEvent) {
const ret = {
host: getHost(ev),
} as StreamInfo;
const matchTag = (tag: Array<string>, k: string, into: (v: string) => void) => {
if (tag[0] === k) {
into(tag[1]);
}
};
for (const t of ev?.tags ?? []) {
matchTag(t, "d", v => (ret.id = v));
matchTag(t, "title", v => (ret.title = v));
matchTag(t, "summary", v => (ret.summary = v));
matchTag(t, "image", v => (ret.image = v));
matchTag(t, "status", v => (ret.status = v as StreamState));
if (t[0] === "streaming" && t[1].startsWith("http")) {
matchTag(t, "streaming", v => (ret.stream = v));
}
matchTag(t, "recording", v => (ret.recording = v));
matchTag(t, "url", v => (ret.recording = v));
matchTag(t, "content-warning", v => (ret.contentWarning = v));
matchTag(t, "current_participants", v => (ret.participants = v));
matchTag(t, "goal", v => (ret.goal = v));
matchTag(t, "starts", v => (ret.starts = v));
matchTag(t, "ends", v => (ret.ends = v));
matchTag(t, "service", v => (ret.service = v));
}
const { regularTags, prefixedTags } = sortStreamTags(ev?.tags ?? []);
ret.tags = regularTags;
const { gameInfo, gameId } = extractGameTag(prefixedTags);
ret.gameId = gameId;
ret.gameInfo = gameInfo;
// video patch
if (ev?.kind === 34_235) {
ret.status = StreamState.VOD;
}
return ret;
}
export function sortStreamTags(tags: Array<string | Array<string>>) {
const plainTags = tags.filter(a => (Array.isArray(a) ? a[0] === "t" : true)).map(a => (Array.isArray(a) ? a[1] : a));
const regularTags = plainTags.filter(a => !a.match(gameTagFormat)) ?? [];
const prefixedTags = plainTags.filter(a => !regularTags.includes(a));
return { regularTags, prefixedTags };
}
export function extractGameTag(tags: Array<string>) {
let gameInfo: GameInfo | undefined = undefined;
const gameId = tags.find(a => a.match(gameTagFormat));
if (gameId?.startsWith("internal:")) {
const internal = AllCategories.find(a => gameId === `internal:${a.id}`);
if (internal) {
gameInfo = {
id: internal?.id,
name: internal.name,
genres: internal.tags,
className: internal.className,
};
}
}
if (gameId === undefined) {
const lowerTags = tags.map(a => a.toLowerCase());
const anyCat = AllCategories.find(a => a.tags.some(b => lowerTags.includes(b)));
if (anyCat) {
gameInfo = {
id: anyCat?.id,
name: anyCat.name,
genres: anyCat.tags,
className: anyCat.className,
};
}
}
return { gameInfo, gameId };
}
export function trackEvent(
event: string,
props?: Record<string, string | boolean>,
e?: { destination?: { url: string } },
) {
if (!import.meta.env.DEV) {
fetch("https://pa.v0l.io/api/event", {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
d: window.location.host,
n: event,
r: document.referrer === location.href ? null : document.referrer,
p: props,
u: e?.destination?.url ?? `${location.protocol}//${location.host}${location.pathname}`,
}),
});
}
}
export function groupBy<T>(val: Array<T>, selector: (a: T) => string | number): Record<string, Array<T>> {
return val.reduce(
(acc, v) => {
const key = selector(v);
acc[key] ??= [];
acc[key].push(v);
return acc;
},
{} as Record<string, Array<T>>,
);
}