[WIP] A (somewhat barebones) atproto app for creating custom sites without hosting!

server: index old appview records

this doesnt include ingesting their blobs since that'll happen at request time

vielle.dev c2087db7 854ac924

verified
Changed files
+136 -36
server
src
+7 -21
server/src/backfill/index.ts
··· 3 3 import * as schema from "../db/schema.ts"; 4 4 import { db as db_type } from "../utils.ts"; 5 5 import { Client, simpleFetchHandler } from "@atcute/client"; 6 + import oldRecords from "./old-records.ts"; 6 7 7 8 const db: db_type = drizzle<typeof schema>(Deno.env.get("DB_FILE_NAME")!); 8 9 ··· 13 14 res = p_res; 14 15 }); 15 16 17 + // clear old records 18 + // accounts could have deleted their sites or anything 19 + // so we should just nuke them 20 + await db.delete(routes); 21 + 16 22 const backfillClient = new Client({ 17 23 handler: simpleFetchHandler({ 18 24 service: ··· 23 29 }), 24 30 }); 25 31 26 - let repos: `did:${string}:${string}`[] = []; 27 - let cursor: string | undefined; 28 - while (true) { 29 - const { data, ok } = await backfillClient.get( 30 - "com.atproto.sync.listReposByCollection", 31 - { 32 - params: { 33 - collection: "dev.atcities.route", 34 - cursor, 35 - }, 36 - } 37 - ); 38 - 39 - if (!ok) { 40 - throw data.error + "\n" + data.message; 41 - } 42 - 43 - repos = [...repos, ...data.repos.map((x) => x.did)]; 44 - cursor = data.cursor; 45 - if (!cursor) break; 46 - } 32 + oldRecords(backfillClient, db); 47 33 48 34 res(db);
+79
server/src/backfill/old-records.ts
··· 1 + import { Client, simpleFetchHandler } from "@atcute/client"; 2 + import { db, getPds, isDid, rkeyToUrl } from "../utils.ts"; 3 + import { is } from "@atcute/lexicons"; 4 + import { DevAtcitiesRoute } from "../lexicons/index.ts"; 5 + import { routes } from "../db/schema.ts"; 6 + 7 + async function index(did: `did:${"web" | "plc"}:${string}`, db: db) { 8 + const pds = await getPds(did); 9 + if (!pds) return console.error(did, "could not be resolved to a pds."); 10 + const pdsClient = new Client({ 11 + handler: simpleFetchHandler({ service: pds }), 12 + }); 13 + let cursor: string | undefined; 14 + while (true) { 15 + const { data, ok } = await pdsClient.get("com.atproto.repo.listRecords", { 16 + params: { 17 + repo: did, 18 + collection: "dev.atcities.route", 19 + cursor, 20 + }, 21 + }); 22 + 23 + if (!ok) { 24 + console.error("Got error:", data.error, data.message); 25 + break; 26 + } 27 + 28 + for (const record of data.records) 29 + if (is(DevAtcitiesRoute.mainSchema, record.value)) { 30 + if (record.value.page.$type === "dev.atcities.route#blob") { 31 + const url = rkeyToUrl(record.uri.split("/")[4]); 32 + if (!url) continue; 33 + const data: typeof routes.$inferInsert = { 34 + did: did, 35 + url_route: url, 36 + blob_cid: 37 + "ref" in record.value.page.blob 38 + ? record.value.page.blob.ref.$link 39 + : record.value.page.blob.cid, 40 + mime: record.value.page.blob.mimeType, 41 + }; 42 + db.insert(routes).values(data); 43 + } 44 + } else console.warn("Invalid record:", record.uri); 45 + 46 + cursor = data.cursor; 47 + if (!cursor) break; 48 + } 49 + } 50 + 51 + export default async function (backfillClient: Client, db: db) { 52 + let repos: `did:${string}:${string}`[] = []; 53 + let cursor: string | undefined; 54 + while (true) { 55 + const { data, ok } = await backfillClient.get( 56 + "com.atproto.sync.listReposByCollection", 57 + { 58 + params: { 59 + collection: "dev.atcities.route", 60 + cursor, 61 + }, 62 + } 63 + ); 64 + 65 + if (!ok) { 66 + throw data.error + "\n" + data.message; 67 + } 68 + 69 + repos = [...repos, ...data.repos.map((x) => x.did)]; 70 + cursor = data.cursor; 71 + if (!cursor) break; 72 + } 73 + 74 + for (const i in repos) { 75 + const did = repos[i]; 76 + console.log(`indexing ${Number(i) + 1}/${repos.length} ${did}`); 77 + if (isDid(did)) index(did, db); 78 + } 79 + }
+16 -15
server/src/routes/user.ts
··· 24 24 req: Request, 25 25 user: 26 26 | { handle: `${string}.${string}` } 27 - | { did: `did:plc:${string}` | `did:web:${string}` }, 27 + | { did: `did:plc:${string}` | `did:web:${string}` } 28 28 ): Promise<Response> { 29 29 // if handle: resolve did 30 30 let did: `did:${"plc" | "web"}:${string}`; ··· 36 36 return new Response( 37 37 `${ascii} 38 38 39 - This handle 39 + This handle could not be resolved. 40 40 `, 41 41 { 42 42 status: 500, 43 43 statusText: "Internal Server Error", 44 - }, 44 + } 45 45 ); 46 46 } 47 47 } else did = user.did; 48 48 49 49 // look up in db 50 - const db_res = ( 51 - await db 52 - .select() 53 - .from(routes) 54 - .where( 55 - and( 56 - eq(routes.did, did), 57 - eq(routes.url_route, new URL(req.url).pathname), 58 - ), 59 - ) 60 - ).at(0) ?? 50 + const db_res = 51 + ( 52 + await db 53 + .select() 54 + .from(routes) 55 + .where( 56 + and( 57 + eq(routes.did, did), 58 + eq(routes.url_route, new URL(req.url).pathname) 59 + ) 60 + ) 61 + ).at(0) ?? 61 62 ( 62 63 await db 63 64 .select() ··· 80 81 } 81 82 try { 82 83 const file = await Deno.readFile( 83 - `./blobs/${db_res.did}/${db_res.blob_cid}`, 84 + `./blobs/${db_res.did}/${db_res.blob_cid}` 84 85 ); 85 86 return new Response(file, { 86 87 headers: {
+34
server/src/utils.ts
··· 1 1 import { LibSQLDatabase } from "drizzle-orm/libsql"; 2 2 import { Client } from "@libsql/client"; 3 3 import * as schema from "./db/schema.ts"; 4 + import { 5 + CompositeDidDocumentResolver, 6 + PlcDidDocumentResolver, 7 + WebDidDocumentResolver, 8 + } from "@atcute/identity-resolver"; 4 9 5 10 export type db = LibSQLDatabase<typeof schema> & { 6 11 $client: Client; ··· 23 28 head.append("Set-Cookie", `${key}=; Max-Age=-1`); 24 29 } 25 30 return head; 31 + } 32 + 33 + const docResolver = new CompositeDidDocumentResolver({ 34 + methods: { 35 + plc: new PlcDidDocumentResolver(), 36 + web: new WebDidDocumentResolver(), 37 + }, 38 + }); 39 + 40 + export async function getPds( 41 + did: `did:${"plc" | "web"}:${string}` 42 + ): Promise<string | undefined> { 43 + try { 44 + const doc = await docResolver.resolve(did); 45 + const pds = doc.service?.filter( 46 + (x) => 47 + x.id.endsWith("#atproto_pds") && x.type === "AtprotoPersonalDataServer" 48 + )[0].serviceEndpoint; 49 + return typeof pds === "string" ? pds : undefined; 50 + } catch { 51 + return undefined; 52 + } 53 + } 54 + 55 + export function isDid(did: unknown): did is `did:${"plc" | "web"}:${string}` { 56 + return ( 57 + typeof did === "string" && 58 + (did.startsWith("did:web:") || did.startsWith("did:plc:")) 59 + ); 26 60 } 27 61 28 62 /**