+4
-2
index/spacedust.ts
+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
+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
-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
+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
+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
+
// }