an attempt to make a lightweight, easily self-hostable, scoped bluesky appview
at main 4.8 kB view raw
1import ky from "npm:ky"; 2import QuickLRU from "npm:quick-lru"; 3import { createHash } from "node:crypto"; 4import { config } from "../config.ts"; 5import * as ATPAPI from "npm:@atproto/api"; 6 7const cache = new QuickLRU({ maxSize: 10000 }); 8 9function simpleHashAuth(auth: string): string { 10 return createHash("sha256").update(auth).digest("hex"); 11} 12export async function cachedFetch(url: string, auth?: string) { 13 const cacheKey = auth ? `${url}|${simpleHashAuth(auth)}` : url; 14 if (cache.has(cacheKey)) return cache.get(cacheKey); 15 16 const data = await ky 17 .get(url, { 18 headers: { 19 Authorization: `${auth}`, 20 }, 21 }) 22 .json(); 23 24 cache.set(cacheKey, data); 25 return data; 26} 27 28export type SlingshotMiniDoc = { 29 did: string; 30 handle: string; 31 pds: string; 32 signing_key: string; 33}; 34let preferences: any = undefined; 35 36export function searchParamsToJson( 37 params: URLSearchParams 38): Record<string, unknown> { 39 const result: Record<string, string | string[]> = {}; 40 41 for (const [key, value] of params.entries()) { 42 if (result.hasOwnProperty(key)) { 43 const existing = result[key]; 44 if (Array.isArray(existing)) { 45 existing.push(value); 46 } else { 47 result[key] = [existing, value]; 48 } 49 } else { 50 result[key] = value; 51 } 52 } 53 54 return result; 55} 56 57export async function resolveIdentity( 58 actor: string 59): Promise<SlingshotMiniDoc> { 60 const url = `${config.slingshot}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${actor}`; 61 return (await cachedFetch(url)) as SlingshotMiniDoc; 62} 63export async function getRecord({ 64 pds, 65 did, 66 collection, 67 rkey, 68}: { 69 pds: string; 70 did: string; 71 collection: string; 72 rkey: string; 73}): Promise<{ cursor?: string; records: GetRecord[] }> { 74 const url = `${pds}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=${collection}&rkey=${rkey}`; 75 const result = (await cachedFetch(url)) as { 76 cursor?: string; 77 records: GetRecord[]; 78 }; 79 return result as { 80 cursor?: string; 81 records: { 82 uri: string; 83 cid: string; 84 value: ATPAPI.AppBskyFeedPost.Record; 85 }[]; 86 }; 87} 88 89export async function listPostRecords({ 90 pds, 91 did, 92 limit = 50, 93 cursor, 94}: { 95 pds: string; 96 did: string; 97 limit: number; 98 cursor?: string; 99}): Promise<{ 100 cursor?: string; 101 records: { uri: string; cid: string; value: ATPAPI.AppBskyFeedPost.Record }[]; 102}> { 103 const url = `${pds}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=app.bsky.feed.post&limit=${limit}${ 104 cursor ? `&cursor=${cursor}` : "" 105 }`; 106 const result = (await cachedFetch(url)) as { 107 cursor?: string; 108 records: GetRecord[]; 109 }; 110 return result as { 111 cursor?: string; 112 records: { 113 uri: string; 114 cid: string; 115 value: ATPAPI.AppBskyFeedPost.Record; 116 }[]; 117 }; 118} 119 120// async function getProfileRecord(did: string): Promise<ATPAPI.AppBskyActorProfile.Record> { 121// const url = `${slingshoturl}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=app.bsky.actor.profile&rkey=self`; 122// const result = await cachedFetch(url) as GetRecord; 123// return result.value as ATPAPI.AppBskyActorProfile.Record; 124// } 125 126export function buildBlobUrl( 127 pds: string, 128 did: string, 129 cid: string 130): string | undefined { 131 if (!pds || !did || !cid) return undefined; 132 return `${pds}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${cid}`; 133} 134 135export type ConstellationDistinctDids = { 136 total: number; 137 linking_dids: string[]; 138 cursor: string; 139}; 140export type GetRecord = { 141 uri: string; 142 cid: string; 143 value: Record<string, unknown>; 144}; 145 146export function didWebToHttps(did: string) { 147 if (!did.startsWith("did:web:")) return null; 148 const parts = did.slice("did:web:".length).split(":"); 149 const [domain, ...path] = parts; 150 return `https://${domain}${path.length ? "/" + path.join("/") : ""}`; 151} 152 153export async function getSlingshotRecord( 154 did: string, 155 collection: string, 156 rkey: string 157): Promise<GetRecord> { 158 const identity = await resolveIdentity(did); 159 //const url = `${config.slingshot}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=${collection}&rkey=${rkey}`; 160 const url = `${identity.pds}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=${collection}&rkey=${rkey}`; 161 const result = (await cachedFetch(url)) as GetRecord; 162 return result as GetRecord; 163} 164 165export async function getUniqueCount({ 166 did, 167 collection, 168 path, 169}: { 170 did: string; 171 collection: string; 172 path: string; 173}): Promise<number> { 174 const url = `${config.constellation}/links/count/distinct-dids?target=${did}&collection=${collection}&path=${path}`; 175 const result = (await cachedFetch(url)) as ConstellationDistinctDids; 176 return result.total; 177} 178 179 180export function withCors(headers: HeadersInit = {}) { 181 return { 182 "Access-Control-Allow-Origin": "*", 183 ...headers, 184 }; 185}