diff --git a/indexserver.ts b/indexserver.ts index dfb0dea..1949929 100644 --- a/indexserver.ts +++ b/indexserver.ts @@ -1,7 +1,12 @@ import { indexHandlerContext } from "./index/types.ts"; import { assertRecord, validateRecord } from "./utils/records.ts"; -import { searchParamsToJson, withCors } from "./utils/server.ts"; +import { +buildBlobUrl, + resolveIdentity, + searchParamsToJson, + withCors, +} from "./utils/server.ts"; import * as IndexServerTypes from "./utils/indexservertypes.ts"; import { Database } from "jsr:@db/sqlite@0.11"; import { setupUserDb } from "./utils/dbuser.ts"; @@ -18,7 +23,6 @@ import { isPostView } from "./indexclient/types/app/bsky/feed/defs.ts"; export interface IndexServerConfig { baseDbPath: string; systemDbPath: string; - jetstreamUrl: string; } interface BaseRow { @@ -86,7 +90,7 @@ export class IndexServer { } // We will move all the global functions into this class as methods... - indexServerHandler(req: Request): Response { + async indexServerHandler(req: Request): Promise { const url = new URL(req.url); const pathname = url.pathname; //const bskyUrl = `https://api.bsky.app${pathname}${url.search}`; @@ -103,7 +107,7 @@ export class IndexServer { const jsonTyped = jsonUntyped as IndexServerTypes.AppBskyActorGetProfile.QueryParams; - const res = this.queryProfileView(jsonTyped.actor, "Detailed"); + const res = await this.queryProfileView(jsonTyped.actor, "Detailed"); if (!res) return new Response( JSON.stringify({ @@ -126,7 +130,7 @@ export class IndexServer { jsonUntyped as IndexServerTypes.AppBskyActorGetProfiles.QueryParams; if (typeof jsonUntyped?.actors === "string") { - const res = this.queryProfileView( + const res = await this.queryProfileView( jsonUntyped.actors as string, "Detailed" ); @@ -151,14 +155,18 @@ export class IndexServer { } const res: ATPAPI.AppBskyActorDefs.ProfileViewDetailed[] = - jsonTyped.actors - .map((actor) => { - return this.queryProfileView(actor, "Detailed"); - }) - .filter( - (x): x is ATPAPI.AppBskyActorDefs.ProfileViewDetailed => - x !== undefined - ); + await Promise.all( + jsonTyped.actors + .map(async (actor) => { + return await this.queryProfileView(actor, "Detailed"); + }) + .filter( + ( + x + ): x is Promise => + x !== undefined + ) + ); if (!res) return new Response( @@ -184,7 +192,7 @@ export class IndexServer { const jsonTyped = jsonUntyped as IndexServerTypes.AppBskyFeedGetActorFeeds.QueryParams; - const qresult = this.queryActorFeeds(jsonTyped.actor); + const qresult = await this.queryActorFeeds(jsonTyped.actor); const response: IndexServerTypes.AppBskyFeedGetActorFeeds.OutputSchema = { @@ -199,7 +207,7 @@ export class IndexServer { const jsonTyped = jsonUntyped as IndexServerTypes.AppBskyFeedGetFeedGenerator.QueryParams; - const qresult = this.queryFeedGenerator(jsonTyped.feed); + const qresult = await this.queryFeedGenerator(jsonTyped.feed); if (!qresult) { return new Response( JSON.stringify({ @@ -227,7 +235,7 @@ export class IndexServer { const jsonTyped = jsonUntyped as IndexServerTypes.AppBskyFeedGetFeedGenerators.QueryParams; - const qresult = this.queryFeedGenerators(jsonTyped.feeds); + const qresult = await this.queryFeedGenerators(jsonTyped.feeds); if (!qresult) { return new Response( JSON.stringify({ @@ -254,11 +262,11 @@ export class IndexServer { jsonUntyped as IndexServerTypes.AppBskyFeedGetPosts.QueryParams; const posts: IndexServerTypes.AppBskyFeedGetPosts.OutputSchema["posts"] = - jsonTyped.uris - .map((uri) => { - return this.queryPostView(uri); - }) - .filter(Boolean) as ATPAPI.AppBskyFeedDefs.PostView[]; + ( + await Promise.all( + jsonTyped.uris.map((uri) => this.queryPostView(uri)) + ) + ).filter((p): p is ATPAPI.AppBskyFeedDefs.PostView => Boolean(p)); const response: IndexServerTypes.AppBskyFeedGetPosts.OutputSchema = { posts, @@ -274,7 +282,7 @@ export class IndexServer { // TODO: not partial yet, currently skips refs - const qresult = this.queryActorLikesPartial( + const qresult = await this.queryActorLikesPartial( jsonTyped.actor, jsonTyped.cursor ); @@ -306,7 +314,10 @@ export class IndexServer { // TODO: not partial yet, currently skips refs - const qresult = this.queryAuthorFeedPartial(jsonTyped.actor, jsonTyped.cursor); + const qresult = await this.queryAuthorFeedPartial( + jsonTyped.actor, + jsonTyped.cursor + ); if (!qresult) { return new Response( JSON.stringify({ @@ -363,7 +374,7 @@ export class IndexServer { // TODO: not partial yet, currently skips refs - const qresult = this.queryPostThreadPartial(jsonTyped.uri); + const qresult = await this.queryPostThreadPartial(jsonTyped.uri); if (!qresult) { return new Response( JSON.stringify({ @@ -388,7 +399,7 @@ export class IndexServer { // TODO: not partial yet, currently skips refs - const qresult = this.queryQuotes(jsonTyped.uri); + const qresult = await this.queryQuotes(jsonTyped.uri); if (!qresult) { return new Response( JSON.stringify({ @@ -418,7 +429,7 @@ export class IndexServer { // TODO: not partial yet, currently skips refs - const qresult = this.queryReposts(jsonTyped.uri); + const qresult = await this.queryReposts(jsonTyped.uri); if (!qresult) { return new Response( JSON.stringify({ @@ -870,26 +881,27 @@ export class IndexServer { } // user data - queryProfileView( + async queryProfileView( did: string, type: "" - ): ATPAPI.AppBskyActorDefs.ProfileView | undefined; - queryProfileView( + ): Promise; + async queryProfileView( did: string, type: "Basic" - ): ATPAPI.AppBskyActorDefs.ProfileViewBasic | undefined; - queryProfileView( + ): Promise; + async queryProfileView( did: string, type: "Detailed" - ): ATPAPI.AppBskyActorDefs.ProfileViewDetailed | undefined; - queryProfileView( + ): Promise; + async queryProfileView( did: string, type: "" | "Basic" | "Detailed" - ): + ): Promise< | ATPAPI.AppBskyActorDefs.ProfileView | ATPAPI.AppBskyActorDefs.ProfileViewBasic | ATPAPI.AppBskyActorDefs.ProfileViewDetailed - | undefined { + | undefined + > { if (!this.isRegisteredIndexUser(did)) return; const db = this.userManager.getDbForDid(did); if (!db) return; @@ -903,16 +915,27 @@ export class IndexServer { const row = stmt.get(did) as ProfileRow; + const identity = await resolveIdentity(did); + const avatar = row.avatarcid ? buildBlobUrl( + identity.pds, + identity.did, + row.avatarcid + ) : undefined + const banner = row.bannercid ? buildBlobUrl( + identity.pds, + identity.did, + row.bannercid + ) : undefined // simulate different types returned switch (type) { case "": { const result: ATPAPI.AppBskyActorDefs.ProfileView = { $type: "app.bsky.actor.defs#profileView", did: did, - handle: "idiot.fuck.shit.example.com", // TODO: Resolve user identity here for the handle + handle: identity.handle, // TODO: Resolve user identity here for the handle displayName: row.displayname ?? undefined, description: row.description ?? undefined, - avatar: "https://google.com/", // create profile URL from resolved identity + avatar: avatar, // create profile URL from resolved identity //associated?: ProfileAssociated, indexedAt: row.createdat ? new Date(row.createdat).toISOString() @@ -931,9 +954,9 @@ export class IndexServer { const result: ATPAPI.AppBskyActorDefs.ProfileViewBasic = { $type: "app.bsky.actor.defs#profileViewBasic", did: did, - handle: "idiot.fuck.shit.example.com", // TODO: Resolve user identity here for the handle + handle: identity.handle, // TODO: Resolve user identity here for the handle displayName: row.displayname ?? undefined, - avatar: "https://google.com/", // create profile URL from resolved identity + avatar: avatar, // create profile URL from resolved identity //associated?: ProfileAssociated, createdAt: row.createdat ? new Date(row.createdat).toISOString() @@ -976,11 +999,11 @@ export class IndexServer { const result: ATPAPI.AppBskyActorDefs.ProfileViewDetailed = { $type: "app.bsky.actor.defs#profileViewDetailed", did: did, - handle: "idiot.fuck.shit.example.com", // TODO: Resolve user identity here for the handle + handle: identity.handle, // TODO: Resolve user identity here for the handle displayName: row.displayname ?? undefined, description: row.description ?? undefined, - avatar: "https://google.com/", // TODO: create profile URL from resolved identity - banner: "https://youtube.com/", // same here + avatar: avatar, // TODO: create profile URL from resolved identity + banner: banner, // same here followersCount: followersCount, followsCount: followsCount, postsCount: postsCount, @@ -1006,7 +1029,9 @@ export class IndexServer { } // post hydration - queryPostView(uri: string): ATPAPI.AppBskyFeedDefs.PostView | undefined { + async queryPostView( + uri: string + ): Promise { const URI = new AtUri(uri); const did = URI.host; if (!this.isRegisteredIndexUser(did)) return; @@ -1021,7 +1046,7 @@ export class IndexServer { `); const row = stmt.get(uri) as PostRow; - const profileView = this.queryProfileView(did, "Basic"); + const profileView = await this.queryProfileView(did, "Basic"); if (!row || !row.cid || !profileView || !row.json) return; const value = JSON.parse(row.json) as ATPAPI.AppBskyFeedPost.Record; @@ -1048,10 +1073,10 @@ export class IndexServer { return post; } - queryFeedViewPost( + async queryFeedViewPost( uri: string - ): ATPAPI.AppBskyFeedDefs.FeedViewPost | undefined { - const post = this.queryPostView(uri); + ): Promise { + const post = await this.queryPostView(uri); if (!post) return; const feedviewpost: ATPAPI.AppBskyFeedDefs.FeedViewPost = { @@ -1080,7 +1105,9 @@ export class IndexServer { // user feedgens - queryActorFeeds(did: string): ATPAPI.AppBskyFeedDefs.GeneratorView[] { + async queryActorFeeds( + did: string + ): Promise { if (!this.isRegisteredIndexUser(did)) return []; const db = this.userManager.getDbForDid(did); if (!db) return []; @@ -1093,7 +1120,7 @@ export class IndexServer { `); const rows = stmt.all(did) as unknown as GeneratorRow[]; - const creatorView = this.queryProfileView(did, "Basic"); + const creatorView = await this.queryProfileView(did, "Basic"); if (!creatorView) return []; return rows @@ -1123,13 +1150,16 @@ export class IndexServer { .filter((v): v is ATPAPI.AppBskyFeedDefs.GeneratorView => !!v); } - queryFeedGenerator( + async queryFeedGenerator( uri: string - ): ATPAPI.AppBskyFeedDefs.GeneratorView | undefined { - return this.queryFeedGenerators([uri])[0]; + ): Promise { + const gens = await this.queryFeedGenerators([uri]); // gens: GeneratorView[] + return gens[0]; } - queryFeedGenerators(uris: string[]): ATPAPI.AppBskyFeedDefs.GeneratorView[] { + async queryFeedGenerators( + uris: string[] + ): Promise { const generators: ATPAPI.AppBskyFeedDefs.GeneratorView[] = []; const urisByDid = new Map(); @@ -1158,7 +1188,7 @@ export class IndexServer { const rows = stmt.all(...didUris) as unknown as GeneratorRow[]; if (rows.length === 0) continue; - const creatorView = this.queryProfileView(did, ""); + const creatorView = await this.queryProfileView(did, ""); if (!creatorView) continue; for (const row of rows) { @@ -1188,10 +1218,10 @@ export class IndexServer { // user feeds - queryAuthorFeedPartial( + async queryAuthorFeedPartial( did: string, cursor?: string - ): + ): Promise< | { items: ( | ATPAPI.AppBskyFeedDefs.FeedViewPost @@ -1199,7 +1229,8 @@ export class IndexServer { )[]; cursor: string | undefined; } - | undefined { + | undefined + > { if (!this.isRegisteredIndexUser(did)) return; const db = this.userManager.getDbForDid(did); if (!db) return; @@ -1234,32 +1265,34 @@ export class IndexServer { subject: string | null; }[]; - const authorProfile = this.queryProfileView(did,"Basic"); + const authorProfile = await this.queryProfileView(did, "Basic"); - const items = rows - .map((row) => { - if (row.type === "repost" && row.subject) { - const subjectDid = new AtUri(row.subject).host + const items = await Promise.all( + rows + .map((row) => { + if (row.type === "repost" && row.subject) { + const subjectDid = new AtUri(row.subject).host; - const originalPost = this.handlesDid(subjectDid) - ? this.queryFeedViewPost(row.subject) - : this.constructFeedViewPostRef(row.subject); + const originalPost = this.handlesDid(subjectDid) + ? this.queryFeedViewPost(row.subject) + : this.constructFeedViewPostRef(row.subject); - if (!originalPost || !authorProfile) return null; + if (!originalPost || !authorProfile) return null; - return { - post: originalPost, - reason: { - $type: "app.bsky.feed.defs#reasonRepost", - by: authorProfile, - indexedAt: new Date(row.indexedat).toISOString(), - }, - }; - } else { - return this.queryFeedViewPost(row.uri); - } - }) - .filter((p): p is ATPAPI.AppBskyFeedDefs.FeedViewPost => !!p); + return { + post: originalPost, + reason: { + $type: "app.bsky.feed.defs#reasonRepost", + by: authorProfile, + indexedAt: new Date(row.indexedat).toISOString(), + }, + }; + } else { + return this.queryFeedViewPost(row.uri); + } + }) + .filter((p): p is Promise => !!p) + ); const lastItem = rows[rows.length - 1]; const nextCursor = lastItem @@ -1281,10 +1314,10 @@ export class IndexServer { return { items: [], cursor: undefined }; } - queryActorLikesPartial( + async queryActorLikesPartial( did: string, cursor?: string - ): + ): Promise< | { items: ( | ATPAPI.AppBskyFeedDefs.FeedViewPost @@ -1292,7 +1325,8 @@ export class IndexServer { )[]; cursor: string | undefined; } - | undefined { + | undefined + > { // early return only if the actor did is not registered if (!this.isRegisteredIndexUser(did)) return; const db = this.userManager.getDbForDid(did); @@ -1320,23 +1354,26 @@ export class IndexServer { cid: string; }[]; - const items = rows - .map((row) => { - const subjectDid = new AtUri(row.subject).host; + const items = await Promise.all( + rows + .map(async (row) => { + const subjectDid = new AtUri(row.subject).host; - if (this.handlesDid(subjectDid)) { - return this.queryFeedViewPost(row.subject); - } else { - return this.constructFeedViewPostRef(row.subject); - } - }) - .filter( - ( - p - ): p is - | ATPAPI.AppBskyFeedDefs.FeedViewPost - | IndexServerAPI.PartyWheyAppBskyFeedDefs.FeedViewPostRef => !!p - ); + if (this.handlesDid(subjectDid)) { + return await this.queryFeedViewPost(row.subject); + } else { + return this.constructFeedViewPostRef(row.subject); + } + }) + .filter( + ( + p + ): p is Promise< + | ATPAPI.AppBskyFeedDefs.FeedViewPost + | IndexServerAPI.PartyWheyAppBskyFeedDefs.FeedViewPostRef + > => !!p + ) + ); const lastItem = rows[rows.length - 1]; const nextCursor = lastItem @@ -1348,7 +1385,9 @@ export class IndexServer { // post metadata - queryLikes(uri: string): ATPAPI.AppBskyFeedGetLikes.Like[] | undefined { + async queryLikes( + uri: string + ): Promise { const postUri = new AtUri(uri); const postAuthorDid = postUri.hostname; if (!this.isRegisteredIndexUser(postAuthorDid)) return; @@ -1364,22 +1403,28 @@ export class IndexServer { const rows = stmt.all(uri) as unknown as BacklinkRow[]; - return rows - .map((row) => { - const actor = this.queryProfileView(row.srcdid, ""); - if (!actor) return; - - return { - // TODO write indexedAt for spacedust indexes - createdAt: new Date(Date.now()).toISOString(), - indexedAt: new Date(Date.now()).toISOString(), - actor: actor, - }; - }) - .filter((like): like is ATPAPI.AppBskyFeedGetLikes.Like => !!like); + return await Promise.all( + rows + .map(async (row) => { + const actor = await this.queryProfileView(row.srcdid, ""); + if (!actor) return; + + return { + // TODO write indexedAt for spacedust indexes + createdAt: new Date(Date.now()).toISOString(), + indexedAt: new Date(Date.now()).toISOString(), + actor: actor, + }; + }) + .filter( + (like): like is Promise => !!like + ) + ); } - queryReposts(uri: string): ATPAPI.AppBskyActorDefs.ProfileView[] { + async queryReposts( + uri: string + ): Promise { const postUri = new AtUri(uri); const postAuthorDid = postUri.hostname; if (!this.isRegisteredIndexUser(postAuthorDid)) return []; @@ -1395,12 +1440,16 @@ export class IndexServer { const rows = stmt.all(uri) as { srcdid: string }[]; - return rows - .map((row) => this.queryProfileView(row.srcdid, "")) - .filter((p): p is ATPAPI.AppBskyActorDefs.ProfileView => !!p); + return await Promise.all( + rows + .map(async (row) => await this.queryProfileView(row.srcdid, "")) + .filter((p): p is Promise => !!p) + ); } - queryQuotes(uri: string): ATPAPI.AppBskyFeedDefs.FeedViewPost[] { + async queryQuotes( + uri: string + ): Promise { const postUri = new AtUri(uri); const postAuthorDid = postUri.hostname; if (!this.isRegisteredIndexUser(postAuthorDid)) return []; @@ -1416,20 +1465,23 @@ export class IndexServer { const rows = stmt.all(uri) as { srcuri: string }[]; - return rows - .map((row) => this.queryFeedViewPost(row.srcuri)) - .filter((p): p is ATPAPI.AppBskyFeedDefs.FeedViewPost => !!p); + return await Promise.all( + rows + .map(async (row) => await this.queryFeedViewPost(row.srcuri)) + .filter((p): p is Promise => !!p) + ); } - _getPostViewUnion( + async _getPostViewUnion( uri: string - ): + ): Promise< | ATPAPI.AppBskyFeedDefs.PostView | IndexServerAPI.PartyWheyAppBskyFeedDefs.PostViewRef - | undefined { + | undefined + > { try { const postDid = new AtUri(uri).hostname; if (this.handlesDid(postDid)) { - return this.queryPostView(uri); + return await this.queryPostView(uri); } else { return this.constructPostViewRef(uri); } @@ -1437,11 +1489,13 @@ export class IndexServer { return undefined; } } - queryPostThreadPartial( + async queryPostThreadPartial( uri: string - ): IndexServerTypes.PartyWheyAppBskyFeedGetPostThreadPartial.OutputSchema | undefined { - - const post = this._getPostViewUnion(uri); + ): Promise< + | IndexServerTypes.PartyWheyAppBskyFeedGetPostThreadPartial.OutputSchema + | undefined + > { + const post = await this._getPostViewUnion(uri); if (!post) { return { @@ -1455,39 +1509,52 @@ export class IndexServer { const thread: IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef = { $type: "party.whey.app.bsky.feed.defs#threadViewPostRef", - post: post as ATPAPI.$Typed | IndexServerUtils.$Typed, + post: post as + | ATPAPI.$Typed + | IndexServerUtils.$Typed, replies: [], }; let current = thread; // we can only climb the parent tree if we have the full post record. // which is not implemented yet (sad i know) - if (isPostView(current.post) && isFeedPostRecord(current.post.record) && current.post.record?.reply?.parent?.uri) { + if ( + isPostView(current.post) && + isFeedPostRecord(current.post.record) && + current.post.record?.reply?.parent?.uri + ) { let parentUri: string | undefined = current.post.record.reply.parent.uri; // keep climbing as long as we find a valid parent post. while (parentUri) { - const parentPost = this._getPostViewUnion(parentUri); + const parentPost = await this._getPostViewUnion(parentUri); if (!parentPost) break; // stop if a parent in the chain is not found. - const parentThread: IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef = { - $type: "party.whey.app.bsky.feed.defs#threadViewPostRef", - post: parentPost as ATPAPI.$Typed, - replies: [current as IndexServerUtils.$Typed], - }; - current.parent = parentThread as IndexServerUtils.$Typed; + const parentThread: IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef = + { + $type: "party.whey.app.bsky.feed.defs#threadViewPostRef", + post: parentPost as ATPAPI.$Typed, + replies: [ + current as IndexServerUtils.$Typed, + ], + }; + current.parent = + parentThread as IndexServerUtils.$Typed; current = parentThread; // check if the new current post has a parent to continue the loop - parentUri = (isPostView(current.post) && isFeedPostRecord(current.post.record)) ? current.post.record?.reply?.parent?.uri : undefined; + parentUri = + isPostView(current.post) && isFeedPostRecord(current.post.record) + ? current.post.record?.reply?.parent?.uri + : undefined; } } - - const seenUris = new Set(); - const fetchReplies = (parentThread: IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef) => { - if (!parentThread.post || !('uri' in parentThread.post)) { + const fetchReplies = async ( + parentThread: IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef + ) => { + if (!parentThread.post || !("uri" in parentThread.post)) { return; } if (seenUris.has(parentThread.post.uri)) return; @@ -1498,7 +1565,7 @@ export class IndexServer { // replies can only be discovered for local posts where we have the backlink data if (!this.handlesDid(parentAuthorDid)) return; - + const db = this.userManager.getDbForDid(parentAuthorDid); if (!db) return; @@ -1509,30 +1576,48 @@ export class IndexServer { `); const replyRows = stmt.all(parentThread.post.uri) as { srcuri: string }[]; - const replies = replyRows - .map((row) => this._getPostViewUnion(row.srcuri)) - .filter((p): p is ATPAPI.AppBskyFeedDefs.PostView | IndexServerAPI.PartyWheyAppBskyFeedDefs.PostViewRef => !!p); + const replies = await Promise.all( + replyRows + .map(async (row) => await this._getPostViewUnion(row.srcuri)) + .filter( + ( + p + ): p is Promise< + | ATPAPI.AppBskyFeedDefs.PostView + | IndexServerAPI.PartyWheyAppBskyFeedDefs.PostViewRef + > => !!p + ) + ); for (const replyPost of replies) { - const replyThread: IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef = { - $type: "party.whey.app.bsky.feed.defs#threadViewPostRef", - post: replyPost as ATPAPI.$Typed | IndexServerUtils.$Typed, - parent: parentThread as IndexServerUtils.$Typed, - replies: [], - }; - parentThread.replies?.push(replyThread as IndexServerUtils.$Typed); + const replyThread: IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef = + { + $type: "party.whey.app.bsky.feed.defs#threadViewPostRef", + post: replyPost as + | ATPAPI.$Typed + | IndexServerUtils.$Typed, + parent: + parentThread as IndexServerUtils.$Typed, + replies: [], + }; + parentThread.replies?.push( + replyThread as IndexServerUtils.$Typed + ); fetchReplies(replyThread); // recurse } }; fetchReplies(thread); - const returned = current as unknown as IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef; + const returned = + current as unknown as IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef; - return { thread: returned as IndexServerUtils.$Typed }; + return { + thread: + returned as IndexServerUtils.$Typed, + }; } - /** * please do not use this, use openDbForDid() instead * @param did diff --git a/main-index.ts b/main-index.ts index 25b2365..f1bdbe9 100644 --- a/main-index.ts +++ b/main-index.ts @@ -2,7 +2,7 @@ import { setupAuth, getAuthenticatedDid, authVerifier } from "./utils/auth.ts"; import { setupSystemDb } from "./utils/dbsystem.ts"; import { didDocument } from "./utils/diddoc.ts"; import { cachedFetch, searchParamsToJson, withCors } from "./utils/server.ts"; -import { IndexServer, IndexServerConfig } from "./indexserver.ts" +import { IndexServer, IndexServerConfig } from "./indexserver.ts"; import { extractDid } from "./utils/identity.ts"; import { config } from "./config.ts"; @@ -11,9 +11,8 @@ import { config } from "./config.ts"; // ------------------------------------------ const indexServerConfig: IndexServerConfig = { - baseDbPath: './dbs/registered-users', // The directory for user databases - systemDbPath: './dbs/registered-users/system.db', // The path for the main system database - jetstreamUrl: config.jetstream + baseDbPath: "./dbs/index/registered-users", // The directory for user databases + systemDbPath: "./dbs/index/registered-users/system.db", // The path for the main system database }; export const genericIndexServer = new IndexServer(indexServerConfig); setupSystemDb(genericIndexServer.systemDB); @@ -35,7 +34,7 @@ genericIndexServer.systemDB.exec(` datetime('now'), 'ready' ); -`) +`); genericIndexServer.start(); @@ -61,65 +60,72 @@ genericIndexServer.start(); // app.bsky.graph.getLists // doesnt need to because theres no items[], and its self ProfileViewBasic // app.bsky.graph.getList // needs to be Partial-ed (items[] union with ProfileViewRef) // app.bsky.graph.getActorStarterPacks // maybe doesnt need to be Partial-ed because its self ProfileViewBasic - + // app.bsky.feed.getListFeed // uhh actually already exists its getListFeedPartial // */ // "/xrpc/party.whey.app.bsky.feed.getListFeedPartial", // ]); -Deno.serve( - { port: config.indexServer.port }, - (req: Request): Response => { - const url = new URL(req.url); - const pathname = url.pathname; - const searchParams = searchParamsToJson(url.searchParams); +Deno.serve({ port: config.indexServer.port }, async (req: Request): Promise => { + const url = new URL(req.url); + const pathname = url.pathname; + const searchParams = searchParamsToJson(url.searchParams); - if (pathname === "/.well-known/did.json") { - return new Response(JSON.stringify(didDocument("index",config.indexServer.did,config.indexServer.host,"whatever")), { + if (pathname === "/.well-known/did.json") { + return new Response( + JSON.stringify( + didDocument( + "index", + config.indexServer.did, + config.indexServer.host, + "whatever" + ) + ), + { headers: withCors({ "Content-Type": "application/json" }), - }); - } - if (pathname === "/health") { - return new Response("OK", { - status: 200, - headers: withCors({ - "Content-Type": "text/plain", - }), - }); - } - if (req.method === "OPTIONS") { - return new Response(null, { - status: 204, - headers: { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET, POST, OPTIONS", - "Access-Control-Allow-Headers": "*", - }, - }); - } - console.log(`request for "${pathname}"`) - const constellation = pathname.startsWith("/links") - - if (constellation) { - const target = searchParams?.target as string - const safeDid = extractDid(target); - const targetserver = genericIndexServer.handlesDid(safeDid) - if (targetserver) { - return genericIndexServer.constellationAPIHandler(req); - } else { - return new Response( - JSON.stringify({ - error: "User not found", - }), - { - status: 404, - headers: withCors({ "Content-Type": "application/json" }), - } - ); } + ); + } + if (pathname === "/health") { + return new Response("OK", { + status: 200, + headers: withCors({ + "Content-Type": "text/plain", + }), + }); + } + if (req.method === "OPTIONS") { + return new Response(null, { + status: 204, + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "*", + }, + }); + } + console.log(`request for "${pathname}"`); + const constellation = pathname.startsWith("/links"); + + if (constellation) { + const target = searchParams?.target as string; + const safeDid = extractDid(target); + const targetserver = genericIndexServer.handlesDid(safeDid); + if (targetserver) { + return genericIndexServer.constellationAPIHandler(req); } else { - // indexServerRoutes.has(pathname) - return genericIndexServer.indexServerHandler(req); + return new Response( + JSON.stringify({ + error: "User not found", + }), + { + status: 404, + headers: withCors({ "Content-Type": "application/json" }), + } + ); } + } else { + // indexServerRoutes.has(pathname) + return await genericIndexServer.indexServerHandler(req); } -); \ No newline at end of file +}); diff --git a/main-view.ts b/main-view.ts index 3983a1b..0abccc7 100644 --- a/main-view.ts +++ b/main-view.ts @@ -2,10 +2,9 @@ import { setupAuth, getAuthenticatedDid, authVerifier } from "./utils/auth.ts"; import { setupSystemDb } from "./utils/dbsystem.ts"; import { didDocument } from "./utils/diddoc.ts"; import { cachedFetch, searchParamsToJson, withCors } from "./utils/server.ts"; -import { IndexServer, IndexServerConfig } from "./indexserver.ts" +import { ViewServer, ViewServerConfig } from "./viewserver.ts"; import { extractDid } from "./utils/identity.ts"; import { config } from "./config.ts"; -import { viewServerHandler } from "./viewserver.ts"; // ------------------------------------------ // AppView Setup @@ -17,11 +16,38 @@ setupAuth({ //keyCacheTTL: 10 * 60 * 1000, }); +const viewServerConfig: ViewServerConfig = { + baseDbPath: "./dbs/view/registered-users", // The directory for user databases + systemDbPath: "./dbs/view/registered-users/system.db", // The path for the main system database +}; +export const genericViewServer = new ViewServer(viewServerConfig); +setupSystemDb(genericViewServer.systemDB); + +// add me lol +genericViewServer.systemDB.exec(` + INSERT OR IGNORE INTO users (did, role, registrationdate, onboardingstatus) + VALUES ( + 'did:plc:mn45tewwnse5btfftvd3powc', + 'admin', + datetime('now'), + 'ready' + ); + + INSERT OR IGNORE INTO users (did, role, registrationdate, onboardingstatus) + VALUES ( + 'did:web:did12.whey.party', + 'admin', + datetime('now'), + 'ready' + ); +`); + +genericViewServer.start(); + // ------------------------------------------ // XRPC Method Implementations // ------------------------------------------ - Deno.serve( { port: config.viewServer.port }, async (req: Request): Promise => { @@ -30,9 +56,19 @@ Deno.serve( const searchParams = searchParamsToJson(url.searchParams); if (pathname === "/.well-known/did.json") { - return new Response(JSON.stringify(didDocument), { - headers: withCors({ "Content-Type": "application/json" }), - }); + return new Response( + JSON.stringify( + didDocument( + "view", + config.viewServer.did, + config.viewServer.host, + "whatever" + ) + ), + { + headers: withCors({ "Content-Type": "application/json" }), + } + ); } if (pathname === "/health") { return new Response("OK", { @@ -52,8 +88,20 @@ Deno.serve( }, }); } - console.log(`request for "${pathname}"`) - - return await viewServerHandler(req) + console.log(`request for "${pathname}"`); + + let authdid: string | undefined = undefined; + try { + authdid = (await getAuthenticatedDid(req)) ?? undefined; + } catch (_e) { + // nothing lol + } + const auth = authdid + ? genericViewServer.handlesDid(authdid) + ? authdid + : undefined + : undefined; + console.log("authed:", auth); + return await genericViewServer.viewServerHandler(req); } -); \ No newline at end of file +); diff --git a/utils/auth.borrowed.ts b/utils/auth.borrowed.ts index 09e01c6..6b2e386 100644 --- a/utils/auth.borrowed.ts +++ b/utils/auth.borrowed.ts @@ -22,10 +22,10 @@ export function getResolver() { return { DidPlcResolver: resolve } } const myResolver = getResolver() -const web = getWebResolver() +const webResolver = getWebResolver() const resolver: ResolverRegistry = { 'plc': myResolver.DidPlcResolver as unknown as DIDResolver, - 'web': web as unknown as DIDResolver, + ...webResolver } export const resolverInstance = new Resolver(resolver) export type Service = { diff --git a/utils/auth.ts b/utils/auth.ts index 66144c7..219386c 100644 --- a/utils/auth.ts +++ b/utils/auth.ts @@ -61,7 +61,11 @@ async function internalGetAuthenticatedDid( return null; } } - +/** + * @deprecated dont use this use getAuthenticatedDid() instead + * @param param0 + * @returns + */ export const authVerifier: MethodAuthVerifier = async ({ req }) => { //console.log("help us all fuck you",req) console.log("you are doing well") diff --git a/viewserver.ts b/viewserver.ts index 2c9f931..10cf472 100644 --- a/viewserver.ts +++ b/viewserver.ts @@ -13,529 +13,840 @@ import { } from "./utils/server.ts"; import { validateRecord } from "./utils/records.ts"; import { indexHandlerContext } from "./index/types.ts"; +import { Database } from "jsr:@db/sqlite@0.11"; +import { JetstreamManager, SpacedustManager } from "./utils/sharders.ts"; +import { SpacedustLinkMessage } from "./index/spacedust.ts"; +import { setupUserDb } from "./utils/dbuser.ts"; + +export interface ViewServerConfig { + baseDbPath: string; + systemDbPath: string; +} + +interface BaseRow { + uri: string; + did: string; + cid: string | null; + rev: string | null; + createdat: number | null; + indexedat: number; + json: string | null; +} +interface GeneratorRow extends BaseRow { + displayname: string | null; + description: string | null; + avatarcid: string | null; +} +interface LikeRow extends BaseRow { + subject: string; +} +interface RepostRow extends BaseRow { + subject: string; +} +interface BacklinkRow { + srcuri: string; + srcdid: string; +} +const FEED_LIMIT = 50; -export async function viewServerHandler(req: Request): Promise { - const url = new URL(req.url); - const pathname = url.pathname; - const bskyUrl = `https://api.bsky.app${pathname}${url.search}`; - const hasAuth = req.headers.has("authorization"); - const xrpcMethod = pathname.startsWith("/xrpc/") - ? pathname.slice("/xrpc/".length) - : null; - const searchParams = searchParamsToJson(url.searchParams); - const jsonUntyped = searchParams; - - if (xrpcMethod === "app.bsky.unspecced.getTrendingTopics") { - // const jsonTyped = - // jsonUntyped as ViewServerTypes.AppBskyUnspeccedGetTrendingTopics.QueryParams; - - const faketopics: ATPAPI.AppBskyUnspeccedDefs.TrendingTopic[] = [ - { - $type: "app.bsky.unspecced.defs#trendingTopic", - topic: "Git Repo", - displayName: "Git Repo", - description: "Git Repo", - link: "https://tangled.sh/@whey.party/skylite", - }, - { - $type: "app.bsky.unspecced.defs#trendingTopic", - topic: "Red Dwarf Lite", - displayName: "Red Dwarf Lite", - description: "Red Dwarf Lite", - link: "https://reddwarflite.whey.party/", - }, - { - $type: "app.bsky.unspecced.defs#trendingTopic", - topic: "whey dot party", - displayName: "whey dot party", - description: "whey dot party", - link: "https://whey.party/", - }, - ]; - - const response: ViewServerTypes.AppBskyUnspeccedGetTrendingTopics.OutputSchema = - { - topics: faketopics, - suggested: faketopics, - }; - - return new Response(JSON.stringify(response), { - headers: withCors({ "Content-Type": "application/json" }), - }); +export class ViewServer { + private config: ViewServerConfig; + public userManager: ViewServerUserManager; + public systemDB: Database; + + constructor(config: ViewServerConfig) { + this.config = config; + + // We will initialize the system DB and user manager here + this.systemDB = new Database(this.config.systemDbPath); + // TODO: We need to setup the system DB schema if it's new + + this.userManager = new ViewServerUserManager(this); // Pass the server instance } - //if (xrpcMethod !== 'app.bsky.actor.getPreferences' && xrpcMethod !== 'app.bsky.notification.listNotifications') { - if ( - !hasAuth - // (!hasAuth || - // xrpcMethod === "app.bsky.labeler.getServices" || - // xrpcMethod === "app.bsky.unspecced.getConfig") && - // xrpcMethod !== "app.bsky.notification.putPreferences" - ) { - return new Response( - JSON.stringify({ - error: "XRPCNotSupported", - message: - "HEY hello there my name is whey dot party and you have used my custom appview that is very cool but have you considered that XRPC Not Supported", - }), - { - status: 404, - headers: withCors({ "Content-Type": "application/json" }), - } - ); - //return await sendItToApiBskyApp(req); + public start() { + // This is where we'll kick things off, like the cold start + this.userManager.coldStart(this.systemDB); + console.log("viewServer started."); } - if ( - // !hasAuth || - xrpcMethod === "app.bsky.labeler.getServices" || - xrpcMethod === "app.bsky.unspecced.getConfig" //&& - //xrpcMethod !== "app.bsky.notification.putPreferences" - ) { - return new Response( - JSON.stringify({ - error: "XRPCNotSupported", - message: - "HEY hello there my name is whey dot party and you have used my custom appview that is very cool but have you considered that XRPC Not Supported", - }), - { - status: 404, + + async viewServerHandler(req: Request): Promise { + const url = new URL(req.url); + const pathname = url.pathname; + const bskyUrl = `https://api.bsky.app${pathname}${url.search}`; + const hasAuth = req.headers.has("authorization"); + const xrpcMethod = pathname.startsWith("/xrpc/") + ? pathname.slice("/xrpc/".length) + : null; + const searchParams = searchParamsToJson(url.searchParams); + const jsonUntyped = searchParams; + + if (xrpcMethod === "app.bsky.unspecced.getTrendingTopics") { + // const jsonTyped = + // jsonUntyped as ViewServerTypes.AppBskyUnspeccedGetTrendingTopics.QueryParams; + + const faketopics: ATPAPI.AppBskyUnspeccedDefs.TrendingTopic[] = [ + { + $type: "app.bsky.unspecced.defs#trendingTopic", + topic: "Git Repo", + displayName: "Git Repo", + description: "Git Repo", + link: "https://tangled.sh/@whey.party/skylite", + }, + { + $type: "app.bsky.unspecced.defs#trendingTopic", + topic: "Red Dwarf Lite", + displayName: "Red Dwarf Lite", + description: "Red Dwarf Lite", + link: "https://reddwarflite.whey.party/", + }, + { + $type: "app.bsky.unspecced.defs#trendingTopic", + topic: "whey dot party", + displayName: "whey dot party", + description: "whey dot party", + link: "https://whey.party/", + }, + ]; + + const response: ViewServerTypes.AppBskyUnspeccedGetTrendingTopics.OutputSchema = + { + topics: faketopics, + suggested: faketopics, + }; + + return new Response(JSON.stringify(response), { headers: withCors({ "Content-Type": "application/json" }), - } - ); - //return await sendItToApiBskyApp(req); - } + }); + } - const authDID = "did:plc:mn45tewwnse5btfftvd3powc"; //getAuthenticatedDid(req); - - switch (xrpcMethod) { - case "app.bsky.feed.getFeedGenerators": { - const jsonTyped = - jsonUntyped as ViewServerTypes.AppBskyFeedGetFeedGenerators.QueryParams; - - const feeds: ATPAPI.AppBskyFeedDefs.GeneratorView[] = ( - await Promise.all( - jsonTyped.feeds.map(async (feed) => { - try { - const did = new ATPAPI.AtUri(feed).hostname; - const rkey = new ATPAPI.AtUri(feed).rkey; - const identity = await resolveIdentity(did); - const feedgetRecord = await getSlingshotRecord( - identity.did, - "app.bsky.feed.generator", - rkey - ); - const profile = ( - await getSlingshotRecord( + //if (xrpcMethod !== 'app.bsky.actor.getPreferences' && xrpcMethod !== 'app.bsky.notification.listNotifications') { + if ( + !hasAuth + // (!hasAuth || + // xrpcMethod === "app.bsky.labeler.getServices" || + // xrpcMethod === "app.bsky.unspecced.getConfig") && + // xrpcMethod !== "app.bsky.notification.putPreferences" + ) { + return new Response( + JSON.stringify({ + error: "XRPCNotSupported", + message: + "HEY hello there my name is whey dot party and you have used my custom appview that is very cool but have you considered that XRPC Not Supported", + }), + { + status: 404, + headers: withCors({ "Content-Type": "application/json" }), + } + ); + //return await sendItToApiBskyApp(req); + } + if ( + // !hasAuth || + xrpcMethod === "app.bsky.labeler.getServices" || + xrpcMethod === "app.bsky.unspecced.getConfig" //&& + //xrpcMethod !== "app.bsky.notification.putPreferences" + ) { + return new Response( + JSON.stringify({ + error: "XRPCNotSupported", + message: + "HEY hello there my name is whey dot party and you have used my custom appview that is very cool but have you considered that XRPC Not Supported", + }), + { + status: 404, + headers: withCors({ "Content-Type": "application/json" }), + } + ); + //return await sendItToApiBskyApp(req); + } + + const authDID = "did:plc:mn45tewwnse5btfftvd3powc"; //getAuthenticatedDid(req); + + switch (xrpcMethod) { + case "app.bsky.feed.getFeedGenerators": { + const jsonTyped = + jsonUntyped as ViewServerTypes.AppBskyFeedGetFeedGenerators.QueryParams; + + const feeds: ATPAPI.AppBskyFeedDefs.GeneratorView[] = ( + await Promise.all( + jsonTyped.feeds.map(async (feed) => { + try { + const did = new ATPAPI.AtUri(feed).hostname; + const rkey = new ATPAPI.AtUri(feed).rkey; + const identity = await resolveIdentity(did); + const feedgetRecord = await getSlingshotRecord( identity.did, - "app.bsky.actor.profile", - "self" - ) - ).value as ATPAPI.AppBskyActorProfile.Record; - const anyprofile = profile as any; - const value = - feedgetRecord.value as ATPAPI.AppBskyFeedGenerator.Record; - - return { - $type: "app.bsky.feed.defs#generatorView", - uri: feed, - cid: feedgetRecord.cid, - did: identity.did, - creator: /*AppBskyActorDefs.ProfileView*/ { - $type: "app.bsky.actor.defs#profileView", + "app.bsky.feed.generator", + rkey + ); + const profile = ( + await getSlingshotRecord( + identity.did, + "app.bsky.actor.profile", + "self" + ) + ).value as ATPAPI.AppBskyActorProfile.Record; + const anyprofile = profile as any; + const value = + feedgetRecord.value as ATPAPI.AppBskyFeedGenerator.Record; + + return { + $type: "app.bsky.feed.defs#generatorView", + uri: feed, + cid: feedgetRecord.cid, did: identity.did, - handle: identity.handle, - displayName: profile.displayName, - description: profile.description, + creator: /*AppBskyActorDefs.ProfileView*/ { + $type: "app.bsky.actor.defs#profileView", + did: identity.did, + handle: identity.handle, + displayName: profile.displayName, + description: profile.description, + avatar: buildBlobUrl( + identity.pds, + identity.did, + anyprofile.avatar.ref["$link"] + ), + //associated?: ProfileAssociated + //indexedAt?: string + //createdAt?: string + //viewer?: ViewerState + //labels?: ComAtprotoLabelDefs.Label[] + //verification?: VerificationState + //status?: StatusView + }, + displayName: value.displayName, + description: value.description, + //descriptionFacets?: AppBskyRichtextFacet.Main[] avatar: buildBlobUrl( identity.pds, identity.did, - anyprofile.avatar.ref["$link"] + (value as any).avatar.ref["$link"] ), - //associated?: ProfileAssociated - //indexedAt?: string - //createdAt?: string - //viewer?: ViewerState + //likeCount?: number + //acceptsInteractions?: boolean //labels?: ComAtprotoLabelDefs.Label[] - //verification?: VerificationState - //status?: StatusView - }, - displayName: value.displayName, - description: value.description, - //descriptionFacets?: AppBskyRichtextFacet.Main[] - avatar: buildBlobUrl( - identity.pds, - identity.did, - (value as any).avatar.ref["$link"] - ), - //likeCount?: number - //acceptsInteractions?: boolean - //labels?: ComAtprotoLabelDefs.Label[] - //viewer?: GeneratorViewerState - contentMode: value.contentMode, - indexedAt: new Date().toISOString(), - }; - } catch (err) { - return undefined; - } - }) - ) - ).filter(isGeneratorView); - - const response: ViewServerTypes.AppBskyFeedGetFeedGenerators.OutputSchema = - { - feeds: feeds ? feeds : [], - }; + //viewer?: GeneratorViewerState + contentMode: value.contentMode, + indexedAt: new Date().toISOString(), + }; + } catch (err) { + return undefined; + } + }) + ) + ).filter(isGeneratorView); + + const response: ViewServerTypes.AppBskyFeedGetFeedGenerators.OutputSchema = + { + feeds: feeds ? feeds : [], + }; - return new Response(JSON.stringify(response), { - headers: withCors({ "Content-Type": "application/json" }), - }); - } - case "app.bsky.feed.getFeed": { - const jsonTyped = - jsonUntyped as ViewServerTypes.AppBskyFeedGetFeed.QueryParams; - const cursor = jsonTyped.cursor; - const feed = jsonTyped.feed; - const limit = jsonTyped.limit; - const proxyauth = req.headers.get("authorization") || ""; - - const did = new ATPAPI.AtUri(feed).hostname; - const rkey = new ATPAPI.AtUri(feed).rkey; - const identity = await resolveIdentity(did); - const feedgetRecord = ( - await getSlingshotRecord(identity.did, "app.bsky.feed.generator", rkey) - ).value as ATPAPI.AppBskyFeedGenerator.Record; - - const skeleton = (await cachedFetch( - `${didWebToHttps( - feedgetRecord.did - )}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${jsonTyped.feed}${ - cursor ? `&cursor=${cursor}` : "" - }${limit ? `&limit=${limit}` : ""}`, - proxyauth - )) as ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema; - - const nextcursor = skeleton.cursor; - const dbgrqstid = skeleton.reqId; - const uriarray = skeleton.feed; - - // Step 1: Chunk into 25 max - const chunks = []; - for (let i = 0; i < uriarray.length; i += 25) { - chunks.push(uriarray.slice(i, i + 25)); + return new Response(JSON.stringify(response), { + headers: withCors({ "Content-Type": "application/json" }), + }); } + case "app.bsky.feed.getFeed": { + const jsonTyped = + jsonUntyped as ViewServerTypes.AppBskyFeedGetFeed.QueryParams; + const cursor = jsonTyped.cursor; + const feed = jsonTyped.feed; + const limit = jsonTyped.limit; + const proxyauth = req.headers.get("authorization") || ""; + + const did = new ATPAPI.AtUri(feed).hostname; + const rkey = new ATPAPI.AtUri(feed).rkey; + const identity = await resolveIdentity(did); + const feedgetRecord = ( + await getSlingshotRecord( + identity.did, + "app.bsky.feed.generator", + rkey + ) + ).value as ATPAPI.AppBskyFeedGenerator.Record; + + const skeleton = (await cachedFetch( + `${didWebToHttps( + feedgetRecord.did + )}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${jsonTyped.feed}${ + cursor ? `&cursor=${cursor}` : "" + }${limit ? `&limit=${limit}` : ""}`, + proxyauth + )) as ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema; + + const nextcursor = skeleton.cursor; + const dbgrqstid = skeleton.reqId; + const uriarray = skeleton.feed; + + // Step 1: Chunk into 25 max + const chunks = []; + for (let i = 0; i < uriarray.length; i += 25) { + chunks.push(uriarray.slice(i, i + 25)); + } - // Step 2: Hydrate via getPosts - const hydratedPosts: ATPAPI.AppBskyFeedDefs.FeedViewPost[] = []; + // Step 2: Hydrate via getPosts + const hydratedPosts: ATPAPI.AppBskyFeedDefs.FeedViewPost[] = []; - for (const chunk of chunks) { - const searchParams = new URLSearchParams(); - for (const uri of chunk.map((item) => item.post)) { - searchParams.append("uris", uri); - } + for (const chunk of chunks) { + const searchParams = new URLSearchParams(); + for (const uri of chunk.map((item) => item.post)) { + searchParams.append("uris", uri); + } - const postResp = await ky - .get(`https://api.bsky.app/xrpc/app.bsky.feed.getPosts`, { - // headers: { - // Authorization: proxyauth, - // }, - searchParams, - }) - .json(); - - for (const post of postResp.posts) { - const matchingSkeleton = uriarray.find( - (item) => item.post === post.uri - ); - if (matchingSkeleton) { - //post.author.handle = post.author.handle + ".percent40.api.bsky.app"; // or any logic to modify it - hydratedPosts.push({ - post, - reason: matchingSkeleton.reason, - //reply: matchingSkeleton, - }); + const postResp = await ky + .get(`https://api.bsky.app/xrpc/app.bsky.feed.getPosts`, { + // headers: { + // Authorization: proxyauth, + // }, + searchParams, + }) + .json(); + + for (const post of postResp.posts) { + const matchingSkeleton = uriarray.find( + (item) => item.post === post.uri + ); + if (matchingSkeleton) { + //post.author.handle = post.author.handle + ".percent40.api.bsky.app"; // or any logic to modify it + hydratedPosts.push({ + post, + reason: matchingSkeleton.reason, + //reply: matchingSkeleton, + }); + } } } + + // Step 3: Compose final response + const response: ViewServerTypes.AppBskyFeedGetFeed.OutputSchema = { + feed: hydratedPosts, + cursor: nextcursor, + }; + + return new Response(JSON.stringify(response), { + headers: withCors({ "Content-Type": "application/json" }), + }); } + case "app.bsky.actor.getProfile": { + const jsonTyped = + jsonUntyped as ViewServerTypes.AppBskyActorGetProfile.QueryParams; + + const userindexservice = ""; + const isbskyfallback = true; + if (isbskyfallback) { + return this.sendItToApiBskyApp(req); + } - // Step 3: Compose final response - const response: ViewServerTypes.AppBskyFeedGetFeed.OutputSchema = { - feed: hydratedPosts, - cursor: nextcursor, - }; + const response: ViewServerTypes.AppBskyActorGetProfile.OutputSchema = + {}; - return new Response(JSON.stringify(response), { - headers: withCors({ "Content-Type": "application/json" }), - }); - } - case "app.bsky.actor.getProfile": { - const jsonTyped = - jsonUntyped as ViewServerTypes.AppBskyActorGetProfile.QueryParams; - - const userindexservice = ""; - const isbskyfallback = true; - if (isbskyfallback) { - return sendItToApiBskyApp(req); + return new Response(JSON.stringify(response), { + headers: withCors({ "Content-Type": "application/json" }), + }); } - const response: ViewServerTypes.AppBskyActorGetProfile.OutputSchema = {}; + case "app.bsky.actor.getProfiles": { + const jsonTyped = + jsonUntyped as ViewServerTypes.AppBskyActorGetProfiles.QueryParams; - return new Response(JSON.stringify(response), { - headers: withCors({ "Content-Type": "application/json" }), - }); - } + const userindexservice = ""; + const isbskyfallback = true; + if (isbskyfallback) { + return this.sendItToApiBskyApp(req); + } - case "app.bsky.actor.getProfiles": { - const jsonTyped = - jsonUntyped as ViewServerTypes.AppBskyActorGetProfiles.QueryParams; + const response: ViewServerTypes.AppBskyActorGetProfiles.OutputSchema = + {}; - const userindexservice = ""; - const isbskyfallback = true; - if (isbskyfallback) { - return sendItToApiBskyApp(req); + return new Response(JSON.stringify(response), { + headers: withCors({ "Content-Type": "application/json" }), + }); } + case "app.bsky.feed.getAuthorFeed": { + const jsonTyped = + jsonUntyped as ViewServerTypes.AppBskyFeedGetAuthorFeed.QueryParams; + + const userindexservice = ""; + const isbskyfallback = true; + if (isbskyfallback) { + return this.sendItToApiBskyApp(req); + } - const response: ViewServerTypes.AppBskyActorGetProfiles.OutputSchema = {}; + const response: ViewServerTypes.AppBskyFeedGetAuthorFeed.OutputSchema = + {}; - return new Response(JSON.stringify(response), { - headers: withCors({ "Content-Type": "application/json" }), - }); - } - case "app.bsky.feed.getAuthorFeed": { - const jsonTyped = - jsonUntyped as ViewServerTypes.AppBskyFeedGetAuthorFeed.QueryParams; - - const userindexservice = ""; - const isbskyfallback = true; - if (isbskyfallback) { - return sendItToApiBskyApp(req); + return new Response(JSON.stringify(response), { + headers: withCors({ "Content-Type": "application/json" }), + }); } + case "app.bsky.feed.getPostThread": { + const jsonTyped = + jsonUntyped as ViewServerTypes.AppBskyFeedGetPostThread.QueryParams; + + const userindexservice = ""; + const isbskyfallback = true; + if (isbskyfallback) { + return this.sendItToApiBskyApp(req); + } - const response: ViewServerTypes.AppBskyFeedGetAuthorFeed.OutputSchema = - {}; + const response: ViewServerTypes.AppBskyFeedGetPostThread.OutputSchema = + {}; - return new Response(JSON.stringify(response), { - headers: withCors({ "Content-Type": "application/json" }), - }); - } - case "app.bsky.feed.getPostThread": { - const jsonTyped = - jsonUntyped as ViewServerTypes.AppBskyFeedGetPostThread.QueryParams; - - const userindexservice = ""; - const isbskyfallback = true; - if (isbskyfallback) { - return sendItToApiBskyApp(req); + return new Response(JSON.stringify(response), { + headers: withCors({ "Content-Type": "application/json" }), + }); } + case "app.bsky.unspecced.getPostThreadV2": { + const jsonTyped = + jsonUntyped as ViewServerTypes.AppBskyUnspeccedGetPostThreadV2.QueryParams; + + const userindexservice = ""; + const isbskyfallback = true; + if (isbskyfallback) { + return this.sendItToApiBskyApp(req); + } - const response: ViewServerTypes.AppBskyFeedGetPostThread.OutputSchema = - {}; + const response: ViewServerTypes.AppBskyUnspeccedGetPostThreadV2.OutputSchema = + {}; - return new Response(JSON.stringify(response), { - headers: withCors({ "Content-Type": "application/json" }), - }); - } - case "app.bsky.unspecced.getPostThreadV2": { - const jsonTyped = - jsonUntyped as ViewServerTypes.AppBskyUnspeccedGetPostThreadV2.QueryParams; - - const userindexservice = ""; - const isbskyfallback = true; - if (isbskyfallback) { - return sendItToApiBskyApp(req); + return new Response(JSON.stringify(response), { + headers: withCors({ "Content-Type": "application/json" }), + }); } - const response: ViewServerTypes.AppBskyUnspeccedGetPostThreadV2.OutputSchema = - {}; + // case "app.bsky.actor.getProfile": { + // const jsonTyped = + // jsonUntyped as ViewServerTypes.AppBskyActorGetProfile.QueryParams; - return new Response(JSON.stringify(response), { - headers: withCors({ "Content-Type": "application/json" }), - }); - } + // const response: ViewServerTypes.AppBskyActorGetProfile.OutputSchema= {}; + + // return new Response(JSON.stringify(response), { + // headers: withCors({ "Content-Type": "application/json" }), + // }); + // } + // case "app.bsky.actor.getProfiles": { + // const jsonTyped = jsonUntyped as ViewServerTypes.AppBskyActorGetProfiles.QueryParams; - // case "app.bsky.actor.getProfile": { - // const jsonTyped = - // jsonUntyped as ViewServerTypes.AppBskyActorGetProfile.QueryParams; + // const response: ViewServerTypes.AppBskyActorGetProfiles.OutputSchema = {}; - // const response: ViewServerTypes.AppBskyActorGetProfile.OutputSchema= {}; + // return new Response(JSON.stringify(response), { + // headers: withCors({ "Content-Type": "application/json" }), + // }); + // } + // case "whatever": { + // const jsonTyped = jsonUntyped as ViewServerTypes.AppBskyFeedGetAuthorFeed.QueryParams; - // return new Response(JSON.stringify(response), { - // headers: withCors({ "Content-Type": "application/json" }), - // }); - // } - // case "app.bsky.actor.getProfiles": { - // const jsonTyped = jsonUntyped as ViewServerTypes.AppBskyActorGetProfiles.QueryParams; + // const response: ViewServerTypes.AppBskyFeedGetAuthorFeed.OutputSchema = {} - // const response: ViewServerTypes.AppBskyActorGetProfiles.OutputSchema = {}; + // return new Response(JSON.stringify(response), { + // headers: withCors({ "Content-Type": "application/json" }), + // }); + // } + // case "app.bsky.notification.listNotifications": { + // const jsonTyped = + // jsonUntyped as ViewServerTypes.AppBskyNotificationListNotifications.QueryParams; - // return new Response(JSON.stringify(response), { - // headers: withCors({ "Content-Type": "application/json" }), - // }); - // } - // case "whatever": { - // const jsonTyped = jsonUntyped as ViewServerTypes.AppBskyFeedGetAuthorFeed.QueryParams; + // const response: ViewServerTypes.AppBskyNotificationListNotifications.OutputSchema = {}; - // const response: ViewServerTypes.AppBskyFeedGetAuthorFeed.OutputSchema = {} + // return new Response(JSON.stringify(response), { + // headers: withCors({ "Content-Type": "application/json" }), + // }); + // } - // return new Response(JSON.stringify(response), { - // headers: withCors({ "Content-Type": "application/json" }), - // }); - // } - // case "app.bsky.notification.listNotifications": { - // const jsonTyped = - // jsonUntyped as ViewServerTypes.AppBskyNotificationListNotifications.QueryParams; + case "app.bsky.unspecced.getConfig": { + const jsonTyped = + jsonUntyped as ViewServerTypes.AppBskyUnspeccedGetConfig.QueryParams; - // const response: ViewServerTypes.AppBskyNotificationListNotifications.OutputSchema = {}; + const response: ViewServerTypes.AppBskyUnspeccedGetConfig.OutputSchema = + { + checkEmailConfirmed: true, + liveNow: [ + { + $type: "app.bsky.unspecced.getConfig#liveNowConfig", + did: "did:plc:mn45tewwnse5btfftvd3powc", + domains: ["local3768forumtest.whey.party"], + }, + ], + }; + + return new Response(JSON.stringify(response), { + headers: withCors({ "Content-Type": "application/json" }), + }); + } + case "app.bsky.graph.getLists": { + const jsonTyped = + jsonUntyped as ViewServerTypes.AppBskyGraphGetLists.QueryParams; - // return new Response(JSON.stringify(response), { - // headers: withCors({ "Content-Type": "application/json" }), - // }); - // } + const response: ViewServerTypes.AppBskyGraphGetLists.OutputSchema = { + lists: [], + }; - case "app.bsky.unspecced.getConfig": { - const jsonTyped = - jsonUntyped as ViewServerTypes.AppBskyUnspeccedGetConfig.QueryParams; + return new Response(JSON.stringify(response), { + headers: withCors({ "Content-Type": "application/json" }), + }); + } + //https://shimeji.us-east.host.bsky.network/xrpc/app.bsky.unspecced.getTrendingTopics?limit=14 + case "app.bsky.unspecced.getTrendingTopics": { + const jsonTyped = + jsonUntyped as ViewServerTypes.AppBskyUnspeccedGetTrendingTopics.QueryParams; - const response: ViewServerTypes.AppBskyUnspeccedGetConfig.OutputSchema = { - checkEmailConfirmed: true, - liveNow: [ + const faketopics: ATPAPI.AppBskyUnspeccedDefs.TrendingTopic[] = [ + { + $type: "app.bsky.unspecced.defs#trendingTopic", + topic: "Git Repo", + displayName: "Git Repo", + description: "Git Repo", + link: "https://tangled.sh/@whey.party/skylite", + }, + { + $type: "app.bsky.unspecced.defs#trendingTopic", + topic: "Red Dwarf Lite", + displayName: "Red Dwarf Lite", + description: "Red Dwarf Lite", + link: "https://reddwarf.whey.party/", + }, { - $type: "app.bsky.unspecced.getConfig#liveNowConfig", - did: "did:plc:mn45tewwnse5btfftvd3powc", - domains: ["local3768forumtest.whey.party"], + $type: "app.bsky.unspecced.defs#trendingTopic", + topic: "whey dot party", + displayName: "whey dot party", + description: "whey dot party", + link: "https://whey.party/", }, - ], - }; + ]; - return new Response(JSON.stringify(response), { - headers: withCors({ "Content-Type": "application/json" }), - }); + const response: ViewServerTypes.AppBskyUnspeccedGetTrendingTopics.OutputSchema = + { + topics: faketopics, + suggested: faketopics, + }; + + return new Response(JSON.stringify(response), { + headers: withCors({ "Content-Type": "application/json" }), + }); + } + default: { + return new Response( + JSON.stringify({ + error: "XRPCNotSupported", + message: + "HEY hello there my name is whey dot party and you have used my custom appview that is very cool but have you considered that XRPC Not Supported", + }), + { + status: 404, + headers: withCors({ "Content-Type": "application/json" }), + } + ); + } } - case "app.bsky.graph.getLists": { - const jsonTyped = - jsonUntyped as ViewServerTypes.AppBskyGraphGetLists.QueryParams; - const response: ViewServerTypes.AppBskyGraphGetLists.OutputSchema = { - lists: [], - }; + // return new Response("Not Found", { status: 404 }); + } - return new Response(JSON.stringify(response), { - headers: withCors({ "Content-Type": "application/json" }), - }); + async sendItToApiBskyApp(req: Request): Promise { + const url = new URL(req.url); + const pathname = url.pathname; + const searchParams = searchParamsToJson(url.searchParams); + let reqBody: undefined | string; + let jsonbody: undefined | Record; + if (req.body) { + const body = await req.json(); + jsonbody = body; + // console.log( + // `called at euh reqreqreqreq: ${pathname}\n\n${JSON.stringify(body)}` + // ); + reqBody = JSON.stringify(body, null, 2); } - //https://shimeji.us-east.host.bsky.network/xrpc/app.bsky.unspecced.getTrendingTopics?limit=14 - case "app.bsky.unspecced.getTrendingTopics": { - const jsonTyped = - jsonUntyped as ViewServerTypes.AppBskyUnspeccedGetTrendingTopics.QueryParams; + const bskyUrl = `https://public.api.bsky.app${pathname}${url.search}`; + console.log("request", searchParams); + const proxyHeaders = new Headers(req.headers); + + // Remove Authorization and set browser-like User-Agent + proxyHeaders.delete("authorization"); + proxyHeaders.delete("Access-Control-Allow-Origin"), + proxyHeaders.set( + "user-agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" + ); + proxyHeaders.set("Access-Control-Allow-Origin", "*"); + + const proxyRes = await fetch(bskyUrl, { + method: req.method, + headers: proxyHeaders, + body: ["GET", "HEAD"].includes(req.method.toUpperCase()) + ? undefined + : reqBody, + }); - const faketopics: ATPAPI.AppBskyUnspeccedDefs.TrendingTopic[] = [ - { - $type: "app.bsky.unspecced.defs#trendingTopic", - topic: "Git Repo", - displayName: "Git Repo", - description: "Git Repo", - link: "https://tangled.sh/@whey.party/skylite", - }, - { - $type: "app.bsky.unspecced.defs#trendingTopic", - topic: "Red Dwarf Lite", - displayName: "Red Dwarf Lite", - description: "Red Dwarf Lite", - link: "https://reddwarf.whey.party/", - }, - { - $type: "app.bsky.unspecced.defs#trendingTopic", - topic: "whey dot party", - displayName: "whey dot party", - description: "whey dot party", - link: "https://whey.party/", - }, - ]; + const resBody = await proxyRes.text(); - const response: ViewServerTypes.AppBskyUnspeccedGetTrendingTopics.OutputSchema = - { - topics: faketopics, - suggested: faketopics, - }; + // console.log( + // "← Response:", + // JSON.stringify(await JSON.parse(resBody), null, 2) + // ); - return new Response(JSON.stringify(response), { - headers: withCors({ "Content-Type": "application/json" }), - }); - } - default: { - return new Response( - JSON.stringify({ - error: "XRPCNotSupported", - message: - "HEY hello there my name is whey dot party and you have used my custom appview that is very cool but have you considered that XRPC Not Supported", - }), - { - status: 404, - headers: withCors({ "Content-Type": "application/json" }), - } - ); + return new Response(resBody, { + status: proxyRes.status, + headers: proxyRes.headers, + }); + } + + viewServerIndexer(ctx: indexHandlerContext) { + const record = validateRecord(ctx.value); + switch (record?.$type) { + case "app.bsky.feed.like": { + return; + } + default: { + // what the hell + return; + } } } - // return new Response("Not Found", { status: 404 }); + /** + * please do not use this, use openDbForDid() instead + * @param did + * @returns + */ + internalCreateDbForDid(did: string): Database { + const path = `${this.config.baseDbPath}/${did}.sqlite`; + const db = new Database(path); + // TODO maybe split the user db schema between view server and index server + setupUserDb(db); + //await db.exec(/* CREATE IF NOT EXISTS statements */); + return db; + } + public handlesDid(did: string): boolean { + return this.userManager.handlesDid(did); + } } -async function sendItToApiBskyApp(req: Request): Promise { - const url = new URL(req.url); - const pathname = url.pathname; - const searchParams = searchParamsToJson(url.searchParams); - let reqBody: undefined | string; - let jsonbody: undefined | Record; - if (req.body) { - const body = await req.json(); - jsonbody = body; - // console.log( - // `called at euh reqreqreqreq: ${pathname}\n\n${JSON.stringify(body)}` - // ); - reqBody = JSON.stringify(body, null, 2); +export class ViewServerUserManager { + public viewServer: ViewServer; + + constructor(viewServer: ViewServer) { + this.viewServer = viewServer; + } + + public users = new Map(); + public handlesDid(did: string): boolean { + return this.users.has(did); + } + + /*async*/ addUser(did: string) { + if (this.users.has(did)) return; + const instance = new UserViewServer(this, did); + //await instance.initialize(); + this.users.set(did, instance); } - const bskyUrl = `https://api.bsky.app${pathname}${url.search}`; - const proxyHeaders = new Headers(req.headers); - - // Remove Authorization and set browser-like User-Agent - proxyHeaders.delete("authorization"); - proxyHeaders.delete("Access-Control-Allow-Origin"), - proxyHeaders.set( - "user-agent", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" - ); - proxyHeaders.set("Access-Control-Allow-Origin", "*"); - - const proxyRes = await fetch(bskyUrl, { - method: req.method, - headers: proxyHeaders, - body: ["GET", "HEAD"].includes(req.method.toUpperCase()) - ? undefined - : reqBody, - }); - - const resBody = await proxyRes.text(); - - // console.log( - // "← Response:", - // JSON.stringify(await JSON.parse(resBody), null, 2) - // ); - - return new Response(resBody, { - status: proxyRes.status, - headers: proxyRes.headers, - }); -} -export function viewServerIndexer(ctx: indexHandlerContext) { - const record = validateRecord(ctx.value); - switch (record?.$type) { - case "app.bsky.feed.like": { - return; + // async handleRequest({ + // did, + // route, + // req, + // }: { + // did: string; + // route: string; + // req: Request; + // }) { + // if (!this.users.has(did)) await this.addUser(did); + // const user = this.users.get(did)!; + // return await user.handleHttpRequest(route, req); + // } + + removeUser(did: string) { + const instance = this.users.get(did); + if (!instance) return; + /*await*/ instance.shutdown(); + this.users.delete(did); + } + + getDbForDid(did: string): Database | null { + if (!this.users.has(did)) { + return null; } - default: { - // what the hell - return; + return this.users.get(did)?.db ?? null; + } + + coldStart(db: Database) { + const rows = db.prepare("SELECT did FROM users").all(); + for (const row of rows) { + this.addUser(row.did); } } } + +class UserViewServer { + public viewServerUserManager: ViewServerUserManager; + did: string; + db: Database; // | undefined; + jetstream: JetstreamManager; // | undefined; + spacedust: SpacedustManager; // | undefined; + + constructor(viewServerUserManager: ViewServerUserManager, did: string) { + this.did = did; + this.viewServerUserManager = viewServerUserManager; + this.db = this.viewServerUserManager.viewServer.internalCreateDbForDid( + this.did + ); + // should probably put the params of exactly what were listening to here + this.jetstream = new JetstreamManager((msg) => { + console.log("Received Jetstream message: ", msg); + + const op = msg.commit.operation; + const doer = msg.did; + const rev = msg.commit.rev; + const aturi = `${msg.did}/${msg.commit.collection}/${msg.commit.rkey}`; + const value = msg.commit.record; + + if (!doer || !value) return; + this.viewServerUserManager.viewServer.viewServerIndexer({ + op, + doer, + cid: msg.commit.cid, + rev, + aturi, + value, + indexsrc: `jetstream-${op}`, + db: this.db, + }); + }); + this.jetstream.start({ + // for realsies pls get from db or something instead of this shit + wantedDids: [ + this.did, + // "did:plc:mn45tewwnse5btfftvd3powc", + // "did:plc:yy6kbriyxtimkjqonqatv2rb", + // "did:plc:zzhzjga3ab5fcs2vnsv2ist3", + // "did:plc:jz4ibztn56hygfld6j6zjszg", + ], + wantedCollections: [ + // View server only needs some of the things related to user views mutes, not all of them + //"app.bsky.actor.profile", + //"app.bsky.feed.generator", + //"app.bsky.feed.like", + //"app.bsky.feed.post", + //"app.bsky.feed.repost", + "app.bsky.feed.threadgate", // mod + "app.bsky.graph.block", // mod + "app.bsky.graph.follow", // graphing + //"app.bsky.graph.list", + "app.bsky.graph.listblock", // mod + //"app.bsky.graph.listitem", + "app.bsky.notification.declaration", // mod + ], + }); + //await connectToJetstream(this.did, this.db); + this.spacedust = new SpacedustManager((msg: SpacedustLinkMessage) => { + console.log("Received Spacedust message: ", msg); + const operation = msg.link.operation; + + const sourceURI = new ATPAPI.AtUri(msg.link.source_record); + const srcUri = msg.link.source_record; + const srcDid = sourceURI.host; + const srcField = msg.link.source; + const srcCol = sourceURI.collection; + const subjectURI = new ATPAPI.AtUri(msg.link.subject); + const subUri = msg.link.subject; + const subDid = subjectURI.host; + const subCol = subjectURI.collection; + + if (operation === "delete") { + this.db.run( + `DELETE FROM backlink_skeleton + WHERE srcuri = ? AND srcfield = ? AND suburi = ?`, + [srcUri, srcField, subUri] + ); + } else if (operation === "create") { + this.db.run( + `INSERT OR REPLACE INTO backlink_skeleton ( + srcuri, + srcdid, + srcfield, + srccol, + suburi, + subdid, + subcol + ) VALUES (?, ?, ?, ?, ?, ?, ?)`, + [ + srcUri, // full AT URI of the source record + srcDid, // did: of the source + srcField, // e.g., "reply.parent.uri" or "facets.features.did" + srcCol, // e.g., "app.bsky.feed.post" + subUri, // full AT URI of the subject (linked record) + subDid, // did: of the subject + subCol, // subject collection (can be inferred or passed) + ] + ); + } + }); + this.spacedust.start({ + wantedSources: [ + // view server keeps all of this because notifications are a thing + "app.bsky.feed.like:subject.uri", // like + "app.bsky.feed.like:via.uri", // liked repost + "app.bsky.feed.repost:subject.uri", // repost + "app.bsky.feed.repost:via.uri", // reposted repost + "app.bsky.feed.post:reply.root.uri", // thread OP + "app.bsky.feed.post:reply.parent.uri", // direct parent + "app.bsky.feed.post:embed.media.record.record.uri", // quote with media + "app.bsky.feed.post:embed.record.uri", // quote without media + "app.bsky.feed.threadgate:post", // threadgate subject + "app.bsky.feed.threadgate:hiddenReplies", // threadgate items (array) + "app.bsky.feed.post:facets.features.did", // facet item (array): mention + "app.bsky.graph.block:subject", // blocks + "app.bsky.graph.follow:subject", // follow + "app.bsky.graph.listblock:subject", // list item (blocks) + "app.bsky.graph.listblock:list", // blocklist mention (might not exist) + "app.bsky.graph.listitem:subject", // list item (blocks) + "app.bsky.graph.listitem:list", // list mention + ], + // should be getting from DB but whatever right + wantedSubjects: [ + // as noted i dont need to write down each post, just the user to listen to ! + // hell yeah + // "at://did:plc:mn45tewwnse5btfftvd3powc/app.bsky.feed.post/3lvybv7b6ic2h", + // "at://did:plc:mn45tewwnse5btfftvd3powc/app.bsky.feed.post/3lvybws4avc2h", + // "at://did:plc:mn45tewwnse5btfftvd3powc/app.bsky.feed.post/3lvvkcxcscs2h", + // "at://did:plc:yy6kbriyxtimkjqonqatv2rb/app.bsky.feed.post/3l63ogxocq42f", + // "at://did:plc:yy6kbriyxtimkjqonqatv2rb/app.bsky.feed.post/3lw3wamvflu23", + ], + wantedSubjectDids: [ + this.did, + //"did:plc:mn45tewwnse5btfftvd3powc", + //"did:plc:yy6kbriyxtimkjqonqatv2rb", + //"did:plc:zzhzjga3ab5fcs2vnsv2ist3", + //"did:plc:jz4ibztn56hygfld6j6zjszg", + ], + }); + //await connectToConstellation(this.did, this.db); + } + + // initialize() { + + // } + + // async handleHttpRequest(route: string, req: Request): Promise { + // if (route === "posts") { + // const posts = await this.queryPosts(); + // return new Response(JSON.stringify(posts), { + // headers: { "content-type": "application/json" }, + // }); + // } + + // return new Response("Unknown route", { status: 404 }); + // } + + // private async queryPosts() { + // return this.db.run( + // "SELECT * FROM posts ORDER BY created_at DESC LIMIT 100" + // ); + // } + + shutdown() { + this.jetstream.stop(); + this.spacedust.stop(); + this.db.close?.(); + } +}