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

initial view server stuff

rimar1337 52208474 b91eeceb

+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
··· 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
··· 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
··· 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
··· 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
··· 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 }