+240
-155
indexserver.ts
+240
-155
indexserver.ts
···
1
1
import { indexHandlerContext } from "./index/types.ts";
2
2
3
3
import { assertRecord, validateRecord } from "./utils/records.ts";
4
-
import { searchParamsToJson, withCors } from "./utils/server.ts";
4
+
import {
5
+
buildBlobUrl,
6
+
resolveIdentity,
7
+
searchParamsToJson,
8
+
withCors,
9
+
} from "./utils/server.ts";
5
10
import * as IndexServerTypes from "./utils/indexservertypes.ts";
6
11
import { Database } from "jsr:@db/sqlite@0.11";
7
12
import { setupUserDb } from "./utils/dbuser.ts";
···
18
23
export interface IndexServerConfig {
19
24
baseDbPath: string;
20
25
systemDbPath: string;
21
-
jetstreamUrl: string;
22
26
}
23
27
24
28
interface BaseRow {
···
86
90
}
87
91
88
92
// We will move all the global functions into this class as methods...
89
-
indexServerHandler(req: Request): Response {
93
+
async indexServerHandler(req: Request): Promise<Response> {
90
94
const url = new URL(req.url);
91
95
const pathname = url.pathname;
92
96
//const bskyUrl = `https://api.bsky.app${pathname}${url.search}`;
···
103
107
const jsonTyped =
104
108
jsonUntyped as IndexServerTypes.AppBskyActorGetProfile.QueryParams;
105
109
106
-
const res = this.queryProfileView(jsonTyped.actor, "Detailed");
110
+
const res = await this.queryProfileView(jsonTyped.actor, "Detailed");
107
111
if (!res)
108
112
return new Response(
109
113
JSON.stringify({
···
126
130
jsonUntyped as IndexServerTypes.AppBskyActorGetProfiles.QueryParams;
127
131
128
132
if (typeof jsonUntyped?.actors === "string") {
129
-
const res = this.queryProfileView(
133
+
const res = await this.queryProfileView(
130
134
jsonUntyped.actors as string,
131
135
"Detailed"
132
136
);
···
151
155
}
152
156
153
157
const res: ATPAPI.AppBskyActorDefs.ProfileViewDetailed[] =
154
-
jsonTyped.actors
155
-
.map((actor) => {
156
-
return this.queryProfileView(actor, "Detailed");
157
-
})
158
-
.filter(
159
-
(x): x is ATPAPI.AppBskyActorDefs.ProfileViewDetailed =>
160
-
x !== undefined
161
-
);
158
+
await Promise.all(
159
+
jsonTyped.actors
160
+
.map(async (actor) => {
161
+
return await this.queryProfileView(actor, "Detailed");
162
+
})
163
+
.filter(
164
+
(
165
+
x
166
+
): x is Promise<ATPAPI.AppBskyActorDefs.ProfileViewDetailed> =>
167
+
x !== undefined
168
+
)
169
+
);
162
170
163
171
if (!res)
164
172
return new Response(
···
184
192
const jsonTyped =
185
193
jsonUntyped as IndexServerTypes.AppBskyFeedGetActorFeeds.QueryParams;
186
194
187
-
const qresult = this.queryActorFeeds(jsonTyped.actor);
195
+
const qresult = await this.queryActorFeeds(jsonTyped.actor);
188
196
189
197
const response: IndexServerTypes.AppBskyFeedGetActorFeeds.OutputSchema =
190
198
{
···
199
207
const jsonTyped =
200
208
jsonUntyped as IndexServerTypes.AppBskyFeedGetFeedGenerator.QueryParams;
201
209
202
-
const qresult = this.queryFeedGenerator(jsonTyped.feed);
210
+
const qresult = await this.queryFeedGenerator(jsonTyped.feed);
203
211
if (!qresult) {
204
212
return new Response(
205
213
JSON.stringify({
···
227
235
const jsonTyped =
228
236
jsonUntyped as IndexServerTypes.AppBskyFeedGetFeedGenerators.QueryParams;
229
237
230
-
const qresult = this.queryFeedGenerators(jsonTyped.feeds);
238
+
const qresult = await this.queryFeedGenerators(jsonTyped.feeds);
231
239
if (!qresult) {
232
240
return new Response(
233
241
JSON.stringify({
···
254
262
jsonUntyped as IndexServerTypes.AppBskyFeedGetPosts.QueryParams;
255
263
256
264
const posts: IndexServerTypes.AppBskyFeedGetPosts.OutputSchema["posts"] =
257
-
jsonTyped.uris
258
-
.map((uri) => {
259
-
return this.queryPostView(uri);
260
-
})
261
-
.filter(Boolean) as ATPAPI.AppBskyFeedDefs.PostView[];
265
+
(
266
+
await Promise.all(
267
+
jsonTyped.uris.map((uri) => this.queryPostView(uri))
268
+
)
269
+
).filter((p): p is ATPAPI.AppBskyFeedDefs.PostView => Boolean(p));
262
270
263
271
const response: IndexServerTypes.AppBskyFeedGetPosts.OutputSchema = {
264
272
posts,
···
274
282
275
283
// TODO: not partial yet, currently skips refs
276
284
277
-
const qresult = this.queryActorLikesPartial(
285
+
const qresult = await this.queryActorLikesPartial(
278
286
jsonTyped.actor,
279
287
jsonTyped.cursor
280
288
);
···
306
314
307
315
// TODO: not partial yet, currently skips refs
308
316
309
-
const qresult = this.queryAuthorFeedPartial(jsonTyped.actor, jsonTyped.cursor);
317
+
const qresult = await this.queryAuthorFeedPartial(
318
+
jsonTyped.actor,
319
+
jsonTyped.cursor
320
+
);
310
321
if (!qresult) {
311
322
return new Response(
312
323
JSON.stringify({
···
363
374
364
375
// TODO: not partial yet, currently skips refs
365
376
366
-
const qresult = this.queryPostThreadPartial(jsonTyped.uri);
377
+
const qresult = await this.queryPostThreadPartial(jsonTyped.uri);
367
378
if (!qresult) {
368
379
return new Response(
369
380
JSON.stringify({
···
388
399
389
400
// TODO: not partial yet, currently skips refs
390
401
391
-
const qresult = this.queryQuotes(jsonTyped.uri);
402
+
const qresult = await this.queryQuotes(jsonTyped.uri);
392
403
if (!qresult) {
393
404
return new Response(
394
405
JSON.stringify({
···
418
429
419
430
// TODO: not partial yet, currently skips refs
420
431
421
-
const qresult = this.queryReposts(jsonTyped.uri);
432
+
const qresult = await this.queryReposts(jsonTyped.uri);
422
433
if (!qresult) {
423
434
return new Response(
424
435
JSON.stringify({
···
870
881
}
871
882
872
883
// user data
873
-
queryProfileView(
884
+
async queryProfileView(
874
885
did: string,
875
886
type: ""
876
-
): ATPAPI.AppBskyActorDefs.ProfileView | undefined;
877
-
queryProfileView(
887
+
): Promise<ATPAPI.AppBskyActorDefs.ProfileView | undefined>;
888
+
async queryProfileView(
878
889
did: string,
879
890
type: "Basic"
880
-
): ATPAPI.AppBskyActorDefs.ProfileViewBasic | undefined;
881
-
queryProfileView(
891
+
): Promise<ATPAPI.AppBskyActorDefs.ProfileViewBasic | undefined>;
892
+
async queryProfileView(
882
893
did: string,
883
894
type: "Detailed"
884
-
): ATPAPI.AppBskyActorDefs.ProfileViewDetailed | undefined;
885
-
queryProfileView(
895
+
): Promise<ATPAPI.AppBskyActorDefs.ProfileViewDetailed | undefined>;
896
+
async queryProfileView(
886
897
did: string,
887
898
type: "" | "Basic" | "Detailed"
888
-
):
899
+
): Promise<
889
900
| ATPAPI.AppBskyActorDefs.ProfileView
890
901
| ATPAPI.AppBskyActorDefs.ProfileViewBasic
891
902
| ATPAPI.AppBskyActorDefs.ProfileViewDetailed
892
-
| undefined {
903
+
| undefined
904
+
> {
893
905
if (!this.isRegisteredIndexUser(did)) return;
894
906
const db = this.userManager.getDbForDid(did);
895
907
if (!db) return;
···
903
915
904
916
const row = stmt.get(did) as ProfileRow;
905
917
918
+
const identity = await resolveIdentity(did);
919
+
const avatar = row.avatarcid ? buildBlobUrl(
920
+
identity.pds,
921
+
identity.did,
922
+
row.avatarcid
923
+
) : undefined
924
+
const banner = row.bannercid ? buildBlobUrl(
925
+
identity.pds,
926
+
identity.did,
927
+
row.bannercid
928
+
) : undefined
906
929
// simulate different types returned
907
930
switch (type) {
908
931
case "": {
909
932
const result: ATPAPI.AppBskyActorDefs.ProfileView = {
910
933
$type: "app.bsky.actor.defs#profileView",
911
934
did: did,
912
-
handle: "idiot.fuck.shit.example.com", // TODO: Resolve user identity here for the handle
935
+
handle: identity.handle, // TODO: Resolve user identity here for the handle
913
936
displayName: row.displayname ?? undefined,
914
937
description: row.description ?? undefined,
915
-
avatar: "https://google.com/", // create profile URL from resolved identity
938
+
avatar: avatar, // create profile URL from resolved identity
916
939
//associated?: ProfileAssociated,
917
940
indexedAt: row.createdat
918
941
? new Date(row.createdat).toISOString()
···
931
954
const result: ATPAPI.AppBskyActorDefs.ProfileViewBasic = {
932
955
$type: "app.bsky.actor.defs#profileViewBasic",
933
956
did: did,
934
-
handle: "idiot.fuck.shit.example.com", // TODO: Resolve user identity here for the handle
957
+
handle: identity.handle, // TODO: Resolve user identity here for the handle
935
958
displayName: row.displayname ?? undefined,
936
-
avatar: "https://google.com/", // create profile URL from resolved identity
959
+
avatar: avatar, // create profile URL from resolved identity
937
960
//associated?: ProfileAssociated,
938
961
createdAt: row.createdat
939
962
? new Date(row.createdat).toISOString()
···
976
999
const result: ATPAPI.AppBskyActorDefs.ProfileViewDetailed = {
977
1000
$type: "app.bsky.actor.defs#profileViewDetailed",
978
1001
did: did,
979
-
handle: "idiot.fuck.shit.example.com", // TODO: Resolve user identity here for the handle
1002
+
handle: identity.handle, // TODO: Resolve user identity here for the handle
980
1003
displayName: row.displayname ?? undefined,
981
1004
description: row.description ?? undefined,
982
-
avatar: "https://google.com/", // TODO: create profile URL from resolved identity
983
-
banner: "https://youtube.com/", // same here
1005
+
avatar: avatar, // TODO: create profile URL from resolved identity
1006
+
banner: banner, // same here
984
1007
followersCount: followersCount,
985
1008
followsCount: followsCount,
986
1009
postsCount: postsCount,
···
1006
1029
}
1007
1030
1008
1031
// post hydration
1009
-
queryPostView(uri: string): ATPAPI.AppBskyFeedDefs.PostView | undefined {
1032
+
async queryPostView(
1033
+
uri: string
1034
+
): Promise<ATPAPI.AppBskyFeedDefs.PostView | undefined> {
1010
1035
const URI = new AtUri(uri);
1011
1036
const did = URI.host;
1012
1037
if (!this.isRegisteredIndexUser(did)) return;
···
1021
1046
`);
1022
1047
1023
1048
const row = stmt.get(uri) as PostRow;
1024
-
const profileView = this.queryProfileView(did, "Basic");
1049
+
const profileView = await this.queryProfileView(did, "Basic");
1025
1050
if (!row || !row.cid || !profileView || !row.json) return;
1026
1051
const value = JSON.parse(row.json) as ATPAPI.AppBskyFeedPost.Record;
1027
1052
···
1048
1073
return post;
1049
1074
}
1050
1075
1051
-
queryFeedViewPost(
1076
+
async queryFeedViewPost(
1052
1077
uri: string
1053
-
): ATPAPI.AppBskyFeedDefs.FeedViewPost | undefined {
1054
-
const post = this.queryPostView(uri);
1078
+
): Promise<ATPAPI.AppBskyFeedDefs.FeedViewPost | undefined> {
1079
+
const post = await this.queryPostView(uri);
1055
1080
if (!post) return;
1056
1081
1057
1082
const feedviewpost: ATPAPI.AppBskyFeedDefs.FeedViewPost = {
···
1080
1105
1081
1106
// user feedgens
1082
1107
1083
-
queryActorFeeds(did: string): ATPAPI.AppBskyFeedDefs.GeneratorView[] {
1108
+
async queryActorFeeds(
1109
+
did: string
1110
+
): Promise<ATPAPI.AppBskyFeedDefs.GeneratorView[]> {
1084
1111
if (!this.isRegisteredIndexUser(did)) return [];
1085
1112
const db = this.userManager.getDbForDid(did);
1086
1113
if (!db) return [];
···
1093
1120
`);
1094
1121
1095
1122
const rows = stmt.all(did) as unknown as GeneratorRow[];
1096
-
const creatorView = this.queryProfileView(did, "Basic");
1123
+
const creatorView = await this.queryProfileView(did, "Basic");
1097
1124
if (!creatorView) return [];
1098
1125
1099
1126
return rows
···
1123
1150
.filter((v): v is ATPAPI.AppBskyFeedDefs.GeneratorView => !!v);
1124
1151
}
1125
1152
1126
-
queryFeedGenerator(
1153
+
async queryFeedGenerator(
1127
1154
uri: string
1128
-
): ATPAPI.AppBskyFeedDefs.GeneratorView | undefined {
1129
-
return this.queryFeedGenerators([uri])[0];
1155
+
): Promise<ATPAPI.AppBskyFeedDefs.GeneratorView | undefined> {
1156
+
const gens = await this.queryFeedGenerators([uri]); // gens: GeneratorView[]
1157
+
return gens[0];
1130
1158
}
1131
1159
1132
-
queryFeedGenerators(uris: string[]): ATPAPI.AppBskyFeedDefs.GeneratorView[] {
1160
+
async queryFeedGenerators(
1161
+
uris: string[]
1162
+
): Promise<ATPAPI.AppBskyFeedDefs.GeneratorView[]> {
1133
1163
const generators: ATPAPI.AppBskyFeedDefs.GeneratorView[] = [];
1134
1164
const urisByDid = new Map<string, string[]>();
1135
1165
···
1158
1188
const rows = stmt.all(...didUris) as unknown as GeneratorRow[];
1159
1189
if (rows.length === 0) continue;
1160
1190
1161
-
const creatorView = this.queryProfileView(did, "");
1191
+
const creatorView = await this.queryProfileView(did, "");
1162
1192
if (!creatorView) continue;
1163
1193
1164
1194
for (const row of rows) {
···
1188
1218
1189
1219
// user feeds
1190
1220
1191
-
queryAuthorFeedPartial(
1221
+
async queryAuthorFeedPartial(
1192
1222
did: string,
1193
1223
cursor?: string
1194
-
):
1224
+
): Promise<
1195
1225
| {
1196
1226
items: (
1197
1227
| ATPAPI.AppBskyFeedDefs.FeedViewPost
···
1199
1229
)[];
1200
1230
cursor: string | undefined;
1201
1231
}
1202
-
| undefined {
1232
+
| undefined
1233
+
> {
1203
1234
if (!this.isRegisteredIndexUser(did)) return;
1204
1235
const db = this.userManager.getDbForDid(did);
1205
1236
if (!db) return;
···
1234
1265
subject: string | null;
1235
1266
}[];
1236
1267
1237
-
const authorProfile = this.queryProfileView(did,"Basic");
1268
+
const authorProfile = await this.queryProfileView(did, "Basic");
1238
1269
1239
-
const items = rows
1240
-
.map((row) => {
1241
-
if (row.type === "repost" && row.subject) {
1242
-
const subjectDid = new AtUri(row.subject).host
1270
+
const items = await Promise.all(
1271
+
rows
1272
+
.map((row) => {
1273
+
if (row.type === "repost" && row.subject) {
1274
+
const subjectDid = new AtUri(row.subject).host;
1243
1275
1244
-
const originalPost = this.handlesDid(subjectDid)
1245
-
? this.queryFeedViewPost(row.subject)
1246
-
: this.constructFeedViewPostRef(row.subject);
1276
+
const originalPost = this.handlesDid(subjectDid)
1277
+
? this.queryFeedViewPost(row.subject)
1278
+
: this.constructFeedViewPostRef(row.subject);
1247
1279
1248
-
if (!originalPost || !authorProfile) return null;
1280
+
if (!originalPost || !authorProfile) return null;
1249
1281
1250
-
return {
1251
-
post: originalPost,
1252
-
reason: {
1253
-
$type: "app.bsky.feed.defs#reasonRepost",
1254
-
by: authorProfile,
1255
-
indexedAt: new Date(row.indexedat).toISOString(),
1256
-
},
1257
-
};
1258
-
} else {
1259
-
return this.queryFeedViewPost(row.uri);
1260
-
}
1261
-
})
1262
-
.filter((p): p is ATPAPI.AppBskyFeedDefs.FeedViewPost => !!p);
1282
+
return {
1283
+
post: originalPost,
1284
+
reason: {
1285
+
$type: "app.bsky.feed.defs#reasonRepost",
1286
+
by: authorProfile,
1287
+
indexedAt: new Date(row.indexedat).toISOString(),
1288
+
},
1289
+
};
1290
+
} else {
1291
+
return this.queryFeedViewPost(row.uri);
1292
+
}
1293
+
})
1294
+
.filter((p): p is Promise<ATPAPI.AppBskyFeedDefs.FeedViewPost> => !!p)
1295
+
);
1263
1296
1264
1297
const lastItem = rows[rows.length - 1];
1265
1298
const nextCursor = lastItem
···
1281
1314
return { items: [], cursor: undefined };
1282
1315
}
1283
1316
1284
-
queryActorLikesPartial(
1317
+
async queryActorLikesPartial(
1285
1318
did: string,
1286
1319
cursor?: string
1287
-
):
1320
+
): Promise<
1288
1321
| {
1289
1322
items: (
1290
1323
| ATPAPI.AppBskyFeedDefs.FeedViewPost
···
1292
1325
)[];
1293
1326
cursor: string | undefined;
1294
1327
}
1295
-
| undefined {
1328
+
| undefined
1329
+
> {
1296
1330
// early return only if the actor did is not registered
1297
1331
if (!this.isRegisteredIndexUser(did)) return;
1298
1332
const db = this.userManager.getDbForDid(did);
···
1320
1354
cid: string;
1321
1355
}[];
1322
1356
1323
-
const items = rows
1324
-
.map((row) => {
1325
-
const subjectDid = new AtUri(row.subject).host;
1357
+
const items = await Promise.all(
1358
+
rows
1359
+
.map(async (row) => {
1360
+
const subjectDid = new AtUri(row.subject).host;
1326
1361
1327
-
if (this.handlesDid(subjectDid)) {
1328
-
return this.queryFeedViewPost(row.subject);
1329
-
} else {
1330
-
return this.constructFeedViewPostRef(row.subject);
1331
-
}
1332
-
})
1333
-
.filter(
1334
-
(
1335
-
p
1336
-
): p is
1337
-
| ATPAPI.AppBskyFeedDefs.FeedViewPost
1338
-
| IndexServerAPI.PartyWheyAppBskyFeedDefs.FeedViewPostRef => !!p
1339
-
);
1362
+
if (this.handlesDid(subjectDid)) {
1363
+
return await this.queryFeedViewPost(row.subject);
1364
+
} else {
1365
+
return this.constructFeedViewPostRef(row.subject);
1366
+
}
1367
+
})
1368
+
.filter(
1369
+
(
1370
+
p
1371
+
): p is Promise<
1372
+
| ATPAPI.AppBskyFeedDefs.FeedViewPost
1373
+
| IndexServerAPI.PartyWheyAppBskyFeedDefs.FeedViewPostRef
1374
+
> => !!p
1375
+
)
1376
+
);
1340
1377
1341
1378
const lastItem = rows[rows.length - 1];
1342
1379
const nextCursor = lastItem
···
1348
1385
1349
1386
// post metadata
1350
1387
1351
-
queryLikes(uri: string): ATPAPI.AppBskyFeedGetLikes.Like[] | undefined {
1388
+
async queryLikes(
1389
+
uri: string
1390
+
): Promise<ATPAPI.AppBskyFeedGetLikes.Like[] | undefined> {
1352
1391
const postUri = new AtUri(uri);
1353
1392
const postAuthorDid = postUri.hostname;
1354
1393
if (!this.isRegisteredIndexUser(postAuthorDid)) return;
···
1364
1403
1365
1404
const rows = stmt.all(uri) as unknown as BacklinkRow[];
1366
1405
1367
-
return rows
1368
-
.map((row) => {
1369
-
const actor = this.queryProfileView(row.srcdid, "");
1370
-
if (!actor) return;
1406
+
return await Promise.all(
1407
+
rows
1408
+
.map(async (row) => {
1409
+
const actor = await this.queryProfileView(row.srcdid, "");
1410
+
if (!actor) return;
1371
1411
1372
-
return {
1373
-
// TODO write indexedAt for spacedust indexes
1374
-
createdAt: new Date(Date.now()).toISOString(),
1375
-
indexedAt: new Date(Date.now()).toISOString(),
1376
-
actor: actor,
1377
-
};
1378
-
})
1379
-
.filter((like): like is ATPAPI.AppBskyFeedGetLikes.Like => !!like);
1412
+
return {
1413
+
// TODO write indexedAt for spacedust indexes
1414
+
createdAt: new Date(Date.now()).toISOString(),
1415
+
indexedAt: new Date(Date.now()).toISOString(),
1416
+
actor: actor,
1417
+
};
1418
+
})
1419
+
.filter(
1420
+
(like): like is Promise<ATPAPI.AppBskyFeedGetLikes.Like> => !!like
1421
+
)
1422
+
);
1380
1423
}
1381
1424
1382
-
queryReposts(uri: string): ATPAPI.AppBskyActorDefs.ProfileView[] {
1425
+
async queryReposts(
1426
+
uri: string
1427
+
): Promise<ATPAPI.AppBskyActorDefs.ProfileView[]> {
1383
1428
const postUri = new AtUri(uri);
1384
1429
const postAuthorDid = postUri.hostname;
1385
1430
if (!this.isRegisteredIndexUser(postAuthorDid)) return [];
···
1395
1440
1396
1441
const rows = stmt.all(uri) as { srcdid: string }[];
1397
1442
1398
-
return rows
1399
-
.map((row) => this.queryProfileView(row.srcdid, ""))
1400
-
.filter((p): p is ATPAPI.AppBskyActorDefs.ProfileView => !!p);
1443
+
return await Promise.all(
1444
+
rows
1445
+
.map(async (row) => await this.queryProfileView(row.srcdid, ""))
1446
+
.filter((p): p is Promise<ATPAPI.AppBskyActorDefs.ProfileView> => !!p)
1447
+
);
1401
1448
}
1402
1449
1403
-
queryQuotes(uri: string): ATPAPI.AppBskyFeedDefs.FeedViewPost[] {
1450
+
async queryQuotes(
1451
+
uri: string
1452
+
): Promise<ATPAPI.AppBskyFeedDefs.FeedViewPost[]> {
1404
1453
const postUri = new AtUri(uri);
1405
1454
const postAuthorDid = postUri.hostname;
1406
1455
if (!this.isRegisteredIndexUser(postAuthorDid)) return [];
···
1416
1465
1417
1466
const rows = stmt.all(uri) as { srcuri: string }[];
1418
1467
1419
-
return rows
1420
-
.map((row) => this.queryFeedViewPost(row.srcuri))
1421
-
.filter((p): p is ATPAPI.AppBskyFeedDefs.FeedViewPost => !!p);
1468
+
return await Promise.all(
1469
+
rows
1470
+
.map(async (row) => await this.queryFeedViewPost(row.srcuri))
1471
+
.filter((p): p is Promise<ATPAPI.AppBskyFeedDefs.FeedViewPost> => !!p)
1472
+
);
1422
1473
}
1423
-
_getPostViewUnion(
1474
+
async _getPostViewUnion(
1424
1475
uri: string
1425
-
):
1476
+
): Promise<
1426
1477
| ATPAPI.AppBskyFeedDefs.PostView
1427
1478
| IndexServerAPI.PartyWheyAppBskyFeedDefs.PostViewRef
1428
-
| undefined {
1479
+
| undefined
1480
+
> {
1429
1481
try {
1430
1482
const postDid = new AtUri(uri).hostname;
1431
1483
if (this.handlesDid(postDid)) {
1432
-
return this.queryPostView(uri);
1484
+
return await this.queryPostView(uri);
1433
1485
} else {
1434
1486
return this.constructPostViewRef(uri);
1435
1487
}
···
1437
1489
return undefined;
1438
1490
}
1439
1491
}
1440
-
queryPostThreadPartial(
1492
+
async queryPostThreadPartial(
1441
1493
uri: string
1442
-
): IndexServerTypes.PartyWheyAppBskyFeedGetPostThreadPartial.OutputSchema | undefined {
1443
-
1444
-
const post = this._getPostViewUnion(uri);
1494
+
): Promise<
1495
+
| IndexServerTypes.PartyWheyAppBskyFeedGetPostThreadPartial.OutputSchema
1496
+
| undefined
1497
+
> {
1498
+
const post = await this._getPostViewUnion(uri);
1445
1499
1446
1500
if (!post) {
1447
1501
return {
···
1455
1509
1456
1510
const thread: IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef = {
1457
1511
$type: "party.whey.app.bsky.feed.defs#threadViewPostRef",
1458
-
post: post as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.PostView> | IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.PostViewRef>,
1512
+
post: post as
1513
+
| ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.PostView>
1514
+
| IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.PostViewRef>,
1459
1515
replies: [],
1460
1516
};
1461
1517
1462
1518
let current = thread;
1463
1519
// we can only climb the parent tree if we have the full post record.
1464
1520
// which is not implemented yet (sad i know)
1465
-
if (isPostView(current.post) && isFeedPostRecord(current.post.record) && current.post.record?.reply?.parent?.uri) {
1521
+
if (
1522
+
isPostView(current.post) &&
1523
+
isFeedPostRecord(current.post.record) &&
1524
+
current.post.record?.reply?.parent?.uri
1525
+
) {
1466
1526
let parentUri: string | undefined = current.post.record.reply.parent.uri;
1467
1527
1468
1528
// keep climbing as long as we find a valid parent post.
1469
1529
while (parentUri) {
1470
-
const parentPost = this._getPostViewUnion(parentUri);
1530
+
const parentPost = await this._getPostViewUnion(parentUri);
1471
1531
if (!parentPost) break; // stop if a parent in the chain is not found.
1472
1532
1473
-
const parentThread: IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef = {
1474
-
$type: "party.whey.app.bsky.feed.defs#threadViewPostRef",
1475
-
post: parentPost as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.PostView>,
1476
-
replies: [current as IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef>],
1477
-
};
1478
-
current.parent = parentThread as IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef>;
1533
+
const parentThread: IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef =
1534
+
{
1535
+
$type: "party.whey.app.bsky.feed.defs#threadViewPostRef",
1536
+
post: parentPost as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.PostView>,
1537
+
replies: [
1538
+
current as IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef>,
1539
+
],
1540
+
};
1541
+
current.parent =
1542
+
parentThread as IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef>;
1479
1543
current = parentThread;
1480
1544
1481
1545
// check if the new current post has a parent to continue the loop
1482
-
parentUri = (isPostView(current.post) && isFeedPostRecord(current.post.record)) ? current.post.record?.reply?.parent?.uri : undefined;
1546
+
parentUri =
1547
+
isPostView(current.post) && isFeedPostRecord(current.post.record)
1548
+
? current.post.record?.reply?.parent?.uri
1549
+
: undefined;
1483
1550
}
1484
1551
}
1485
-
1486
-
1487
1552
1488
1553
const seenUris = new Set<string>();
1489
-
const fetchReplies = (parentThread: IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef) => {
1490
-
if (!parentThread.post || !('uri' in parentThread.post)) {
1554
+
const fetchReplies = async (
1555
+
parentThread: IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef
1556
+
) => {
1557
+
if (!parentThread.post || !("uri" in parentThread.post)) {
1491
1558
return;
1492
1559
}
1493
1560
if (seenUris.has(parentThread.post.uri)) return;
···
1498
1565
1499
1566
// replies can only be discovered for local posts where we have the backlink data
1500
1567
if (!this.handlesDid(parentAuthorDid)) return;
1501
-
1568
+
1502
1569
const db = this.userManager.getDbForDid(parentAuthorDid);
1503
1570
if (!db) return;
1504
1571
···
1509
1576
`);
1510
1577
const replyRows = stmt.all(parentThread.post.uri) as { srcuri: string }[];
1511
1578
1512
-
const replies = replyRows
1513
-
.map((row) => this._getPostViewUnion(row.srcuri))
1514
-
.filter((p): p is ATPAPI.AppBskyFeedDefs.PostView | IndexServerAPI.PartyWheyAppBskyFeedDefs.PostViewRef => !!p);
1579
+
const replies = await Promise.all(
1580
+
replyRows
1581
+
.map(async (row) => await this._getPostViewUnion(row.srcuri))
1582
+
.filter(
1583
+
(
1584
+
p
1585
+
): p is Promise<
1586
+
| ATPAPI.AppBskyFeedDefs.PostView
1587
+
| IndexServerAPI.PartyWheyAppBskyFeedDefs.PostViewRef
1588
+
> => !!p
1589
+
)
1590
+
);
1515
1591
1516
1592
for (const replyPost of replies) {
1517
-
const replyThread: IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef = {
1518
-
$type: "party.whey.app.bsky.feed.defs#threadViewPostRef",
1519
-
post: replyPost as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.PostView> | IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.PostViewRef>,
1520
-
parent: parentThread as IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef>,
1521
-
replies: [],
1522
-
};
1523
-
parentThread.replies?.push(replyThread as IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef>);
1593
+
const replyThread: IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef =
1594
+
{
1595
+
$type: "party.whey.app.bsky.feed.defs#threadViewPostRef",
1596
+
post: replyPost as
1597
+
| ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.PostView>
1598
+
| IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.PostViewRef>,
1599
+
parent:
1600
+
parentThread as IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef>,
1601
+
replies: [],
1602
+
};
1603
+
parentThread.replies?.push(
1604
+
replyThread as IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef>
1605
+
);
1524
1606
fetchReplies(replyThread); // recurse
1525
1607
}
1526
1608
};
1527
1609
1528
1610
fetchReplies(thread);
1529
1611
1530
-
const returned = current as unknown as IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef;
1612
+
const returned =
1613
+
current as unknown as IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef;
1531
1614
1532
-
return { thread: returned as IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef> };
1615
+
return {
1616
+
thread:
1617
+
returned as IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef>,
1618
+
};
1533
1619
}
1534
-
1535
1620
1536
1621
/**
1537
1622
* please do not use this, use openDbForDid() instead
+62
-56
main-index.ts
+62
-56
main-index.ts
···
2
2
import { setupSystemDb } from "./utils/dbsystem.ts";
3
3
import { didDocument } from "./utils/diddoc.ts";
4
4
import { cachedFetch, searchParamsToJson, withCors } from "./utils/server.ts";
5
-
import { IndexServer, IndexServerConfig } from "./indexserver.ts"
5
+
import { IndexServer, IndexServerConfig } from "./indexserver.ts";
6
6
import { extractDid } from "./utils/identity.ts";
7
7
import { config } from "./config.ts";
8
8
···
11
11
// ------------------------------------------
12
12
13
13
const indexServerConfig: IndexServerConfig = {
14
-
baseDbPath: './dbs/registered-users', // The directory for user databases
15
-
systemDbPath: './dbs/registered-users/system.db', // The path for the main system database
16
-
jetstreamUrl: config.jetstream
14
+
baseDbPath: "./dbs/index/registered-users", // The directory for user databases
15
+
systemDbPath: "./dbs/index/registered-users/system.db", // The path for the main system database
17
16
};
18
17
export const genericIndexServer = new IndexServer(indexServerConfig);
19
18
setupSystemDb(genericIndexServer.systemDB);
···
35
34
datetime('now'),
36
35
'ready'
37
36
);
38
-
`)
37
+
`);
39
38
40
39
genericIndexServer.start();
41
40
···
61
60
// app.bsky.graph.getLists // doesnt need to because theres no items[], and its self ProfileViewBasic
62
61
// app.bsky.graph.getList // needs to be Partial-ed (items[] union with ProfileViewRef)
63
62
// app.bsky.graph.getActorStarterPacks // maybe doesnt need to be Partial-ed because its self ProfileViewBasic
64
-
63
+
65
64
// app.bsky.feed.getListFeed // uhh actually already exists its getListFeedPartial
66
65
// */
67
66
// "/xrpc/party.whey.app.bsky.feed.getListFeedPartial",
68
67
// ]);
69
68
70
-
Deno.serve(
71
-
{ port: config.indexServer.port },
72
-
(req: Request): Response => {
73
-
const url = new URL(req.url);
74
-
const pathname = url.pathname;
75
-
const searchParams = searchParamsToJson(url.searchParams);
69
+
Deno.serve({ port: config.indexServer.port }, async (req: Request): Promise<Response> => {
70
+
const url = new URL(req.url);
71
+
const pathname = url.pathname;
72
+
const searchParams = searchParamsToJson(url.searchParams);
76
73
77
-
if (pathname === "/.well-known/did.json") {
78
-
return new Response(JSON.stringify(didDocument("index",config.indexServer.did,config.indexServer.host,"whatever")), {
74
+
if (pathname === "/.well-known/did.json") {
75
+
return new Response(
76
+
JSON.stringify(
77
+
didDocument(
78
+
"index",
79
+
config.indexServer.did,
80
+
config.indexServer.host,
81
+
"whatever"
82
+
)
83
+
),
84
+
{
79
85
headers: withCors({ "Content-Type": "application/json" }),
80
-
});
81
-
}
82
-
if (pathname === "/health") {
83
-
return new Response("OK", {
84
-
status: 200,
85
-
headers: withCors({
86
-
"Content-Type": "text/plain",
87
-
}),
88
-
});
89
-
}
90
-
if (req.method === "OPTIONS") {
91
-
return new Response(null, {
92
-
status: 204,
93
-
headers: {
94
-
"Access-Control-Allow-Origin": "*",
95
-
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
96
-
"Access-Control-Allow-Headers": "*",
97
-
},
98
-
});
99
-
}
100
-
console.log(`request for "${pathname}"`)
101
-
const constellation = pathname.startsWith("/links")
102
-
103
-
if (constellation) {
104
-
const target = searchParams?.target as string
105
-
const safeDid = extractDid(target);
106
-
const targetserver = genericIndexServer.handlesDid(safeDid)
107
-
if (targetserver) {
108
-
return genericIndexServer.constellationAPIHandler(req);
109
-
} else {
110
-
return new Response(
111
-
JSON.stringify({
112
-
error: "User not found",
113
-
}),
114
-
{
115
-
status: 404,
116
-
headers: withCors({ "Content-Type": "application/json" }),
117
-
}
118
-
);
119
86
}
87
+
);
88
+
}
89
+
if (pathname === "/health") {
90
+
return new Response("OK", {
91
+
status: 200,
92
+
headers: withCors({
93
+
"Content-Type": "text/plain",
94
+
}),
95
+
});
96
+
}
97
+
if (req.method === "OPTIONS") {
98
+
return new Response(null, {
99
+
status: 204,
100
+
headers: {
101
+
"Access-Control-Allow-Origin": "*",
102
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
103
+
"Access-Control-Allow-Headers": "*",
104
+
},
105
+
});
106
+
}
107
+
console.log(`request for "${pathname}"`);
108
+
const constellation = pathname.startsWith("/links");
109
+
110
+
if (constellation) {
111
+
const target = searchParams?.target as string;
112
+
const safeDid = extractDid(target);
113
+
const targetserver = genericIndexServer.handlesDid(safeDid);
114
+
if (targetserver) {
115
+
return genericIndexServer.constellationAPIHandler(req);
120
116
} else {
121
-
// indexServerRoutes.has(pathname)
122
-
return genericIndexServer.indexServerHandler(req);
117
+
return new Response(
118
+
JSON.stringify({
119
+
error: "User not found",
120
+
}),
121
+
{
122
+
status: 404,
123
+
headers: withCors({ "Content-Type": "application/json" }),
124
+
}
125
+
);
123
126
}
127
+
} else {
128
+
// indexServerRoutes.has(pathname)
129
+
return await genericIndexServer.indexServerHandler(req);
124
130
}
125
-
);
131
+
});
+58
-10
main-view.ts
+58
-10
main-view.ts
···
2
2
import { setupSystemDb } from "./utils/dbsystem.ts";
3
3
import { didDocument } from "./utils/diddoc.ts";
4
4
import { cachedFetch, searchParamsToJson, withCors } from "./utils/server.ts";
5
-
import { IndexServer, IndexServerConfig } from "./indexserver.ts"
5
+
import { ViewServer, ViewServerConfig } from "./viewserver.ts";
6
6
import { extractDid } from "./utils/identity.ts";
7
7
import { config } from "./config.ts";
8
-
import { viewServerHandler } from "./viewserver.ts";
9
8
10
9
// ------------------------------------------
11
10
// AppView Setup
···
17
16
//keyCacheTTL: 10 * 60 * 1000,
18
17
});
19
18
19
+
const viewServerConfig: ViewServerConfig = {
20
+
baseDbPath: "./dbs/view/registered-users", // The directory for user databases
21
+
systemDbPath: "./dbs/view/registered-users/system.db", // The path for the main system database
22
+
};
23
+
export const genericViewServer = new ViewServer(viewServerConfig);
24
+
setupSystemDb(genericViewServer.systemDB);
25
+
26
+
// add me lol
27
+
genericViewServer.systemDB.exec(`
28
+
INSERT OR IGNORE INTO users (did, role, registrationdate, onboardingstatus)
29
+
VALUES (
30
+
'did:plc:mn45tewwnse5btfftvd3powc',
31
+
'admin',
32
+
datetime('now'),
33
+
'ready'
34
+
);
35
+
36
+
INSERT OR IGNORE INTO users (did, role, registrationdate, onboardingstatus)
37
+
VALUES (
38
+
'did:web:did12.whey.party',
39
+
'admin',
40
+
datetime('now'),
41
+
'ready'
42
+
);
43
+
`);
44
+
45
+
genericViewServer.start();
46
+
20
47
// ------------------------------------------
21
48
// XRPC Method Implementations
22
49
// ------------------------------------------
23
-
24
50
25
51
Deno.serve(
26
52
{ port: config.viewServer.port },
···
30
56
const searchParams = searchParamsToJson(url.searchParams);
31
57
32
58
if (pathname === "/.well-known/did.json") {
33
-
return new Response(JSON.stringify(didDocument), {
34
-
headers: withCors({ "Content-Type": "application/json" }),
35
-
});
59
+
return new Response(
60
+
JSON.stringify(
61
+
didDocument(
62
+
"view",
63
+
config.viewServer.did,
64
+
config.viewServer.host,
65
+
"whatever"
66
+
)
67
+
),
68
+
{
69
+
headers: withCors({ "Content-Type": "application/json" }),
70
+
}
71
+
);
36
72
}
37
73
if (pathname === "/health") {
38
74
return new Response("OK", {
···
52
88
},
53
89
});
54
90
}
55
-
console.log(`request for "${pathname}"`)
56
-
57
-
return await viewServerHandler(req)
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
+
return await genericViewServer.viewServerHandler(req);
58
106
}
59
-
);
107
+
);
+2
-2
utils/auth.borrowed.ts
+2
-2
utils/auth.borrowed.ts
···
22
22
return { DidPlcResolver: resolve }
23
23
}
24
24
const myResolver = getResolver()
25
-
const web = getWebResolver()
25
+
const webResolver = getWebResolver()
26
26
const resolver: ResolverRegistry = {
27
27
'plc': myResolver.DidPlcResolver as unknown as DIDResolver,
28
-
'web': web as unknown as DIDResolver,
28
+
...webResolver
29
29
}
30
30
export const resolverInstance = new Resolver(resolver)
31
31
export type Service = {
+5
-1
utils/auth.ts
+5
-1
utils/auth.ts
···
61
61
return null;
62
62
}
63
63
}
64
-
64
+
/**
65
+
* @deprecated dont use this use getAuthenticatedDid() instead
66
+
* @param param0
67
+
* @returns
68
+
*/
65
69
export const authVerifier: MethodAuthVerifier<AuthResult> = async ({ req }) => {
66
70
//console.log("help us all fuck you",req)
67
71
console.log("you are doing well")
+744
-433
viewserver.ts
+744
-433
viewserver.ts
···
13
13
} from "./utils/server.ts";
14
14
import { validateRecord } from "./utils/records.ts";
15
15
import { indexHandlerContext } from "./index/types.ts";
16
+
import { Database } from "jsr:@db/sqlite@0.11";
17
+
import { JetstreamManager, SpacedustManager } from "./utils/sharders.ts";
18
+
import { SpacedustLinkMessage } from "./index/spacedust.ts";
19
+
import { setupUserDb } from "./utils/dbuser.ts";
16
20
21
+
export interface ViewServerConfig {
22
+
baseDbPath: string;
23
+
systemDbPath: string;
24
+
}
17
25
18
-
export async function viewServerHandler(req: Request): Promise<Response> {
19
-
const url = new URL(req.url);
20
-
const pathname = url.pathname;
21
-
const bskyUrl = `https://api.bsky.app${pathname}${url.search}`;
22
-
const hasAuth = req.headers.has("authorization");
23
-
const xrpcMethod = pathname.startsWith("/xrpc/")
24
-
? pathname.slice("/xrpc/".length)
25
-
: null;
26
-
const searchParams = searchParamsToJson(url.searchParams);
27
-
const jsonUntyped = searchParams;
26
+
interface BaseRow {
27
+
uri: string;
28
+
did: string;
29
+
cid: string | null;
30
+
rev: string | null;
31
+
createdat: number | null;
32
+
indexedat: number;
33
+
json: string | null;
34
+
}
35
+
interface GeneratorRow extends BaseRow {
36
+
displayname: string | null;
37
+
description: string | null;
38
+
avatarcid: string | null;
39
+
}
40
+
interface LikeRow extends BaseRow {
41
+
subject: string;
42
+
}
43
+
interface RepostRow extends BaseRow {
44
+
subject: string;
45
+
}
46
+
interface BacklinkRow {
47
+
srcuri: string;
48
+
srcdid: string;
49
+
}
28
50
29
-
if (xrpcMethod === "app.bsky.unspecced.getTrendingTopics") {
30
-
// const jsonTyped =
31
-
// jsonUntyped as ViewServerTypes.AppBskyUnspeccedGetTrendingTopics.QueryParams;
51
+
const FEED_LIMIT = 50;
32
52
33
-
const faketopics: ATPAPI.AppBskyUnspeccedDefs.TrendingTopic[] = [
34
-
{
35
-
$type: "app.bsky.unspecced.defs#trendingTopic",
36
-
topic: "Git Repo",
37
-
displayName: "Git Repo",
38
-
description: "Git Repo",
39
-
link: "https://tangled.sh/@whey.party/skylite",
40
-
},
41
-
{
42
-
$type: "app.bsky.unspecced.defs#trendingTopic",
43
-
topic: "Red Dwarf Lite",
44
-
displayName: "Red Dwarf Lite",
45
-
description: "Red Dwarf Lite",
46
-
link: "https://reddwarflite.whey.party/",
47
-
},
48
-
{
49
-
$type: "app.bsky.unspecced.defs#trendingTopic",
50
-
topic: "whey dot party",
51
-
displayName: "whey dot party",
52
-
description: "whey dot party",
53
-
link: "https://whey.party/",
54
-
},
55
-
];
53
+
export class ViewServer {
54
+
private config: ViewServerConfig;
55
+
public userManager: ViewServerUserManager;
56
+
public systemDB: Database;
56
57
57
-
const response: ViewServerTypes.AppBskyUnspeccedGetTrendingTopics.OutputSchema =
58
-
{
59
-
topics: faketopics,
60
-
suggested: faketopics,
61
-
};
58
+
constructor(config: ViewServerConfig) {
59
+
this.config = config;
60
+
61
+
// We will initialize the system DB and user manager here
62
+
this.systemDB = new Database(this.config.systemDbPath);
63
+
// TODO: We need to setup the system DB schema if it's new
62
64
63
-
return new Response(JSON.stringify(response), {
64
-
headers: withCors({ "Content-Type": "application/json" }),
65
-
});
65
+
this.userManager = new ViewServerUserManager(this); // Pass the server instance
66
66
}
67
67
68
-
//if (xrpcMethod !== 'app.bsky.actor.getPreferences' && xrpcMethod !== 'app.bsky.notification.listNotifications') {
69
-
if (
70
-
!hasAuth
71
-
// (!hasAuth ||
72
-
// xrpcMethod === "app.bsky.labeler.getServices" ||
73
-
// xrpcMethod === "app.bsky.unspecced.getConfig") &&
74
-
// xrpcMethod !== "app.bsky.notification.putPreferences"
75
-
) {
76
-
return new Response(
77
-
JSON.stringify({
78
-
error: "XRPCNotSupported",
79
-
message:
80
-
"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",
81
-
}),
82
-
{
83
-
status: 404,
84
-
headers: withCors({ "Content-Type": "application/json" }),
85
-
}
86
-
);
87
-
//return await sendItToApiBskyApp(req);
68
+
public start() {
69
+
// This is where we'll kick things off, like the cold start
70
+
this.userManager.coldStart(this.systemDB);
71
+
console.log("viewServer started.");
88
72
}
89
-
if (
90
-
// !hasAuth ||
91
-
xrpcMethod === "app.bsky.labeler.getServices" ||
92
-
xrpcMethod === "app.bsky.unspecced.getConfig" //&&
93
-
//xrpcMethod !== "app.bsky.notification.putPreferences"
94
-
) {
95
-
return new Response(
96
-
JSON.stringify({
97
-
error: "XRPCNotSupported",
98
-
message:
99
-
"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",
100
-
}),
101
-
{
102
-
status: 404,
73
+
74
+
async viewServerHandler(req: Request): Promise<Response> {
75
+
const url = new URL(req.url);
76
+
const pathname = url.pathname;
77
+
const bskyUrl = `https://api.bsky.app${pathname}${url.search}`;
78
+
const hasAuth = req.headers.has("authorization");
79
+
const xrpcMethod = pathname.startsWith("/xrpc/")
80
+
? pathname.slice("/xrpc/".length)
81
+
: null;
82
+
const searchParams = searchParamsToJson(url.searchParams);
83
+
const jsonUntyped = searchParams;
84
+
85
+
if (xrpcMethod === "app.bsky.unspecced.getTrendingTopics") {
86
+
// const jsonTyped =
87
+
// jsonUntyped as ViewServerTypes.AppBskyUnspeccedGetTrendingTopics.QueryParams;
88
+
89
+
const faketopics: ATPAPI.AppBskyUnspeccedDefs.TrendingTopic[] = [
90
+
{
91
+
$type: "app.bsky.unspecced.defs#trendingTopic",
92
+
topic: "Git Repo",
93
+
displayName: "Git Repo",
94
+
description: "Git Repo",
95
+
link: "https://tangled.sh/@whey.party/skylite",
96
+
},
97
+
{
98
+
$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/",
103
+
},
104
+
{
105
+
$type: "app.bsky.unspecced.defs#trendingTopic",
106
+
topic: "whey dot party",
107
+
displayName: "whey dot party",
108
+
description: "whey dot party",
109
+
link: "https://whey.party/",
110
+
},
111
+
];
112
+
113
+
const response: ViewServerTypes.AppBskyUnspeccedGetTrendingTopics.OutputSchema =
114
+
{
115
+
topics: faketopics,
116
+
suggested: faketopics,
117
+
};
118
+
119
+
return new Response(JSON.stringify(response), {
103
120
headers: withCors({ "Content-Type": "application/json" }),
104
-
}
105
-
);
106
-
//return await sendItToApiBskyApp(req);
107
-
}
121
+
});
122
+
}
108
123
109
-
const authDID = "did:plc:mn45tewwnse5btfftvd3powc"; //getAuthenticatedDid(req);
124
+
//if (xrpcMethod !== 'app.bsky.actor.getPreferences' && xrpcMethod !== 'app.bsky.notification.listNotifications') {
125
+
if (
126
+
!hasAuth
127
+
// (!hasAuth ||
128
+
// xrpcMethod === "app.bsky.labeler.getServices" ||
129
+
// xrpcMethod === "app.bsky.unspecced.getConfig") &&
130
+
// xrpcMethod !== "app.bsky.notification.putPreferences"
131
+
) {
132
+
return new Response(
133
+
JSON.stringify({
134
+
error: "XRPCNotSupported",
135
+
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",
137
+
}),
138
+
{
139
+
status: 404,
140
+
headers: withCors({ "Content-Type": "application/json" }),
141
+
}
142
+
);
143
+
//return await sendItToApiBskyApp(req);
144
+
}
145
+
if (
146
+
// !hasAuth ||
147
+
xrpcMethod === "app.bsky.labeler.getServices" ||
148
+
xrpcMethod === "app.bsky.unspecced.getConfig" //&&
149
+
//xrpcMethod !== "app.bsky.notification.putPreferences"
150
+
) {
151
+
return new Response(
152
+
JSON.stringify({
153
+
error: "XRPCNotSupported",
154
+
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",
156
+
}),
157
+
{
158
+
status: 404,
159
+
headers: withCors({ "Content-Type": "application/json" }),
160
+
}
161
+
);
162
+
//return await sendItToApiBskyApp(req);
163
+
}
164
+
165
+
const authDID = "did:plc:mn45tewwnse5btfftvd3powc"; //getAuthenticatedDid(req);
110
166
111
-
switch (xrpcMethod) {
112
-
case "app.bsky.feed.getFeedGenerators": {
113
-
const jsonTyped =
114
-
jsonUntyped as ViewServerTypes.AppBskyFeedGetFeedGenerators.QueryParams;
167
+
switch (xrpcMethod) {
168
+
case "app.bsky.feed.getFeedGenerators": {
169
+
const jsonTyped =
170
+
jsonUntyped as ViewServerTypes.AppBskyFeedGetFeedGenerators.QueryParams;
115
171
116
-
const feeds: ATPAPI.AppBskyFeedDefs.GeneratorView[] = (
117
-
await Promise.all(
118
-
jsonTyped.feeds.map(async (feed) => {
119
-
try {
120
-
const did = new ATPAPI.AtUri(feed).hostname;
121
-
const rkey = new ATPAPI.AtUri(feed).rkey;
122
-
const identity = await resolveIdentity(did);
123
-
const feedgetRecord = await getSlingshotRecord(
124
-
identity.did,
125
-
"app.bsky.feed.generator",
126
-
rkey
127
-
);
128
-
const profile = (
129
-
await getSlingshotRecord(
172
+
const feeds: ATPAPI.AppBskyFeedDefs.GeneratorView[] = (
173
+
await Promise.all(
174
+
jsonTyped.feeds.map(async (feed) => {
175
+
try {
176
+
const did = new ATPAPI.AtUri(feed).hostname;
177
+
const rkey = new ATPAPI.AtUri(feed).rkey;
178
+
const identity = await resolveIdentity(did);
179
+
const feedgetRecord = await getSlingshotRecord(
130
180
identity.did,
131
-
"app.bsky.actor.profile",
132
-
"self"
133
-
)
134
-
).value as ATPAPI.AppBskyActorProfile.Record;
135
-
const anyprofile = profile as any;
136
-
const value =
137
-
feedgetRecord.value as ATPAPI.AppBskyFeedGenerator.Record;
181
+
"app.bsky.feed.generator",
182
+
rkey
183
+
);
184
+
const profile = (
185
+
await getSlingshotRecord(
186
+
identity.did,
187
+
"app.bsky.actor.profile",
188
+
"self"
189
+
)
190
+
).value as ATPAPI.AppBskyActorProfile.Record;
191
+
const anyprofile = profile as any;
192
+
const value =
193
+
feedgetRecord.value as ATPAPI.AppBskyFeedGenerator.Record;
138
194
139
-
return {
140
-
$type: "app.bsky.feed.defs#generatorView",
141
-
uri: feed,
142
-
cid: feedgetRecord.cid,
143
-
did: identity.did,
144
-
creator: /*AppBskyActorDefs.ProfileView*/ {
145
-
$type: "app.bsky.actor.defs#profileView",
195
+
return {
196
+
$type: "app.bsky.feed.defs#generatorView",
197
+
uri: feed,
198
+
cid: feedgetRecord.cid,
146
199
did: identity.did,
147
-
handle: identity.handle,
148
-
displayName: profile.displayName,
149
-
description: profile.description,
200
+
creator: /*AppBskyActorDefs.ProfileView*/ {
201
+
$type: "app.bsky.actor.defs#profileView",
202
+
did: identity.did,
203
+
handle: identity.handle,
204
+
displayName: profile.displayName,
205
+
description: profile.description,
206
+
avatar: buildBlobUrl(
207
+
identity.pds,
208
+
identity.did,
209
+
anyprofile.avatar.ref["$link"]
210
+
),
211
+
//associated?: ProfileAssociated
212
+
//indexedAt?: string
213
+
//createdAt?: string
214
+
//viewer?: ViewerState
215
+
//labels?: ComAtprotoLabelDefs.Label[]
216
+
//verification?: VerificationState
217
+
//status?: StatusView
218
+
},
219
+
displayName: value.displayName,
220
+
description: value.description,
221
+
//descriptionFacets?: AppBskyRichtextFacet.Main[]
150
222
avatar: buildBlobUrl(
151
223
identity.pds,
152
224
identity.did,
153
-
anyprofile.avatar.ref["$link"]
225
+
(value as any).avatar.ref["$link"]
154
226
),
155
-
//associated?: ProfileAssociated
156
-
//indexedAt?: string
157
-
//createdAt?: string
158
-
//viewer?: ViewerState
227
+
//likeCount?: number
228
+
//acceptsInteractions?: boolean
159
229
//labels?: ComAtprotoLabelDefs.Label[]
160
-
//verification?: VerificationState
161
-
//status?: StatusView
162
-
},
163
-
displayName: value.displayName,
164
-
description: value.description,
165
-
//descriptionFacets?: AppBskyRichtextFacet.Main[]
166
-
avatar: buildBlobUrl(
167
-
identity.pds,
168
-
identity.did,
169
-
(value as any).avatar.ref["$link"]
170
-
),
171
-
//likeCount?: number
172
-
//acceptsInteractions?: boolean
173
-
//labels?: ComAtprotoLabelDefs.Label[]
174
-
//viewer?: GeneratorViewerState
175
-
contentMode: value.contentMode,
176
-
indexedAt: new Date().toISOString(),
177
-
};
178
-
} catch (err) {
179
-
return undefined;
180
-
}
181
-
})
182
-
)
183
-
).filter(isGeneratorView);
230
+
//viewer?: GeneratorViewerState
231
+
contentMode: value.contentMode,
232
+
indexedAt: new Date().toISOString(),
233
+
};
234
+
} catch (err) {
235
+
return undefined;
236
+
}
237
+
})
238
+
)
239
+
).filter(isGeneratorView);
184
240
185
-
const response: ViewServerTypes.AppBskyFeedGetFeedGenerators.OutputSchema =
186
-
{
187
-
feeds: feeds ? feeds : [],
188
-
};
241
+
const response: ViewServerTypes.AppBskyFeedGetFeedGenerators.OutputSchema =
242
+
{
243
+
feeds: feeds ? feeds : [],
244
+
};
189
245
190
-
return new Response(JSON.stringify(response), {
191
-
headers: withCors({ "Content-Type": "application/json" }),
192
-
});
193
-
}
194
-
case "app.bsky.feed.getFeed": {
195
-
const jsonTyped =
196
-
jsonUntyped as ViewServerTypes.AppBskyFeedGetFeed.QueryParams;
197
-
const cursor = jsonTyped.cursor;
198
-
const feed = jsonTyped.feed;
199
-
const limit = jsonTyped.limit;
200
-
const proxyauth = req.headers.get("authorization") || "";
246
+
return new Response(JSON.stringify(response), {
247
+
headers: withCors({ "Content-Type": "application/json" }),
248
+
});
249
+
}
250
+
case "app.bsky.feed.getFeed": {
251
+
const jsonTyped =
252
+
jsonUntyped as ViewServerTypes.AppBskyFeedGetFeed.QueryParams;
253
+
const cursor = jsonTyped.cursor;
254
+
const feed = jsonTyped.feed;
255
+
const limit = jsonTyped.limit;
256
+
const proxyauth = req.headers.get("authorization") || "";
201
257
202
-
const did = new ATPAPI.AtUri(feed).hostname;
203
-
const rkey = new ATPAPI.AtUri(feed).rkey;
204
-
const identity = await resolveIdentity(did);
205
-
const feedgetRecord = (
206
-
await getSlingshotRecord(identity.did, "app.bsky.feed.generator", rkey)
207
-
).value as ATPAPI.AppBskyFeedGenerator.Record;
258
+
const did = new ATPAPI.AtUri(feed).hostname;
259
+
const rkey = new ATPAPI.AtUri(feed).rkey;
260
+
const identity = await resolveIdentity(did);
261
+
const feedgetRecord = (
262
+
await getSlingshotRecord(
263
+
identity.did,
264
+
"app.bsky.feed.generator",
265
+
rkey
266
+
)
267
+
).value as ATPAPI.AppBskyFeedGenerator.Record;
208
268
209
-
const skeleton = (await cachedFetch(
210
-
`${didWebToHttps(
211
-
feedgetRecord.did
212
-
)}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${jsonTyped.feed}${
213
-
cursor ? `&cursor=${cursor}` : ""
214
-
}${limit ? `&limit=${limit}` : ""}`,
215
-
proxyauth
216
-
)) as ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema;
269
+
const skeleton = (await cachedFetch(
270
+
`${didWebToHttps(
271
+
feedgetRecord.did
272
+
)}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${jsonTyped.feed}${
273
+
cursor ? `&cursor=${cursor}` : ""
274
+
}${limit ? `&limit=${limit}` : ""}`,
275
+
proxyauth
276
+
)) as ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema;
217
277
218
-
const nextcursor = skeleton.cursor;
219
-
const dbgrqstid = skeleton.reqId;
220
-
const uriarray = skeleton.feed;
278
+
const nextcursor = skeleton.cursor;
279
+
const dbgrqstid = skeleton.reqId;
280
+
const uriarray = skeleton.feed;
221
281
222
-
// Step 1: Chunk into 25 max
223
-
const chunks = [];
224
-
for (let i = 0; i < uriarray.length; i += 25) {
225
-
chunks.push(uriarray.slice(i, i + 25));
226
-
}
282
+
// Step 1: Chunk into 25 max
283
+
const chunks = [];
284
+
for (let i = 0; i < uriarray.length; i += 25) {
285
+
chunks.push(uriarray.slice(i, i + 25));
286
+
}
227
287
228
-
// Step 2: Hydrate via getPosts
229
-
const hydratedPosts: ATPAPI.AppBskyFeedDefs.FeedViewPost[] = [];
288
+
// Step 2: Hydrate via getPosts
289
+
const hydratedPosts: ATPAPI.AppBskyFeedDefs.FeedViewPost[] = [];
230
290
231
-
for (const chunk of chunks) {
232
-
const searchParams = new URLSearchParams();
233
-
for (const uri of chunk.map((item) => item.post)) {
234
-
searchParams.append("uris", uri);
235
-
}
291
+
for (const chunk of chunks) {
292
+
const searchParams = new URLSearchParams();
293
+
for (const uri of chunk.map((item) => item.post)) {
294
+
searchParams.append("uris", uri);
295
+
}
236
296
237
-
const postResp = await ky
238
-
.get(`https://api.bsky.app/xrpc/app.bsky.feed.getPosts`, {
239
-
// headers: {
240
-
// Authorization: proxyauth,
241
-
// },
242
-
searchParams,
243
-
})
244
-
.json<ATPAPI.AppBskyFeedGetPosts.OutputSchema>();
297
+
const postResp = await ky
298
+
.get(`https://api.bsky.app/xrpc/app.bsky.feed.getPosts`, {
299
+
// headers: {
300
+
// Authorization: proxyauth,
301
+
// },
302
+
searchParams,
303
+
})
304
+
.json<ATPAPI.AppBskyFeedGetPosts.OutputSchema>();
245
305
246
-
for (const post of postResp.posts) {
247
-
const matchingSkeleton = uriarray.find(
248
-
(item) => item.post === post.uri
249
-
);
250
-
if (matchingSkeleton) {
251
-
//post.author.handle = post.author.handle + ".percent40.api.bsky.app"; // or any logic to modify it
252
-
hydratedPosts.push({
253
-
post,
254
-
reason: matchingSkeleton.reason,
255
-
//reply: matchingSkeleton,
256
-
});
306
+
for (const post of postResp.posts) {
307
+
const matchingSkeleton = uriarray.find(
308
+
(item) => item.post === post.uri
309
+
);
310
+
if (matchingSkeleton) {
311
+
//post.author.handle = post.author.handle + ".percent40.api.bsky.app"; // or any logic to modify it
312
+
hydratedPosts.push({
313
+
post,
314
+
reason: matchingSkeleton.reason,
315
+
//reply: matchingSkeleton,
316
+
});
317
+
}
257
318
}
258
319
}
320
+
321
+
// Step 3: Compose final response
322
+
const response: ViewServerTypes.AppBskyFeedGetFeed.OutputSchema = {
323
+
feed: hydratedPosts,
324
+
cursor: nextcursor,
325
+
};
326
+
327
+
return new Response(JSON.stringify(response), {
328
+
headers: withCors({ "Content-Type": "application/json" }),
329
+
});
259
330
}
331
+
case "app.bsky.actor.getProfile": {
332
+
const jsonTyped =
333
+
jsonUntyped as ViewServerTypes.AppBskyActorGetProfile.QueryParams;
260
334
261
-
// Step 3: Compose final response
262
-
const response: ViewServerTypes.AppBskyFeedGetFeed.OutputSchema = {
263
-
feed: hydratedPosts,
264
-
cursor: nextcursor,
265
-
};
335
+
const userindexservice = "";
336
+
const isbskyfallback = true;
337
+
if (isbskyfallback) {
338
+
return this.sendItToApiBskyApp(req);
339
+
}
266
340
267
-
return new Response(JSON.stringify(response), {
268
-
headers: withCors({ "Content-Type": "application/json" }),
269
-
});
270
-
}
271
-
case "app.bsky.actor.getProfile": {
272
-
const jsonTyped =
273
-
jsonUntyped as ViewServerTypes.AppBskyActorGetProfile.QueryParams;
341
+
const response: ViewServerTypes.AppBskyActorGetProfile.OutputSchema =
342
+
{};
274
343
275
-
const userindexservice = "";
276
-
const isbskyfallback = true;
277
-
if (isbskyfallback) {
278
-
return sendItToApiBskyApp(req);
344
+
return new Response(JSON.stringify(response), {
345
+
headers: withCors({ "Content-Type": "application/json" }),
346
+
});
279
347
}
280
348
281
-
const response: ViewServerTypes.AppBskyActorGetProfile.OutputSchema = {};
349
+
case "app.bsky.actor.getProfiles": {
350
+
const jsonTyped =
351
+
jsonUntyped as ViewServerTypes.AppBskyActorGetProfiles.QueryParams;
282
352
283
-
return new Response(JSON.stringify(response), {
284
-
headers: withCors({ "Content-Type": "application/json" }),
285
-
});
286
-
}
353
+
const userindexservice = "";
354
+
const isbskyfallback = true;
355
+
if (isbskyfallback) {
356
+
return this.sendItToApiBskyApp(req);
357
+
}
287
358
288
-
case "app.bsky.actor.getProfiles": {
289
-
const jsonTyped =
290
-
jsonUntyped as ViewServerTypes.AppBskyActorGetProfiles.QueryParams;
359
+
const response: ViewServerTypes.AppBskyActorGetProfiles.OutputSchema =
360
+
{};
291
361
292
-
const userindexservice = "";
293
-
const isbskyfallback = true;
294
-
if (isbskyfallback) {
295
-
return sendItToApiBskyApp(req);
362
+
return new Response(JSON.stringify(response), {
363
+
headers: withCors({ "Content-Type": "application/json" }),
364
+
});
296
365
}
366
+
case "app.bsky.feed.getAuthorFeed": {
367
+
const jsonTyped =
368
+
jsonUntyped as ViewServerTypes.AppBskyFeedGetAuthorFeed.QueryParams;
297
369
298
-
const response: ViewServerTypes.AppBskyActorGetProfiles.OutputSchema = {};
370
+
const userindexservice = "";
371
+
const isbskyfallback = true;
372
+
if (isbskyfallback) {
373
+
return this.sendItToApiBskyApp(req);
374
+
}
299
375
300
-
return new Response(JSON.stringify(response), {
301
-
headers: withCors({ "Content-Type": "application/json" }),
302
-
});
303
-
}
304
-
case "app.bsky.feed.getAuthorFeed": {
305
-
const jsonTyped =
306
-
jsonUntyped as ViewServerTypes.AppBskyFeedGetAuthorFeed.QueryParams;
376
+
const response: ViewServerTypes.AppBskyFeedGetAuthorFeed.OutputSchema =
377
+
{};
307
378
308
-
const userindexservice = "";
309
-
const isbskyfallback = true;
310
-
if (isbskyfallback) {
311
-
return sendItToApiBskyApp(req);
379
+
return new Response(JSON.stringify(response), {
380
+
headers: withCors({ "Content-Type": "application/json" }),
381
+
});
312
382
}
383
+
case "app.bsky.feed.getPostThread": {
384
+
const jsonTyped =
385
+
jsonUntyped as ViewServerTypes.AppBskyFeedGetPostThread.QueryParams;
313
386
314
-
const response: ViewServerTypes.AppBskyFeedGetAuthorFeed.OutputSchema =
315
-
{};
387
+
const userindexservice = "";
388
+
const isbskyfallback = true;
389
+
if (isbskyfallback) {
390
+
return this.sendItToApiBskyApp(req);
391
+
}
316
392
317
-
return new Response(JSON.stringify(response), {
318
-
headers: withCors({ "Content-Type": "application/json" }),
319
-
});
320
-
}
321
-
case "app.bsky.feed.getPostThread": {
322
-
const jsonTyped =
323
-
jsonUntyped as ViewServerTypes.AppBskyFeedGetPostThread.QueryParams;
393
+
const response: ViewServerTypes.AppBskyFeedGetPostThread.OutputSchema =
394
+
{};
324
395
325
-
const userindexservice = "";
326
-
const isbskyfallback = true;
327
-
if (isbskyfallback) {
328
-
return sendItToApiBskyApp(req);
396
+
return new Response(JSON.stringify(response), {
397
+
headers: withCors({ "Content-Type": "application/json" }),
398
+
});
329
399
}
400
+
case "app.bsky.unspecced.getPostThreadV2": {
401
+
const jsonTyped =
402
+
jsonUntyped as ViewServerTypes.AppBskyUnspeccedGetPostThreadV2.QueryParams;
330
403
331
-
const response: ViewServerTypes.AppBskyFeedGetPostThread.OutputSchema =
332
-
{};
404
+
const userindexservice = "";
405
+
const isbskyfallback = true;
406
+
if (isbskyfallback) {
407
+
return this.sendItToApiBskyApp(req);
408
+
}
333
409
334
-
return new Response(JSON.stringify(response), {
335
-
headers: withCors({ "Content-Type": "application/json" }),
336
-
});
337
-
}
338
-
case "app.bsky.unspecced.getPostThreadV2": {
339
-
const jsonTyped =
340
-
jsonUntyped as ViewServerTypes.AppBskyUnspeccedGetPostThreadV2.QueryParams;
410
+
const response: ViewServerTypes.AppBskyUnspeccedGetPostThreadV2.OutputSchema =
411
+
{};
341
412
342
-
const userindexservice = "";
343
-
const isbskyfallback = true;
344
-
if (isbskyfallback) {
345
-
return sendItToApiBskyApp(req);
413
+
return new Response(JSON.stringify(response), {
414
+
headers: withCors({ "Content-Type": "application/json" }),
415
+
});
346
416
}
347
417
348
-
const response: ViewServerTypes.AppBskyUnspeccedGetPostThreadV2.OutputSchema =
349
-
{};
418
+
// case "app.bsky.actor.getProfile": {
419
+
// const jsonTyped =
420
+
// jsonUntyped as ViewServerTypes.AppBskyActorGetProfile.QueryParams;
421
+
422
+
// const response: ViewServerTypes.AppBskyActorGetProfile.OutputSchema= {};
423
+
424
+
// return new Response(JSON.stringify(response), {
425
+
// headers: withCors({ "Content-Type": "application/json" }),
426
+
// });
427
+
// }
428
+
// case "app.bsky.actor.getProfiles": {
429
+
// const jsonTyped = jsonUntyped as ViewServerTypes.AppBskyActorGetProfiles.QueryParams;
350
430
351
-
return new Response(JSON.stringify(response), {
352
-
headers: withCors({ "Content-Type": "application/json" }),
353
-
});
354
-
}
431
+
// const response: ViewServerTypes.AppBskyActorGetProfiles.OutputSchema = {};
355
432
356
-
// case "app.bsky.actor.getProfile": {
357
-
// const jsonTyped =
358
-
// jsonUntyped as ViewServerTypes.AppBskyActorGetProfile.QueryParams;
433
+
// return new Response(JSON.stringify(response), {
434
+
// headers: withCors({ "Content-Type": "application/json" }),
435
+
// });
436
+
// }
437
+
// case "whatever": {
438
+
// const jsonTyped = jsonUntyped as ViewServerTypes.AppBskyFeedGetAuthorFeed.QueryParams;
359
439
360
-
// const response: ViewServerTypes.AppBskyActorGetProfile.OutputSchema= {};
440
+
// const response: ViewServerTypes.AppBskyFeedGetAuthorFeed.OutputSchema = {}
361
441
362
-
// return new Response(JSON.stringify(response), {
363
-
// headers: withCors({ "Content-Type": "application/json" }),
364
-
// });
365
-
// }
366
-
// case "app.bsky.actor.getProfiles": {
367
-
// const jsonTyped = jsonUntyped as ViewServerTypes.AppBskyActorGetProfiles.QueryParams;
442
+
// return new Response(JSON.stringify(response), {
443
+
// headers: withCors({ "Content-Type": "application/json" }),
444
+
// });
445
+
// }
446
+
// case "app.bsky.notification.listNotifications": {
447
+
// const jsonTyped =
448
+
// jsonUntyped as ViewServerTypes.AppBskyNotificationListNotifications.QueryParams;
368
449
369
-
// const response: ViewServerTypes.AppBskyActorGetProfiles.OutputSchema = {};
450
+
// const response: ViewServerTypes.AppBskyNotificationListNotifications.OutputSchema = {};
370
451
371
-
// return new Response(JSON.stringify(response), {
372
-
// headers: withCors({ "Content-Type": "application/json" }),
373
-
// });
374
-
// }
375
-
// case "whatever": {
376
-
// const jsonTyped = jsonUntyped as ViewServerTypes.AppBskyFeedGetAuthorFeed.QueryParams;
452
+
// return new Response(JSON.stringify(response), {
453
+
// headers: withCors({ "Content-Type": "application/json" }),
454
+
// });
455
+
// }
377
456
378
-
// const response: ViewServerTypes.AppBskyFeedGetAuthorFeed.OutputSchema = {}
457
+
case "app.bsky.unspecced.getConfig": {
458
+
const jsonTyped =
459
+
jsonUntyped as ViewServerTypes.AppBskyUnspeccedGetConfig.QueryParams;
379
460
380
-
// return new Response(JSON.stringify(response), {
381
-
// headers: withCors({ "Content-Type": "application/json" }),
382
-
// });
383
-
// }
384
-
// case "app.bsky.notification.listNotifications": {
385
-
// const jsonTyped =
386
-
// jsonUntyped as ViewServerTypes.AppBskyNotificationListNotifications.QueryParams;
461
+
const response: ViewServerTypes.AppBskyUnspeccedGetConfig.OutputSchema =
462
+
{
463
+
checkEmailConfirmed: true,
464
+
liveNow: [
465
+
{
466
+
$type: "app.bsky.unspecced.getConfig#liveNowConfig",
467
+
did: "did:plc:mn45tewwnse5btfftvd3powc",
468
+
domains: ["local3768forumtest.whey.party"],
469
+
},
470
+
],
471
+
};
387
472
388
-
// const response: ViewServerTypes.AppBskyNotificationListNotifications.OutputSchema = {};
473
+
return new Response(JSON.stringify(response), {
474
+
headers: withCors({ "Content-Type": "application/json" }),
475
+
});
476
+
}
477
+
case "app.bsky.graph.getLists": {
478
+
const jsonTyped =
479
+
jsonUntyped as ViewServerTypes.AppBskyGraphGetLists.QueryParams;
389
480
390
-
// return new Response(JSON.stringify(response), {
391
-
// headers: withCors({ "Content-Type": "application/json" }),
392
-
// });
393
-
// }
481
+
const response: ViewServerTypes.AppBskyGraphGetLists.OutputSchema = {
482
+
lists: [],
483
+
};
394
484
395
-
case "app.bsky.unspecced.getConfig": {
396
-
const jsonTyped =
397
-
jsonUntyped as ViewServerTypes.AppBskyUnspeccedGetConfig.QueryParams;
485
+
return new Response(JSON.stringify(response), {
486
+
headers: withCors({ "Content-Type": "application/json" }),
487
+
});
488
+
}
489
+
//https://shimeji.us-east.host.bsky.network/xrpc/app.bsky.unspecced.getTrendingTopics?limit=14
490
+
case "app.bsky.unspecced.getTrendingTopics": {
491
+
const jsonTyped =
492
+
jsonUntyped as ViewServerTypes.AppBskyUnspeccedGetTrendingTopics.QueryParams;
398
493
399
-
const response: ViewServerTypes.AppBskyUnspeccedGetConfig.OutputSchema = {
400
-
checkEmailConfirmed: true,
401
-
liveNow: [
494
+
const faketopics: ATPAPI.AppBskyUnspeccedDefs.TrendingTopic[] = [
402
495
{
403
-
$type: "app.bsky.unspecced.getConfig#liveNowConfig",
404
-
did: "did:plc:mn45tewwnse5btfftvd3powc",
405
-
domains: ["local3768forumtest.whey.party"],
496
+
$type: "app.bsky.unspecced.defs#trendingTopic",
497
+
topic: "Git Repo",
498
+
displayName: "Git Repo",
499
+
description: "Git Repo",
500
+
link: "https://tangled.sh/@whey.party/skylite",
406
501
},
407
-
],
408
-
};
502
+
{
503
+
$type: "app.bsky.unspecced.defs#trendingTopic",
504
+
topic: "Red Dwarf Lite",
505
+
displayName: "Red Dwarf Lite",
506
+
description: "Red Dwarf Lite",
507
+
link: "https://reddwarf.whey.party/",
508
+
},
509
+
{
510
+
$type: "app.bsky.unspecced.defs#trendingTopic",
511
+
topic: "whey dot party",
512
+
displayName: "whey dot party",
513
+
description: "whey dot party",
514
+
link: "https://whey.party/",
515
+
},
516
+
];
517
+
518
+
const response: ViewServerTypes.AppBskyUnspeccedGetTrendingTopics.OutputSchema =
519
+
{
520
+
topics: faketopics,
521
+
suggested: faketopics,
522
+
};
409
523
410
-
return new Response(JSON.stringify(response), {
411
-
headers: withCors({ "Content-Type": "application/json" }),
412
-
});
524
+
return new Response(JSON.stringify(response), {
525
+
headers: withCors({ "Content-Type": "application/json" }),
526
+
});
527
+
}
528
+
default: {
529
+
return new Response(
530
+
JSON.stringify({
531
+
error: "XRPCNotSupported",
532
+
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",
534
+
}),
535
+
{
536
+
status: 404,
537
+
headers: withCors({ "Content-Type": "application/json" }),
538
+
}
539
+
);
540
+
}
413
541
}
414
-
case "app.bsky.graph.getLists": {
415
-
const jsonTyped =
416
-
jsonUntyped as ViewServerTypes.AppBskyGraphGetLists.QueryParams;
417
542
418
-
const response: ViewServerTypes.AppBskyGraphGetLists.OutputSchema = {
419
-
lists: [],
420
-
};
543
+
// return new Response("Not Found", { status: 404 });
544
+
}
421
545
422
-
return new Response(JSON.stringify(response), {
423
-
headers: withCors({ "Content-Type": "application/json" }),
424
-
});
546
+
async sendItToApiBskyApp(req: Request): Promise<Response> {
547
+
const url = new URL(req.url);
548
+
const pathname = url.pathname;
549
+
const searchParams = searchParamsToJson(url.searchParams);
550
+
let reqBody: undefined | string;
551
+
let jsonbody: undefined | Record<string, unknown>;
552
+
if (req.body) {
553
+
const body = await req.json();
554
+
jsonbody = body;
555
+
// console.log(
556
+
// `called at euh reqreqreqreq: ${pathname}\n\n${JSON.stringify(body)}`
557
+
// );
558
+
reqBody = JSON.stringify(body, null, 2);
425
559
}
426
-
//https://shimeji.us-east.host.bsky.network/xrpc/app.bsky.unspecced.getTrendingTopics?limit=14
427
-
case "app.bsky.unspecced.getTrendingTopics": {
428
-
const jsonTyped =
429
-
jsonUntyped as ViewServerTypes.AppBskyUnspeccedGetTrendingTopics.QueryParams;
560
+
const bskyUrl = `https://public.api.bsky.app${pathname}${url.search}`;
561
+
console.log("request", searchParams);
562
+
const proxyHeaders = new Headers(req.headers);
563
+
564
+
// Remove Authorization and set browser-like User-Agent
565
+
proxyHeaders.delete("authorization");
566
+
proxyHeaders.delete("Access-Control-Allow-Origin"),
567
+
proxyHeaders.set(
568
+
"user-agent",
569
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
570
+
);
571
+
proxyHeaders.set("Access-Control-Allow-Origin", "*");
572
+
573
+
const proxyRes = await fetch(bskyUrl, {
574
+
method: req.method,
575
+
headers: proxyHeaders,
576
+
body: ["GET", "HEAD"].includes(req.method.toUpperCase())
577
+
? undefined
578
+
: reqBody,
579
+
});
580
+
581
+
const resBody = await proxyRes.text();
430
582
431
-
const faketopics: ATPAPI.AppBskyUnspeccedDefs.TrendingTopic[] = [
432
-
{
433
-
$type: "app.bsky.unspecced.defs#trendingTopic",
434
-
topic: "Git Repo",
435
-
displayName: "Git Repo",
436
-
description: "Git Repo",
437
-
link: "https://tangled.sh/@whey.party/skylite",
438
-
},
439
-
{
440
-
$type: "app.bsky.unspecced.defs#trendingTopic",
441
-
topic: "Red Dwarf Lite",
442
-
displayName: "Red Dwarf Lite",
443
-
description: "Red Dwarf Lite",
444
-
link: "https://reddwarf.whey.party/",
445
-
},
446
-
{
447
-
$type: "app.bsky.unspecced.defs#trendingTopic",
448
-
topic: "whey dot party",
449
-
displayName: "whey dot party",
450
-
description: "whey dot party",
451
-
link: "https://whey.party/",
452
-
},
453
-
];
583
+
// console.log(
584
+
// "← Response:",
585
+
// JSON.stringify(await JSON.parse(resBody), null, 2)
586
+
// );
454
587
455
-
const response: ViewServerTypes.AppBskyUnspeccedGetTrendingTopics.OutputSchema =
456
-
{
457
-
topics: faketopics,
458
-
suggested: faketopics,
459
-
};
588
+
return new Response(resBody, {
589
+
status: proxyRes.status,
590
+
headers: proxyRes.headers,
591
+
});
592
+
}
460
593
461
-
return new Response(JSON.stringify(response), {
462
-
headers: withCors({ "Content-Type": "application/json" }),
463
-
});
464
-
}
465
-
default: {
466
-
return new Response(
467
-
JSON.stringify({
468
-
error: "XRPCNotSupported",
469
-
message:
470
-
"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",
471
-
}),
472
-
{
473
-
status: 404,
474
-
headers: withCors({ "Content-Type": "application/json" }),
475
-
}
476
-
);
594
+
viewServerIndexer(ctx: indexHandlerContext) {
595
+
const record = validateRecord(ctx.value);
596
+
switch (record?.$type) {
597
+
case "app.bsky.feed.like": {
598
+
return;
599
+
}
600
+
default: {
601
+
// what the hell
602
+
return;
603
+
}
477
604
}
478
605
}
479
606
480
-
// return new Response("Not Found", { status: 404 });
607
+
/**
608
+
* please do not use this, use openDbForDid() instead
609
+
* @param did
610
+
* @returns
611
+
*/
612
+
internalCreateDbForDid(did: string): Database {
613
+
const path = `${this.config.baseDbPath}/${did}.sqlite`;
614
+
const db = new Database(path);
615
+
// TODO maybe split the user db schema between view server and index server
616
+
setupUserDb(db);
617
+
//await db.exec(/* CREATE IF NOT EXISTS statements */);
618
+
return db;
619
+
}
620
+
public handlesDid(did: string): boolean {
621
+
return this.userManager.handlesDid(did);
622
+
}
481
623
}
482
624
483
-
async function sendItToApiBskyApp(req: Request): Promise<Response> {
484
-
const url = new URL(req.url);
485
-
const pathname = url.pathname;
486
-
const searchParams = searchParamsToJson(url.searchParams);
487
-
let reqBody: undefined | string;
488
-
let jsonbody: undefined | Record<string, unknown>;
489
-
if (req.body) {
490
-
const body = await req.json();
491
-
jsonbody = body;
492
-
// console.log(
493
-
// `called at euh reqreqreqreq: ${pathname}\n\n${JSON.stringify(body)}`
494
-
// );
495
-
reqBody = JSON.stringify(body, null, 2);
625
+
export class ViewServerUserManager {
626
+
public viewServer: ViewServer;
627
+
628
+
constructor(viewServer: ViewServer) {
629
+
this.viewServer = viewServer;
630
+
}
631
+
632
+
public users = new Map<string, UserViewServer>();
633
+
public handlesDid(did: string): boolean {
634
+
return this.users.has(did);
496
635
}
497
-
const bskyUrl = `https://api.bsky.app${pathname}${url.search}`;
498
-
const proxyHeaders = new Headers(req.headers);
499
636
500
-
// Remove Authorization and set browser-like User-Agent
501
-
proxyHeaders.delete("authorization");
502
-
proxyHeaders.delete("Access-Control-Allow-Origin"),
503
-
proxyHeaders.set(
504
-
"user-agent",
505
-
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
506
-
);
507
-
proxyHeaders.set("Access-Control-Allow-Origin", "*");
637
+
/*async*/ addUser(did: string) {
638
+
if (this.users.has(did)) return;
639
+
const instance = new UserViewServer(this, did);
640
+
//await instance.initialize();
641
+
this.users.set(did, instance);
642
+
}
508
643
509
-
const proxyRes = await fetch(bskyUrl, {
510
-
method: req.method,
511
-
headers: proxyHeaders,
512
-
body: ["GET", "HEAD"].includes(req.method.toUpperCase())
513
-
? undefined
514
-
: reqBody,
515
-
});
644
+
// async handleRequest({
645
+
// did,
646
+
// route,
647
+
// req,
648
+
// }: {
649
+
// did: string;
650
+
// route: string;
651
+
// req: Request;
652
+
// }) {
653
+
// if (!this.users.has(did)) await this.addUser(did);
654
+
// const user = this.users.get(did)!;
655
+
// return await user.handleHttpRequest(route, req);
656
+
// }
516
657
517
-
const resBody = await proxyRes.text();
658
+
removeUser(did: string) {
659
+
const instance = this.users.get(did);
660
+
if (!instance) return;
661
+
/*await*/ instance.shutdown();
662
+
this.users.delete(did);
663
+
}
518
664
519
-
// console.log(
520
-
// "← Response:",
521
-
// JSON.stringify(await JSON.parse(resBody), null, 2)
522
-
// );
665
+
getDbForDid(did: string): Database | null {
666
+
if (!this.users.has(did)) {
667
+
return null;
668
+
}
669
+
return this.users.get(did)?.db ?? null;
670
+
}
523
671
524
-
return new Response(resBody, {
525
-
status: proxyRes.status,
526
-
headers: proxyRes.headers,
527
-
});
672
+
coldStart(db: Database) {
673
+
const rows = db.prepare("SELECT did FROM users").all();
674
+
for (const row of rows) {
675
+
this.addUser(row.did);
676
+
}
677
+
}
528
678
}
529
679
530
-
export function viewServerIndexer(ctx: indexHandlerContext) {
531
-
const record = validateRecord(ctx.value);
532
-
switch (record?.$type) {
533
-
case "app.bsky.feed.like": {
534
-
return;
535
-
}
536
-
default: {
537
-
// what the hell
538
-
return;
539
-
}
680
+
class UserViewServer {
681
+
public viewServerUserManager: ViewServerUserManager;
682
+
did: string;
683
+
db: Database; // | undefined;
684
+
jetstream: JetstreamManager; // | undefined;
685
+
spacedust: SpacedustManager; // | undefined;
686
+
687
+
constructor(viewServerUserManager: ViewServerUserManager, did: string) {
688
+
this.did = did;
689
+
this.viewServerUserManager = viewServerUserManager;
690
+
this.db = this.viewServerUserManager.viewServer.internalCreateDbForDid(
691
+
this.did
692
+
);
693
+
// should probably put the params of exactly what were listening to here
694
+
this.jetstream = new JetstreamManager((msg) => {
695
+
console.log("Received Jetstream message: ", msg);
696
+
697
+
const op = msg.commit.operation;
698
+
const doer = msg.did;
699
+
const rev = msg.commit.rev;
700
+
const aturi = `${msg.did}/${msg.commit.collection}/${msg.commit.rkey}`;
701
+
const value = msg.commit.record;
702
+
703
+
if (!doer || !value) return;
704
+
this.viewServerUserManager.viewServer.viewServerIndexer({
705
+
op,
706
+
doer,
707
+
cid: msg.commit.cid,
708
+
rev,
709
+
aturi,
710
+
value,
711
+
indexsrc: `jetstream-${op}`,
712
+
db: this.db,
713
+
});
714
+
});
715
+
this.jetstream.start({
716
+
// for realsies pls get from db or something instead of this shit
717
+
wantedDids: [
718
+
this.did,
719
+
// "did:plc:mn45tewwnse5btfftvd3powc",
720
+
// "did:plc:yy6kbriyxtimkjqonqatv2rb",
721
+
// "did:plc:zzhzjga3ab5fcs2vnsv2ist3",
722
+
// "did:plc:jz4ibztn56hygfld6j6zjszg",
723
+
],
724
+
wantedCollections: [
725
+
// View server only needs some of the things related to user views mutes, not all of them
726
+
//"app.bsky.actor.profile",
727
+
//"app.bsky.feed.generator",
728
+
//"app.bsky.feed.like",
729
+
//"app.bsky.feed.post",
730
+
//"app.bsky.feed.repost",
731
+
"app.bsky.feed.threadgate", // mod
732
+
"app.bsky.graph.block", // mod
733
+
"app.bsky.graph.follow", // graphing
734
+
//"app.bsky.graph.list",
735
+
"app.bsky.graph.listblock", // mod
736
+
//"app.bsky.graph.listitem",
737
+
"app.bsky.notification.declaration", // mod
738
+
],
739
+
});
740
+
//await connectToJetstream(this.did, this.db);
741
+
this.spacedust = new SpacedustManager((msg: SpacedustLinkMessage) => {
742
+
console.log("Received Spacedust message: ", msg);
743
+
const operation = msg.link.operation;
744
+
745
+
const sourceURI = new ATPAPI.AtUri(msg.link.source_record);
746
+
const srcUri = msg.link.source_record;
747
+
const srcDid = sourceURI.host;
748
+
const srcField = msg.link.source;
749
+
const srcCol = sourceURI.collection;
750
+
const subjectURI = new ATPAPI.AtUri(msg.link.subject);
751
+
const subUri = msg.link.subject;
752
+
const subDid = subjectURI.host;
753
+
const subCol = subjectURI.collection;
754
+
755
+
if (operation === "delete") {
756
+
this.db.run(
757
+
`DELETE FROM backlink_skeleton
758
+
WHERE srcuri = ? AND srcfield = ? AND suburi = ?`,
759
+
[srcUri, srcField, subUri]
760
+
);
761
+
} else if (operation === "create") {
762
+
this.db.run(
763
+
`INSERT OR REPLACE INTO backlink_skeleton (
764
+
srcuri,
765
+
srcdid,
766
+
srcfield,
767
+
srccol,
768
+
suburi,
769
+
subdid,
770
+
subcol
771
+
) VALUES (?, ?, ?, ?, ?, ?, ?)`,
772
+
[
773
+
srcUri, // full AT URI of the source record
774
+
srcDid, // did: of the source
775
+
srcField, // e.g., "reply.parent.uri" or "facets.features.did"
776
+
srcCol, // e.g., "app.bsky.feed.post"
777
+
subUri, // full AT URI of the subject (linked record)
778
+
subDid, // did: of the subject
779
+
subCol, // subject collection (can be inferred or passed)
780
+
]
781
+
);
782
+
}
783
+
});
784
+
this.spacedust.start({
785
+
wantedSources: [
786
+
// view server keeps all of this because notifications are a thing
787
+
"app.bsky.feed.like:subject.uri", // like
788
+
"app.bsky.feed.like:via.uri", // liked repost
789
+
"app.bsky.feed.repost:subject.uri", // repost
790
+
"app.bsky.feed.repost:via.uri", // reposted repost
791
+
"app.bsky.feed.post:reply.root.uri", // thread OP
792
+
"app.bsky.feed.post:reply.parent.uri", // direct parent
793
+
"app.bsky.feed.post:embed.media.record.record.uri", // quote with media
794
+
"app.bsky.feed.post:embed.record.uri", // quote without media
795
+
"app.bsky.feed.threadgate:post", // threadgate subject
796
+
"app.bsky.feed.threadgate:hiddenReplies", // threadgate items (array)
797
+
"app.bsky.feed.post:facets.features.did", // facet item (array): mention
798
+
"app.bsky.graph.block:subject", // blocks
799
+
"app.bsky.graph.follow:subject", // follow
800
+
"app.bsky.graph.listblock:subject", // list item (blocks)
801
+
"app.bsky.graph.listblock:list", // blocklist mention (might not exist)
802
+
"app.bsky.graph.listitem:subject", // list item (blocks)
803
+
"app.bsky.graph.listitem:list", // list mention
804
+
],
805
+
// should be getting from DB but whatever right
806
+
wantedSubjects: [
807
+
// as noted i dont need to write down each post, just the user to listen to !
808
+
// hell yeah
809
+
// "at://did:plc:mn45tewwnse5btfftvd3powc/app.bsky.feed.post/3lvybv7b6ic2h",
810
+
// "at://did:plc:mn45tewwnse5btfftvd3powc/app.bsky.feed.post/3lvybws4avc2h",
811
+
// "at://did:plc:mn45tewwnse5btfftvd3powc/app.bsky.feed.post/3lvvkcxcscs2h",
812
+
// "at://did:plc:yy6kbriyxtimkjqonqatv2rb/app.bsky.feed.post/3l63ogxocq42f",
813
+
// "at://did:plc:yy6kbriyxtimkjqonqatv2rb/app.bsky.feed.post/3lw3wamvflu23",
814
+
],
815
+
wantedSubjectDids: [
816
+
this.did,
817
+
//"did:plc:mn45tewwnse5btfftvd3powc",
818
+
//"did:plc:yy6kbriyxtimkjqonqatv2rb",
819
+
//"did:plc:zzhzjga3ab5fcs2vnsv2ist3",
820
+
//"did:plc:jz4ibztn56hygfld6j6zjszg",
821
+
],
822
+
});
823
+
//await connectToConstellation(this.did, this.db);
824
+
}
825
+
826
+
// initialize() {
827
+
828
+
// }
829
+
830
+
// async handleHttpRequest(route: string, req: Request): Promise<Response> {
831
+
// if (route === "posts") {
832
+
// const posts = await this.queryPosts();
833
+
// return new Response(JSON.stringify(posts), {
834
+
// headers: { "content-type": "application/json" },
835
+
// });
836
+
// }
837
+
838
+
// return new Response("Unknown route", { status: 404 });
839
+
// }
840
+
841
+
// private async queryPosts() {
842
+
// return this.db.run(
843
+
// "SELECT * FROM posts ORDER BY created_at DESC LIMIT 100"
844
+
// );
845
+
// }
846
+
847
+
shutdown() {
848
+
this.jetstream.stop();
849
+
this.spacedust.stop();
850
+
this.db.close?.();
540
851
}
541
852
}