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

view server notifs

rimar1337 90bc2e09 52208474

+4 -2
index/spacedust.ts
··· 79 79 srccol, 80 80 suburi, 81 81 subdid, 82 - subcol 82 + subcol, 83 + indexedAt 83 84 ) VALUES ( 84 85 '${aturi}', 85 86 '${srcdid}', ··· 87 88 '${msg.link.source}', 88 89 '${subject}', 89 90 '${subdid}', 90 - '${subscol}' 91 + '${subscol}', 92 + '${Date.now()}' 91 93 ); 92 94 `); 93 95 //if (!value) return;
+5 -3
indexserver.ts
··· 1793 1793 srccol, 1794 1794 suburi, 1795 1795 subdid, 1796 - subcol 1797 - ) VALUES (?, ?, ?, ?, ?, ?, ?)`, 1796 + subcol, 1797 + indexedAt 1798 + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, 1798 1799 [ 1799 1800 srcUri, // full AT URI of the source record 1800 1801 srcDid, // did: of the source ··· 1803 1804 subUri, // full AT URI of the subject (linked record) 1804 1805 subDid, // did: of the subject 1805 1806 subCol, // subject collection (can be inferred or passed) 1807 + Date.now() 1806 1808 ] 1807 1809 ); 1808 1810 } ··· 2103 2105 ); 2104 2106 } 2105 2107 2106 - function uncid(anything: any): string | null { 2108 + export function uncid(anything: any): string | null { 2107 2109 return ( 2108 2110 ((anything as Record<string, unknown>)?.["$link"] as string | null) || null 2109 2111 );
-13
main-view.ts
··· 89 89 }); 90 90 } 91 91 console.log(`request for "${pathname}"`); 92 - 93 - let authdid: string | undefined = undefined; 94 - try { 95 - authdid = (await getAuthenticatedDid(req)) ?? undefined; 96 - } catch (_e) { 97 - // nothing lol 98 - } 99 - const auth = authdid 100 - ? genericViewServer.handlesDid(authdid) 101 - ? authdid 102 - : undefined 103 - : undefined; 104 - console.log("authed:", auth); 105 92 return await genericViewServer.viewServerHandler(req); 106 93 } 107 94 );
+4 -4
utils/dbuser.ts
··· 32 32 avatarcid TEXT, 33 33 avatarmime TEXT, 34 34 bannercid TEXT, 35 - bannermime TEXT 36 - -- TODO please add pinned posts 35 + bannermime TEXT, 36 + pinned TEXT 37 37 ); 38 38 ${createIndexINE} idx_actor_profile_did ON app_bsky_actor_profile(did); 39 39 ··· 142 142 srccol TEXT, 143 143 suburi TEXT, 144 144 subdid TEXT, 145 - subcol TEXT 146 - -- TODO please add indexedAt 145 + subcol TEXT, 146 + indexedAt INTEGER NOT NULL 147 147 ); 148 148 ${createIndexINE} idx_backlink_subdid_mod ON backlink_skeleton(subdid, srcdid); 149 149 ${createIndexINE} idx_backlink_suburi_mod ON backlink_skeleton(suburi, srcdid);
+408 -32
viewserver.ts
··· 11 11 getSlingshotRecord, 12 12 withCors, 13 13 } from "./utils/server.ts"; 14 + import QuickLRU from "npm:quick-lru"; 14 15 import { validateRecord } from "./utils/records.ts"; 15 16 import { indexHandlerContext } from "./index/types.ts"; 16 17 import { Database } from "jsr:@db/sqlite@0.11"; 17 18 import { JetstreamManager, SpacedustManager } from "./utils/sharders.ts"; 18 19 import { SpacedustLinkMessage } from "./index/spacedust.ts"; 19 20 import { setupUserDb } from "./utils/dbuser.ts"; 21 + import { config } from "./config.ts"; 22 + import { AtUri } from "npm:@atproto/api"; 23 + import { CID } from "../../Library/Caches/deno/npm/registry.npmjs.org/multiformats/9.9.0/cjs/src/cid.js"; 24 + import { uncid } from "./indexserver.ts"; 25 + import { getAuthenticatedDid } from "./utils/auth.ts"; 20 26 21 27 export interface ViewServerConfig { 22 28 baseDbPath: string; ··· 82 88 const searchParams = searchParamsToJson(url.searchParams); 83 89 const jsonUntyped = searchParams; 84 90 91 + let tempauthdid: string | undefined = undefined; 92 + try { 93 + tempauthdid = (await getAuthenticatedDid(req)) ?? undefined; 94 + } catch (_e) { 95 + // nothing lol 96 + } 97 + const authdid = tempauthdid 98 + ? this.handlesDid(tempauthdid) 99 + ? tempauthdid 100 + : undefined 101 + : undefined; 102 + console.log("authed:", authdid); 103 + 85 104 if (xrpcMethod === "app.bsky.unspecced.getTrendingTopics") { 86 105 // const jsonTyped = 87 106 // jsonUntyped as ViewServerTypes.AppBskyUnspeccedGetTrendingTopics.QueryParams; ··· 96 115 }, 97 116 { 98 117 $type: "app.bsky.unspecced.defs#trendingTopic", 99 - topic: "Red Dwarf Lite", 100 - displayName: "Red Dwarf Lite", 101 - description: "Red Dwarf Lite", 102 - link: "https://reddwarflite.whey.party/", 118 + topic: "this View Server url", 119 + displayName: "this View Server url", 120 + description: "this View Server url", 121 + link: config.viewServer.host, 122 + }, 123 + { 124 + $type: "app.bsky.unspecced.defs#trendingTopic", 125 + topic: "this social-app fork url", 126 + displayName: "this social-app fork url", 127 + description: "this social-app fork url", 128 + link: "https://github.com/rimar1337/social-app/tree/publicappview-colorable", 103 129 }, 104 130 { 105 131 $type: "app.bsky.unspecced.defs#trendingTopic", ··· 133 159 JSON.stringify({ 134 160 error: "XRPCNotSupported", 135 161 message: 136 - "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", 162 + "(no auth) 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", 137 163 }), 138 164 { 139 165 status: 404, ··· 152 178 JSON.stringify({ 153 179 error: "XRPCNotSupported", 154 180 message: 155 - "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", 181 + "(getservices / getconfig) 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", 156 182 }), 157 183 { 158 184 status: 404, ··· 295 321 } 296 322 297 323 const postResp = await ky 324 + // TODO aaaaaa dont do this please use the new getServiceEndpointFromIdentity() 298 325 .get(`https://api.bsky.app/xrpc/app.bsky.feed.getPosts`, { 299 326 // headers: { 300 327 // Authorization: proxyauth, ··· 332 359 const jsonTyped = 333 360 jsonUntyped as ViewServerTypes.AppBskyActorGetProfile.QueryParams; 334 361 335 - const userindexservice = ""; 336 - const isbskyfallback = true; 337 - if (isbskyfallback) { 338 - return this.sendItToApiBskyApp(req); 339 - } 340 - 341 362 const response: ViewServerTypes.AppBskyActorGetProfile.OutputSchema = 342 - {}; 363 + ((await this.resolveGetProfiles([jsonTyped.actor])) ?? [])[0]; 343 364 344 365 return new Response(JSON.stringify(response), { 345 366 headers: withCors({ "Content-Type": "application/json" }), ··· 350 371 const jsonTyped = 351 372 jsonUntyped as ViewServerTypes.AppBskyActorGetProfiles.QueryParams; 352 373 353 - const userindexservice = ""; 354 - const isbskyfallback = true; 355 - if (isbskyfallback) { 356 - return this.sendItToApiBskyApp(req); 357 - } 358 - 359 - const response: ViewServerTypes.AppBskyActorGetProfiles.OutputSchema = 360 - {}; 374 + const response: ViewServerTypes.AppBskyActorGetProfiles.OutputSchema = { 375 + profiles: (await this.resolveGetProfiles(jsonTyped.actors)) ?? [], 376 + }; 361 377 362 378 return new Response(JSON.stringify(response), { 363 379 headers: withCors({ "Content-Type": "application/json" }), ··· 443 459 // headers: withCors({ "Content-Type": "application/json" }), 444 460 // }); 445 461 // } 446 - // case "app.bsky.notification.listNotifications": { 447 - // const jsonTyped = 448 - // jsonUntyped as ViewServerTypes.AppBskyNotificationListNotifications.QueryParams; 462 + case "app.bsky.notification.listNotifications": { 463 + if (!authdid) return new Response("Not Found", { status: 404 }); 464 + const jsonTyped = 465 + jsonUntyped as ViewServerTypes.AppBskyNotificationListNotifications.QueryParams; 466 + 467 + const response: ViewServerTypes.AppBskyNotificationListNotifications.OutputSchema = 468 + await this.queryNotificationsList(authdid, jsonTyped.cursor); 449 469 450 - // const response: ViewServerTypes.AppBskyNotificationListNotifications.OutputSchema = {}; 470 + return new Response(JSON.stringify(response), { 471 + headers: withCors({ "Content-Type": "application/json" }), 472 + }); 473 + } 474 + case "app.bsky.feed.getPosts": { 475 + const jsonTyped = 476 + jsonUntyped as ViewServerTypes.AppBskyFeedGetPosts.QueryParams; 477 + const inputUris = Array.isArray(jsonTyped.uris) 478 + ? jsonTyped.uris 479 + : [jsonTyped.uris]; 480 + const response: ViewServerTypes.AppBskyFeedGetPosts.OutputSchema = { 481 + posts: await this.resolveGetPosts(inputUris), 482 + }; 451 483 452 - // return new Response(JSON.stringify(response), { 453 - // headers: withCors({ "Content-Type": "application/json" }), 454 - // }); 455 - // } 484 + return new Response(JSON.stringify(response), { 485 + headers: withCors({ "Content-Type": "application/json" }), 486 + }); 487 + } 456 488 457 489 case "app.bsky.unspecced.getConfig": { 458 490 const jsonTyped = ··· 530 562 JSON.stringify({ 531 563 error: "XRPCNotSupported", 532 564 message: 533 - "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", 565 + "(default) 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", 534 566 }), 535 567 { 536 568 status: 404, ··· 620 652 public handlesDid(did: string): boolean { 621 653 return this.userManager.handlesDid(did); 622 654 } 655 + 656 + async resolveGetPosts( 657 + uris: string[] 658 + ): Promise<ATPAPI.AppBskyFeedDefs.PostView[]> { 659 + const grouped: Record<string, string[]> = {}; 660 + 661 + // Group URIs by resolved endpoint 662 + for (const uri of uris) { 663 + const did = new AtUri(uri).host; 664 + const endpoint = await getSkyliteEndpoint(did); 665 + if (!endpoint) continue; 666 + 667 + if (!grouped[endpoint]) { 668 + grouped[endpoint] = []; 669 + } 670 + grouped[endpoint].push(uri); 671 + } 672 + 673 + const postviews: ATPAPI.AppBskyFeedDefs.PostView[] = []; 674 + 675 + // Fetch posts per endpoint 676 + for (const [endpoint, urisForEndpoint] of Object.entries(grouped)) { 677 + const query = urisForEndpoint 678 + .map((u) => `uris=${encodeURIComponent(u)}`) 679 + .join("&"); 680 + 681 + const url = `${endpoint}/xrpc/app.bsky.feed.getPosts?${query}`; 682 + const resp = await fetch(url); 683 + if (!resp.ok) { 684 + throw new Error( 685 + `Failed to fetch posts from ${endpoint} for uris=${urisForEndpoint.join( 686 + "," 687 + )}` 688 + ); 689 + } 690 + 691 + const raw = 692 + (await resp.json()) as ATPAPI.AppBskyFeedGetPosts.OutputSchema; 693 + postviews.push(...raw.posts); 694 + } 695 + 696 + return postviews; 697 + } 698 + 699 + async resolveGetProfiles( 700 + dids: string[] 701 + ): Promise<ATPAPI.AppBskyActorDefs.ProfileViewDetailed[] | undefined> { 702 + const profiles: ATPAPI.AppBskyActorDefs.ProfileViewDetailed[] = []; 703 + 704 + for (const did of dids) { 705 + const endpoint = await getSkyliteEndpoint(did); 706 + const url = `${endpoint}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent( 707 + did 708 + )}`; 709 + const resp = await fetch(url); 710 + if (!resp.ok) 711 + throw new Error(`Failed to fetch profile for ${did} via ${url}`); 712 + 713 + const raw = 714 + (await resp.json()) as ATPAPI.AppBskyActorGetProfile.OutputSchema; 715 + profiles.push(raw); 716 + } 717 + 718 + return profiles; 719 + } 720 + async queryNotificationsList( 721 + did: string, 722 + cursor?: string 723 + ): Promise<ATPAPI.AppBskyNotificationListNotifications.OutputSchema> { 724 + if (!this.handlesDid(did)) { 725 + return { notifications: [] }; 726 + } 727 + const db = this.userManager.getDbForDid(did); 728 + if (!db) { 729 + return { notifications: [] }; 730 + } 731 + 732 + const NOTIFS_LIMIT = 30; 733 + const mapReason = ( 734 + field: string 735 + ): 736 + | ATPAPI.AppBskyNotificationListNotifications.Notification["reason"] 737 + | undefined => { 738 + switch (field) { 739 + //'like' | 'repost' | 'follow' | 'mention' | 'reply' | 'quote' | 'starterpack-joined' | 'verified' | 'unverified' | 'like-via-repost' | 'repost-via-repost' | 740 + case "app.bsky.feed.like:subject.uri": 741 + return "like"; 742 + case "app.bsky.feed.like:via.uri": 743 + return "liked-via-repost"; 744 + case "app.bsky.feed.repost:subject.uri": 745 + return "repost"; 746 + case "app.bsky.feed.repost:via.uri": 747 + return "repost-via-repost"; 748 + case "app.bsky.feed.post:reply.root.uri": 749 + return "reply"; 750 + case "app.bsky.feed.post:reply.parent.uri": 751 + return "reply"; 752 + case "app.bsky.feed.post:embed.media.record.record.uri": 753 + return "quote"; 754 + case "app.bsky.feed.post:embed.record.uri": 755 + return "quote"; 756 + //case"app.bsky.feed.threadgate:post": return "threadgate subject 757 + //case"app.bsky.feed.threadgate:hiddenReplies": return "threadgate items (array) 758 + case "app.bsky.feed.post:facets.features.did": 759 + return "mention"; 760 + //case"app.bsky.graph.block:subject": return "blocks 761 + case "app.bsky.graph.follow:subject": 762 + return "follow"; 763 + //case"app.bsky.graph.listblock:subject": return "list item (blocks) 764 + //case"app.bsky.graph.listblock:list": return "blocklist mention (might not exist) 765 + //case"app.bsky.graph.listitem:subject": return "list item (blocks) 766 + //"app.bsky.graph.listitem:list": return "list mention 767 + // case "like": return "like"; 768 + // case "repost": return "repost"; 769 + // case "follow": return "follow"; 770 + // case "replyparent": return "reply"; 771 + // case "replyroot": return "reply"; 772 + // case "mention": return "mention"; 773 + default: 774 + return undefined; 775 + } 776 + }; 777 + 778 + // --- Build Query --- 779 + let query = ` 780 + SELECT srcuri, suburi, srcfield, indexedat 781 + FROM backlink_skeleton 782 + WHERE 783 + -- Find actions targeting the user's content or profile 784 + (suburi LIKE ? OR suburi = ?) 785 + -- Exclude notifications from the user themselves 786 + AND srcuri NOT LIKE ? 787 + `; 788 + const params: (string | number)[] = [`at://${did}/%`, did, `at://${did}/%`]; 789 + 790 + if (cursor) { 791 + const [indexedat, srcuri] = cursor.split("::"); 792 + if (indexedat && srcuri) { 793 + query += ` AND (indexedat < ? OR (indexedat = ? AND srcuri < ?))`; 794 + params.push(parseInt(indexedat, 10), parseInt(indexedat, 10), srcuri); 795 + } 796 + } 797 + 798 + query += ` ORDER BY indexedat DESC, srcuri DESC LIMIT ${NOTIFS_LIMIT}`; 799 + 800 + // --- Fetch and Process --- 801 + const stmt = db.prepare(query); 802 + const rows = stmt.all(...params) as { 803 + srcuri: string; 804 + suburi: string; 805 + srcfield: string; 806 + indexedat: number; 807 + }[]; 808 + 809 + const notificationPromises = rows.map(async (row) => { 810 + const reason = mapReason(row.srcfield); 811 + // Skip if it's a backlink type we don't have a notification for 812 + if (!reason) return null; 813 + 814 + const srcURI = new AtUri(row.srcuri); 815 + const [author, getrecord] = await Promise.all([ 816 + await this.resolveProfileView(srcURI.host, ""), 817 + await getSlingshotRecord(srcURI.host, srcURI.collection, srcURI.rkey), 818 + // TODO: shit histhi shitshit 819 + //this.queryProfile(authorDid), 820 + // Assumes you have a method to fetch the raw record content by URI and CID 821 + // This would fetch the like, repost, follow, or post record itself. 822 + //this.getRecord(row.srcuri, row.srccid), 823 + ]); 824 + const reasonsubject = row.suburi; 825 + // If we can't resolve the author or the record, we can't form a valid notification 826 + if (!author || !getrecord || !reason || !reasonsubject) return null; 827 + 828 + return { 829 + uri: row.srcuri, 830 + cid: getrecord.cid, 831 + author: author, 832 + reason: reason, 833 + // The reasonSubject is the URI of the post that was liked, reposted, or replied to 834 + reasonSubject: reasonsubject, 835 + record: getrecord.value, 836 + isRead: false, // Placeholder for read-state logic 837 + indexedAt: new Date(/*row.indexedat*/).toISOString(), 838 + // labels: [], // Placeholder for label logic 839 + } as ATPAPI.AppBskyNotificationListNotifications.Notification; 840 + }); 841 + 842 + const notifications = (await Promise.all(notificationPromises)).filter( 843 + (n): n is ATPAPI.AppBskyNotificationListNotifications.Notification => !!n 844 + ); 845 + 846 + // --- Create next cursor --- 847 + const lastItem = rows[rows.length - 1]; 848 + const nextCursor = lastItem 849 + ? `${lastItem.indexedat}::${lastItem.srcuri}` 850 + : undefined; 851 + 852 + return { 853 + cursor: nextCursor, 854 + notifications: notifications, 855 + }; 856 + } 857 + async resolveProfileView( 858 + did: string, 859 + type: "" 860 + ): Promise<ATPAPI.AppBskyActorDefs.ProfileView | undefined>; 861 + async resolveProfileView( 862 + did: string, 863 + type: "Basic" 864 + ): Promise<ATPAPI.AppBskyActorDefs.ProfileViewBasic | undefined>; 865 + async resolveProfileView( 866 + did: string, 867 + type: "Detailed" 868 + ): Promise<ATPAPI.AppBskyActorDefs.ProfileViewDetailed | undefined>; 869 + async resolveProfileView( 870 + did: string, 871 + type: "" | "Basic" | "Detailed" 872 + ): Promise< 873 + | ATPAPI.AppBskyActorDefs.ProfileView 874 + | ATPAPI.AppBskyActorDefs.ProfileViewBasic 875 + | ATPAPI.AppBskyActorDefs.ProfileViewDetailed 876 + | undefined 877 + > { 878 + const record = ( 879 + await getSlingshotRecord(did, "app.bsky.actor.profile", "self") 880 + ).value as ATPAPI.AppBskyActorProfile.Record; 881 + 882 + const identity = await resolveIdentity(did); 883 + const avatarcid = uncid(record.avatar?.ref); 884 + const avatar = avatarcid 885 + ? buildBlobUrl(identity.pds, identity.did, avatarcid) 886 + : undefined; 887 + const bannercid = uncid(record.banner?.ref); 888 + const banner = bannercid 889 + ? buildBlobUrl(identity.pds, identity.did, bannercid) 890 + : undefined; 891 + // simulate different types returned 892 + switch (type) { 893 + case "": { 894 + const result: ATPAPI.AppBskyActorDefs.ProfileView = { 895 + $type: "app.bsky.actor.defs#profileView", 896 + did: did, 897 + handle: identity.handle, 898 + displayName: record.displayName ?? identity.handle, 899 + description: record.description ?? undefined, 900 + avatar: avatar, // create profile URL from resolved identity 901 + //associated?: ProfileAssociated, 902 + indexedAt: record.createdAt 903 + ? new Date(record.createdAt).toISOString() 904 + : undefined, 905 + createdAt: record.createdAt 906 + ? new Date(record.createdAt).toISOString() 907 + : undefined, 908 + //viewer?: ViewerState, 909 + //labels?: ComAtprotoLabelDefs.Label[], 910 + //verification?: VerificationState, 911 + //status?: StatusView, 912 + }; 913 + return result; 914 + } 915 + case "Basic": { 916 + const result: ATPAPI.AppBskyActorDefs.ProfileViewBasic = { 917 + $type: "app.bsky.actor.defs#profileViewBasic", 918 + did: did, 919 + handle: identity.handle, 920 + displayName: record.displayName ?? identity.handle, 921 + avatar: avatar, // create profile URL from resolved identity 922 + //associated?: ProfileAssociated, 923 + createdAt: record.createdAt 924 + ? new Date(record.createdAt).toISOString() 925 + : undefined, 926 + //viewer?: ViewerState, 927 + //labels?: ComAtprotoLabelDefs.Label[], 928 + //verification?: VerificationState, 929 + //status?: StatusView, 930 + }; 931 + return result; 932 + } 933 + case "Detailed": { 934 + const response: ViewServerTypes.AppBskyActorGetProfile.OutputSchema = 935 + ((await this.resolveGetProfiles([did])) ?? [])[0]; 936 + return response; 937 + } 938 + default: 939 + throw new Error("Invalid type"); 940 + } 941 + } 623 942 } 624 943 625 944 export class ViewServerUserManager { ··· 767 1086 srccol, 768 1087 suburi, 769 1088 subdid, 770 - subcol 771 - ) VALUES (?, ?, ?, ?, ?, ?, ?)`, 1089 + subcol, 1090 + indexedAt 1091 + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, 772 1092 [ 773 1093 srcUri, // full AT URI of the source record 774 1094 srcDid, // did: of the source ··· 777 1097 subUri, // full AT URI of the subject (linked record) 778 1098 subDid, // did: of the subject 779 1099 subCol, // subject collection (can be inferred or passed) 1100 + Date.now(), 780 1101 ] 781 1102 ); 782 1103 } ··· 850 1171 this.db.close?.(); 851 1172 } 852 1173 } 1174 + 1175 + async function getServiceEndpointFromIdentity( 1176 + did: string, 1177 + kind: "skylite_index" | "bsky_appview" 1178 + ): Promise<string | null> { 1179 + const identity = await resolveIdentity(did); 1180 + const declUrl = `${identity.pds}/xrpc/com.atproto.repo.getRecord?repo=${identity.did}&collection=party.whey.skylite.declaration&rkey=self`; 1181 + 1182 + const data = (await cachedFetch(declUrl)) as any; 1183 + //if (!resp.ok) throw new Error(`Failed to fetch declaration for ${did}`); 1184 + //const data = await resp.json(); 1185 + 1186 + const svc = data?.value?.service?.find((s: any) => s.id === `#${kind}`); 1187 + return svc?.serviceEndpoint ?? null; 1188 + } 1189 + 1190 + const cache = new QuickLRU({ maxSize: 10000 }); 1191 + 1192 + async function getSkyliteEndpoint(did: string): Promise<string | null> { 1193 + if (cache.has(did)) return cache.get(did) as string; 1194 + for (const resolver of config.viewServer.indexPriority) { 1195 + try { 1196 + const [prefix, suffix] = resolver.split("#") as [ 1197 + "user" | `did:web:${string}`, 1198 + "skylite_index" | "bsky_appview" 1199 + ]; 1200 + if (prefix === "user") { 1201 + return await getServiceEndpointFromIdentity(did, suffix); 1202 + } else if (prefix.startsWith("did:web:")) { 1203 + // map did:web:foo.com -> https://foo.com 1204 + return prefix.replace("did:web:", "https://"); 1205 + } 1206 + } catch (err) { 1207 + // continue to next resolver 1208 + continue; 1209 + } 1210 + } 1211 + return null; //throw new Error(`No endpoint found for ${resolver}`); 1212 + } 1213 + 1214 + // export interface Notification { 1215 + // $type?: 'app.bsky.notification.listNotifications#notification'; 1216 + // uri: string; 1217 + // cid: string; 1218 + // author: AppBskyActorDefs.ProfileView; 1219 + // /** The reason why this notification was delivered - e.g. your post was liked, or you received a new follower. */ 1220 + // reason: 'like' | 'repost' | 'follow' | 'mention' | 'reply' | 'quote' | 'starterpack-joined' | 'verified' | 'unverified' | 'like-via-repost' | 'repost-via-repost' | (string & {}); 1221 + // reasonSubject?: string; 1222 + // record: { 1223 + // [_ in string]: unknown; 1224 + // }; 1225 + // isRead: boolean; 1226 + // indexedAt: string; 1227 + // labels?: ComAtprotoLabelDefs.Label[]; 1228 + // }