···1212import * as ATPAPI from "npm:@atproto/api";
1313import { AtUri } from "npm:@atproto/api";
1414import * as IndexServerAPI from "./indexclient/index.ts";
1515-import * as IndexServerUtils from "./indexclient/util.ts"
1515+import * as IndexServerUtils from "./indexclient/util.ts";
1616+import { isPostView } from "./indexclient/types/app/bsky/feed/defs.ts";
16171718export interface IndexServerConfig {
1819 baseDbPath: string;
···273274274275 // TODO: not partial yet, currently skips refs
275276276276- const qresult = this.queryActorLikesPartial(jsonTyped.actor, jsonTyped.cursor);
277277+ const qresult = this.queryActorLikesPartial(
278278+ jsonTyped.actor,
279279+ jsonTyped.cursor
280280+ );
277281 if (!qresult) {
278282 return new Response(
279283 JSON.stringify({
···302306303307 // TODO: not partial yet, currently skips refs
304308305305- const qresult = this.queryAuthorFeed(jsonTyped.actor, jsonTyped.cursor);
309309+ const qresult = this.queryAuthorFeedPartial(jsonTyped.actor, jsonTyped.cursor);
306310 if (!qresult) {
307311 return new Response(
308312 JSON.stringify({
···359363360364 // TODO: not partial yet, currently skips refs
361365362362- const qresult = this.queryPostThread(jsonTyped.uri);
366366+ const qresult = this.queryPostThreadPartial(jsonTyped.uri);
363367 if (!qresult) {
364368 return new Response(
365369 JSON.stringify({
···1032103610331037 return post;
10341038 }
10351035-10361036- constructPostViewRef(uri: string): IndexServerAPI.PartyWheyAppBskyFeedDefs.PostViewRef {
10391039+10401040+ constructPostViewRef(
10411041+ uri: string
10421042+ ): IndexServerAPI.PartyWheyAppBskyFeedDefs.PostViewRef {
10371043 const post: IndexServerAPI.PartyWheyAppBskyFeedDefs.PostViewRef = {
10381044 uri: uri,
10391045 cid: "cid.invalid", // oh shit we dont know the cid TODO: major design flaw
···10631069 ): IndexServerAPI.PartyWheyAppBskyFeedDefs.FeedViewPostRef {
10641070 const post = this.constructPostViewRef(uri);
1065107110661066- const feedviewpostref: IndexServerAPI.PartyWheyAppBskyFeedDefs.FeedViewPostRef = {
10671067- $type: "party.whey.app.bsky.feed.defs#feedViewPostRef",
10681068- post: post as IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.PostViewRef>,
10691069- }
10721072+ const feedviewpostref: IndexServerAPI.PartyWheyAppBskyFeedDefs.FeedViewPostRef =
10731073+ {
10741074+ $type: "party.whey.app.bsky.feed.defs#feedViewPostRef",
10751075+ post: post as IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.PostViewRef>,
10761076+ };
1070107710711071- return feedviewpostref
10781078+ return feedviewpostref;
10721079 }
1073108010741081 // user feedgens
···1181118811821189 // user feeds
1183119011841184- queryAuthorFeed(
11911191+ queryAuthorFeedPartial(
11851192 did: string,
11861193 cursor?: string
11871194 ):
11881195 | {
11891189- items: ATPAPI.AppBskyFeedDefs.FeedViewPost[];
11961196+ items: (
11971197+ | ATPAPI.AppBskyFeedDefs.FeedViewPost
11981198+ | IndexServerAPI.PartyWheyAppBskyFeedDefs.FeedViewPostRef
11991199+ )[];
11901200 cursor: string | undefined;
11911201 }
11921202 | undefined {
···11941204 const db = this.userManager.getDbForDid(did);
11951205 if (!db) return;
1196120611971197- // TODO: implement this for real
11981198- let query = `
11991199- SELECT uri, indexedat, cid
12001200- FROM app_bsky_feed_post
12011201- WHERE did = ?
12021202- `;
12031203- const params: (string | number)[] = [did];
12071207+ const subquery = `
12081208+ SELECT uri, cid, indexedat, 'post' as type, null as subject
12091209+ FROM app_bsky_feed_post
12101210+ WHERE did = ?
12111211+ UNION ALL
12121212+ SELECT uri, cid, indexedat, 'repost' as type, subject
12131213+ FROM app_bsky_feed_repost
12141214+ WHERE did = ?
12151215+ `;
12161216+12171217+ let query = `SELECT * FROM (${subquery}) as feed_items`;
12181218+ const params: (string | number)[] = [did, did];
1204121912051220 if (cursor) {
12061221 const [indexedat, cid] = cursor.split("::");
12071207- query += ` AND (indexedat < ? OR (indexedat = ? AND cid < ?))`;
12221222+ query += ` WHERE (indexedat < ? OR (indexedat = ? AND cid < ?))`;
12081223 params.push(parseInt(indexedat, 10), parseInt(indexedat, 10), cid);
12091224 }
12101225···12151230 uri: string;
12161231 indexedat: number;
12171232 cid: string;
12331233+ type: "post" | "repost";
12341234+ subject: string | null;
12181235 }[];
12361236+12371237+ const authorProfile = this.queryProfileView(did,"Basic");
1219123812201239 const items = rows
12211221- .map((row) => this.queryFeedViewPost(row.uri)) // TODO: for replies and repost i should inject the reason here
12401240+ .map((row) => {
12411241+ if (row.type === "repost" && row.subject) {
12421242+ const subjectDid = new AtUri(row.subject).host
12431243+12441244+ const originalPost = this.handlesDid(subjectDid)
12451245+ ? this.queryFeedViewPost(row.subject)
12461246+ : this.constructFeedViewPostRef(row.subject);
12471247+12481248+ if (!originalPost || !authorProfile) return null;
12491249+12501250+ return {
12511251+ post: originalPost,
12521252+ reason: {
12531253+ $type: "app.bsky.feed.defs#reasonRepost",
12541254+ by: authorProfile,
12551255+ indexedAt: new Date(row.indexedat).toISOString(),
12561256+ },
12571257+ };
12581258+ } else {
12591259+ return this.queryFeedViewPost(row.uri);
12601260+ }
12611261+ })
12221262 .filter((p): p is ATPAPI.AppBskyFeedDefs.FeedViewPost => !!p);
1223126312241264 const lastItem = rows[rows.length - 1];
···12461286 cursor?: string
12471287 ):
12481288 | {
12491249- items: (ATPAPI.AppBskyFeedDefs.FeedViewPost | IndexServerAPI.PartyWheyAppBskyFeedDefs.FeedViewPostRef)[];
12891289+ items: (
12901290+ | ATPAPI.AppBskyFeedDefs.FeedViewPost
12911291+ | IndexServerAPI.PartyWheyAppBskyFeedDefs.FeedViewPostRef
12921292+ )[];
12501293 cursor: string | undefined;
12511294 }
12521295 | undefined {
···1279132212801323 const items = rows
12811324 .map((row) => {
12821282- const subjectDid = new AtUri(row.subject).host;
13251325+ const subjectDid = new AtUri(row.subject).host;
1283132612841327 if (this.handlesDid(subjectDid)) {
12851328 return this.queryFeedViewPost(row.subject);
···12871330 return this.constructFeedViewPostRef(row.subject);
12881331 }
12891332 })
12901290- .filter((p): p is ATPAPI.AppBskyFeedDefs.FeedViewPost | IndexServerAPI.PartyWheyAppBskyFeedDefs.FeedViewPostRef => !!p);
13331333+ .filter(
13341334+ (
13351335+ p
13361336+ ): p is
13371337+ | ATPAPI.AppBskyFeedDefs.FeedViewPost
13381338+ | IndexServerAPI.PartyWheyAppBskyFeedDefs.FeedViewPostRef => !!p
13391339+ );
1291134012921341 const lastItem = rows[rows.length - 1];
12931342 const nextCursor = lastItem
···13711420 .map((row) => this.queryFeedViewPost(row.srcuri))
13721421 .filter((p): p is ATPAPI.AppBskyFeedDefs.FeedViewPost => !!p);
13731422 }
13741374-13751375- queryPostThread(
14231423+ _getPostViewUnion(
13761424 uri: string
13771377- ): ATPAPI.AppBskyFeedGetPostThread.OutputSchema | undefined {
13781378- const post = this.queryPostView(uri);
14251425+ ):
14261426+ | ATPAPI.AppBskyFeedDefs.PostView
14271427+ | IndexServerAPI.PartyWheyAppBskyFeedDefs.PostViewRef
14281428+ | undefined {
14291429+ try {
14301430+ const postDid = new AtUri(uri).hostname;
14311431+ if (this.handlesDid(postDid)) {
14321432+ return this.queryPostView(uri);
14331433+ } else {
14341434+ return this.constructPostViewRef(uri);
14351435+ }
14361436+ } catch (_e) {
14371437+ return undefined;
14381438+ }
14391439+ }
14401440+ queryPostThreadPartial(
14411441+ uri: string
14421442+ ): IndexServerTypes.PartyWheyAppBskyFeedGetPostThreadPartial.OutputSchema | undefined {
14431443+14441444+ const post = this._getPostViewUnion(uri);
14451445+13791446 if (!post) {
13801447 return {
13811448 thread: {
···13861453 };
13871454 }
1388145513891389- const thread: ATPAPI.AppBskyFeedDefs.ThreadViewPost = {
13901390- $type: "app.bsky.feed.defs#threadViewPost",
13911391- post: post,
14561456+ const thread: IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef = {
14571457+ $type: "party.whey.app.bsky.feed.defs#threadViewPostRef",
14581458+ post: post as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.PostView> | IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.PostViewRef>,
13921459 replies: [],
13931460 };
1394146113951462 let current = thread;
13961396- while ((current.post.record.reply as any)?.parent?.uri) {
13971397- const parentUri = (current.post.record.reply as any)?.parent?.uri;
13981398- const parentPost = this.queryPostView(parentUri);
13991399- if (!parentPost) break;
14631463+ // we can only climb the parent tree if we have the full post record.
14641464+ // which is not implemented yet (sad i know)
14651465+ if (isPostView(current.post) && isFeedPostRecord(current.post.record) && current.post.record?.reply?.parent?.uri) {
14661466+ let parentUri: string | undefined = current.post.record.reply.parent.uri;
14671467+14681468+ // keep climbing as long as we find a valid parent post.
14691469+ while (parentUri) {
14701470+ const parentPost = this._getPostViewUnion(parentUri);
14711471+ if (!parentPost) break; // stop if a parent in the chain is not found.
14721472+14731473+ const parentThread: IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef = {
14741474+ $type: "party.whey.app.bsky.feed.defs#threadViewPostRef",
14751475+ post: parentPost as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.PostView>,
14761476+ replies: [current as IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef>],
14771477+ };
14781478+ current.parent = parentThread as IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef>;
14791479+ current = parentThread;
1400148014011401- const parentThread: ATPAPI.AppBskyFeedDefs.ThreadViewPost = {
14021402- $type: "app.bsky.feed.defs#threadViewPost",
14031403- post: parentPost,
14041404- replies: [
14051405- current as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.ThreadViewPost>,
14061406- ],
14071407- };
14081408- current.parent =
14091409- parentThread as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.ThreadViewPost>;
14101410- current = parentThread;
14811481+ // check if the new current post has a parent to continue the loop
14821482+ parentUri = (isPostView(current.post) && isFeedPostRecord(current.post.record)) ? current.post.record?.reply?.parent?.uri : undefined;
14831483+ }
14111484 }
1412148514861486+14871487+14131488 const seenUris = new Set<string>();
14141414- const fetchReplies = (
14151415- parentThread: ATPAPI.AppBskyFeedDefs.ThreadViewPost
14161416- ) => {
14891489+ const fetchReplies = (parentThread: IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef) => {
14901490+ if (!parentThread.post || !('uri' in parentThread.post)) {
14911491+ return;
14921492+ }
14171493 if (seenUris.has(parentThread.post.uri)) return;
14181494 seenUris.add(parentThread.post.uri);
1419149514201496 const parentUri = new AtUri(parentThread.post.uri);
14211497 const parentAuthorDid = parentUri.hostname;
14981498+14991499+ // replies can only be discovered for local posts where we have the backlink data
15001500+ if (!this.handlesDid(parentAuthorDid)) return;
15011501+14221502 const db = this.userManager.getDbForDid(parentAuthorDid);
14231503 if (!db) return;
1424150414251505 const stmt = db.prepare(`
14261426- SELECT srcuri
14271427- FROM backlink_skeleton
14281428- WHERE suburi = ? AND srccol = 'app_bsky_feed_post' AND srcfield = 'replyparent'
14291429- `);
15061506+ SELECT srcuri
15071507+ FROM backlink_skeleton
15081508+ WHERE suburi = ? AND srccol = 'app_bsky_feed_post' AND srcfield = 'replyparent'
15091509+ `);
14301510 const replyRows = stmt.all(parentThread.post.uri) as { srcuri: string }[];
1431151114321512 const replies = replyRows
14331433- .map((row) => this.queryPostView(row.srcuri))
14341434- .filter((p): p is ATPAPI.AppBskyFeedDefs.PostView => !!p);
15131513+ .map((row) => this._getPostViewUnion(row.srcuri))
15141514+ .filter((p): p is ATPAPI.AppBskyFeedDefs.PostView | IndexServerAPI.PartyWheyAppBskyFeedDefs.PostViewRef => !!p);
1435151514361516 for (const replyPost of replies) {
14371437- const replyThread: ATPAPI.AppBskyFeedDefs.ThreadViewPost = {
14381438- $type: "app.bsky.feed.defs#threadViewPost",
14391439- post: replyPost,
14401440- parent:
14411441- parentThread as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.ThreadViewPost>,
15171517+ const replyThread: IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef = {
15181518+ $type: "party.whey.app.bsky.feed.defs#threadViewPostRef",
15191519+ post: replyPost as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.PostView> | IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.PostViewRef>,
15201520+ parent: parentThread as IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef>,
14421521 replies: [],
14431522 };
14441444- parentThread.replies?.push(
14451445- replyThread as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.ThreadViewPost>
14461446- );
14471447- fetchReplies(replyThread);
15231523+ parentThread.replies?.push(replyThread as IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef>);
15241524+ fetchReplies(replyThread); // recurse
14481525 }
14491526 };
1450152714511528 fetchReplies(thread);
1452152914531453- const returned =
14541454- thread as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.ThreadViewPost>;
15301530+ const returned = current as unknown as IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef;
1455153114561456- return { thread: returned };
15321532+ return { thread: returned as IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef> };
14571533 }
15341534+1458153514591536 /**
14601537 * please do not use this, use openDbForDid() instead
···14701547 }
14711548 /**
14721549 * @deprecated use handlesDid() instead
14731473- * @param did
14741474- * @returns
15501550+ * @param did
15511551+ * @returns
14751552 */
14761553 isRegisteredIndexUser(did: string): boolean {
14771554 const stmt = this.systemDB.prepare(`
···1888196518891966export function isDid(str: string): boolean {
18901967 return typeof str === "string" && str.startsWith("did:");
19681968+}
19691969+19701970+function isFeedPostRecord(
19711971+ post: unknown
19721972+): post is ATPAPI.AppBskyFeedPost.Record {
19731973+ return (
19741974+ typeof post === "object" &&
19751975+ post !== null &&
19761976+ "$type" in post &&
19771977+ (post as any).$type === "app.bsky.feed.post"
19781978+ );
18911979}
1892198018931981function isImageEmbed(embed: unknown): embed is ATPAPI.AppBskyEmbedImages.Main {
+1-1
readme.md
···77- the server stuff: [sqlite](https://jsr.io/@db/sqlite) db, typescript with [codegen](https://www.npmjs.com/package/@atproto/lex-cli), and [deno](https://deno.com/)
8899## Status
1010-(as of 25 aug 2025)
1010+(as of 26 aug 2025)
1111currently the state of the project is:
1212### Index Server
1313- Database: