···55import * as IndexServerTypes from "./utils/indexservertypes.ts";
66import { Database } from "jsr:@db/sqlite@0.11";
77import { setupUserDb } from "./utils/dbuser.ts";
88-import { indexerUserManager, jetstreamurl, systemDB } from "./main.ts";
88+// import { systemDB } from "./main.ts";
99import { JetstreamManager, SpacedustManager } from "./utils/sharders.ts";
1010import { handleSpacedust, SpacedustLinkMessage } from "./index/spacedust.ts";
1111import { handleJetstream } from "./index/jetstream.ts";
···1313import { AtUri } from "npm:@atproto/api";
1414import * as IndexServerAPI from "./indexclient/index.ts";
15151616+export interface IndexServerConfig {
1717+ baseDbPath: string;
1818+ systemDbPath: string;
1919+ jetstreamUrl: string;
2020+}
2121+2222+interface BaseRow {
2323+ uri: string;
2424+ did: string;
2525+ cid: string | null;
2626+ rev: string | null;
2727+ createdat: number | null;
2828+ indexedat: number;
2929+ json: string | null;
3030+}
3131+interface GeneratorRow extends BaseRow {
3232+ displayname: string | null;
3333+ description: string | null;
3434+ avatarcid: string | null;
3535+}
3636+interface LikeRow extends BaseRow {
3737+ subject: string;
3838+}
3939+interface RepostRow extends BaseRow {
4040+ subject: string;
4141+}
4242+interface BacklinkRow {
4343+ srcuri: string;
4444+ srcdid: string;
4545+}
4646+4747+const FEED_LIMIT = 50;
4848+4949+export class IndexServer {
5050+ private config: IndexServerConfig;
5151+ public userManager: IndexServerUserManager;
5252+ public systemDB: Database;
5353+5454+ constructor(config: IndexServerConfig) {
5555+ this.config = config;
5656+5757+ // We will initialize the system DB and user manager here
5858+ this.systemDB = new Database(this.config.systemDbPath);
5959+ // TODO: We need to setup the system DB schema if it's new
6060+6161+ this.userManager = new IndexServerUserManager(this); // Pass the server instance
6262+ }
6363+6464+ public start() {
6565+ // This is where we'll kick things off, like the cold start
6666+ this.userManager.coldStart(this.systemDB);
6767+ console.log("IndexServer started.");
6868+ }
6969+7070+ public async handleRequest(req: Request): Promise<Response> {
7171+ const url = new URL(req.url);
7272+ // We will add routing logic here later to call our handlers
7373+ if (url.pathname.startsWith("/xrpc/")) {
7474+ return this.indexServerHandler(req);
7575+ }
7676+ if (url.pathname.startsWith("/links")) {
7777+ return this.constellationAPIHandler(req);
7878+ }
7979+ return new Response("Not Found", { status: 404 });
8080+ }
8181+8282+ // We will move all the global functions into this class as methods...
8383+ indexServerHandler(req: Request): Response {
8484+ const url = new URL(req.url);
8585+ const pathname = url.pathname;
8686+ //const bskyUrl = `https://api.bsky.app${pathname}${url.search}`;
8787+ //const hasAuth = req.headers.has("authorization");
8888+ const xrpcMethod = pathname.startsWith("/xrpc/")
8989+ ? pathname.slice("/xrpc/".length)
9090+ : null;
9191+ const searchParams = searchParamsToJson(url.searchParams);
9292+ console.log(JSON.stringify(searchParams, null, 2));
9393+ const jsonUntyped = searchParams;
9494+9595+ switch (xrpcMethod) {
9696+ case "app.bsky.actor.getProfile": {
9797+ const jsonTyped =
9898+ jsonUntyped as IndexServerTypes.AppBskyActorGetProfile.QueryParams;
9999+100100+ const res = this.queryProfileView(jsonTyped.actor, "Detailed");
101101+ if (!res)
102102+ return new Response(
103103+ JSON.stringify({
104104+ error: "User not found",
105105+ }),
106106+ {
107107+ status: 404,
108108+ headers: withCors({ "Content-Type": "application/json" }),
109109+ }
110110+ );
111111+ const response: IndexServerTypes.AppBskyActorGetProfile.OutputSchema =
112112+ res;
113113+114114+ return new Response(JSON.stringify(response), {
115115+ headers: withCors({ "Content-Type": "application/json" }),
116116+ });
117117+ }
118118+ case "app.bsky.actor.getProfiles": {
119119+ const jsonTyped =
120120+ jsonUntyped as IndexServerTypes.AppBskyActorGetProfiles.QueryParams;
121121+122122+ if (typeof jsonUntyped?.actors === "string") {
123123+ const res = this.queryProfileView(
124124+ jsonUntyped.actors as string,
125125+ "Detailed"
126126+ );
127127+ if (!res)
128128+ return new Response(
129129+ JSON.stringify({
130130+ error: "User not found",
131131+ }),
132132+ {
133133+ status: 404,
134134+ headers: withCors({ "Content-Type": "application/json" }),
135135+ }
136136+ );
137137+ const response: IndexServerTypes.AppBskyActorGetProfiles.OutputSchema =
138138+ {
139139+ profiles: [res],
140140+ };
141141+142142+ return new Response(JSON.stringify(response), {
143143+ headers: withCors({ "Content-Type": "application/json" }),
144144+ });
145145+ }
146146+147147+ const res: ATPAPI.AppBskyActorDefs.ProfileViewDetailed[] =
148148+ jsonTyped.actors
149149+ .map((actor) => {
150150+ return this.queryProfileView(actor, "Detailed");
151151+ })
152152+ .filter(
153153+ (x): x is ATPAPI.AppBskyActorDefs.ProfileViewDetailed =>
154154+ x !== undefined
155155+ );
156156+157157+ if (!res)
158158+ return new Response(
159159+ JSON.stringify({
160160+ error: "User not found",
161161+ }),
162162+ {
163163+ status: 404,
164164+ headers: withCors({ "Content-Type": "application/json" }),
165165+ }
166166+ );
167167+168168+ const response: IndexServerTypes.AppBskyActorGetProfiles.OutputSchema =
169169+ {
170170+ profiles: res,
171171+ };
172172+173173+ return new Response(JSON.stringify(response), {
174174+ headers: withCors({ "Content-Type": "application/json" }),
175175+ });
176176+ }
177177+ case "app.bsky.feed.getActorFeeds": {
178178+ const jsonTyped =
179179+ jsonUntyped as IndexServerTypes.AppBskyFeedGetActorFeeds.QueryParams;
180180+181181+ const qresult = this.queryActorFeeds(jsonTyped.actor);
182182+183183+ const response: IndexServerTypes.AppBskyFeedGetActorFeeds.OutputSchema =
184184+ {
185185+ feeds: qresult,
186186+ };
187187+188188+ return new Response(JSON.stringify(response), {
189189+ headers: withCors({ "Content-Type": "application/json" }),
190190+ });
191191+ }
192192+ case "app.bsky.feed.getFeedGenerator": {
193193+ const jsonTyped =
194194+ jsonUntyped as IndexServerTypes.AppBskyFeedGetFeedGenerator.QueryParams;
195195+196196+ const qresult = this.queryFeedGenerator(jsonTyped.feed);
197197+ if (!qresult) {
198198+ return new Response(
199199+ JSON.stringify({
200200+ error: "Feed not found",
201201+ }),
202202+ {
203203+ status: 404,
204204+ headers: withCors({ "Content-Type": "application/json" }),
205205+ }
206206+ );
207207+ }
208208+209209+ const response: IndexServerTypes.AppBskyFeedGetFeedGenerator.OutputSchema =
210210+ {
211211+ view: qresult,
212212+ isOnline: true, // lmao
213213+ isValid: true, // lmao
214214+ };
215215+216216+ return new Response(JSON.stringify(response), {
217217+ headers: withCors({ "Content-Type": "application/json" }),
218218+ });
219219+ }
220220+ case "app.bsky.feed.getFeedGenerators": {
221221+ const jsonTyped =
222222+ jsonUntyped as IndexServerTypes.AppBskyFeedGetFeedGenerators.QueryParams;
223223+224224+ const qresult = this.queryFeedGenerators(jsonTyped.feeds);
225225+ if (!qresult) {
226226+ return new Response(
227227+ JSON.stringify({
228228+ error: "Feed not found",
229229+ }),
230230+ {
231231+ status: 404,
232232+ headers: withCors({ "Content-Type": "application/json" }),
233233+ }
234234+ );
235235+ }
236236+237237+ const response: IndexServerTypes.AppBskyFeedGetFeedGenerators.OutputSchema =
238238+ {
239239+ feeds: qresult,
240240+ };
241241+242242+ return new Response(JSON.stringify(response), {
243243+ headers: withCors({ "Content-Type": "application/json" }),
244244+ });
245245+ }
246246+ case "app.bsky.feed.getPosts": {
247247+ const jsonTyped =
248248+ jsonUntyped as IndexServerTypes.AppBskyFeedGetPosts.QueryParams;
249249+250250+ const posts: IndexServerTypes.AppBskyFeedGetPosts.OutputSchema["posts"] =
251251+ jsonTyped.uris
252252+ .map((uri) => {
253253+ return this.queryPostView(uri);
254254+ })
255255+ .filter(Boolean) as ATPAPI.AppBskyFeedDefs.PostView[];
256256+257257+ const response: IndexServerTypes.AppBskyFeedGetPosts.OutputSchema = {
258258+ posts,
259259+ };
260260+261261+ return new Response(JSON.stringify(response), {
262262+ headers: withCors({ "Content-Type": "application/json" }),
263263+ });
264264+ }
265265+ case "party.whey.app.bsky.feed.getActorLikesPartial": {
266266+ const jsonTyped =
267267+ jsonUntyped as IndexServerTypes.PartyWheyAppBskyFeedGetActorLikesPartial.QueryParams;
268268+269269+ // TODO: not partial yet, currently skips refs
270270+271271+ const qresult = this.queryActorLikes(jsonTyped.actor, jsonTyped.cursor);
272272+ if (!qresult) {
273273+ return new Response(
274274+ JSON.stringify({
275275+ error: "Feed not found",
276276+ }),
277277+ {
278278+ status: 404,
279279+ headers: withCors({ "Content-Type": "application/json" }),
280280+ }
281281+ );
282282+ }
283283+284284+ const response: IndexServerTypes.PartyWheyAppBskyFeedGetActorLikesPartial.OutputSchema =
285285+ {
286286+ feed: qresult.items as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.FeedViewPost>[],
287287+ cursor: qresult.cursor,
288288+ };
289289+290290+ return new Response(JSON.stringify(response), {
291291+ headers: withCors({ "Content-Type": "application/json" }),
292292+ });
293293+ }
294294+ case "party.whey.app.bsky.feed.getAuthorFeedPartial": {
295295+ const jsonTyped =
296296+ jsonUntyped as IndexServerTypes.PartyWheyAppBskyFeedGetAuthorFeedPartial.QueryParams;
297297+298298+ // TODO: not partial yet, currently skips refs
299299+300300+ const qresult = this.queryAuthorFeed(jsonTyped.actor, jsonTyped.cursor);
301301+ if (!qresult) {
302302+ return new Response(
303303+ JSON.stringify({
304304+ error: "Feed not found",
305305+ }),
306306+ {
307307+ status: 404,
308308+ headers: withCors({ "Content-Type": "application/json" }),
309309+ }
310310+ );
311311+ }
312312+313313+ const response: IndexServerTypes.PartyWheyAppBskyFeedGetAuthorFeedPartial.OutputSchema =
314314+ {
315315+ feed: qresult.items as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.FeedViewPost>[],
316316+ cursor: qresult.cursor,
317317+ };
318318+319319+ return new Response(JSON.stringify(response), {
320320+ headers: withCors({ "Content-Type": "application/json" }),
321321+ });
322322+ }
323323+ case "party.whey.app.bsky.feed.getLikesPartial": {
324324+ const jsonTyped =
325325+ jsonUntyped as IndexServerTypes.PartyWheyAppBskyFeedGetLikesPartial.QueryParams;
326326+327327+ // TODO: not partial yet, currently skips refs
328328+329329+ const qresult = this.queryLikes(jsonTyped.uri);
330330+ if (!qresult) {
331331+ return new Response(
332332+ JSON.stringify({
333333+ error: "Feed not found",
334334+ }),
335335+ {
336336+ status: 404,
337337+ headers: withCors({ "Content-Type": "application/json" }),
338338+ }
339339+ );
340340+ }
341341+ const response: IndexServerTypes.PartyWheyAppBskyFeedGetLikesPartial.OutputSchema =
342342+ {
343343+ // @ts-ignore whatever i dont care TODO: fix ts ignores
344344+ likes: qresult,
345345+ };
346346+347347+ return new Response(JSON.stringify(response), {
348348+ headers: withCors({ "Content-Type": "application/json" }),
349349+ });
350350+ }
351351+ case "party.whey.app.bsky.feed.getPostThreadPartial": {
352352+ const jsonTyped =
353353+ jsonUntyped as IndexServerTypes.PartyWheyAppBskyFeedGetPostThreadPartial.QueryParams;
354354+355355+ // TODO: not partial yet, currently skips refs
356356+357357+ const qresult = this.queryPostThread(jsonTyped.uri);
358358+ if (!qresult) {
359359+ return new Response(
360360+ JSON.stringify({
361361+ error: "Feed not found",
362362+ }),
363363+ {
364364+ status: 404,
365365+ headers: withCors({ "Content-Type": "application/json" }),
366366+ }
367367+ );
368368+ }
369369+ const response: IndexServerTypes.PartyWheyAppBskyFeedGetPostThreadPartial.OutputSchema =
370370+ qresult;
371371+372372+ return new Response(JSON.stringify(response), {
373373+ headers: withCors({ "Content-Type": "application/json" }),
374374+ });
375375+ }
376376+ case "party.whey.app.bsky.feed.getQuotesPartial": {
377377+ const jsonTyped =
378378+ jsonUntyped as IndexServerTypes.PartyWheyAppBskyFeedGetQuotesPartial.QueryParams;
379379+380380+ // TODO: not partial yet, currently skips refs
381381+382382+ const qresult = this.queryQuotes(jsonTyped.uri);
383383+ if (!qresult) {
384384+ return new Response(
385385+ JSON.stringify({
386386+ error: "Feed not found",
387387+ }),
388388+ {
389389+ status: 404,
390390+ headers: withCors({ "Content-Type": "application/json" }),
391391+ }
392392+ );
393393+ }
394394+ const response: IndexServerTypes.PartyWheyAppBskyFeedGetQuotesPartial.OutputSchema =
395395+ {
396396+ uri: jsonTyped.uri,
397397+ posts: qresult.map((feedviewpost) => {
398398+ return feedviewpost.post as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.PostView>;
399399+ }),
400400+ };
401401+402402+ return new Response(JSON.stringify(response), {
403403+ headers: withCors({ "Content-Type": "application/json" }),
404404+ });
405405+ }
406406+ case "party.whey.app.bsky.feed.getRepostedByPartial": {
407407+ const jsonTyped =
408408+ jsonUntyped as IndexServerTypes.PartyWheyAppBskyFeedGetRepostedByPartial.QueryParams;
409409+410410+ // TODO: not partial yet, currently skips refs
411411+412412+ const qresult = this.queryReposts(jsonTyped.uri);
413413+ if (!qresult) {
414414+ return new Response(
415415+ JSON.stringify({
416416+ error: "Feed not found",
417417+ }),
418418+ {
419419+ status: 404,
420420+ headers: withCors({ "Content-Type": "application/json" }),
421421+ }
422422+ );
423423+ }
424424+ const response: IndexServerTypes.PartyWheyAppBskyFeedGetRepostedByPartial.OutputSchema =
425425+ {
426426+ uri: jsonTyped.uri,
427427+ repostedBy:
428428+ qresult as ATPAPI.$Typed<ATPAPI.AppBskyActorDefs.ProfileView>[],
429429+ };
430430+431431+ return new Response(JSON.stringify(response), {
432432+ headers: withCors({ "Content-Type": "application/json" }),
433433+ });
434434+ }
435435+ // TODO: too hard for now
436436+ // case "party.whey.app.bsky.feed.getListFeedPartial": {
437437+ // const jsonTyped =
438438+ // jsonUntyped as IndexServerTypes.PartyWheyAppBskyFeedGetListFeedPartial.QueryParams;
439439+440440+ // const response: IndexServerTypes.PartyWheyAppBskyFeedGetListFeedPartial.OutputSchema =
441441+ // {};
442442+443443+ // return new Response(JSON.stringify(response), {
444444+ // headers: withCors({ "Content-Type": "application/json" }),
445445+ // });
446446+ // }
447447+ /* three more coming soon
448448+ app.bsky.graph.getLists
449449+ app.bsky.graph.getList
450450+ app.bsky.graph.getActorStarterPacks
451451+ */
452452+ default: {
453453+ return new Response(
454454+ JSON.stringify({
455455+ error: "XRPCNotSupported",
456456+ message:
457457+ "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",
458458+ }),
459459+ {
460460+ status: 404,
461461+ headers: withCors({ "Content-Type": "application/json" }),
462462+ }
463463+ );
464464+ }
465465+ }
466466+467467+ // return new Response("Not Found", { status: 404 });
468468+ }
469469+470470+ constellationAPIHandler(req: Request): Response {
471471+ const url = new URL(req.url);
472472+ const pathname = url.pathname;
473473+ const searchParams = searchParamsToJson(url.searchParams) as linksQuery;
474474+ const jsonUntyped = searchParams;
475475+476476+ if (!jsonUntyped.target) {
477477+ return new Response(
478478+ JSON.stringify({ error: "Missing required parameter: target" }),
479479+ {
480480+ status: 400,
481481+ headers: withCors({ "Content-Type": "application/json" }),
482482+ }
483483+ );
484484+ }
485485+486486+ const did = isDid(searchParams.target)
487487+ ? searchParams.target
488488+ : new AtUri(searchParams.target).host;
489489+ const db = this.userManager.getDbForDid(did);
490490+ if (!db) {
491491+ return new Response(
492492+ JSON.stringify({
493493+ error: "User not found",
494494+ }),
495495+ {
496496+ status: 404,
497497+ headers: withCors({ "Content-Type": "application/json" }),
498498+ }
499499+ );
500500+ }
501501+502502+ const limit = 16; //Math.min(parseInt(searchParams.limit || "50", 10), 100);
503503+ const offset = parseInt(searchParams.cursor || "0", 10);
504504+505505+ switch (pathname) {
506506+ case "/links": {
507507+ const jsonTyped = jsonUntyped as linksQuery;
508508+ if (!jsonTyped.collection || !jsonTyped.path) {
509509+ return new Response(
510510+ JSON.stringify({
511511+ error: "Missing required parameters: collection, path",
512512+ }),
513513+ {
514514+ status: 400,
515515+ headers: withCors({ "Content-Type": "application/json" }),
516516+ }
517517+ );
518518+ }
519519+520520+ const field = `${jsonTyped.collection}:${jsonTyped.path.replace(
521521+ /^\./,
522522+ ""
523523+ )}`;
524524+525525+ const paginatedSql = `${SQL.links} LIMIT ? OFFSET ?`;
526526+ const rows = db
527527+ .prepare(paginatedSql)
528528+ .all(jsonTyped.target, jsonTyped.collection, field, limit, offset);
529529+530530+ const countResult = db
531531+ .prepare(SQL.count)
532532+ .get(jsonTyped.target, jsonTyped.collection, field);
533533+ const total = countResult ? Number(countResult.total) : 0;
534534+535535+ const linking_records: linksRecord[] = rows.map((row: any) => {
536536+ const rkey = row.srcuri.split("/").pop()!;
537537+ return {
538538+ did: row.srcdid,
539539+ collection: row.srccol,
540540+ rkey,
541541+ };
542542+ });
543543+544544+ const response: linksRecordsResponse = {
545545+ total: total.toString(),
546546+ linking_records,
547547+ };
548548+549549+ const nextCursor = offset + linking_records.length;
550550+ if (nextCursor < total) {
551551+ response.cursor = nextCursor.toString();
552552+ }
553553+554554+ return new Response(JSON.stringify(response), {
555555+ headers: withCors({ "Content-Type": "application/json" }),
556556+ });
557557+ }
558558+ case "/links/distinct-dids": {
559559+ const jsonTyped = jsonUntyped as linksQuery;
560560+ if (!jsonTyped.collection || !jsonTyped.path) {
561561+ return new Response(
562562+ JSON.stringify({
563563+ error: "Missing required parameters: collection, path",
564564+ }),
565565+ {
566566+ status: 400,
567567+ headers: withCors({ "Content-Type": "application/json" }),
568568+ }
569569+ );
570570+ }
571571+572572+ const field = `${jsonTyped.collection}:${jsonTyped.path.replace(
573573+ /^\./,
574574+ ""
575575+ )}`;
576576+577577+ const paginatedSql = `${SQL.distinctDids} LIMIT ? OFFSET ?`;
578578+ const rows = db
579579+ .prepare(paginatedSql)
580580+ .all(jsonTyped.target, jsonTyped.collection, field, limit, offset);
581581+582582+ const countResult = db
583583+ .prepare(SQL.countDistinctDids)
584584+ .get(jsonTyped.target, jsonTyped.collection, field);
585585+ const total = countResult ? Number(countResult.total) : 0;
586586+587587+ const linking_dids: string[] = rows.map((row: any) => row.srcdid);
588588+589589+ const response: linksDidsResponse = {
590590+ total: total.toString(),
591591+ linking_dids,
592592+ };
593593+594594+ const nextCursor = offset + linking_dids.length;
595595+ if (nextCursor < total) {
596596+ response.cursor = nextCursor.toString();
597597+ }
598598+599599+ return new Response(JSON.stringify(response), {
600600+ headers: withCors({ "Content-Type": "application/json" }),
601601+ });
602602+ }
603603+ case "/links/count": {
604604+ const jsonTyped = jsonUntyped as linksQuery;
605605+ if (!jsonTyped.collection || !jsonTyped.path) {
606606+ return new Response(
607607+ JSON.stringify({
608608+ error: "Missing required parameters: collection, path",
609609+ }),
610610+ {
611611+ status: 400,
612612+ headers: withCors({ "Content-Type": "application/json" }),
613613+ }
614614+ );
615615+ }
616616+617617+ const field = `${jsonTyped.collection}:${jsonTyped.path.replace(
618618+ /^\./,
619619+ ""
620620+ )}`;
621621+622622+ const result = db
623623+ .prepare(SQL.count)
624624+ .get(jsonTyped.target, jsonTyped.collection, field);
625625+626626+ const response: linksCountResponse = {
627627+ total: result && result.total ? result.total.toString() : "0",
628628+ };
629629+630630+ return new Response(JSON.stringify(response), {
631631+ headers: withCors({ "Content-Type": "application/json" }),
632632+ });
633633+ }
634634+ case "/links/count/distinct-dids": {
635635+ const jsonTyped = jsonUntyped as linksQuery;
636636+ if (!jsonTyped.collection || !jsonTyped.path) {
637637+ return new Response(
638638+ JSON.stringify({
639639+ error: "Missing required parameters: collection, path",
640640+ }),
641641+ {
642642+ status: 400,
643643+ headers: withCors({ "Content-Type": "application/json" }),
644644+ }
645645+ );
646646+ }
647647+648648+ const field = `${jsonTyped.collection}:${jsonTyped.path.replace(
649649+ /^\./,
650650+ ""
651651+ )}`;
652652+653653+ const result = db
654654+ .prepare(SQL.countDistinctDids)
655655+ .get(jsonTyped.target, jsonTyped.collection, field);
656656+657657+ const response: linksCountResponse = {
658658+ total: result && result.total ? result.total.toString() : "0",
659659+ };
660660+661661+ return new Response(JSON.stringify(response), {
662662+ headers: withCors({ "Content-Type": "application/json" }),
663663+ });
664664+ }
665665+ case "/links/all": {
666666+ const jsonTyped = jsonUntyped as linksAllQuery;
667667+668668+ const rows = db.prepare(SQL.all).all(jsonTyped.target) as any[];
669669+670670+ const links: linksAllResponse["links"] = {};
671671+672672+ for (const row of rows) {
673673+ if (!links[row.suburi]) {
674674+ links[row.suburi] = {};
675675+ }
676676+ links[row.suburi][row.srccol] = {
677677+ records: row.records,
678678+ distinct_dids: row.distinct_dids,
679679+ };
680680+ }
681681+682682+ const response: linksAllResponse = {
683683+ links,
684684+ };
685685+686686+ return new Response(JSON.stringify(response), {
687687+ headers: withCors({ "Content-Type": "application/json" }),
688688+ });
689689+ }
690690+ default: {
691691+ return new Response(
692692+ JSON.stringify({
693693+ error: "NotSupported",
694694+ message:
695695+ "The requested endpoint is not supported by this Constellation implementation.",
696696+ }),
697697+ {
698698+ status: 404,
699699+ headers: withCors({ "Content-Type": "application/json" }),
700700+ }
701701+ );
702702+ }
703703+ }
704704+ }
705705+706706+ indexServerIndexer(ctx: indexHandlerContext) {
707707+ const record = assertRecord(ctx.value);
708708+ //const record = validateRecord(ctx.value);
709709+ const db = this.userManager.getDbForDid(ctx.doer);
710710+ if (!db) return;
711711+ console.log("indexering");
712712+ switch (record?.$type) {
713713+ case "app.bsky.feed.like": {
714714+ return;
715715+ }
716716+ case "app.bsky.actor.profile": {
717717+ console.log("bsky profuile");
718718+719719+ try {
720720+ const stmt = db.prepare(`
721721+ INSERT OR IGNORE INTO app_bsky_actor_profile (
722722+ uri, did, cid, rev, createdat, indexedat, json,
723723+ displayname,
724724+ description,
725725+ avatarcid,
726726+ avatarmime,
727727+ bannercid,
728728+ bannermime
729729+ ) VALUES (?, ?, ?, ?, ?, ?, ?,
730730+ ?, ?, ?,
731731+ ?, ?, ?)
732732+ `);
733733+ console.log({
734734+ uri: ctx.aturi,
735735+ did: ctx.doer,
736736+ cid: ctx.cid,
737737+ rev: ctx.rev,
738738+ createdat: record.createdAt,
739739+ indexedat: Date.now(),
740740+ json: JSON.stringify(record),
741741+ displayname: record.displayName,
742742+ description: record.description,
743743+ avatarcid: uncid(record.avatar?.ref),
744744+ avatarmime: record.avatar?.mimeType,
745745+ bannercid: uncid(record.banner?.ref),
746746+ bannermime: record.banner?.mimeType,
747747+ });
748748+ stmt.run(
749749+ ctx.aturi ?? null,
750750+ ctx.doer ?? null,
751751+ ctx.cid ?? null,
752752+ ctx.rev ?? null,
753753+ record.createdAt ?? null,
754754+ Date.now(),
755755+ JSON.stringify(record),
756756+757757+ record.displayName ?? null,
758758+ record.description ?? null,
759759+ uncid(record.avatar?.ref) ?? null,
760760+ record.avatar?.mimeType ?? null,
761761+ uncid(record.banner?.ref) ?? null,
762762+ record.banner?.mimeType ?? null
763763+ // TODO please add pinned posts
764764+ );
765765+ } catch (err) {
766766+ console.error("stmt.run failed:", err);
767767+ }
768768+ return;
769769+ }
770770+ case "app.bsky.feed.post": {
771771+ console.log("bsky post");
772772+ const stmt = db.prepare(`
773773+ INSERT OR IGNORE INTO app_bsky_feed_post (
774774+ uri, did, cid, rev, createdat, indexedat, json,
775775+ text, replyroot, replyparent, quote,
776776+ imagecount, image1cid, image1mime, image1aspect,
777777+ image2cid, image2mime, image2aspect,
778778+ image3cid, image3mime, image3aspect,
779779+ image4cid, image4mime, image4aspect,
780780+ videocount, videocid, videomime, videoaspect
781781+ ) VALUES (?, ?, ?, ?, ?, ?, ?,
782782+ ?, ?, ?, ?,
783783+ ?, ?, ?, ?,
784784+ ?, ?, ?,
785785+ ?, ?, ?,
786786+ ?, ?, ?,
787787+ ?, ?, ?, ?)
788788+ `);
789789+790790+ const embed = record.embed;
791791+792792+ const images = extractImages(embed);
793793+ const video = extractVideo(embed);
794794+ const quoteUri = extractQuoteUri(embed);
795795+ try {
796796+ stmt.run(
797797+ ctx.aturi ?? null,
798798+ ctx.doer ?? null,
799799+ ctx.cid ?? null,
800800+ ctx.rev ?? null,
801801+ record.createdAt,
802802+ Date.now(),
803803+ JSON.stringify(record),
804804+805805+ record.text ?? null,
806806+ record.reply?.root?.uri ?? null,
807807+ record.reply?.parent?.uri ?? null,
808808+809809+ quoteUri,
810810+811811+ images.length,
812812+ uncid(images[0]?.image?.ref) ?? null,
813813+ images[0]?.image?.mimeType ?? null,
814814+ images[0]?.aspectRatio &&
815815+ images[0].aspectRatio.width &&
816816+ images[0].aspectRatio.height
817817+ ? `${images[0].aspectRatio.width}:${images[0].aspectRatio.height}`
818818+ : null,
819819+820820+ uncid(images[1]?.image?.ref) ?? null,
821821+ images[1]?.image?.mimeType ?? null,
822822+ images[1]?.aspectRatio &&
823823+ images[1].aspectRatio.width &&
824824+ images[1].aspectRatio.height
825825+ ? `${images[1].aspectRatio.width}:${images[1].aspectRatio.height}`
826826+ : null,
827827+828828+ uncid(images[2]?.image?.ref) ?? null,
829829+ images[2]?.image?.mimeType ?? null,
830830+ images[2]?.aspectRatio &&
831831+ images[2].aspectRatio.width &&
832832+ images[2].aspectRatio.height
833833+ ? `${images[2].aspectRatio.width}:${images[2].aspectRatio.height}`
834834+ : null,
835835+836836+ uncid(images[3]?.image?.ref) ?? null,
837837+ images[3]?.image?.mimeType ?? null,
838838+ images[3]?.aspectRatio &&
839839+ images[3].aspectRatio.width &&
840840+ images[3].aspectRatio.height
841841+ ? `${images[3].aspectRatio.width}:${images[3].aspectRatio.height}`
842842+ : null,
843843+844844+ uncid(video?.video) ? 1 : 0,
845845+ uncid(video?.video) ?? null,
846846+ uncid(video?.video) ? "video/mp4" : null,
847847+ video?.aspectRatio
848848+ ? `${video.aspectRatio.width}:${video.aspectRatio.height}`
849849+ : null
850850+ );
851851+ } catch (err) {
852852+ console.error("stmt.run failed:", err);
853853+ }
854854+ return;
855855+ }
856856+ default: {
857857+ // what the hell
858858+ return;
859859+ }
860860+ }
861861+ }
862862+863863+ // user data
864864+ queryProfileView(
865865+ did: string,
866866+ type: ""
867867+ ): ATPAPI.AppBskyActorDefs.ProfileView | undefined;
868868+ queryProfileView(
869869+ did: string,
870870+ type: "Basic"
871871+ ): ATPAPI.AppBskyActorDefs.ProfileViewBasic | undefined;
872872+ queryProfileView(
873873+ did: string,
874874+ type: "Detailed"
875875+ ): ATPAPI.AppBskyActorDefs.ProfileViewDetailed | undefined;
876876+ queryProfileView(
877877+ did: string,
878878+ type: "" | "Basic" | "Detailed"
879879+ ):
880880+ | ATPAPI.AppBskyActorDefs.ProfileView
881881+ | ATPAPI.AppBskyActorDefs.ProfileViewBasic
882882+ | ATPAPI.AppBskyActorDefs.ProfileViewDetailed
883883+ | undefined {
884884+ if (!this.isRegisteredIndexUser(did)) return;
885885+ const db = this.userManager.getDbForDid(did);
886886+ if (!db) return;
887887+888888+ const stmt = db.prepare(`
889889+ SELECT *
890890+ FROM app_bsky_actor_profile
891891+ WHERE did = ?
892892+ LIMIT 1;
893893+ `);
894894+895895+ const row = stmt.get(did) as ProfileRow;
896896+897897+ // simulate different types returned
898898+ switch (type) {
899899+ case "": {
900900+ const result: ATPAPI.AppBskyActorDefs.ProfileView = {
901901+ $type: "app.bsky.actor.defs#profileView",
902902+ did: did,
903903+ handle: "idiot.fuck.shit.example.com", // TODO: Resolve user identity here for the handle
904904+ displayName: row.displayname ?? undefined,
905905+ description: row.description ?? undefined,
906906+ avatar: "https://google.com/", // create profile URL from resolved identity
907907+ //associated?: ProfileAssociated,
908908+ indexedAt: row.createdat
909909+ ? new Date(row.createdat).toISOString()
910910+ : undefined,
911911+ createdAt: row.createdat
912912+ ? new Date(row.createdat).toISOString()
913913+ : undefined,
914914+ //viewer?: ViewerState,
915915+ //labels?: ComAtprotoLabelDefs.Label[],
916916+ //verification?: VerificationState,
917917+ //status?: StatusView,
918918+ };
919919+ return result;
920920+ }
921921+ case "Basic": {
922922+ const result: ATPAPI.AppBskyActorDefs.ProfileViewBasic = {
923923+ $type: "app.bsky.actor.defs#profileViewBasic",
924924+ did: did,
925925+ handle: "idiot.fuck.shit.example.com", // TODO: Resolve user identity here for the handle
926926+ displayName: row.displayname ?? undefined,
927927+ avatar: "https://google.com/", // create profile URL from resolved identity
928928+ //associated?: ProfileAssociated,
929929+ createdAt: row.createdat
930930+ ? new Date(row.createdat).toISOString()
931931+ : undefined,
932932+ //viewer?: ViewerState,
933933+ //labels?: ComAtprotoLabelDefs.Label[],
934934+ //verification?: VerificationState,
935935+ //status?: StatusView,
936936+ };
937937+ return result;
938938+ }
939939+ case "Detailed": {
940940+ // Query for follower count from the backlink_skeleton table
941941+ const followersStmt = db.prepare(`
942942+ SELECT COUNT(*) as count
943943+ FROM backlink_skeleton
944944+ WHERE subdid = ? AND srccol = 'app.bsky.graph.follow'
945945+ `);
946946+ const followersResult = followersStmt.get(did) as { count: number };
947947+ const followersCount = followersResult?.count ?? 0;
948948+949949+ // Query for following count from the app_bsky_graph_follow table
950950+ const followingStmt = db.prepare(`
951951+ SELECT COUNT(*) as count
952952+ FROM app_bsky_graph_follow
953953+ WHERE did = ?
954954+ `);
955955+ const followingResult = followingStmt.get(did) as { count: number };
956956+ const followsCount = followingResult?.count ?? 0;
957957+958958+ // Query for post count from the app_bsky_feed_post table
959959+ const postsStmt = db.prepare(`
960960+ SELECT COUNT(*) as count
961961+ FROM app_bsky_feed_post
962962+ WHERE did = ?
963963+ `);
964964+ const postsResult = postsStmt.get(did) as { count: number };
965965+ const postsCount = postsResult?.count ?? 0;
966966+967967+ const result: ATPAPI.AppBskyActorDefs.ProfileViewDetailed = {
968968+ $type: "app.bsky.actor.defs#profileViewDetailed",
969969+ did: did,
970970+ handle: "idiot.fuck.shit.example.com", // TODO: Resolve user identity here for the handle
971971+ displayName: row.displayname ?? undefined,
972972+ description: row.description ?? undefined,
973973+ avatar: "https://google.com/", // TODO: create profile URL from resolved identity
974974+ banner: "https://youtube.com/", // same here
975975+ followersCount: followersCount,
976976+ followsCount: followsCount,
977977+ postsCount: postsCount,
978978+ //associated?: ProfileAssociated,
979979+ //joinedViaStarterPack?: // AppBskyGraphDefs.StarterPackViewBasic;
980980+ indexedAt: row.createdat
981981+ ? new Date(row.createdat).toISOString()
982982+ : undefined,
983983+ createdAt: row.createdat
984984+ ? new Date(row.createdat).toISOString()
985985+ : undefined,
986986+ //viewer?: ViewerState,
987987+ //labels?: ComAtprotoLabelDefs.Label[],
988988+ pinnedPost: undefined, //row.; // TODO: i forgot to put pinnedp posts in db schema oops
989989+ //verification?: VerificationState,
990990+ //status?: StatusView,
991991+ };
992992+ return result;
993993+ }
994994+ default:
995995+ throw new Error("Invalid type");
996996+ }
997997+ }
998998+999999+ // post hydration
10001000+ queryPostView(uri: string): ATPAPI.AppBskyFeedDefs.PostView | undefined {
10011001+ const URI = new AtUri(uri);
10021002+ const did = URI.host;
10031003+ if (!this.isRegisteredIndexUser(did)) return;
10041004+ const db = this.userManager.getDbForDid(did);
10051005+ if (!db) return;
10061006+10071007+ const stmt = db.prepare(`
10081008+ SELECT *
10091009+ FROM app_bsky_feed_post
10101010+ WHERE uri = ?
10111011+ LIMIT 1;
10121012+ `);
10131013+10141014+ const row = stmt.get(uri) as PostRow;
10151015+ const profileView = this.queryProfileView(did, "Basic");
10161016+ if (!row || !row.cid || !profileView || !row.json) return;
10171017+ const value = JSON.parse(row.json) as ATPAPI.AppBskyFeedPost.Record;
10181018+10191019+ const post: ATPAPI.AppBskyFeedDefs.PostView = {
10201020+ uri: row.uri,
10211021+ cid: row.cid,
10221022+ author: profileView,
10231023+ record: value,
10241024+ indexedAt: new Date(row.indexedat).toISOString(),
10251025+ embed: value.embed,
10261026+ };
10271027+10281028+ return post;
10291029+ }
10301030+ queryFeedViewPost(
10311031+ uri: string
10321032+ ): ATPAPI.AppBskyFeedDefs.FeedViewPost | undefined {
10331033+ const post = this.queryPostView(uri);
10341034+ if (!post) return;
10351035+10361036+ const feedviewpost: ATPAPI.AppBskyFeedDefs.FeedViewPost = {
10371037+ $type: "app.bsky.feed.defs#feedViewPost",
10381038+ post: post,
10391039+ //reply: ReplyRef,
10401040+ //reason: ,
10411041+ };
10421042+10431043+ return feedviewpost;
10441044+ }
10451045+10461046+ // user feedgens
10471047+10481048+ queryActorFeeds(did: string): ATPAPI.AppBskyFeedDefs.GeneratorView[] {
10491049+ if (!this.isRegisteredIndexUser(did)) return [];
10501050+ const db = this.userManager.getDbForDid(did);
10511051+ if (!db) return [];
10521052+10531053+ const stmt = db.prepare(`
10541054+ SELECT uri, cid, did, json, indexedat
10551055+ FROM app_bsky_feed_generator
10561056+ WHERE did = ?
10571057+ ORDER BY createdat DESC;
10581058+ `);
10591059+10601060+ const rows = stmt.all(did) as unknown as GeneratorRow[];
10611061+ const creatorView = this.queryProfileView(did, "Basic");
10621062+ if (!creatorView) return [];
10631063+10641064+ return rows
10651065+ .map((row) => {
10661066+ try {
10671067+ if (!row.json) return;
10681068+ const record = JSON.parse(
10691069+ row.json
10701070+ ) as ATPAPI.AppBskyFeedGenerator.Record;
10711071+ return {
10721072+ $type: "app.bsky.feed.defs#generatorView",
10731073+ uri: row.uri,
10741074+ cid: row.cid,
10751075+ did: row.did,
10761076+ creator: creatorView,
10771077+ displayName: record.displayName,
10781078+ description: record.description,
10791079+ descriptionFacets: record.descriptionFacets,
10801080+ avatar: record.avatar,
10811081+ likeCount: 0, // TODO: this should be easy
10821082+ indexedAt: new Date(row.indexedat).toISOString(),
10831083+ } as ATPAPI.AppBskyFeedDefs.GeneratorView;
10841084+ } catch {
10851085+ return undefined;
10861086+ }
10871087+ })
10881088+ .filter((v): v is ATPAPI.AppBskyFeedDefs.GeneratorView => !!v);
10891089+ }
10901090+10911091+ queryFeedGenerator(
10921092+ uri: string
10931093+ ): ATPAPI.AppBskyFeedDefs.GeneratorView | undefined {
10941094+ return this.queryFeedGenerators([uri])[0];
10951095+ }
10961096+10971097+ queryFeedGenerators(uris: string[]): ATPAPI.AppBskyFeedDefs.GeneratorView[] {
10981098+ const generators: ATPAPI.AppBskyFeedDefs.GeneratorView[] = [];
10991099+ const urisByDid = new Map<string, string[]>();
11001100+11011101+ for (const uri of uris) {
11021102+ try {
11031103+ const { host: did } = new AtUri(uri);
11041104+ if (!urisByDid.has(did)) {
11051105+ urisByDid.set(did, []);
11061106+ }
11071107+ urisByDid.get(did)!.push(uri);
11081108+ } catch {}
11091109+ }
11101110+11111111+ for (const [did, didUris] of urisByDid.entries()) {
11121112+ if (!this.isRegisteredIndexUser(did)) continue;
11131113+ const db = this.userManager.getDbForDid(did);
11141114+ if (!db) continue;
11151115+11161116+ const placeholders = didUris.map(() => "?").join(",");
11171117+ const stmt = db.prepare(`
11181118+ SELECT uri, cid, did, json, indexedat
11191119+ FROM app_bsky_feed_generator
11201120+ WHERE uri IN (${placeholders});
11211121+ `);
11221122+11231123+ const rows = stmt.all(...didUris) as unknown as GeneratorRow[];
11241124+ if (rows.length === 0) continue;
11251125+11261126+ const creatorView = this.queryProfileView(did, "");
11271127+ if (!creatorView) continue;
11281128+11291129+ for (const row of rows) {
11301130+ try {
11311131+ if (!row.json || !row.cid) continue;
11321132+ const record = JSON.parse(
11331133+ row.json
11341134+ ) as ATPAPI.AppBskyFeedGenerator.Record;
11351135+ generators.push({
11361136+ $type: "app.bsky.feed.defs#generatorView",
11371137+ uri: row.uri,
11381138+ cid: row.cid,
11391139+ did: row.did,
11401140+ creator: creatorView,
11411141+ displayName: record.displayName,
11421142+ description: record.description,
11431143+ descriptionFacets: record.descriptionFacets,
11441144+ avatar: record.avatar as string | undefined,
11451145+ likeCount: 0,
11461146+ indexedAt: new Date(row.indexedat).toISOString(),
11471147+ });
11481148+ } catch {}
11491149+ }
11501150+ }
11511151+ return generators;
11521152+ }
11531153+11541154+ // user feeds
11551155+11561156+ queryAuthorFeed(
11571157+ did: string,
11581158+ cursor?: string
11591159+ ):
11601160+ | {
11611161+ items: ATPAPI.AppBskyFeedDefs.FeedViewPost[];
11621162+ cursor: string | undefined;
11631163+ }
11641164+ | undefined {
11651165+ if (!this.isRegisteredIndexUser(did)) return;
11661166+ const db = this.userManager.getDbForDid(did);
11671167+ if (!db) return;
11681168+11691169+ // TODO: implement this for real
11701170+ let query = `
11711171+ SELECT uri, indexedat, cid
11721172+ FROM app_bsky_feed_post
11731173+ WHERE did = ?
11741174+ `;
11751175+ const params: (string | number)[] = [did];
11761176+11771177+ if (cursor) {
11781178+ const [indexedat, cid] = cursor.split("::");
11791179+ query += ` AND (indexedat < ? OR (indexedat = ? AND cid < ?))`;
11801180+ params.push(parseInt(indexedat, 10), parseInt(indexedat, 10), cid);
11811181+ }
11821182+11831183+ query += ` ORDER BY indexedat DESC, cid DESC LIMIT ${FEED_LIMIT}`;
11841184+11851185+ const stmt = db.prepare(query);
11861186+ const rows = stmt.all(...params) as {
11871187+ uri: string;
11881188+ indexedat: number;
11891189+ cid: string;
11901190+ }[];
11911191+11921192+ const items = rows
11931193+ .map((row) => this.queryFeedViewPost(row.uri)) // TODO: for replies and repost i should inject the reason here
11941194+ .filter((p): p is ATPAPI.AppBskyFeedDefs.FeedViewPost => !!p);
11951195+11961196+ const lastItem = rows[rows.length - 1];
11971197+ const nextCursor = lastItem
11981198+ ? `${lastItem.indexedat}::${lastItem.cid}`
11991199+ : undefined;
12001200+12011201+ return { items, cursor: nextCursor };
12021202+ }
12031203+12041204+ queryListFeed(
12051205+ uri: string,
12061206+ cursor?: string
12071207+ ):
12081208+ | {
12091209+ items: ATPAPI.AppBskyFeedDefs.FeedViewPost[];
12101210+ cursor: string | undefined;
12111211+ }
12121212+ | undefined {
12131213+ return { items: [], cursor: undefined };
12141214+ }
12151215+12161216+ queryActorLikes(
12171217+ did: string,
12181218+ cursor?: string
12191219+ ):
12201220+ | {
12211221+ items: ATPAPI.AppBskyFeedDefs.FeedViewPost[];
12221222+ cursor: string | undefined;
12231223+ }
12241224+ | undefined {
12251225+ if (!this.isRegisteredIndexUser(did)) return;
12261226+ const db = this.userManager.getDbForDid(did);
12271227+ if (!db) return;
12281228+12291229+ let query = `
12301230+ SELECT subject, indexedat, cid
12311231+ FROM app_bsky_feed_like
12321232+ WHERE did = ?
12331233+ `;
12341234+ const params: (string | number)[] = [did];
12351235+12361236+ if (cursor) {
12371237+ const [indexedat, cid] = cursor.split("::");
12381238+ query += ` AND (indexedat < ? OR (indexedat = ? AND cid < ?))`;
12391239+ params.push(parseInt(indexedat, 10), parseInt(indexedat, 10), cid);
12401240+ }
12411241+12421242+ query += ` ORDER BY indexedat DESC, cid DESC LIMIT ${FEED_LIMIT}`;
12431243+12441244+ const stmt = db.prepare(query);
12451245+ const rows = stmt.all(...params) as {
12461246+ subject: string;
12471247+ indexedat: number;
12481248+ cid: string;
12491249+ }[];
12501250+12511251+ const items = rows
12521252+ .map((row) => this.queryFeedViewPost(row.subject))
12531253+ .filter((p): p is ATPAPI.AppBskyFeedDefs.FeedViewPost => !!p);
12541254+12551255+ const lastItem = rows[rows.length - 1];
12561256+ const nextCursor = lastItem
12571257+ ? `${lastItem.indexedat}::${lastItem.cid}`
12581258+ : undefined;
12591259+12601260+ return { items, cursor: nextCursor };
12611261+ }
12621262+12631263+ // post metadata
12641264+12651265+ queryLikes(uri: string): ATPAPI.AppBskyFeedGetLikes.Like[] | undefined {
12661266+ const postUri = new AtUri(uri);
12671267+ const postAuthorDid = postUri.hostname;
12681268+ if (!this.isRegisteredIndexUser(postAuthorDid)) return;
12691269+ const db = this.userManager.getDbForDid(postAuthorDid);
12701270+ if (!db) return;
12711271+12721272+ const stmt = db.prepare(`
12731273+ SELECT b.srcdid, b.srcuri
12741274+ FROM backlink_skeleton AS b
12751275+ WHERE b.suburi = ? AND b.srccol = 'app_bsky_feed_like'
12761276+ ORDER BY b.id DESC;
12771277+ `);
12781278+12791279+ const rows = stmt.all(uri) as unknown as BacklinkRow[];
12801280+12811281+ return rows
12821282+ .map((row) => {
12831283+ const actor = this.queryProfileView(row.srcdid, "");
12841284+ if (!actor) return;
12851285+12861286+ return {
12871287+ // TODO write indexedAt for spacedust indexes
12881288+ createdAt: new Date(Date.now()).toISOString(),
12891289+ indexedAt: new Date(Date.now()).toISOString(),
12901290+ actor: actor,
12911291+ };
12921292+ })
12931293+ .filter((like): like is ATPAPI.AppBskyFeedGetLikes.Like => !!like);
12941294+ }
12951295+12961296+ queryReposts(uri: string): ATPAPI.AppBskyActorDefs.ProfileView[] {
12971297+ const postUri = new AtUri(uri);
12981298+ const postAuthorDid = postUri.hostname;
12991299+ if (!this.isRegisteredIndexUser(postAuthorDid)) return [];
13001300+ const db = this.userManager.getDbForDid(postAuthorDid);
13011301+ if (!db) return [];
13021302+13031303+ const stmt = db.prepare(`
13041304+ SELECT srcdid
13051305+ FROM backlink_skeleton
13061306+ WHERE suburi = ? AND srccol = 'app_bsky_feed_repost'
13071307+ ORDER BY id DESC;
13081308+ `);
13091309+13101310+ const rows = stmt.all(uri) as { srcdid: string }[];
13111311+13121312+ return rows
13131313+ .map((row) => this.queryProfileView(row.srcdid, ""))
13141314+ .filter((p): p is ATPAPI.AppBskyActorDefs.ProfileView => !!p);
13151315+ }
13161316+13171317+ queryQuotes(uri: string): ATPAPI.AppBskyFeedDefs.FeedViewPost[] {
13181318+ const postUri = new AtUri(uri);
13191319+ const postAuthorDid = postUri.hostname;
13201320+ if (!this.isRegisteredIndexUser(postAuthorDid)) return [];
13211321+ const db = this.userManager.getDbForDid(postAuthorDid);
13221322+ if (!db) return [];
13231323+13241324+ const stmt = db.prepare(`
13251325+ SELECT srcuri
13261326+ FROM backlink_skeleton
13271327+ WHERE suburi = ? AND srccol = 'app_bsky_feed_post' AND srcfield = 'quote'
13281328+ ORDER BY id DESC;
13291329+ `);
13301330+13311331+ const rows = stmt.all(uri) as { srcuri: string }[];
13321332+13331333+ return rows
13341334+ .map((row) => this.queryFeedViewPost(row.srcuri))
13351335+ .filter((p): p is ATPAPI.AppBskyFeedDefs.FeedViewPost => !!p);
13361336+ }
13371337+13381338+ queryPostThread(
13391339+ uri: string
13401340+ ): ATPAPI.AppBskyFeedGetPostThread.OutputSchema | undefined {
13411341+ const post = this.queryPostView(uri);
13421342+ if (!post) {
13431343+ return {
13441344+ thread: {
13451345+ $type: "app.bsky.feed.defs#notFoundPost",
13461346+ uri: uri,
13471347+ notFound: true,
13481348+ } as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.NotFoundPost>,
13491349+ };
13501350+ }
13511351+13521352+ const thread: ATPAPI.AppBskyFeedDefs.ThreadViewPost = {
13531353+ $type: "app.bsky.feed.defs#threadViewPost",
13541354+ post: post,
13551355+ replies: [],
13561356+ };
13571357+13581358+ let current = thread;
13591359+ while ((current.post.record.reply as any)?.parent?.uri) {
13601360+ const parentUri = (current.post.record.reply as any)?.parent?.uri;
13611361+ const parentPost = this.queryPostView(parentUri);
13621362+ if (!parentPost) break;
13631363+13641364+ const parentThread: ATPAPI.AppBskyFeedDefs.ThreadViewPost = {
13651365+ $type: "app.bsky.feed.defs#threadViewPost",
13661366+ post: parentPost,
13671367+ replies: [
13681368+ current as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.ThreadViewPost>,
13691369+ ],
13701370+ };
13711371+ current.parent =
13721372+ parentThread as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.ThreadViewPost>;
13731373+ current = parentThread;
13741374+ }
13751375+13761376+ const seenUris = new Set<string>();
13771377+ const fetchReplies = (
13781378+ parentThread: ATPAPI.AppBskyFeedDefs.ThreadViewPost
13791379+ ) => {
13801380+ if (seenUris.has(parentThread.post.uri)) return;
13811381+ seenUris.add(parentThread.post.uri);
13821382+13831383+ const parentUri = new AtUri(parentThread.post.uri);
13841384+ const parentAuthorDid = parentUri.hostname;
13851385+ const db = this.userManager.getDbForDid(parentAuthorDid);
13861386+ if (!db) return;
13871387+13881388+ const stmt = db.prepare(`
13891389+ SELECT srcuri
13901390+ FROM backlink_skeleton
13911391+ WHERE suburi = ? AND srccol = 'app_bsky_feed_post' AND srcfield = 'replyparent'
13921392+ `);
13931393+ const replyRows = stmt.all(parentThread.post.uri) as { srcuri: string }[];
13941394+13951395+ const replies = replyRows
13961396+ .map((row) => this.queryPostView(row.srcuri))
13971397+ .filter((p): p is ATPAPI.AppBskyFeedDefs.PostView => !!p);
13981398+13991399+ for (const replyPost of replies) {
14001400+ const replyThread: ATPAPI.AppBskyFeedDefs.ThreadViewPost = {
14011401+ $type: "app.bsky.feed.defs#threadViewPost",
14021402+ post: replyPost,
14031403+ parent:
14041404+ parentThread as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.ThreadViewPost>,
14051405+ replies: [],
14061406+ };
14071407+ parentThread.replies?.push(
14081408+ replyThread as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.ThreadViewPost>
14091409+ );
14101410+ fetchReplies(replyThread);
14111411+ }
14121412+ };
14131413+14141414+ fetchReplies(thread);
14151415+14161416+ const returned =
14171417+ thread as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.ThreadViewPost>;
14181418+14191419+ return { thread: returned };
14201420+ }
14211421+14221422+ /**
14231423+ * please do not use this, use openDbForDid() instead
14241424+ * @param did
14251425+ * @returns
14261426+ */
14271427+ internalCreateDbForDid(did: string): Database {
14281428+ const path = `${this.config.baseDbPath}/${did}.sqlite`;
14291429+ const db = new Database(path);
14301430+ setupUserDb(db);
14311431+ //await db.exec(/* CREATE IF NOT EXISTS statements */);
14321432+ return db;
14331433+ }
14341434+14351435+ isRegisteredIndexUser(did: string): boolean {
14361436+ const stmt = this.systemDB.prepare(`
14371437+ SELECT 1
14381438+ FROM users
14391439+ WHERE did = ?
14401440+ AND onboardingstatus != 'onboarding-backfill'
14411441+ LIMIT 1;
14421442+ `);
14431443+ const result = stmt.value<[number]>(did);
14441444+ const exists = result !== undefined;
14451445+ return exists;
14461446+ }
14471447+}
14481448+161449export class IndexServerUserManager {
14501450+ public indexServer: IndexServer;
14511451+14521452+ constructor(indexServer: IndexServer) {
14531453+ this.indexServer = indexServer;
14541454+ }
14551455+171456 private users = new Map<string, UserIndexServer>();
181457191458 /*async*/ addUser(did: string) {
201459 if (this.users.has(did)) return;
2121- const instance = new UserIndexServer(did);
14601460+ const instance = new UserIndexServer(this, did);
221461 //await instance.initialize();
231462 this.users.set(did, instance);
241463 }
···601499}
611500621501class UserIndexServer {
15021502+ public indexServerUserManager: IndexServerUserManager;
631503 did: string;
641504 db: Database; // | undefined;
651505 jetstream: JetstreamManager; // | undefined;
661506 spacedust: SpacedustManager; // | undefined;
6715076868- constructor(did: string) {
15081508+ constructor(indexServerUserManager: IndexServerUserManager, did: string) {
691509 this.did = did;
7070- this.db = internalCreateDbForDid(this.did);
15101510+ this.indexServerUserManager = indexServerUserManager;
15111511+ this.db = this.indexServerUserManager.indexServer.internalCreateDbForDid(this.did);
711512 // should probably put the params of exactly what were listening to here
721513 this.jetstream = new JetstreamManager((msg) => {
731514 console.log("Received Jetstream message: ", msg);
···791520 const value = msg.commit.record;
801521811522 if (!doer || !value) return;
8282- indexServerIndexer({
15231523+ this.indexServerUserManager.indexServer.indexServerIndexer({
831524 op,
841525 doer,
851526 cid: msg.commit.cid,
···2271668 }
2281669}
2291670230230-function isRegisteredIndexUser(did: string): boolean {
231231- const stmt = systemDB.prepare(`
232232- SELECT 1
233233- FROM users
234234- WHERE did = ?
235235- AND onboardingstatus != 'onboarding-backfill'
236236- LIMIT 1;
237237- `);
238238- const result = stmt.value<[number]>(did);
239239- const exists = result !== undefined;
240240- return exists;
241241-}
242242-243243-/**
244244- * please do not use this, use openDbForDid() instead
245245- * @param did
246246- * @returns
247247- */
248248-function internalCreateDbForDid(did: string): Database {
249249- const path = `./dbs/${did}.sqlite`;
250250- const db = new Database(path);
251251- setupUserDb(db);
252252- //await db.exec(/* CREATE IF NOT EXISTS statements */);
253253- return db;
254254-}
16711671+// /**
16721672+// * please do not use this, use openDbForDid() instead
16731673+// * @param did
16741674+// * @returns
16751675+// */
16761676+// function internalCreateDbForDid(did: string): Database {
16771677+// const path = `./dbs/${did}.sqlite`;
16781678+// const db = new Database(path);
16791679+// setupUserDb(db);
16801680+// //await db.exec(/* CREATE IF NOT EXISTS statements */);
16811681+// return db;
16821682+// }
2551683256256-function getDbForDid(did: string): Database | undefined {
257257- const db = indexerUserManager.getDbForDid(did);
258258- if (!db) return;
259259- return db;
260260-}
16841684+// function getDbForDid(did: string): Database | undefined {
16851685+// const db = indexerUserManager.getDbForDid(did);
16861686+// if (!db) return;
16871687+// return db;
16881688+// }
26116892621690// async function connectToJetstream(did: string, db: Database): Promise<WebSocket> {
2631691// const url = `${jetstreamurl}/xrpc/com.atproto.sync.subscribeRepos?did=${did}`;
···3421770 bannermime: string | null;
3431771};
3441772345345-export async function indexServerHandler(req: Request): Promise<Response> {
346346- const url = new URL(req.url);
347347- const pathname = url.pathname;
348348- //const bskyUrl = `https://api.bsky.app${pathname}${url.search}`;
349349- //const hasAuth = req.headers.has("authorization");
350350- const xrpcMethod = pathname.startsWith("/xrpc/")
351351- ? pathname.slice("/xrpc/".length)
352352- : null;
353353- const searchParams = searchParamsToJson(url.searchParams);
354354- console.log(JSON.stringify(searchParams, null, 2));
355355- const jsonUntyped = searchParams;
356356-357357- switch (xrpcMethod) {
358358- case "app.bsky.actor.getProfile": {
359359- const jsonTyped =
360360- jsonUntyped as IndexServerTypes.AppBskyActorGetProfile.QueryParams;
361361-362362- const res = queryProfileView(jsonTyped.actor, "Detailed");
363363- if (!res)
364364- return new Response(
365365- JSON.stringify({
366366- error: "User not found",
367367- }),
368368- {
369369- status: 404,
370370- headers: withCors({ "Content-Type": "application/json" }),
371371- }
372372- );
373373- const response: IndexServerTypes.AppBskyActorGetProfile.OutputSchema =
374374- res;
375375-376376- return new Response(JSON.stringify(response), {
377377- headers: withCors({ "Content-Type": "application/json" }),
378378- });
379379- }
380380- case "app.bsky.actor.getProfiles": {
381381- const jsonTyped =
382382- jsonUntyped as IndexServerTypes.AppBskyActorGetProfiles.QueryParams;
383383-384384- if (typeof jsonUntyped?.actors === "string" ) {
385385- const res = queryProfileView(jsonUntyped.actors as string, "Detailed");
386386- if (!res)
387387- return new Response(
388388- JSON.stringify({
389389- error: "User not found",
390390- }),
391391- {
392392- status: 404,
393393- headers: withCors({ "Content-Type": "application/json" }),
394394- }
395395- );
396396- const response: IndexServerTypes.AppBskyActorGetProfiles.OutputSchema = {
397397- profiles: [res],
398398- };
399399-400400- return new Response(JSON.stringify(response), {
401401- headers: withCors({ "Content-Type": "application/json" }),
402402- });
403403- }
404404-405405- const res: ATPAPI.AppBskyActorDefs.ProfileViewDetailed[] =
406406- jsonTyped.actors
407407- .map((actor) => {
408408- return queryProfileView(actor, "Detailed");
409409- })
410410- .filter(
411411- (x): x is ATPAPI.AppBskyActorDefs.ProfileViewDetailed =>
412412- x !== undefined
413413- );
414414-415415- if (!res)
416416- return new Response(
417417- JSON.stringify({
418418- error: "User not found",
419419- }),
420420- {
421421- status: 404,
422422- headers: withCors({ "Content-Type": "application/json" }),
423423- }
424424- );
425425-426426- const response: IndexServerTypes.AppBskyActorGetProfiles.OutputSchema = {
427427- profiles: res,
428428- };
429429-430430- return new Response(JSON.stringify(response), {
431431- headers: withCors({ "Content-Type": "application/json" }),
432432- });
433433- }
434434- case "app.bsky.feed.getActorFeeds": {
435435- const jsonTyped =
436436- jsonUntyped as IndexServerTypes.AppBskyFeedGetActorFeeds.QueryParams;
437437-438438- const qresult = queryActorFeeds(jsonTyped.actor)
439439-440440- const response: IndexServerTypes.AppBskyFeedGetActorFeeds.OutputSchema =
441441- {
442442- feeds: qresult
443443- };
444444-445445- return new Response(JSON.stringify(response), {
446446- headers: withCors({ "Content-Type": "application/json" }),
447447- });
448448- }
449449- case "app.bsky.feed.getFeedGenerator": {
450450- const jsonTyped =
451451- jsonUntyped as IndexServerTypes.AppBskyFeedGetFeedGenerator.QueryParams;
452452-453453- const qresult = queryFeedGenerator(jsonTyped.feed)
454454- if (!qresult) {
455455- return new Response(
456456- JSON.stringify({
457457- error: "Feed not found",
458458- }),
459459- {
460460- status: 404,
461461- headers: withCors({ "Content-Type": "application/json" }),
462462- }
463463- );
464464- }
465465-466466- const response: IndexServerTypes.AppBskyFeedGetFeedGenerator.OutputSchema =
467467- {
468468- view: qresult,
469469- isOnline: true, // lmao
470470- isValid: true, // lmao
471471- };
472472-473473- return new Response(JSON.stringify(response), {
474474- headers: withCors({ "Content-Type": "application/json" }),
475475- });
476476- }
477477- case "app.bsky.feed.getFeedGenerators": {
478478- const jsonTyped =
479479- jsonUntyped as IndexServerTypes.AppBskyFeedGetFeedGenerators.QueryParams;
480480-481481- const qresult = queryFeedGenerators(jsonTyped.feeds)
482482- if (!qresult) {
483483- return new Response(
484484- JSON.stringify({
485485- error: "Feed not found",
486486- }),
487487- {
488488- status: 404,
489489- headers: withCors({ "Content-Type": "application/json" }),
490490- }
491491- );
492492- }
493493-494494- const response: IndexServerTypes.AppBskyFeedGetFeedGenerators.OutputSchema =
495495- {
496496- feeds: qresult
497497- };
498498-499499- return new Response(JSON.stringify(response), {
500500- headers: withCors({ "Content-Type": "application/json" }),
501501- });
502502- }
503503- case "app.bsky.feed.getPosts": {
504504- const jsonTyped =
505505- jsonUntyped as IndexServerTypes.AppBskyFeedGetPosts.QueryParams;
506506-507507- const posts: IndexServerTypes.AppBskyFeedGetPosts.OutputSchema["posts"] =
508508- jsonTyped.uris
509509- .map((uri) => {
510510- return queryPostView(uri);
511511- })
512512- .filter(Boolean) as ATPAPI.AppBskyFeedDefs.PostView[];
513513-514514- const response: IndexServerTypes.AppBskyFeedGetPosts.OutputSchema = {
515515- posts,
516516- };
517517-518518- return new Response(JSON.stringify(response), {
519519- headers: withCors({ "Content-Type": "application/json" }),
520520- });
521521- }
522522- case "party.whey.app.bsky.feed.getActorLikesPartial": {
523523- const jsonTyped =
524524- jsonUntyped as IndexServerTypes.PartyWheyAppBskyFeedGetActorLikesPartial.QueryParams;
525525-526526- // TODO: not partial yet, currently skips refs
527527-528528- const qresult = queryActorLikes(jsonTyped.actor, jsonTyped.cursor)
529529- if (!qresult) {
530530- return new Response(
531531- JSON.stringify({
532532- error: "Feed not found",
533533- }),
534534- {
535535- status: 404,
536536- headers: withCors({ "Content-Type": "application/json" }),
537537- }
538538- );
539539- }
540540-541541- const response: IndexServerTypes.PartyWheyAppBskyFeedGetActorLikesPartial.OutputSchema =
542542- {
543543- feed: qresult.items as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.FeedViewPost>[],
544544- cursor: qresult.cursor
545545- };
546546-547547- return new Response(JSON.stringify(response), {
548548- headers: withCors({ "Content-Type": "application/json" }),
549549- });
550550- }
551551- case "party.whey.app.bsky.feed.getAuthorFeedPartial": {
552552- const jsonTyped =
553553- jsonUntyped as IndexServerTypes.PartyWheyAppBskyFeedGetAuthorFeedPartial.QueryParams;
554554-555555- // TODO: not partial yet, currently skips refs
556556-557557- const qresult = queryAuthorFeed(jsonTyped.actor, jsonTyped.cursor)
558558- if (!qresult) {
559559- return new Response(
560560- JSON.stringify({
561561- error: "Feed not found",
562562- }),
563563- {
564564- status: 404,
565565- headers: withCors({ "Content-Type": "application/json" }),
566566- }
567567- );
568568- }
569569-570570- const response: IndexServerTypes.PartyWheyAppBskyFeedGetAuthorFeedPartial.OutputSchema =
571571- {
572572- feed: qresult.items as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.FeedViewPost>[],
573573- cursor: qresult.cursor
574574- };
575575-576576- return new Response(JSON.stringify(response), {
577577- headers: withCors({ "Content-Type": "application/json" }),
578578- });
579579- }
580580- case "party.whey.app.bsky.feed.getLikesPartial": {
581581- const jsonTyped =
582582- jsonUntyped as IndexServerTypes.PartyWheyAppBskyFeedGetLikesPartial.QueryParams;
583583-584584- // TODO: not partial yet, currently skips refs
585585-586586- const qresult = queryLikes(jsonTyped.uri)
587587- if (!qresult) {
588588- return new Response(
589589- JSON.stringify({
590590- error: "Feed not found",
591591- }),
592592- {
593593- status: 404,
594594- headers: withCors({ "Content-Type": "application/json" }),
595595- }
596596- );
597597- }
598598- const response: IndexServerTypes.PartyWheyAppBskyFeedGetLikesPartial.OutputSchema =
599599- {
600600- // @ts-ignore whatever i dont care TODO: fix ts ignores
601601- likes: qresult
602602- };
603603-604604- return new Response(JSON.stringify(response), {
605605- headers: withCors({ "Content-Type": "application/json" }),
606606- });
607607- }
608608- case "party.whey.app.bsky.feed.getPostThreadPartial": {
609609- const jsonTyped =
610610- jsonUntyped as IndexServerTypes.PartyWheyAppBskyFeedGetPostThreadPartial.QueryParams;
611611-612612- // TODO: not partial yet, currently skips refs
613613-614614- const qresult = queryPostThread(jsonTyped.uri)
615615- if (!qresult) {
616616- return new Response(
617617- JSON.stringify({
618618- error: "Feed not found",
619619- }),
620620- {
621621- status: 404,
622622- headers: withCors({ "Content-Type": "application/json" }),
623623- }
624624- );
625625- }
626626- const response: IndexServerTypes.PartyWheyAppBskyFeedGetPostThreadPartial.OutputSchema =
627627- qresult
628628-629629- return new Response(JSON.stringify(response), {
630630- headers: withCors({ "Content-Type": "application/json" }),
631631- });
632632- }
633633- case "party.whey.app.bsky.feed.getQuotesPartial": {
634634- const jsonTyped =
635635- jsonUntyped as IndexServerTypes.PartyWheyAppBskyFeedGetQuotesPartial.QueryParams;
636636-637637- // TODO: not partial yet, currently skips refs
638638-639639- const qresult = queryQuotes(jsonTyped.uri)
640640- if (!qresult) {
641641- return new Response(
642642- JSON.stringify({
643643- error: "Feed not found",
644644- }),
645645- {
646646- status: 404,
647647- headers: withCors({ "Content-Type": "application/json" }),
648648- }
649649- );
650650- }
651651- const response: IndexServerTypes.PartyWheyAppBskyFeedGetQuotesPartial.OutputSchema =
652652- {
653653- uri: jsonTyped.uri,
654654- posts: qresult.map((feedviewpost)=>{return feedviewpost.post as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.PostView>})
655655- };
656656-657657- return new Response(JSON.stringify(response), {
658658- headers: withCors({ "Content-Type": "application/json" }),
659659- });
660660- }
661661- case "party.whey.app.bsky.feed.getRepostedByPartial": {
662662- const jsonTyped =
663663- jsonUntyped as IndexServerTypes.PartyWheyAppBskyFeedGetRepostedByPartial.QueryParams;
664664-665665- // TODO: not partial yet, currently skips refs
666666-667667- const qresult = queryReposts(jsonTyped.uri)
668668- if (!qresult) {
669669- return new Response(
670670- JSON.stringify({
671671- error: "Feed not found",
672672- }),
673673- {
674674- status: 404,
675675- headers: withCors({ "Content-Type": "application/json" }),
676676- }
677677- );
678678- }
679679- const response: IndexServerTypes.PartyWheyAppBskyFeedGetRepostedByPartial.OutputSchema =
680680- {
681681- uri: jsonTyped.uri,
682682- repostedBy: qresult as ATPAPI.$Typed<ATPAPI.AppBskyActorDefs.ProfileView>[]
683683- };
684684-685685- return new Response(JSON.stringify(response), {
686686- headers: withCors({ "Content-Type": "application/json" }),
687687- });
688688- }
689689- // TODO: too hard for now
690690- // case "party.whey.app.bsky.feed.getListFeedPartial": {
691691- // const jsonTyped =
692692- // jsonUntyped as IndexServerTypes.PartyWheyAppBskyFeedGetListFeedPartial.QueryParams;
693693-694694- // const response: IndexServerTypes.PartyWheyAppBskyFeedGetListFeedPartial.OutputSchema =
695695- // {};
696696-697697- // return new Response(JSON.stringify(response), {
698698- // headers: withCors({ "Content-Type": "application/json" }),
699699- // });
700700- // }
701701- /* three more coming soon
702702- app.bsky.graph.getLists
703703- app.bsky.graph.getList
704704- app.bsky.graph.getActorStarterPacks
705705- */
706706- default: {
707707- return new Response(
708708- JSON.stringify({
709709- error: "XRPCNotSupported",
710710- message:
711711- "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",
712712- }),
713713- {
714714- status: 404,
715715- headers: withCors({ "Content-Type": "application/json" }),
716716- }
717717- );
718718- }
719719- }
720720-721721- // return new Response("Not Found", { status: 404 });
722722-}
723723-7241773type linksQuery = {
7251774 target: string;
7261775 collection: string;
···7951844 return typeof str === "string" && str.startsWith("did:");
7961845}
7971846798798-export async function constellationAPIHandler(req: Request): Promise<Response> {
799799- const url = new URL(req.url);
800800- const pathname = url.pathname;
801801- const searchParams = searchParamsToJson(url.searchParams) as linksQuery;
802802- const jsonUntyped = searchParams;
803803-804804- if (!jsonUntyped.target) {
805805- return new Response(
806806- JSON.stringify({ error: "Missing required parameter: target" }),
807807- {
808808- status: 400,
809809- headers: withCors({ "Content-Type": "application/json" }),
810810- }
811811- );
812812- }
813813-814814- const did = isDid(searchParams.target)
815815- ? searchParams.target
816816- : new AtUri(searchParams.target).host;
817817- const db = getDbForDid(did);
818818- if (!db) {
819819- return new Response(
820820- JSON.stringify({
821821- error: "User not found",
822822- }),
823823- {
824824- status: 404,
825825- headers: withCors({ "Content-Type": "application/json" }),
826826- }
827827- );
828828- }
829829-830830- const limit = 16; //Math.min(parseInt(searchParams.limit || "50", 10), 100);
831831- const offset = parseInt(searchParams.cursor || "0", 10);
832832-833833- switch (pathname) {
834834- case "/links": {
835835- const jsonTyped = jsonUntyped as linksQuery;
836836- if (!jsonTyped.collection || !jsonTyped.path) {
837837- return new Response(
838838- JSON.stringify({
839839- error: "Missing required parameters: collection, path",
840840- }),
841841- {
842842- status: 400,
843843- headers: withCors({ "Content-Type": "application/json" }),
844844- }
845845- );
846846- }
847847-848848- const field = `${jsonTyped.collection}:${jsonTyped.path.replace(
849849- /^\./,
850850- ""
851851- )}`;
852852-853853- const paginatedSql = `${SQL.links} LIMIT ? OFFSET ?`;
854854- const rows = db
855855- .prepare(paginatedSql)
856856- .all(jsonTyped.target, jsonTyped.collection, field, limit, offset);
857857-858858- const countResult = db
859859- .prepare(SQL.count)
860860- .get(jsonTyped.target, jsonTyped.collection, field);
861861- const total = countResult ? Number(countResult.total) : 0;
862862-863863- const linking_records: linksRecord[] = rows.map((row: any) => {
864864- const rkey = row.srcuri.split("/").pop()!;
865865- return {
866866- did: row.srcdid,
867867- collection: row.srccol,
868868- rkey,
869869- };
870870- });
871871-872872- const response: linksRecordsResponse = {
873873- total: total.toString(),
874874- linking_records,
875875- };
876876-877877- const nextCursor = offset + linking_records.length;
878878- if (nextCursor < total) {
879879- response.cursor = nextCursor.toString();
880880- }
881881-882882- return new Response(JSON.stringify(response), {
883883- headers: withCors({ "Content-Type": "application/json" }),
884884- });
885885- }
886886- case "/links/distinct-dids": {
887887- const jsonTyped = jsonUntyped as linksQuery;
888888- if (!jsonTyped.collection || !jsonTyped.path) {
889889- return new Response(
890890- JSON.stringify({
891891- error: "Missing required parameters: collection, path",
892892- }),
893893- {
894894- status: 400,
895895- headers: withCors({ "Content-Type": "application/json" }),
896896- }
897897- );
898898- }
899899-900900- const field = `${jsonTyped.collection}:${jsonTyped.path.replace(
901901- /^\./,
902902- ""
903903- )}`;
904904-905905- const paginatedSql = `${SQL.distinctDids} LIMIT ? OFFSET ?`;
906906- const rows = db
907907- .prepare(paginatedSql)
908908- .all(jsonTyped.target, jsonTyped.collection, field, limit, offset);
909909-910910- const countResult = db
911911- .prepare(SQL.countDistinctDids)
912912- .get(jsonTyped.target, jsonTyped.collection, field);
913913- const total = countResult ? Number(countResult.total) : 0;
914914-915915- const linking_dids: string[] = rows.map((row: any) => row.srcdid);
916916-917917- const response: linksDidsResponse = {
918918- total: total.toString(),
919919- linking_dids,
920920- };
921921-922922- const nextCursor = offset + linking_dids.length;
923923- if (nextCursor < total) {
924924- response.cursor = nextCursor.toString();
925925- }
926926-927927- return new Response(JSON.stringify(response), {
928928- headers: withCors({ "Content-Type": "application/json" }),
929929- });
930930- }
931931- case "/links/count": {
932932- const jsonTyped = jsonUntyped as linksQuery;
933933- if (!jsonTyped.collection || !jsonTyped.path) {
934934- return new Response(
935935- JSON.stringify({
936936- error: "Missing required parameters: collection, path",
937937- }),
938938- {
939939- status: 400,
940940- headers: withCors({ "Content-Type": "application/json" }),
941941- }
942942- );
943943- }
944944-945945- const field = `${jsonTyped.collection}:${jsonTyped.path.replace(
946946- /^\./,
947947- ""
948948- )}`;
949949-950950- const result = db
951951- .prepare(SQL.count)
952952- .get(jsonTyped.target, jsonTyped.collection, field);
953953-954954- const response: linksCountResponse = {
955955- total: result && result.total ? result.total.toString() : "0",
956956- };
957957-958958- return new Response(JSON.stringify(response), {
959959- headers: withCors({ "Content-Type": "application/json" }),
960960- });
961961- }
962962- case "/links/count/distinct-dids": {
963963- const jsonTyped = jsonUntyped as linksQuery;
964964- if (!jsonTyped.collection || !jsonTyped.path) {
965965- return new Response(
966966- JSON.stringify({
967967- error: "Missing required parameters: collection, path",
968968- }),
969969- {
970970- status: 400,
971971- headers: withCors({ "Content-Type": "application/json" }),
972972- }
973973- );
974974- }
975975-976976- const field = `${jsonTyped.collection}:${jsonTyped.path.replace(
977977- /^\./,
978978- ""
979979- )}`;
980980-981981- const result = db
982982- .prepare(SQL.countDistinctDids)
983983- .get(jsonTyped.target, jsonTyped.collection, field);
984984-985985- const response: linksCountResponse = {
986986- total: result && result.total ? result.total.toString() : "0",
987987- };
988988-989989- return new Response(JSON.stringify(response), {
990990- headers: withCors({ "Content-Type": "application/json" }),
991991- });
992992- }
993993- case "/links/all": {
994994- const jsonTyped = jsonUntyped as linksAllQuery;
995995-996996- const rows = db.prepare(SQL.all).all(jsonTyped.target) as any[];
997997-998998- const links: linksAllResponse["links"] = {};
999999-10001000- for (const row of rows) {
10011001- if (!links[row.suburi]) {
10021002- links[row.suburi] = {};
10031003- }
10041004- links[row.suburi][row.srccol] = {
10051005- records: row.records,
10061006- distinct_dids: row.distinct_dids,
10071007- };
10081008- }
10091009-10101010- const response: linksAllResponse = {
10111011- links,
10121012- };
10131013-10141014- return new Response(JSON.stringify(response), {
10151015- headers: withCors({ "Content-Type": "application/json" }),
10161016- });
10171017- }
10181018- default: {
10191019- return new Response(
10201020- JSON.stringify({
10211021- error: "NotSupported",
10221022- message:
10231023- "The requested endpoint is not supported by this Constellation implementation.",
10241024- }),
10251025- {
10261026- status: 404,
10271027- headers: withCors({ "Content-Type": "application/json" }),
10281028- }
10291029- );
10301030- }
10311031- }
10321032-}
10331033-10341847function isImageEmbed(embed: unknown): embed is ATPAPI.AppBskyEmbedImages.Main {
10351848 return (
10361849 typeof embed === "object" &&
···10961909 if (isRecordWithMediaEmbed(embed)) return embed.record.record.uri;
10971910 return null;
10981911}
10991099-11001100-export function indexServerIndexer(ctx: indexHandlerContext) {
11011101- const record = assertRecord(ctx.value);
11021102- //const record = validateRecord(ctx.value);
11031103- const db = getDbForDid(ctx.doer);
11041104- if (!db) return;
11051105- console.log("indexering");
11061106- switch (record?.$type) {
11071107- case "app.bsky.feed.like": {
11081108- return;
11091109- }
11101110- case "app.bsky.actor.profile": {
11111111- console.log("bsky profuile");
11121112-11131113- try {
11141114- const stmt = db.prepare(`
11151115- INSERT OR IGNORE INTO app_bsky_actor_profile (
11161116- uri, did, cid, rev, createdat, indexedat, json,
11171117- displayname,
11181118- description,
11191119- avatarcid,
11201120- avatarmime,
11211121- bannercid,
11221122- bannermime
11231123- ) VALUES (?, ?, ?, ?, ?, ?, ?,
11241124- ?, ?, ?,
11251125- ?, ?, ?)
11261126- `);
11271127- console.log({
11281128- uri: ctx.aturi,
11291129- did: ctx.doer,
11301130- cid: ctx.cid,
11311131- rev: ctx.rev,
11321132- createdat: record.createdAt,
11331133- indexedat: Date.now(),
11341134- json: JSON.stringify(record),
11351135- displayname: record.displayName,
11361136- description: record.description,
11371137- avatarcid: uncid(record.avatar?.ref),
11381138- avatarmime: record.avatar?.mimeType,
11391139- bannercid: uncid(record.banner?.ref),
11401140- bannermime: record.banner?.mimeType,
11411141- });
11421142- stmt.run(
11431143- ctx.aturi ?? null,
11441144- ctx.doer ?? null,
11451145- ctx.cid ?? null,
11461146- ctx.rev ?? null,
11471147- record.createdAt ?? null,
11481148- Date.now(),
11491149- JSON.stringify(record),
11501150-11511151- record.displayName ?? null,
11521152- record.description ?? null,
11531153- uncid(record.avatar?.ref) ?? null,
11541154- record.avatar?.mimeType ?? null,
11551155- uncid(record.banner?.ref) ?? null,
11561156- record.banner?.mimeType ?? null,
11571157- // TODO please add pinned posts
11581158-11591159- );
11601160- } catch (err) {
11611161- console.error("stmt.run failed:", err);
11621162- }
11631163- return;
11641164- }
11651165- case "app.bsky.feed.post": {
11661166- console.log("bsky post");
11671167- const stmt = db.prepare(`
11681168- INSERT OR IGNORE INTO app_bsky_feed_post (
11691169- uri, did, cid, rev, createdat, indexedat, json,
11701170- text, replyroot, replyparent, quote,
11711171- imagecount, image1cid, image1mime, image1aspect,
11721172- image2cid, image2mime, image2aspect,
11731173- image3cid, image3mime, image3aspect,
11741174- image4cid, image4mime, image4aspect,
11751175- videocount, videocid, videomime, videoaspect
11761176- ) VALUES (?, ?, ?, ?, ?, ?, ?,
11771177- ?, ?, ?, ?,
11781178- ?, ?, ?, ?,
11791179- ?, ?, ?,
11801180- ?, ?, ?,
11811181- ?, ?, ?,
11821182- ?, ?, ?, ?)
11831183- `);
11841184-11851185- const embed = record.embed;
11861186-11871187- const images = extractImages(embed);
11881188- const video = extractVideo(embed);
11891189- const quoteUri = extractQuoteUri(embed);
11901190- try {
11911191- stmt.run(
11921192- ctx.aturi ?? null,
11931193- ctx.doer ?? null,
11941194- ctx.cid ?? null,
11951195- ctx.rev ?? null,
11961196- record.createdAt,
11971197- Date.now(),
11981198- JSON.stringify(record),
11991199-12001200- record.text ?? null,
12011201- record.reply?.root?.uri ?? null,
12021202- record.reply?.parent?.uri ?? null,
12031203-12041204- quoteUri,
12051205-12061206- images.length,
12071207- uncid(images[0]?.image?.ref) ?? null,
12081208- images[0]?.image?.mimeType ?? null,
12091209- images[0]?.aspectRatio &&
12101210- images[0].aspectRatio.width &&
12111211- images[0].aspectRatio.height
12121212- ? `${images[0].aspectRatio.width}:${images[0].aspectRatio.height}`
12131213- : null,
12141214-12151215- uncid(images[1]?.image?.ref) ?? null,
12161216- images[1]?.image?.mimeType ?? null,
12171217- images[1]?.aspectRatio &&
12181218- images[1].aspectRatio.width &&
12191219- images[1].aspectRatio.height
12201220- ? `${images[1].aspectRatio.width}:${images[1].aspectRatio.height}`
12211221- : null,
12221222-12231223- uncid(images[2]?.image?.ref) ?? null,
12241224- images[2]?.image?.mimeType ?? null,
12251225- images[2]?.aspectRatio &&
12261226- images[2].aspectRatio.width &&
12271227- images[2].aspectRatio.height
12281228- ? `${images[2].aspectRatio.width}:${images[2].aspectRatio.height}`
12291229- : null,
12301230-12311231- uncid(images[3]?.image?.ref) ?? null,
12321232- images[3]?.image?.mimeType ?? null,
12331233- images[3]?.aspectRatio &&
12341234- images[3].aspectRatio.width &&
12351235- images[3].aspectRatio.height
12361236- ? `${images[3].aspectRatio.width}:${images[3].aspectRatio.height}`
12371237- : null,
12381238-12391239- uncid(video?.video) ? 1 : 0,
12401240- uncid(video?.video) ?? null,
12411241- uncid(video?.video) ? "video/mp4" : null,
12421242- video?.aspectRatio
12431243- ? `${video.aspectRatio.width}:${video.aspectRatio.height}`
12441244- : null
12451245- );
12461246- } catch (err) {
12471247- console.error("stmt.run failed:", err);
12481248- }
12491249- return;
12501250- }
12511251- default: {
12521252- // what the hell
12531253- return;
12541254- }
12551255- }
12561256-}
12571257-12581258-// user data
12591259-function queryProfileView(
12601260- did: string,
12611261- type: ""
12621262-): ATPAPI.AppBskyActorDefs.ProfileView | undefined;
12631263-function queryProfileView(
12641264- did: string,
12651265- type: "Basic"
12661266-): ATPAPI.AppBskyActorDefs.ProfileViewBasic | undefined;
12671267-function queryProfileView(
12681268- did: string,
12691269- type: "Detailed"
12701270-): ATPAPI.AppBskyActorDefs.ProfileViewDetailed | undefined;
12711271-function queryProfileView(
12721272- did: string,
12731273- type: "" | "Basic" | "Detailed"
12741274-):
12751275- | ATPAPI.AppBskyActorDefs.ProfileView
12761276- | ATPAPI.AppBskyActorDefs.ProfileViewBasic
12771277- | ATPAPI.AppBskyActorDefs.ProfileViewDetailed
12781278- | undefined {
12791279- if (!isRegisteredIndexUser(did)) return;
12801280- const db = getDbForDid(did);
12811281- if (!db) return;
12821282-12831283- const stmt = db.prepare(`
12841284- SELECT *
12851285- FROM app_bsky_actor_profile
12861286- WHERE did = ?
12871287- LIMIT 1;
12881288- `);
12891289-12901290- const row = stmt.get(did) as ProfileRow;
12911291-12921292- // simulate different types returned
12931293- switch (type) {
12941294- case "": {
12951295- const result: ATPAPI.AppBskyActorDefs.ProfileView = {
12961296- $type: "app.bsky.actor.defs#profileView",
12971297- did: did,
12981298- handle: "idiot.fuck.shit.example.com", // TODO: Resolve user identity here for the handle
12991299- displayName: row.displayname ?? undefined,
13001300- description: row.description ?? undefined,
13011301- avatar: "https://google.com/", // create profile URL from resolved identity
13021302- //associated?: ProfileAssociated,
13031303- indexedAt: row.createdat
13041304- ? new Date(row.createdat).toISOString()
13051305- : undefined,
13061306- createdAt: row.createdat
13071307- ? new Date(row.createdat).toISOString()
13081308- : undefined,
13091309- //viewer?: ViewerState,
13101310- //labels?: ComAtprotoLabelDefs.Label[],
13111311- //verification?: VerificationState,
13121312- //status?: StatusView,
13131313- };
13141314- return result;
13151315- }
13161316- case "Basic": {
13171317- const result: ATPAPI.AppBskyActorDefs.ProfileViewBasic = {
13181318- $type: "app.bsky.actor.defs#profileViewBasic",
13191319- did: did,
13201320- handle: "idiot.fuck.shit.example.com", // TODO: Resolve user identity here for the handle
13211321- displayName: row.displayname ?? undefined,
13221322- avatar: "https://google.com/", // create profile URL from resolved identity
13231323- //associated?: ProfileAssociated,
13241324- createdAt: row.createdat
13251325- ? new Date(row.createdat).toISOString()
13261326- : undefined,
13271327- //viewer?: ViewerState,
13281328- //labels?: ComAtprotoLabelDefs.Label[],
13291329- //verification?: VerificationState,
13301330- //status?: StatusView,
13311331- };
13321332- return result;
13331333- }
13341334- case "Detailed": {
13351335- // Query for follower count from the backlink_skeleton table
13361336- const followersStmt = db.prepare(`
13371337- SELECT COUNT(*) as count
13381338- FROM backlink_skeleton
13391339- WHERE subdid = ? AND srccol = 'app.bsky.graph.follow'
13401340- `);
13411341- const followersResult = followersStmt.get(did) as { count: number };
13421342- const followersCount = followersResult?.count ?? 0;
13431343-13441344- // Query for following count from the app_bsky_graph_follow table
13451345- const followingStmt = db.prepare(`
13461346- SELECT COUNT(*) as count
13471347- FROM app_bsky_graph_follow
13481348- WHERE did = ?
13491349- `);
13501350- const followingResult = followingStmt.get(did) as { count: number };
13511351- const followsCount = followingResult?.count ?? 0;
13521352-13531353- // Query for post count from the app_bsky_feed_post table
13541354- const postsStmt = db.prepare(`
13551355- SELECT COUNT(*) as count
13561356- FROM app_bsky_feed_post
13571357- WHERE did = ?
13581358- `);
13591359- const postsResult = postsStmt.get(did) as { count: number };
13601360- const postsCount = postsResult?.count ?? 0;
13611361-13621362- const result: ATPAPI.AppBskyActorDefs.ProfileViewDetailed = {
13631363- $type: "app.bsky.actor.defs#profileViewDetailed",
13641364- did: did,
13651365- handle: "idiot.fuck.shit.example.com", // TODO: Resolve user identity here for the handle
13661366- displayName: row.displayname ?? undefined,
13671367- description: row.description ?? undefined,
13681368- avatar: "https://google.com/", // TODO: create profile URL from resolved identity
13691369- banner: "https://youtube.com/", // same here
13701370- followersCount: followersCount,
13711371- followsCount: followsCount,
13721372- postsCount: postsCount,
13731373- //associated?: ProfileAssociated,
13741374- //joinedViaStarterPack?: // AppBskyGraphDefs.StarterPackViewBasic;
13751375- indexedAt: row.createdat
13761376- ? new Date(row.createdat).toISOString()
13771377- : undefined,
13781378- createdAt: row.createdat
13791379- ? new Date(row.createdat).toISOString()
13801380- : undefined,
13811381- //viewer?: ViewerState,
13821382- //labels?: ComAtprotoLabelDefs.Label[],
13831383- pinnedPost: undefined, //row.; // TODO: i forgot to put pinnedp posts in db schema oops
13841384- //verification?: VerificationState,
13851385- //status?: StatusView,
13861386- };
13871387- return result;
13881388- }
13891389- default:
13901390- throw new Error("Invalid type");
13911391- }
13921392-}
13931393-13941394-// post hydration
13951395-function queryPostView(
13961396- uri: string
13971397-): ATPAPI.AppBskyFeedDefs.PostView | undefined {
13981398- const URI = new AtUri(uri);
13991399- const did = URI.host;
14001400- if (!isRegisteredIndexUser(did)) return;
14011401- const db = getDbForDid(did);
14021402- if (!db) return;
14031403-14041404- const stmt = db.prepare(`
14051405- SELECT *
14061406- FROM app_bsky_feed_post
14071407- WHERE uri = ?
14081408- LIMIT 1;
14091409- `);
14101410-14111411- const row = stmt.get(uri) as PostRow;
14121412- const profileView = queryProfileView(did, "Basic");
14131413- if (!row || !row.cid || !profileView || !row.json) return;
14141414- const value = JSON.parse(row.json) as ATPAPI.AppBskyFeedPost.Record;
14151415-14161416- const post: ATPAPI.AppBskyFeedDefs.PostView = {
14171417- uri: row.uri,
14181418- cid: row.cid,
14191419- author: profileView,
14201420- record: value,
14211421- indexedAt: new Date(row.indexedat).toISOString(),
14221422- embed: value.embed,
14231423- };
14241424-14251425- return post;
14261426-}
14271427-function queryFeedViewPost(
14281428- uri: string
14291429-): ATPAPI.AppBskyFeedDefs.FeedViewPost | undefined {
14301430-14311431- const post = queryPostView(uri)
14321432- if (!post) return;
14331433-14341434- const feedviewpost: ATPAPI.AppBskyFeedDefs.FeedViewPost = {
14351435- $type: 'app.bsky.feed.defs#feedViewPost',
14361436- post: post,
14371437- //reply: ReplyRef,
14381438- //reason: ,
14391439- };
14401440-14411441- return feedviewpost;
14421442-}
14431443-14441444-interface BaseRow {
14451445- uri: string;
14461446- did: string;
14471447- cid: string | null;
14481448- rev: string | null;
14491449- createdat: number | null;
14501450- indexedat: number;
14511451- json: string | null;
14521452-}
14531453-interface GeneratorRow extends BaseRow {
14541454- displayname: string | null;
14551455- description: string | null;
14561456- avatarcid: string | null;
14571457-}
14581458-interface LikeRow extends BaseRow {
14591459- subject: string;
14601460-}
14611461-interface RepostRow extends BaseRow {
14621462- subject: string;
14631463-}
14641464-interface BacklinkRow {
14651465- srcuri: string;
14661466- srcdid: string;
14671467-}
14681468-14691469-const FEED_LIMIT = 50;
14701470-14711471-// user feedgens
14721472-14731473-function queryActorFeeds(did: string): ATPAPI.AppBskyFeedDefs.GeneratorView[] {
14741474- if (!isRegisteredIndexUser(did)) return [];
14751475- const db = getDbForDid(did);
14761476- if (!db) return [];
14771477-14781478- const stmt = db.prepare(`
14791479- SELECT uri, cid, did, json, indexedat
14801480- FROM app_bsky_feed_generator
14811481- WHERE did = ?
14821482- ORDER BY createdat DESC;
14831483- `);
14841484-14851485- const rows = stmt.all(did) as unknown as GeneratorRow[];
14861486- const creatorView = queryProfileView(did, "Basic");
14871487- if (!creatorView) return [];
14881488-14891489- return rows
14901490- .map((row) => {
14911491- try {
14921492- if (!row.json) return;
14931493- const record = JSON.parse(
14941494- row.json
14951495- ) as ATPAPI.AppBskyFeedGenerator.Record;
14961496- return {
14971497- $type: "app.bsky.feed.defs#generatorView",
14981498- uri: row.uri,
14991499- cid: row.cid,
15001500- did: row.did,
15011501- creator: creatorView,
15021502- displayName: record.displayName,
15031503- description: record.description,
15041504- descriptionFacets: record.descriptionFacets,
15051505- avatar: record.avatar,
15061506- likeCount: 0, // TODO: this should be easy
15071507- indexedAt: new Date(row.indexedat).toISOString(),
15081508- } as ATPAPI.AppBskyFeedDefs.GeneratorView;
15091509- } catch {
15101510- return undefined;
15111511- }
15121512- })
15131513- .filter((v): v is ATPAPI.AppBskyFeedDefs.GeneratorView => !!v);
15141514-}
15151515-15161516-function queryFeedGenerator(
15171517- uri: string
15181518-): ATPAPI.AppBskyFeedDefs.GeneratorView | undefined {
15191519- return queryFeedGenerators([uri])[0];
15201520-}
15211521-15221522-function queryFeedGenerators(
15231523- uris: string[]
15241524-): ATPAPI.AppBskyFeedDefs.GeneratorView[] {
15251525- const generators: ATPAPI.AppBskyFeedDefs.GeneratorView[] = [];
15261526- const urisByDid = new Map<string, string[]>();
15271527-15281528- for (const uri of uris) {
15291529- try {
15301530- const { host: did } = new AtUri(uri);
15311531- if (!urisByDid.has(did)) {
15321532- urisByDid.set(did, []);
15331533- }
15341534- urisByDid.get(did)!.push(uri);
15351535- } catch {
15361536- }
15371537- }
15381538-15391539- for (const [did, didUris] of urisByDid.entries()) {
15401540- if (!isRegisteredIndexUser(did)) continue;
15411541- const db = getDbForDid(did);
15421542- if (!db) continue;
15431543-15441544- const placeholders = didUris.map(() => "?").join(",");
15451545- const stmt = db.prepare(`
15461546- SELECT uri, cid, did, json, indexedat
15471547- FROM app_bsky_feed_generator
15481548- WHERE uri IN (${placeholders});
15491549- `);
15501550-15511551- const rows = stmt.all(...didUris) as unknown as GeneratorRow[];
15521552- if (rows.length === 0) continue;
15531553-15541554- const creatorView = queryProfileView(did, "");
15551555- if (!creatorView) continue;
15561556-15571557- for (const row of rows) {
15581558- try {
15591559- if (!row.json || !row.cid ) continue;
15601560- const record = JSON.parse(
15611561- row.json
15621562- ) as ATPAPI.AppBskyFeedGenerator.Record;
15631563- generators.push({
15641564- $type: "app.bsky.feed.defs#generatorView",
15651565- uri: row.uri,
15661566- cid: row.cid,
15671567- did: row.did,
15681568- creator: creatorView,
15691569- displayName: record.displayName,
15701570- description: record.description,
15711571- descriptionFacets: record.descriptionFacets,
15721572- avatar: record.avatar as string | undefined,
15731573- likeCount: 0,
15741574- indexedAt: new Date(row.indexedat).toISOString(),
15751575- });
15761576- } catch {}
15771577- }
15781578- }
15791579- return generators;
15801580-}
15811581-15821582-// user feeds
15831583-15841584-function queryAuthorFeed(
15851585- did: string,
15861586- cursor?: string
15871587-):
15881588- | {
15891589- items: ATPAPI.AppBskyFeedDefs.FeedViewPost[];
15901590- cursor: string | undefined;
15911591- }
15921592- | undefined {
15931593- if (!isRegisteredIndexUser(did)) return;
15941594- const db = getDbForDid(did);
15951595- if (!db) return;
15961596-15971597- // TODO: implement this for real
15981598- let query = `
15991599- SELECT uri, indexedat, cid
16001600- FROM app_bsky_feed_post
16011601- WHERE did = ?
16021602- `;
16031603- const params: (string | number)[] = [did];
16041604-16051605- if (cursor) {
16061606- const [indexedat, cid] = cursor.split("::");
16071607- query += ` AND (indexedat < ? OR (indexedat = ? AND cid < ?))`;
16081608- params.push(parseInt(indexedat, 10), parseInt(indexedat, 10), cid);
16091609- }
16101610-16111611- query += ` ORDER BY indexedat DESC, cid DESC LIMIT ${FEED_LIMIT}`;
16121612-16131613- const stmt = db.prepare(query);
16141614- const rows = stmt.all(...params) as {
16151615- uri: string;
16161616- indexedat: number;
16171617- cid: string;
16181618- }[];
16191619-16201620- const items = rows
16211621- .map((row) => queryFeedViewPost(row.uri)) // TODO: for replies and repost i should inject the reason here
16221622- .filter((p): p is ATPAPI.AppBskyFeedDefs.FeedViewPost => !!p);
16231623-16241624- const lastItem = rows[rows.length - 1];
16251625- const nextCursor = lastItem
16261626- ? `${lastItem.indexedat}::${lastItem.cid}`
16271627- : undefined;
16281628-16291629- return { items, cursor: nextCursor };
16301630-}
16311631-16321632-function queryListFeed(
16331633- uri: string,
16341634- cursor?: string
16351635-):
16361636- | {
16371637- items: ATPAPI.AppBskyFeedDefs.FeedViewPost[];
16381638- cursor: string | undefined;
16391639- }
16401640- | undefined {
16411641- return { items: [], cursor: undefined };
16421642-}
16431643-16441644-function queryActorLikes(
16451645- did: string,
16461646- cursor?: string
16471647-):
16481648- | {
16491649- items: ATPAPI.AppBskyFeedDefs.FeedViewPost[];
16501650- cursor: string | undefined;
16511651- }
16521652- | undefined {
16531653- if (!isRegisteredIndexUser(did)) return;
16541654- const db = getDbForDid(did);
16551655- if (!db) return;
16561656-16571657- let query = `
16581658- SELECT subject, indexedat, cid
16591659- FROM app_bsky_feed_like
16601660- WHERE did = ?
16611661- `;
16621662- const params: (string | number)[] = [did];
16631663-16641664- if (cursor) {
16651665- const [indexedat, cid] = cursor.split("::");
16661666- query += ` AND (indexedat < ? OR (indexedat = ? AND cid < ?))`;
16671667- params.push(parseInt(indexedat, 10), parseInt(indexedat, 10), cid);
16681668- }
16691669-16701670- query += ` ORDER BY indexedat DESC, cid DESC LIMIT ${FEED_LIMIT}`;
16711671-16721672- const stmt = db.prepare(query);
16731673- const rows = stmt.all(...params) as {
16741674- subject: string;
16751675- indexedat: number;
16761676- cid: string;
16771677- }[];
16781678-16791679- const items = rows
16801680- .map((row) => queryFeedViewPost(row.subject))
16811681- .filter((p): p is ATPAPI.AppBskyFeedDefs.FeedViewPost => !!p);
16821682-16831683- const lastItem = rows[rows.length - 1];
16841684- const nextCursor = lastItem
16851685- ? `${lastItem.indexedat}::${lastItem.cid}`
16861686- : undefined;
16871687-16881688- return { items, cursor: nextCursor };
16891689-}
16901690-16911691-// post metadata
16921692-16931693-function queryLikes(
16941694- uri: string
16951695-): ATPAPI.AppBskyFeedGetLikes.Like[] | undefined {
16961696- const postUri = new AtUri(uri);
16971697- const postAuthorDid = postUri.hostname;
16981698- if (!isRegisteredIndexUser(postAuthorDid)) return;
16991699- const db = getDbForDid(postAuthorDid);
17001700- if (!db) return;
17011701-17021702- const stmt = db.prepare(`
17031703- SELECT b.srcdid, b.srcuri
17041704- FROM backlink_skeleton AS b
17051705- WHERE b.suburi = ? AND b.srccol = 'app_bsky_feed_like'
17061706- ORDER BY b.id DESC;
17071707- `);
17081708-17091709- const rows = stmt.all(uri) as unknown as BacklinkRow[];
17101710-17111711- return rows
17121712- .map((row) => {
17131713- const actor = queryProfileView(row.srcdid, "");
17141714- if (!actor) return;
17151715-17161716- return {
17171717- // TODO write indexedAt for spacedust indexes
17181718- createdAt: new Date(Date.now()).toISOString(),
17191719- indexedAt: new Date(Date.now()).toISOString(),
17201720- actor: actor,
17211721- };
17221722- })
17231723- .filter((like): like is ATPAPI.AppBskyFeedGetLikes.Like => !!like);
17241724-}
17251725-17261726-function queryReposts(uri: string): ATPAPI.AppBskyActorDefs.ProfileView[] {
17271727- const postUri = new AtUri(uri);
17281728- const postAuthorDid = postUri.hostname;
17291729- if (!isRegisteredIndexUser(postAuthorDid)) return [];
17301730- const db = getDbForDid(postAuthorDid);
17311731- if (!db) return [];
17321732-17331733- const stmt = db.prepare(`
17341734- SELECT srcdid
17351735- FROM backlink_skeleton
17361736- WHERE suburi = ? AND srccol = 'app_bsky_feed_repost'
17371737- ORDER BY id DESC;
17381738- `);
17391739-17401740- const rows = stmt.all(uri) as { srcdid: string }[];
17411741-17421742- return rows
17431743- .map((row) => queryProfileView(row.srcdid, ""))
17441744- .filter((p): p is ATPAPI.AppBskyActorDefs.ProfileView => !!p);
17451745-}
17461746-17471747-function queryQuotes(uri: string): ATPAPI.AppBskyFeedDefs.FeedViewPost[] {
17481748- const postUri = new AtUri(uri);
17491749- const postAuthorDid = postUri.hostname;
17501750- if (!isRegisteredIndexUser(postAuthorDid)) return [];
17511751- const db = getDbForDid(postAuthorDid);
17521752- if (!db) return [];
17531753-17541754- const stmt = db.prepare(`
17551755- SELECT srcuri
17561756- FROM backlink_skeleton
17571757- WHERE suburi = ? AND srccol = 'app_bsky_feed_post' AND srcfield = 'quote'
17581758- ORDER BY id DESC;
17591759- `);
17601760-17611761- const rows = stmt.all(uri) as { srcuri: string }[];
17621762-17631763- return rows
17641764- .map((row) => queryFeedViewPost(row.srcuri))
17651765- .filter((p): p is ATPAPI.AppBskyFeedDefs.FeedViewPost => !!p);
17661766-}
17671767-17681768-function queryPostThread(
17691769- uri: string
17701770-): ATPAPI.AppBskyFeedGetPostThread.OutputSchema | undefined {
17711771- const post = queryPostView(uri);
17721772- if (!post) {
17731773- return {
17741774- thread: {
17751775- $type: "app.bsky.feed.defs#notFoundPost",
17761776- uri: uri,
17771777- notFound: true,
17781778- } as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.NotFoundPost>
17791779- }
17801780- }
17811781-17821782- const thread: ATPAPI.AppBskyFeedDefs.ThreadViewPost = {
17831783- $type: "app.bsky.feed.defs#threadViewPost",
17841784- post: post,
17851785- replies: [],
17861786- };
17871787-17881788- let current = thread;
17891789- while ((current.post.record.reply as any)?.parent?.uri) {
17901790- const parentUri = (current.post.record.reply as any)?.parent?.uri;
17911791- const parentPost = queryPostView(parentUri);
17921792- if (!parentPost) break;
17931793-17941794- const parentThread: ATPAPI.AppBskyFeedDefs.ThreadViewPost = {
17951795- $type: "app.bsky.feed.defs#threadViewPost",
17961796- post: parentPost,
17971797- replies: [current as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.ThreadViewPost>],
17981798- };
17991799- current.parent = parentThread as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.ThreadViewPost>;
18001800- current = parentThread;
18011801- }
18021802-18031803- const seenUris = new Set<string>();
18041804- const fetchReplies = (
18051805- parentThread: ATPAPI.AppBskyFeedDefs.ThreadViewPost
18061806- ) => {
18071807- if (seenUris.has(parentThread.post.uri)) return;
18081808- seenUris.add(parentThread.post.uri);
18091809-18101810- const parentUri = new AtUri(parentThread.post.uri);
18111811- const parentAuthorDid = parentUri.hostname;
18121812- const db = getDbForDid(parentAuthorDid);
18131813- if (!db) return;
18141814-18151815- const stmt = db.prepare(`
18161816- SELECT srcuri
18171817- FROM backlink_skeleton
18181818- WHERE suburi = ? AND srccol = 'app_bsky_feed_post' AND srcfield = 'replyparent'
18191819- `);
18201820- const replyRows = stmt.all(parentThread.post.uri) as { srcuri: string }[];
18211821-18221822- const replies = replyRows
18231823- .map((row) => queryPostView(row.srcuri))
18241824- .filter((p): p is ATPAPI.AppBskyFeedDefs.PostView => !!p);
18251825-18261826- for (const replyPost of replies) {
18271827- const replyThread: ATPAPI.AppBskyFeedDefs.ThreadViewPost = {
18281828- $type: "app.bsky.feed.defs#threadViewPost",
18291829- post: replyPost,
18301830- parent: parentThread as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.ThreadViewPost>,
18311831- replies: [],
18321832- };
18331833- parentThread.replies?.push(replyThread as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.ThreadViewPost>);
18341834- fetchReplies(replyThread);
18351835- }
18361836- };
18371837-18381838- fetchReplies(thread);
18391839-18401840- const returned = thread as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.ThreadViewPost>
18411841-18421842- return { thread: returned };
18431843-}
+12-8
main.ts
···1414import * as ATPAPI from "npm:@atproto/api";
1515import { didDocument } from "./utils/diddoc.ts";
1616import { cachedFetch, searchParamsToJson, withCors } from "./utils/server.ts";
1717-import { constellationAPIHandler, indexServerHandler, IndexServerUserManager } from "./indexserver.ts";
1717+import { IndexServer, IndexServerConfig } from "./indexserver.ts"
1818import { viewServerHandler } from "./viewserver.ts";
19192020export const jetstreamurl = Deno.env.get("JETSTREAM_URL");
···2626// AppView Setup
2727// ------------------------------------------
28282929-export const systemDB = new Database("system.db");
3030-setupSystemDb(systemDB);
2929+const config: IndexServerConfig = {
3030+ baseDbPath: './dbs', // The directory for user databases
3131+ systemDbPath: './system.db', // The path for the main system database
3232+ jetstreamUrl: jetstreamurl || ""
3333+};
3434+const registeredUsersIndexServer = new IndexServer(config);
3535+setupSystemDb(registeredUsersIndexServer.systemDB);
31363237// add me lol
3333-systemDB.exec(`
3838+registeredUsersIndexServer.systemDB.exec(`
3439 INSERT OR IGNORE INTO users (did, role, registrationdate, onboardingstatus)
3540 VALUES (
3641 'did:plc:mn45tewwnse5btfftvd3powc',
···4853 );
4954`)
50555151-export const indexerUserManager = new IndexServerUserManager();
5252-indexerUserManager.coldStart(systemDB)
5656+registeredUsersIndexServer.start();
53575458// should do both of these per user actually, since now each user has their own db
5559// also the set of records and backlinks to listen should be seperate between index and view servers
···152156 // return await viewServerHandler(req)
153157154158 if (constellation) {
155155- return await constellationAPIHandler(req);
159159+ return registeredUsersIndexServer.constellationAPIHandler(req);
156160 }
157161158162 if (indexServerRoutes.has(pathname)) {
159159- return await indexServerHandler(req);
163163+ return registeredUsersIndexServer.indexServerHandler(req);
160164 } else {
161165 return await viewServerHandler(req);
162166 }
+2-2
readme.md
···11# skylite (pre alpha)
22-an attempt to make a lightweight, easily self-hostable, scoped appview
22+an attempt to make a lightweight, easily self-hostable, scoped Bluesky appview
3344this project uses:
55- live sync systems: [jetstream](https://github.com/bluesky-social/jetstream) and [spacedust](https://spacedust.microcosm.blue/)
···2222 - its a backlink index so i only needed one table, and so it is complete
2323- Server:
2424 - Initial implementation is done
2525- - uses per-user instantiaion thing so it can add or remove users as needed
2525+ - uses per-user instantiation thing so it can add or remove users as needed
2626 - pagination is not a thing yet \:\(
2727 - does not implement the Ref / Partial routes yet (currently strips undefineds) (fixing this soon)
2828 - also implements the entirety of the Constellation API routes as a bonus (under `/links/`)
+2-1
utils/identity.ts
···1122import { DidResolver, HandleResolver } from "npm:@atproto/identity";
33-import { systemDB } from "../main.ts";
33+import { Database } from "jsr:@db/sqlite@0.11";
44+const systemDB = new Database("./system.db") // TODO: temporary shim. should seperate this to its own central system db instead of the now instantiated system dbs
45type DidMethod = "web" | "plc";
56type DidDoc = {
67 "@context"?: unknown;