an attempt to make a lightweight, easily self-hostable, scoped bluesky appview

getProfile and the rest of Constellation APIs

rimar1337 a7628821 fdcbde96

+498 -85
+496 -83
indexserver.ts
··· 5 5 import * as IndexServerTypes from "./utils/indexservertypes.ts"; 6 6 import { Database } from "jsr:@db/sqlite@0.11"; 7 7 import { setupUserDb } from "./utils/dbuser.ts"; 8 - import { jetstreamurl } from "./main.ts"; 8 + import { indexerUserManager, jetstreamurl, systemDB } from "./main.ts"; 9 9 import { JetstreamManager, SpacedustManager } from "./utils/sharders.ts"; 10 10 import { handleSpacedust, SpacedustLinkMessage } from "./index/spacedust.ts"; 11 11 import { handleJetstream } from "./index/jetstream.ts"; ··· 66 66 67 67 constructor(did: string) { 68 68 this.did = did; 69 - this.db = openDbForDid(this.did); 69 + this.db = internalCreateDbForDid(this.did); 70 70 // should probably put the params of exactly what were listening to here 71 71 this.jetstream = new JetstreamManager((msg) => { 72 72 console.log("Received Jetstream message: ", msg); ··· 226 226 } 227 227 } 228 228 229 - function openDbForDid(did: string): Database { 230 - // TODO: we should disallow non users to open a db 229 + function isRegisteredIndexUser(did: string): boolean { 230 + const stmt = systemDB.prepare(` 231 + SELECT 1 232 + FROM users 233 + WHERE did = ? 234 + AND onboardingstatus != 'onboarding-backfill' 235 + LIMIT 1; 236 + `); 237 + const result = stmt.value<[number]>(did); 238 + const exists = result !== undefined; 239 + return exists; 240 + } 241 + 242 + /** 243 + * please do not use this, use openDbForDid() instead 244 + * @param did 245 + * @returns 246 + */ 247 + function internalCreateDbForDid(did: string): Database { 231 248 const path = `./dbs/${did}.sqlite`; 232 249 const db = new Database(path); 233 250 setupUserDb(db); 234 251 //await db.exec(/* CREATE IF NOT EXISTS statements */); 252 + return db; 253 + } 254 + 255 + function getDbForDid(did: string): Database | undefined { 256 + const db = indexerUserManager.getDbForDid(did); 257 + if (!db) return; 235 258 return db; 236 259 } 237 260 ··· 269 292 // return ws; 270 293 // } 271 294 295 + type PostRow = { 296 + uri: string; 297 + did: string; 298 + cid: string | null; 299 + rev: string | null; 300 + createdat: number | null; 301 + indexedat: number; 302 + json: string | null; 303 + 304 + text: string | null; 305 + replyroot: string | null; 306 + replyparent: string | null; 307 + quote: string | null; 308 + 309 + imagecount: number | null; 310 + image1cid: string | null; 311 + image1mime: string | null; 312 + image1aspect: string | null; 313 + image2cid: string | null; 314 + image2mime: string | null; 315 + image2aspect: string | null; 316 + image3cid: string | null; 317 + image3mime: string | null; 318 + image3aspect: string | null; 319 + image4cid: string | null; 320 + image4mime: string | null; 321 + image4aspect: string | null; 322 + 323 + videocount: number | null; 324 + videocid: string | null; 325 + videomime: string | null; 326 + videoaspect: string | null; 327 + }; 328 + 329 + type ProfileRow = { 330 + uri: string; 331 + cid: string | null; 332 + rev: string | null; 333 + createdat: number | null; 334 + indexedat: number; 335 + json: string | null; 336 + displayname: string | null; 337 + description: string | null; 338 + avatarcid: string | null; 339 + avatarmime: string | null; 340 + bannercid: string | null; 341 + bannermime: string | null; 342 + }; 343 + 272 344 export async function indexServerHandler(req: Request): Promise<Response> { 273 345 const url = new URL(req.url); 274 346 const pathname = url.pathname; 275 - const bskyUrl = `https://api.bsky.app${pathname}${url.search}`; 276 - const hasAuth = req.headers.has("authorization"); 347 + //const bskyUrl = `https://api.bsky.app${pathname}${url.search}`; 348 + //const hasAuth = req.headers.has("authorization"); 277 349 const xrpcMethod = pathname.startsWith("/xrpc/") 278 350 ? pathname.slice("/xrpc/".length) 279 351 : null; ··· 339 411 const jsonTyped = 340 412 jsonUntyped as IndexServerTypes.AppBskyFeedGetPosts.QueryParams; 341 413 342 - const response: IndexServerTypes.AppBskyFeedGetPosts.OutputSchema = {}; 414 + const posts: IndexServerTypes.AppBskyFeedGetPosts.OutputSchema["posts"] = 415 + jsonTyped.uris 416 + .map((uri) => { 417 + return queryPostView(uri); 418 + }) 419 + .filter(Boolean) as ATPAPI.AppBskyFeedDefs.PostView[]; 420 + 421 + const response: IndexServerTypes.AppBskyFeedGetPosts.OutputSchema = { 422 + posts, 423 + }; 343 424 344 425 return new Response(JSON.stringify(response), { 345 426 headers: withCors({ "Content-Type": "application/json" }), ··· 522 603 export async function constellationAPIHandler(req: Request): Promise<Response> { 523 604 const url = new URL(req.url); 524 605 const pathname = url.pathname; 525 - // const bskyUrl = `https://api.bsky.app${pathname}${url.search}`; 526 - // const hasAuth = req.headers.has("authorization"); 527 - // const constellationMethod = pathname.startsWith("/links") 528 - // ? pathname.slice("/links".length) 529 - // : null; 530 606 const searchParams = searchParamsToJson(url.searchParams) as linksQuery; 531 607 const jsonUntyped = searchParams; 608 + 609 + if (!jsonUntyped.target) { 610 + return new Response(JSON.stringify({ error: "Missing required parameter: target" }), { 611 + status: 400, 612 + headers: withCors({ "Content-Type": "application/json" }), 613 + }); 614 + } 615 + 532 616 const did = isDid(searchParams.target) 533 617 ? searchParams.target 534 618 : new AtUri(searchParams.target).host; 535 - const db = openDbForDid(did); 619 + const db = getDbForDid(did); 620 + if (!db) { 621 + return new Response( 622 + JSON.stringify({ 623 + error: "User not found", 624 + }), 625 + { 626 + status: 404, 627 + headers: withCors({ "Content-Type": "application/json" }), 628 + } 629 + ); 630 + } 631 + 632 + const limit = 16 //Math.min(parseInt(searchParams.limit || "50", 10), 100); 633 + const offset = parseInt(searchParams.cursor || "0", 10); 536 634 537 635 switch (pathname) { 538 636 case "/links": { 539 637 const jsonTyped = jsonUntyped as linksQuery; 540 - // probably need to do pagination or something 541 - console.log(JSON.stringify(jsonTyped, null, 2)); 638 + if (!jsonTyped.collection || !jsonTyped.path) { 639 + return new Response(JSON.stringify({ error: "Missing required parameters: collection, path" }), { 640 + status: 400, 641 + headers: withCors({ "Content-Type": "application/json" }), 642 + }); 643 + } 644 + 542 645 const field = `${jsonTyped.collection}:${jsonTyped.path.replace( 543 646 /^\./, 544 647 "" 545 648 )}`; 546 649 650 + const paginatedSql = `${SQL.links} LIMIT ? OFFSET ?`; 547 651 const rows = db 548 - .prepare(SQL.links) 549 - .all(jsonTyped.target, jsonTyped.collection, field); 652 + .prepare(paginatedSql) 653 + .all(jsonTyped.target, jsonTyped.collection, field, limit, offset); 654 + 655 + const countResult = db 656 + .prepare(SQL.count) 657 + .get(jsonTyped.target, jsonTyped.collection, field); 658 + const total = countResult ? Number(countResult.total) : 0; 550 659 551 660 const linking_records: linksRecord[] = rows.map((row: any) => { 552 661 const rkey = row.srcuri.split("/").pop()!; ··· 558 667 }); 559 668 560 669 const response: linksRecordsResponse = { 561 - total: linking_records.length.toString(), 670 + total: total.toString(), 562 671 linking_records, 563 672 }; 673 + 674 + const nextCursor = offset + linking_records.length; 675 + if (nextCursor < total) { 676 + response.cursor = nextCursor.toString(); 677 + } 678 + 564 679 return new Response(JSON.stringify(response), { 565 680 headers: withCors({ "Content-Type": "application/json" }), 566 681 }); 567 682 } 568 683 case "/links/distinct-dids": { 569 684 const jsonTyped = jsonUntyped as linksQuery; 685 + if (!jsonTyped.collection || !jsonTyped.path) { 686 + return new Response( 687 + JSON.stringify({ 688 + error: "Missing required parameters: collection, path", 689 + }), 690 + { 691 + status: 400, 692 + headers: withCors({ "Content-Type": "application/json" }), 693 + } 694 + ); 695 + } 570 696 571 - const response: linksDidsResponse = {}; 697 + const field = `${jsonTyped.collection}:${jsonTyped.path.replace( 698 + /^\./, 699 + "" 700 + )}`; 701 + 702 + const paginatedSql = `${SQL.distinctDids} LIMIT ? OFFSET ?`; 703 + const rows = db 704 + .prepare(paginatedSql) 705 + .all(jsonTyped.target, jsonTyped.collection, field, limit, offset); 706 + 707 + const countResult = db 708 + .prepare(SQL.countDistinctDids) 709 + .get(jsonTyped.target, jsonTyped.collection, field); 710 + const total = countResult ? Number(countResult.total) : 0; 711 + 712 + const linking_dids: string[] = rows.map((row: any) => row.srcdid); 713 + 714 + const response: linksDidsResponse = { 715 + total: total.toString(), 716 + linking_dids, 717 + }; 718 + 719 + const nextCursor = offset + linking_dids.length; 720 + if (nextCursor < total) { 721 + response.cursor = nextCursor.toString(); 722 + } 572 723 573 724 return new Response(JSON.stringify(response), { 574 725 headers: withCors({ "Content-Type": "application/json" }), ··· 576 727 } 577 728 case "/links/count": { 578 729 const jsonTyped = jsonUntyped as linksQuery; 730 + if (!jsonTyped.collection || !jsonTyped.path) { 731 + return new Response( 732 + JSON.stringify({ 733 + error: "Missing required parameters: collection, path", 734 + }), 735 + { 736 + status: 400, 737 + headers: withCors({ "Content-Type": "application/json" }), 738 + } 739 + ); 740 + } 579 741 580 - const response: linksCountResponse = {}; 742 + const field = `${jsonTyped.collection}:${jsonTyped.path.replace( 743 + /^\./, 744 + "" 745 + )}`; 746 + 747 + const result = db 748 + .prepare(SQL.count) 749 + .get(jsonTyped.target, jsonTyped.collection, field); 750 + 751 + const response: linksCountResponse = { 752 + total: (result && result.total) ? result.total.toString() : "0", 753 + }; 581 754 582 755 return new Response(JSON.stringify(response), { 583 756 headers: withCors({ "Content-Type": "application/json" }), ··· 585 758 } 586 759 case "/links/count/distinct-dids": { 587 760 const jsonTyped = jsonUntyped as linksQuery; 761 + if (!jsonTyped.collection || !jsonTyped.path) { 762 + return new Response( 763 + JSON.stringify({ 764 + error: "Missing required parameters: collection, path", 765 + }), 766 + { 767 + status: 400, 768 + headers: withCors({ "Content-Type": "application/json" }), 769 + } 770 + ); 771 + } 588 772 589 - const response: linksCountResponse = {}; 773 + const field = `${jsonTyped.collection}:${jsonTyped.path.replace( 774 + /^\./, 775 + "" 776 + )}`; 777 + 778 + const result = db 779 + .prepare(SQL.countDistinctDids) 780 + .get(jsonTyped.target, jsonTyped.collection, field); 781 + 782 + const response: linksCountResponse = { 783 + total: (result && result.total) ? result.total.toString() : "0", 784 + }; 590 785 591 786 return new Response(JSON.stringify(response), { 592 787 headers: withCors({ "Content-Type": "application/json" }), ··· 595 790 case "/links/all": { 596 791 const jsonTyped = jsonUntyped as linksAllQuery; 597 792 598 - const response: linksAllResponse = {}; 793 + const rows = db.prepare(SQL.all).all(jsonTyped.target) as any[]; 794 + 795 + const links: linksAllResponse["links"] = {}; 796 + 797 + for (const row of rows) { 798 + if (!links[row.suburi]) { 799 + links[row.suburi] = {}; 800 + } 801 + links[row.suburi][row.srccol] = { 802 + records: row.records, 803 + distinct_dids: row.distinct_dids, 804 + }; 805 + } 806 + 807 + const response: linksAllResponse = { 808 + links, 809 + }; 599 810 600 811 return new Response(JSON.stringify(response), { 601 812 headers: withCors({ "Content-Type": "application/json" }), ··· 604 815 default: { 605 816 return new Response( 606 817 JSON.stringify({ 607 - error: "idk NotSupported", 818 + error: "NotSupported", 608 819 message: 609 - "HEY hello there my name is whey dot party and you have used my custom constellation implementation that is very cool but have you considered that idk Not Supported", 820 + "The requested endpoint is not supported by this Constellation implementation.", 610 821 }), 611 822 { 612 823 status: 404, ··· 618 829 } 619 830 620 831 function isImageEmbed(embed: unknown): embed is ATPAPI.AppBskyEmbedImages.Main { 621 - return typeof embed === "object" && embed !== null && "$type" in embed && 622 - (embed as any).$type === "app.bsky.embed.images"; 832 + return ( 833 + typeof embed === "object" && 834 + embed !== null && 835 + "$type" in embed && 836 + (embed as any).$type === "app.bsky.embed.images" 837 + ); 623 838 } 624 839 625 840 function isVideoEmbed(embed: unknown): embed is ATPAPI.AppBskyEmbedVideo.Main { 626 - return typeof embed === "object" && embed !== null && "$type" in embed && 627 - (embed as any).$type === "app.bsky.embed.video"; 841 + return ( 842 + typeof embed === "object" && 843 + embed !== null && 844 + "$type" in embed && 845 + (embed as any).$type === "app.bsky.embed.video" 846 + ); 628 847 } 629 848 630 - function isRecordEmbed(embed: unknown): embed is ATPAPI.AppBskyEmbedRecord.Main { 631 - return typeof embed === "object" && embed !== null && "$type" in embed && 632 - (embed as any).$type === "app.bsky.embed.record"; 849 + function isRecordEmbed( 850 + embed: unknown 851 + ): embed is ATPAPI.AppBskyEmbedRecord.Main { 852 + return ( 853 + typeof embed === "object" && 854 + embed !== null && 855 + "$type" in embed && 856 + (embed as any).$type === "app.bsky.embed.record" 857 + ); 633 858 } 634 859 635 - function isRecordWithMediaEmbed(embed: unknown): embed is ATPAPI.AppBskyEmbedRecordWithMedia.Main { 636 - return typeof embed === "object" && embed !== null && "$type" in embed && 637 - (embed as any).$type === "app.bsky.embed.recordWithMedia"; 860 + function isRecordWithMediaEmbed( 861 + embed: unknown 862 + ): embed is ATPAPI.AppBskyEmbedRecordWithMedia.Main { 863 + return ( 864 + typeof embed === "object" && 865 + embed !== null && 866 + "$type" in embed && 867 + (embed as any).$type === "app.bsky.embed.recordWithMedia" 868 + ); 638 869 } 639 870 640 871 function uncid(anything: any): string | null { 641 - return (anything as Record<string, unknown>)?.["$link"] as string | null || null; 872 + return ( 873 + ((anything as Record<string, unknown>)?.["$link"] as string | null) || null 874 + ); 642 875 } 643 876 644 877 function extractImages(embed: unknown) { 645 878 if (isImageEmbed(embed)) return embed.images; 646 - if (isRecordWithMediaEmbed(embed) && isImageEmbed(embed.media)) return embed.media.images; 879 + if (isRecordWithMediaEmbed(embed) && isImageEmbed(embed.media)) 880 + return embed.media.images; 647 881 return []; 648 882 } 649 883 650 884 function extractVideo(embed: unknown) { 651 885 if (isVideoEmbed(embed)) return embed; 652 - if (isRecordWithMediaEmbed(embed) && isVideoEmbed(embed.media)) return embed.media; 886 + if (isRecordWithMediaEmbed(embed) && isVideoEmbed(embed.media)) 887 + return embed.media; 653 888 return null; 654 889 } 655 890 ··· 662 897 export function indexServerIndexer(ctx: indexHandlerContext) { 663 898 const record = assertRecord(ctx.value); 664 899 //const record = validateRecord(ctx.value); 665 - const db = openDbForDid(ctx.doer); 666 - console.log("indexering") 900 + const db = getDbForDid(ctx.doer); 901 + if (!db) return; 902 + console.log("indexering"); 667 903 switch (record?.$type) { 668 904 case "app.bsky.feed.like": { 669 905 return; 670 906 } 671 907 case "app.bsky.feed.post": { 672 - console.log("bsky post") 908 + console.log("bsky post"); 673 909 const stmt = db.prepare(` 674 910 INSERT OR IGNORE INTO app_bsky_feed_post ( 675 911 uri, did, cid, rev, createdat, indexedat, json, ··· 687 923 ?, ?, ?, 688 924 ?, ?, ?, ?) 689 925 `); 690 - 926 + 691 927 const embed = record.embed; 692 928 693 929 const images = extractImages(embed); 694 930 const video = extractVideo(embed); 695 931 const quoteUri = extractQuoteUri(embed); 696 932 try { 697 - stmt.run( 698 - ctx.aturi?? null, 699 - ctx.doer?? null, 700 - ctx.cid?? null, 701 - ctx.rev?? null, 702 - record.createdAt, 703 - Date.now(), 704 - JSON.stringify(record), 933 + stmt.run( 934 + ctx.aturi ?? null, 935 + ctx.doer ?? null, 936 + ctx.cid ?? null, 937 + ctx.rev ?? null, 938 + record.createdAt, 939 + Date.now(), 940 + JSON.stringify(record), 705 941 706 - record.text ?? null, 707 - record.reply?.root?.uri ?? null, 708 - record.reply?.parent?.uri ?? null, 942 + record.text ?? null, 943 + record.reply?.root?.uri ?? null, 944 + record.reply?.parent?.uri ?? null, 709 945 710 - quoteUri, 946 + quoteUri, 711 947 712 - images.length, 713 - uncid(images[0]?.image?.ref) ?? null, 714 - images[0]?.image?.mimeType ?? null, 715 - (images[0]?.aspectRatio && images[0].aspectRatio.width && images[0].aspectRatio.height) 716 - ? `${images[0].aspectRatio.width}:${images[0].aspectRatio.height}` 717 - : null, 948 + images.length, 949 + uncid(images[0]?.image?.ref) ?? null, 950 + images[0]?.image?.mimeType ?? null, 951 + images[0]?.aspectRatio && 952 + images[0].aspectRatio.width && 953 + images[0].aspectRatio.height 954 + ? `${images[0].aspectRatio.width}:${images[0].aspectRatio.height}` 955 + : null, 718 956 719 - uncid(images[1]?.image?.ref) ?? null, 720 - images[1]?.image?.mimeType ?? null, 721 - (images[1]?.aspectRatio && images[1].aspectRatio.width && images[1].aspectRatio.height) 722 - ? `${images[1].aspectRatio.width}:${images[1].aspectRatio.height}` 723 - : null, 957 + uncid(images[1]?.image?.ref) ?? null, 958 + images[1]?.image?.mimeType ?? null, 959 + images[1]?.aspectRatio && 960 + images[1].aspectRatio.width && 961 + images[1].aspectRatio.height 962 + ? `${images[1].aspectRatio.width}:${images[1].aspectRatio.height}` 963 + : null, 724 964 725 - uncid(images[2]?.image?.ref) ?? null, 726 - images[2]?.image?.mimeType ?? null, 727 - (images[2]?.aspectRatio && images[2].aspectRatio.width && images[2].aspectRatio.height) 728 - ? `${images[2].aspectRatio.width}:${images[2].aspectRatio.height}` 729 - : null, 965 + uncid(images[2]?.image?.ref) ?? null, 966 + images[2]?.image?.mimeType ?? null, 967 + images[2]?.aspectRatio && 968 + images[2].aspectRatio.width && 969 + images[2].aspectRatio.height 970 + ? `${images[2].aspectRatio.width}:${images[2].aspectRatio.height}` 971 + : null, 730 972 731 - uncid(images[3]?.image?.ref) ?? null, 732 - images[3]?.image?.mimeType ?? null, 733 - (images[3]?.aspectRatio && images[3].aspectRatio.width && images[3].aspectRatio.height) 734 - ? `${images[3].aspectRatio.width}:${images[3].aspectRatio.height}` 735 - : null, 973 + uncid(images[3]?.image?.ref) ?? null, 974 + images[3]?.image?.mimeType ?? null, 975 + images[3]?.aspectRatio && 976 + images[3].aspectRatio.width && 977 + images[3].aspectRatio.height 978 + ? `${images[3].aspectRatio.width}:${images[3].aspectRatio.height}` 979 + : null, 736 980 737 - uncid(video?.video) ? 1 : 0, 738 - uncid(video?.video) ?? null, 739 - uncid(video?.video) ? "video/mp4" : null, 740 - video?.aspectRatio 741 - ? `${video.aspectRatio.width}:${video.aspectRatio.height}` 742 - : null 743 - ); 981 + uncid(video?.video) ? 1 : 0, 982 + uncid(video?.video) ?? null, 983 + uncid(video?.video) ? "video/mp4" : null, 984 + video?.aspectRatio 985 + ? `${video.aspectRatio.width}:${video.aspectRatio.height}` 986 + : null 987 + ); 744 988 } catch (err) { 745 - console.error("stmt.run failed:", err); 746 - } 989 + console.error("stmt.run failed:", err); 990 + } 747 991 return; 748 992 } 749 993 default: { ··· 752 996 } 753 997 } 754 998 } 999 + 1000 + function queryProfileView( 1001 + did: string, 1002 + type: "" 1003 + ): ATPAPI.AppBskyActorDefs.ProfileView | undefined; 1004 + function queryProfileView( 1005 + did: string, 1006 + type: "Basic" 1007 + ): ATPAPI.AppBskyActorDefs.ProfileViewBasic | undefined; 1008 + function queryProfileView( 1009 + did: string, 1010 + type: "Detailed" 1011 + ): ATPAPI.AppBskyActorDefs.ProfileViewDetailed | undefined; 1012 + function queryProfileView( 1013 + did: string, 1014 + type: "" | "Basic" | "Detailed" 1015 + ): 1016 + | ATPAPI.AppBskyActorDefs.ProfileView 1017 + | ATPAPI.AppBskyActorDefs.ProfileViewBasic 1018 + | ATPAPI.AppBskyActorDefs.ProfileViewDetailed 1019 + | undefined { 1020 + if (!isRegisteredIndexUser(did)) return; 1021 + const db = getDbForDid(did); 1022 + if (!db) return; 1023 + 1024 + const stmt = db.prepare(` 1025 + SELECT * 1026 + FROM app_bsky_actor_profile 1027 + WHERE did = ? 1028 + LIMIT 1; 1029 + `); 1030 + 1031 + const row = stmt.get(did) as ProfileRow; 1032 + const profileView = queryProfileView(did, "Basic"); 1033 + if (!row || !row.cid || !profileView || !row.json) return; 1034 + const value = JSON.parse(row.json) as ATPAPI.AppBskyActorProfile.Record; 1035 + 1036 + // simulate different types returned 1037 + switch (type) { 1038 + case "": { 1039 + const result: ATPAPI.AppBskyActorDefs.ProfileView = { 1040 + $type: "app.bsky.actor.defs#profileView", 1041 + did: did, 1042 + handle: "@idiot.fuck", // TODO: Resolve user identity here for the handle 1043 + displayName: row.displayname ?? undefined, 1044 + description: row.description ?? undefined, 1045 + avatar: "https://google.com/", // create profile URL from resolved identity 1046 + //associated?: ProfileAssociated, 1047 + indexedAt: new Date(row.indexedat).toString() ?? undefined, 1048 + createdAt: row.createdat 1049 + ? new Date(row.createdat).toString() 1050 + : undefined, 1051 + //viewer?: ViewerState, 1052 + //labels?: ComAtprotoLabelDefs.Label[], 1053 + //verification?: VerificationState, 1054 + //status?: StatusView, 1055 + }; 1056 + return result; 1057 + } 1058 + case "Basic": { 1059 + const result: ATPAPI.AppBskyActorDefs.ProfileViewBasic = { 1060 + $type: "app.bsky.actor.defs#profileViewBasic", 1061 + did: did, 1062 + handle: "@idiot.fuck", // TODO: Resolve user identity here for the handle 1063 + displayName: row.displayname ?? undefined, 1064 + avatar: "https://google.com/", // create profile URL from resolved identity 1065 + //associated?: ProfileAssociated, 1066 + createdAt: row.createdat 1067 + ? new Date(row.createdat).toString() 1068 + : undefined, 1069 + //viewer?: ViewerState, 1070 + //labels?: ComAtprotoLabelDefs.Label[], 1071 + //verification?: VerificationState, 1072 + //status?: StatusView, 1073 + }; 1074 + return result; 1075 + } 1076 + case "Detailed": { 1077 + // Query for follower count from the backlink_skeleton table 1078 + const followersStmt = db.prepare(` 1079 + SELECT COUNT(*) as count 1080 + FROM backlink_skeleton 1081 + WHERE subdid = ? AND srccol = 'app.bsky.graph.follow' 1082 + `); 1083 + const followersResult = followersStmt.get(did) as { count: number }; 1084 + const followersCount = followersResult?.count ?? 0; 1085 + 1086 + // Query for following count from the app_bsky_graph_follow table 1087 + const followingStmt = db.prepare(` 1088 + SELECT COUNT(*) as count 1089 + FROM app_bsky_graph_follow 1090 + WHERE did = ? 1091 + `); 1092 + const followingResult = followingStmt.get(did) as { count: number }; 1093 + const followsCount = followingResult?.count ?? 0; 1094 + 1095 + // Query for post count from the app_bsky_feed_post table 1096 + const postsStmt = db.prepare(` 1097 + SELECT COUNT(*) as count 1098 + FROM app_bsky_feed_post 1099 + WHERE did = ? 1100 + `); 1101 + const postsResult = postsStmt.get(did) as { count: number }; 1102 + const postsCount = postsResult?.count ?? 0; 1103 + 1104 + // -- end of changes -- 1105 + const result: ATPAPI.AppBskyActorDefs.ProfileViewDetailed = { 1106 + $type: "app.bsky.actor.defs#profileViewDetailed", 1107 + did: did, 1108 + handle: "@idiot.fuck", // TODO: Resolve user identity here for the handle 1109 + displayName: row.displayname ?? undefined, 1110 + description: row.description ?? undefined, 1111 + avatar: "https://google.com/", // create profile URL from resolved identity 1112 + banner: "https://youtube.com/", // same here 1113 + followersCount: followersCount, 1114 + followsCount: followsCount, 1115 + postsCount: postsCount, 1116 + //associated?: ProfileAssociated, 1117 + //joinedViaStarterPack?: // AppBskyGraphDefs.StarterPackViewBasic; 1118 + indexedAt: new Date(row.indexedat).toString() ?? undefined, 1119 + createdAt: row.createdat 1120 + ? new Date(row.createdat).toString() 1121 + : undefined, 1122 + //viewer?: ViewerState, 1123 + //labels?: ComAtprotoLabelDefs.Label[], 1124 + pinnedPost: undefined, //row.; // TODO: i forgot to put pinnedp posts in db schema oops 1125 + //verification?: VerificationState, 1126 + //status?: StatusView, 1127 + }; 1128 + return result; 1129 + } 1130 + default: 1131 + throw new Error("Invalid type"); 1132 + } 1133 + } 1134 + 1135 + function queryPostView( 1136 + uri: string 1137 + ): ATPAPI.AppBskyFeedDefs.PostView | undefined { 1138 + const URI = new AtUri(uri); 1139 + const did = URI.host; 1140 + if (!isRegisteredIndexUser(did)) return; 1141 + const db = getDbForDid(did); 1142 + if (!db) return; 1143 + 1144 + const stmt = db.prepare(` 1145 + SELECT * 1146 + FROM app_bsky_feed_post 1147 + WHERE uri = ? 1148 + LIMIT 1; 1149 + `); 1150 + 1151 + const row = stmt.get(uri) as PostRow; 1152 + const profileView = queryProfileView(did, "Basic"); 1153 + if (!row || !row.cid || !profileView || !row.json) return; 1154 + const value = JSON.parse(row.json) as ATPAPI.AppBskyFeedPost.Record; 1155 + 1156 + const post: ATPAPI.AppBskyFeedDefs.PostView = { 1157 + uri: row.uri, 1158 + cid: row.cid, 1159 + author: profileView, 1160 + record: value, 1161 + indexedAt: new Date(row.indexedat).toISOString(), 1162 + // These can be filled in later if you support them 1163 + embed: value.embed, 1164 + }; 1165 + 1166 + return post; 1167 + }
+2 -2
main.ts
··· 48 48 ); 49 49 `) 50 50 51 - const userManager = new IndexServerUserManager(); 52 - userManager.coldStart(systemDB) 51 + export const indexerUserManager = new IndexServerUserManager(); 52 + indexerUserManager.coldStart(systemDB) 53 53 54 54 // should do both of these per user actually, since now each user has their own db 55 55 // also the set of records and backlinks to listen should be seperate between index and view servers