forked from
juliet.paris/streamplace-spa
minimal streamplace frontend
1import "@atcute/atproto";
2import { type DidDocument, getPdsEndpoint, isAtprotoDid } from "@atcute/identity";
3import {
4 AtprotoWebDidDocumentResolver,
5 CompositeDidDocumentResolver,
6 CompositeHandleResolver,
7 DohJsonHandleResolver,
8 PlcDidDocumentResolver,
9 WellKnownHandleResolver,
10} from "@atcute/identity-resolver";
11
12export const didDocumentResolver = new CompositeDidDocumentResolver({
13 methods: {
14 plc: new PlcDidDocumentResolver(),
15 web: new AtprotoWebDidDocumentResolver(),
16 },
17});
18
19export const handleResolver = new CompositeHandleResolver({
20 strategy: "dns-first",
21 methods: {
22 dns: new DohJsonHandleResolver({ dohUrl: "https://cloudflare-dns.com/dns-query" }),
23 http: new WellKnownHandleResolver(),
24 },
25});
26
27export const resolveHandle = async (handle: string): Promise<string> => {
28 return await handleResolver.resolve(handle as `${string}.${string}`);
29};
30
31export const resolveDidDoc = async (did: string): Promise<DidDocument> => {
32 if (!isAtprotoDid(did)) {
33 throw new Error("Not a valid DID identifier");
34 }
35 return await didDocumentResolver.resolve(did);
36};
37
38const didPDSCache: Record<string, Promise<string>> = {};
39
40export const getPDS = (did: string): Promise<string> => {
41 if (did in didPDSCache) return didPDSCache[did];
42
43 if (!isAtprotoDid(did)) {
44 return Promise.reject(new Error("Not a valid DID identifier"));
45 }
46
47 didPDSCache[did] = (async () => {
48 const doc = await didDocumentResolver.resolve(did);
49 const pds = getPdsEndpoint(doc);
50 if (!pds) {
51 delete didPDSCache[did];
52 throw new Error("No PDS found");
53 }
54 return pds;
55 })();
56
57 return didPDSCache[did];
58};
59
60export interface LiveUser {
61 did: string;
62 handle: string;
63 title: string;
64 viewerCount: number;
65 thumbRef?: string;
66}
67
68// eslint-disable-next-line @typescript-eslint/no-explicit-any
69function mapStream(stream: any): LiveUser {
70 return {
71 did: stream.author?.did || "",
72 handle: stream.author?.handle || "unknown",
73 title: stream.record?.title || "Untitled stream",
74 viewerCount: stream.viewerCount?.count ?? 0,
75 thumbRef: stream.record?.thumb?.ref?.$link,
76 };
77}
78
79export const fetchLiveUsers = async (): Promise<LiveUser[]> => {
80 const res = await fetch("https://stream.place/xrpc/place.stream.live.getLiveUsers");
81 if (!res.ok) throw new Error("Failed to fetch live users");
82 const data = await res.json();
83 const streams = data.streams || [];
84 return streams.map(mapStream);
85};