an independent Bluesky client using Constellation, PDS Queries, and other services reddwarf.app
frontend spa bluesky reddwarf microcosm client app
at cde0d3df87971f7d23abb5245efc0610caa108c2 977 lines 28 kB view raw
1import * as ATPAPI from "@atproto/api"; 2import { 3 infiniteQueryOptions, 4 type QueryFunctionContext, 5 queryOptions, 6 useInfiniteQuery, 7 useQuery, 8 type UseQueryResult, 9} from "@tanstack/react-query"; 10import { useAtom } from "jotai"; 11 12import { useAuth } from "~/providers/UnifiedAuthProvider"; 13 14import { constellationURLAtom, lycanURLAtom, slingshotURLAtom } from "./atoms"; 15 16export function constructIdentityQuery( 17 didorhandle?: string, 18 slingshoturl?: string, 19) { 20 return queryOptions({ 21 queryKey: ["identity", didorhandle], 22 queryFn: async () => { 23 if (!didorhandle) return undefined as undefined; 24 const res = await fetch( 25 `https://${slingshoturl}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(didorhandle)}`, 26 ); 27 if (!res.ok) throw new Error("Failed to fetch post"); 28 try { 29 return (await res.json()) as { 30 did: string; 31 handle: string; 32 pds: string; 33 signing_key: string; 34 }; 35 } catch (_e) { 36 return undefined; 37 } 38 }, 39 staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes 40 gcTime: /*0//*/ 5 * 60 * 1000, 41 }); 42} 43export function useQueryIdentity(didorhandle: string): UseQueryResult< 44 { 45 did: string; 46 handle: string; 47 pds: string; 48 signing_key: string; 49 }, 50 Error 51>; 52export function useQueryIdentity(): UseQueryResult<undefined, Error>; 53export function useQueryIdentity(didorhandle?: string): UseQueryResult< 54 | { 55 did: string; 56 handle: string; 57 pds: string; 58 signing_key: string; 59 } 60 | undefined, 61 Error 62>; 63export function useQueryIdentity(didorhandle?: string) { 64 const [slingshoturl] = useAtom(slingshotURLAtom); 65 return useQuery(constructIdentityQuery(didorhandle, slingshoturl)); 66} 67 68export function constructPostQuery(uri?: string, slingshoturl?: string) { 69 return queryOptions({ 70 queryKey: ["post", uri], 71 queryFn: async () => { 72 if (!uri) return undefined as undefined; 73 const res = await fetch( 74 `https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`, 75 ); 76 let data: any; 77 try { 78 data = await res.json(); 79 } catch { 80 return undefined; 81 } 82 if (res.status === 400) return undefined; 83 if ( 84 data?.error === "InvalidRequest" && 85 data.message?.includes("Could not find repo") 86 ) { 87 return undefined; // cache “not found” 88 } 89 try { 90 if (!res.ok) throw new Error("Failed to fetch post"); 91 return data as { 92 uri: string; 93 cid: string; 94 value: any; 95 }; 96 } catch (_e) { 97 return undefined; 98 } 99 }, 100 retry: (failureCount, error) => { 101 // dont retry 400 errors 102 if ((error as any)?.message?.includes("400")) return false; 103 return failureCount < 2; 104 }, 105 staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes 106 gcTime: /*0//*/ 5 * 60 * 1000, 107 }); 108} 109export function useQueryPost(uri: string): UseQueryResult< 110 { 111 uri: string; 112 cid: string; 113 value: ATPAPI.AppBskyFeedPost.Record; 114 }, 115 Error 116>; 117export function useQueryPost(): UseQueryResult<undefined, Error>; 118export function useQueryPost(uri?: string): UseQueryResult< 119 | { 120 uri: string; 121 cid: string; 122 value: ATPAPI.AppBskyFeedPost.Record; 123 } 124 | undefined, 125 Error 126>; 127export function useQueryPost(uri?: string) { 128 const [slingshoturl] = useAtom(slingshotURLAtom); 129 return useQuery(constructPostQuery(uri, slingshoturl)); 130} 131 132export function constructProfileQuery(uri?: string, slingshoturl?: string) { 133 return queryOptions({ 134 queryKey: ["profile", uri], 135 queryFn: async () => { 136 if (!uri) return undefined as undefined; 137 const res = await fetch( 138 `https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`, 139 ); 140 let data: any; 141 try { 142 data = await res.json(); 143 } catch { 144 return undefined; 145 } 146 if (res.status === 400) return undefined; 147 if ( 148 data?.error === "InvalidRequest" && 149 data.message?.includes("Could not find repo") 150 ) { 151 return undefined; // cache “not found” 152 } 153 try { 154 if (!res.ok) throw new Error("Failed to fetch post"); 155 return data as { 156 uri: string; 157 cid: string; 158 value: any; 159 }; 160 } catch (_e) { 161 return undefined; 162 } 163 }, 164 retry: (failureCount, error) => { 165 // dont retry 400 errors 166 if ((error as any)?.message?.includes("400")) return false; 167 return failureCount < 2; 168 }, 169 staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes 170 gcTime: /*0//*/ 5 * 60 * 1000, 171 }); 172} 173export function useQueryProfile(uri: string): UseQueryResult< 174 { 175 uri: string; 176 cid: string; 177 value: ATPAPI.AppBskyActorProfile.Record; 178 }, 179 Error 180>; 181export function useQueryProfile(): UseQueryResult<undefined, Error>; 182export function useQueryProfile(uri?: string): UseQueryResult< 183 | { 184 uri: string; 185 cid: string; 186 value: ATPAPI.AppBskyActorProfile.Record; 187 } 188 | undefined, 189 Error 190>; 191export function useQueryProfile(uri?: string) { 192 const [slingshoturl] = useAtom(slingshotURLAtom); 193 return useQuery(constructProfileQuery(uri, slingshoturl)); 194} 195 196// export function constructConstellationQuery( 197// method: "/links", 198// target: string, 199// collection: string, 200// path: string, 201// cursor?: string 202// ): QueryOptions<linksRecordsResponse, Error>; 203// export function constructConstellationQuery( 204// method: "/links/distinct-dids", 205// target: string, 206// collection: string, 207// path: string, 208// cursor?: string 209// ): QueryOptions<linksDidsResponse, Error>; 210// export function constructConstellationQuery( 211// method: "/links/count", 212// target: string, 213// collection: string, 214// path: string, 215// cursor?: string 216// ): QueryOptions<linksCountResponse, Error>; 217// export function constructConstellationQuery( 218// method: "/links/count/distinct-dids", 219// target: string, 220// collection: string, 221// path: string, 222// cursor?: string 223// ): QueryOptions<linksCountResponse, Error>; 224// export function constructConstellationQuery( 225// method: "/links/all", 226// target: string 227// ): QueryOptions<linksAllResponse, Error>; 228export function constructConstellationQuery(query?: { 229 constellation: string; 230 method: 231 | "/links" 232 | "/links/distinct-dids" 233 | "/links/count" 234 | "/links/count/distinct-dids" 235 | "/links/all" 236 | "undefined"; 237 target: string; 238 collection?: string; 239 path?: string; 240 cursor?: string; 241 dids?: string[]; 242 customkey?: string; 243}) { 244 // : QueryOptions< 245 // | linksRecordsResponse 246 // | linksDidsResponse 247 // | linksCountResponse 248 // | linksAllResponse 249 // | undefined, 250 // Error 251 // > 252 return queryOptions({ 253 queryKey: [ 254 "constellation", 255 query?.method, 256 query?.target, 257 query?.collection, 258 query?.path, 259 query?.cursor, 260 query?.dids, 261 query?.customkey, 262 ] as const, 263 queryFn: async () => { 264 if (!query || query.method === "undefined") return undefined as undefined; 265 const method = query.method; 266 const target = query.target; 267 const collection = query?.collection; 268 const path = query?.path; 269 const cursor = query.cursor; 270 const dids = query?.dids; 271 const res = await fetch( 272 `https://${query.constellation}${method}?target=${encodeURIComponent(target)}${collection ? `&collection=${encodeURIComponent(collection)}` : ""}${path ? `&path=${encodeURIComponent(path)}` : ""}${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}${dids ? dids.map((did) => `&did=${encodeURIComponent(did)}`).join("") : ""}`, 273 ); 274 if (!res.ok) throw new Error("Failed to fetch post"); 275 try { 276 switch (method) { 277 case "/links": 278 return (await res.json()) as linksRecordsResponse; 279 case "/links/distinct-dids": 280 return (await res.json()) as linksDidsResponse; 281 case "/links/count": 282 return (await res.json()) as linksCountResponse; 283 case "/links/count/distinct-dids": 284 return (await res.json()) as linksCountResponse; 285 case "/links/all": 286 return (await res.json()) as linksAllResponse; 287 default: 288 return undefined; 289 } 290 } catch (_e) { 291 return undefined; 292 } 293 }, 294 // enforce short lifespan 295 staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes 296 gcTime: /*0//*/ 5 * 60 * 1000, 297 }); 298} 299// todo do more of these instead of overloads since overloads sucks so much apparently 300export function useQueryConstellationLinksCountDistinctDids(query?: { 301 method: "/links/count/distinct-dids"; 302 target: string; 303 collection: string; 304 path: string; 305 cursor?: string; 306}): UseQueryResult<linksCountResponse, Error> | undefined { 307 //if (!query) return; 308 const [constellationurl] = useAtom(constellationURLAtom); 309 const queryres = useQuery( 310 constructConstellationQuery( 311 query && { constellation: constellationurl, ...query }, 312 ), 313 ) as unknown as UseQueryResult<linksCountResponse, Error>; 314 if (!query) { 315 return undefined as undefined; 316 } 317 return queryres as UseQueryResult<linksCountResponse, Error>; 318} 319 320export function useQueryConstellation(query: { 321 method: "/links"; 322 target: string; 323 collection: string; 324 path: string; 325 cursor?: string; 326 dids?: string[]; 327 customkey?: string; 328}): UseQueryResult<linksRecordsResponse, Error>; 329export function useQueryConstellation(query: { 330 method: "/links/distinct-dids"; 331 target: string; 332 collection: string; 333 path: string; 334 cursor?: string; 335 customkey?: string; 336}): UseQueryResult<linksDidsResponse, Error>; 337export function useQueryConstellation(query: { 338 method: "/links/count"; 339 target: string; 340 collection: string; 341 path: string; 342 cursor?: string; 343 customkey?: string; 344}): UseQueryResult<linksCountResponse, Error>; 345export function useQueryConstellation(query: { 346 method: "/links/count/distinct-dids"; 347 target: string; 348 collection: string; 349 path: string; 350 cursor?: string; 351 customkey?: string; 352}): UseQueryResult<linksCountResponse, Error>; 353export function useQueryConstellation(query: { 354 method: "/links/all"; 355 target: string; 356 customkey?: string; 357}): UseQueryResult<linksAllResponse, Error>; 358export function useQueryConstellation(): undefined; 359export function useQueryConstellation(query: { 360 method: "undefined"; 361 target: string; 362 customkey?: string; 363}): undefined; 364export function useQueryConstellation(query?: { 365 method: 366 | "/links" 367 | "/links/distinct-dids" 368 | "/links/count" 369 | "/links/count/distinct-dids" 370 | "/links/all" 371 | "undefined"; 372 target: string; 373 collection?: string; 374 path?: string; 375 cursor?: string; 376 dids?: string[]; 377 customkey?: string; 378}): 379 | UseQueryResult< 380 | linksRecordsResponse 381 | linksDidsResponse 382 | linksCountResponse 383 | linksAllResponse 384 | undefined, 385 Error 386 > 387 | undefined { 388 //if (!query) return; 389 const [constellationurl] = useAtom(constellationURLAtom); 390 return useQuery( 391 constructConstellationQuery( 392 query && { constellation: constellationurl, ...query }, 393 ), 394 ); 395} 396 397export type linksRecord = { 398 did: string; 399 collection: string; 400 rkey: string; 401}; 402export type linksRecordsResponse = { 403 total: string; 404 linking_records: linksRecord[]; 405 cursor?: string; 406}; 407type linksDidsResponse = { 408 total: string; 409 linking_dids: string[]; 410 cursor?: string; 411}; 412type linksCountResponse = { 413 total: string; 414}; 415export type linksAllResponse = { 416 links: Record< 417 string, 418 Record< 419 string, 420 { 421 records: number; 422 distinct_dids: number; 423 } 424 > 425 >; 426}; 427 428export function constructFeedSkeletonQuery(options?: { 429 feedUri: string; 430 agent?: ATPAPI.Agent; 431 isAuthed: boolean; 432 pdsUrl?: string; 433 feedServiceDid?: string; 434}) { 435 return queryOptions({ 436 // The query key includes all dependencies to ensure it refetches when they change 437 queryKey: [ 438 "feedSkeleton", 439 options?.feedUri, 440 { isAuthed: options?.isAuthed, did: options?.agent?.did }, 441 ], 442 queryFn: async () => { 443 if (!options) return undefined as undefined; 444 const { feedUri, agent, isAuthed, pdsUrl, feedServiceDid } = options; 445 if (isAuthed) { 446 // Authenticated flow 447 if (!agent || !pdsUrl || !feedServiceDid) { 448 throw new Error( 449 "Missing required info for authenticated feed fetch.", 450 ); 451 } 452 const url = `${pdsUrl}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}`; 453 const res = await agent.fetchHandler(url, { 454 method: "GET", 455 headers: { 456 "atproto-proxy": `${feedServiceDid}#bsky_fg`, 457 "Content-Type": "application/json", 458 }, 459 }); 460 if (!res.ok) 461 throw new Error(`Authenticated feed fetch failed: ${res.statusText}`); 462 return (await res.json()) as ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema; 463 } else { 464 // Unauthenticated flow (using a public PDS/AppView) 465 const url = `https://discover.bsky.app/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}`; 466 const res = await fetch(url); 467 if (!res.ok) 468 throw new Error(`Public feed fetch failed: ${res.statusText}`); 469 return (await res.json()) as ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema; 470 } 471 }, 472 //enabled: !!feedUri && (isAuthed ? !!agent && !!pdsUrl && !!feedServiceDid : true), 473 }); 474} 475 476export function useQueryFeedSkeleton(options?: { 477 feedUri: string; 478 agent?: ATPAPI.Agent; 479 isAuthed: boolean; 480 pdsUrl?: string; 481 feedServiceDid?: string; 482}) { 483 return useQuery(constructFeedSkeletonQuery(options)); 484} 485 486export function constructRecordQuery( 487 did?: string, 488 collection?: string, 489 rkey?: string, 490 pdsUrl?: string, 491) { 492 return queryOptions({ 493 queryKey: ["record", did, collection, rkey], 494 queryFn: async () => { 495 if (!did || !collection || !rkey || !pdsUrl) 496 return undefined as undefined; 497 const url = `${pdsUrl}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=${encodeURIComponent(collection)}&rkey=${encodeURIComponent(rkey)}`; 498 const res = await fetch(url); 499 if (!res.ok) throw new Error("Failed to fetch record"); 500 try { 501 return (await res.json()) as { 502 uri: string; 503 cid: string; 504 value: any; 505 }; 506 } catch (_e) { 507 return undefined; 508 } 509 }, 510 staleTime: 5 * 60 * 1000, // 5 minutes 511 gcTime: 5 * 60 * 1000, 512 }); 513} 514 515export function useQueryRecord( 516 did?: string, 517 collection?: string, 518 rkey?: string, 519 pdsUrl?: string, 520) { 521 return useQuery(constructRecordQuery(did, collection, rkey, pdsUrl)); 522} 523 524export function constructPreferencesQuery( 525 agent?: ATPAPI.Agent | undefined, 526 pdsUrl?: string | undefined, 527) { 528 return queryOptions({ 529 queryKey: ["preferences", agent?.did], 530 queryFn: async () => { 531 if (!agent || !pdsUrl) throw new Error("Agent or PDS URL not available"); 532 const url = `${pdsUrl}/xrpc/app.bsky.actor.getPreferences`; 533 const res = await agent.fetchHandler(url, { method: "GET" }); 534 if (!res.ok) throw new Error("Failed to fetch preferences"); 535 return res.json(); 536 }, 537 }); 538} 539export function useQueryPreferences(options: { 540 agent?: ATPAPI.Agent | undefined; 541 pdsUrl?: string | undefined; 542}) { 543 return useQuery(constructPreferencesQuery(options.agent, options.pdsUrl)); 544} 545 546export function constructArbitraryQuery(uri?: string, slingshoturl?: string) { 547 return queryOptions({ 548 queryKey: ["arbitrary", uri], 549 queryFn: async () => { 550 if (!uri) return undefined as undefined; 551 const res = await fetch( 552 `https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`, 553 ); 554 let data: any; 555 try { 556 data = await res.json(); 557 } catch { 558 return undefined; 559 } 560 if (res.status === 400) return undefined; 561 if ( 562 data?.error === "InvalidRequest" && 563 data.message?.includes("Could not find repo") 564 ) { 565 return undefined; // cache “not found” 566 } 567 try { 568 if (!res.ok) throw new Error("Failed to fetch post"); 569 return data as { 570 uri: string; 571 cid: string; 572 value: any; 573 }; 574 } catch (_e) { 575 return undefined; 576 } 577 }, 578 retry: (failureCount, error) => { 579 // dont retry 400 errors 580 if ((error as any)?.message?.includes("400")) return false; 581 return failureCount < 2; 582 }, 583 staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes 584 gcTime: /*0//*/ 5 * 60 * 1000, 585 }); 586} 587export function useQueryArbitrary(uri: string): UseQueryResult< 588 { 589 uri: string; 590 cid: string; 591 value: any; 592 }, 593 Error 594>; 595export function useQueryArbitrary(): UseQueryResult<undefined, Error>; 596export function useQueryArbitrary(uri?: string): UseQueryResult< 597 | { 598 uri: string; 599 cid: string; 600 value: any; 601 } 602 | undefined, 603 Error 604>; 605export function useQueryArbitrary(uri?: string) { 606 const [slingshoturl] = useAtom(slingshotURLAtom); 607 return useQuery(constructArbitraryQuery(uri, slingshoturl)); 608} 609 610export function constructFallbackNothingQuery() { 611 return queryOptions({ 612 queryKey: ["nothing"], 613 queryFn: async () => { 614 return undefined; 615 }, 616 }); 617} 618 619type ListRecordsResponse = { 620 cursor?: string; 621 records: { 622 uri: string; 623 cid: string; 624 value: ATPAPI.AppBskyFeedPost.Record; 625 }[]; 626}; 627 628export function constructAuthorFeedQuery( 629 did: string, 630 pdsUrl: string, 631 collection: string = "app.bsky.feed.post", 632) { 633 return queryOptions({ 634 queryKey: ["authorFeed", did, collection], 635 queryFn: async ({ pageParam }: QueryFunctionContext) => { 636 const limit = 25; 637 638 const cursor = pageParam as string | undefined; 639 const cursorParam = cursor ? `&cursor=${cursor}` : ""; 640 641 const url = `${pdsUrl}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=${collection}&limit=${limit}${cursorParam}`; 642 643 const res = await fetch(url); 644 if (!res.ok) throw new Error("Failed to fetch author's posts"); 645 646 return res.json() as Promise<ListRecordsResponse>; 647 }, 648 }); 649} 650 651export function useInfiniteQueryAuthorFeed( 652 did: string | undefined, 653 pdsUrl: string | undefined, 654 collection?: string, 655) { 656 const { queryKey, queryFn } = constructAuthorFeedQuery( 657 did!, 658 pdsUrl!, 659 collection, 660 ); 661 662 return useInfiniteQuery({ 663 queryKey, 664 queryFn, 665 initialPageParam: undefined as never, // ???? what is this shit 666 getNextPageParam: (lastPage) => lastPage.cursor as null | undefined, 667 enabled: !!did && !!pdsUrl, 668 }); 669} 670 671type FeedSkeletonPage = ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema; 672 673export function constructInfiniteFeedSkeletonQuery(options: { 674 feedUri: string; 675 agent?: ATPAPI.Agent; 676 isAuthed: boolean; 677 pdsUrl?: string; 678 feedServiceDid?: string; 679 // todo the hell is a unauthedfeedurl 680 unauthedfeedurl?: string; 681}) { 682 const { feedUri, agent, isAuthed, pdsUrl, feedServiceDid, unauthedfeedurl } = 683 options; 684 685 return queryOptions({ 686 queryKey: ["feedSkeleton", feedUri, { isAuthed, did: agent?.did }], 687 688 queryFn: async ({ 689 pageParam, 690 }: QueryFunctionContext): Promise<FeedSkeletonPage> => { 691 const cursorParam = pageParam ? `&cursor=${pageParam}` : ""; 692 693 if (isAuthed && !unauthedfeedurl) { 694 if (!agent || !pdsUrl || !feedServiceDid) { 695 throw new Error( 696 "Missing required info for authenticated feed fetch.", 697 ); 698 } 699 const url = `${pdsUrl}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}${cursorParam}`; 700 const res = await agent.fetchHandler(url, { 701 method: "GET", 702 headers: { 703 "atproto-proxy": `${feedServiceDid}#bsky_fg`, 704 "Content-Type": "application/json", 705 }, 706 }); 707 if (!res.ok) 708 throw new Error(`Authenticated feed fetch failed: ${res.statusText}`); 709 return (await res.json()) as FeedSkeletonPage; 710 } else { 711 const url = `https://${unauthedfeedurl ? unauthedfeedurl : "discover.bsky.app"}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}${cursorParam}`; 712 const res = await fetch(url); 713 if (!res.ok) 714 throw new Error(`Public feed fetch failed: ${res.statusText}`); 715 return (await res.json()) as FeedSkeletonPage; 716 } 717 }, 718 }); 719} 720 721export function useInfiniteQueryFeedSkeleton(options: { 722 feedUri: string; 723 agent?: ATPAPI.Agent; 724 isAuthed: boolean; 725 pdsUrl?: string; 726 feedServiceDid?: string; 727 unauthedfeedurl?: string; 728}) { 729 const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery(options); 730 731 return { 732 ...useInfiniteQuery({ 733 queryKey, 734 queryFn, 735 initialPageParam: undefined as never, 736 getNextPageParam: (lastPage) => lastPage.cursor as null | undefined, 737 staleTime: Infinity, 738 refetchOnWindowFocus: false, 739 enabled: 740 !!options.feedUri && 741 (options.isAuthed 742 ? ((!!options.agent && !!options.pdsUrl) || 743 !!options.unauthedfeedurl) && 744 !!options.feedServiceDid 745 : true), 746 }), 747 queryKey: queryKey, 748 }; 749} 750 751export function yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(query?: { 752 constellation: string; 753 method: "/links"; 754 target?: string; 755 collection: string; 756 path: string; 757 staleMult?: number; 758}) { 759 const safemult = query?.staleMult ?? 1; 760 // console.log( 761 // 'yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks', 762 // query, 763 // ) 764 765 return infiniteQueryOptions({ 766 enabled: !!query?.target, 767 queryKey: [ 768 "reddwarf_constellation", 769 query?.method, 770 query?.target, 771 query?.collection, 772 query?.path, 773 ] as const, 774 775 queryFn: async ({ pageParam }: { pageParam?: string }) => { 776 if (!query || !query?.target) return undefined; 777 778 const method = query.method; 779 const target = query.target; 780 const collection = query.collection; 781 const path = query.path; 782 const cursor = pageParam; 783 784 const res = await fetch( 785 `https://${query.constellation}${method}?target=${encodeURIComponent(target)}${ 786 collection ? `&collection=${encodeURIComponent(collection)}` : "" 787 }${path ? `&path=${encodeURIComponent(path)}` : ""}${ 788 cursor ? `&cursor=${encodeURIComponent(cursor)}` : "" 789 }`, 790 ); 791 792 if (!res.ok) throw new Error("Failed to fetch"); 793 794 return (await res.json()) as linksRecordsResponse; 795 }, 796 797 getNextPageParam: (lastPage) => { 798 return (lastPage as any)?.cursor ?? undefined; 799 }, 800 initialPageParam: undefined, 801 staleTime: 5 * 60 * 1000 * safemult, 802 gcTime: 5 * 60 * 1000 * safemult, 803 }); 804} 805 806export function useQueryLycanStatus() { 807 const [lycanurl] = useAtom(lycanURLAtom); 808 const { agent, status } = useAuth(); 809 const { data: identity } = useQueryIdentity(agent?.did); 810 return useQuery( 811 constructLycanStatusCheckQuery({ 812 agent: agent || undefined, 813 isAuthed: status === "signedIn", 814 pdsUrl: identity?.pds, 815 feedServiceDid: "did:web:" + lycanurl, 816 }), 817 ); 818} 819 820export function constructLycanStatusCheckQuery(options: { 821 agent?: ATPAPI.Agent; 822 isAuthed: boolean; 823 pdsUrl?: string; 824 feedServiceDid?: string; 825}) { 826 const { agent, isAuthed, pdsUrl, feedServiceDid } = options; 827 828 return queryOptions({ 829 queryKey: ["lycanStatus", { isAuthed, did: agent?.did }], 830 831 queryFn: async () => { 832 if (isAuthed && agent && pdsUrl && feedServiceDid) { 833 const url = `${pdsUrl}/xrpc/blue.feeds.lycan.getImportStatus`; 834 const res = await agent.fetchHandler(url, { 835 method: "GET", 836 headers: { 837 "atproto-proxy": `${feedServiceDid}#lycan`, 838 "Content-Type": "application/json", 839 }, 840 }); 841 if (!res.ok) 842 throw new Error( 843 `Authenticated lycan status fetch failed: ${res.statusText}`, 844 ); 845 return (await res.json()) as statuschek; 846 } 847 return undefined; 848 }, 849 }); 850} 851 852type statuschek = { 853 [key: string]: unknown; 854 error?: "MethodNotImplemented"; 855 message?: "Method Not Implemented"; 856 status?: "finished" | "in_progress"; 857 position?: string; 858 progress?: number; 859}; 860 861//{"status":"in_progress","position":"2025-08-30T06:53:18Z","progress":0.0878319661441268} 862type importtype = { 863 message?: "Import has already started" | "Import has been scheduled"; 864}; 865 866export function constructLycanRequestIndexQuery(options: { 867 agent?: ATPAPI.Agent; 868 isAuthed: boolean; 869 pdsUrl?: string; 870 feedServiceDid?: string; 871}) { 872 const { agent, isAuthed, pdsUrl, feedServiceDid } = options; 873 874 return queryOptions({ 875 queryKey: ["lycanIndex", { isAuthed, did: agent?.did }], 876 877 queryFn: async () => { 878 if (isAuthed && agent && pdsUrl && feedServiceDid) { 879 const url = `${pdsUrl}/xrpc/blue.feeds.lycan.startImport`; 880 const res = await agent.fetchHandler(url, { 881 method: "POST", 882 headers: { 883 "atproto-proxy": `${feedServiceDid}#lycan`, 884 "Content-Type": "application/json", 885 }, 886 }); 887 if (!res.ok) 888 throw new Error( 889 `Authenticated lycan status fetch failed: ${res.statusText}`, 890 ); 891 return (await res.json()) as importtype; 892 } 893 return undefined; 894 }, 895 }); 896} 897 898type LycanSearchPage = { 899 terms: string[]; 900 posts: string[]; 901 cursor?: string; 902}; 903 904export function useInfiniteQueryLycanSearch(options: { 905 query: string; 906 type: "likes" | "pins" | "reposts" | "quotes"; 907}) { 908 const [lycanurl] = useAtom(lycanURLAtom); 909 const { agent, status } = useAuth(); 910 const { data: identity } = useQueryIdentity(agent?.did); 911 912 const { queryKey, queryFn } = constructLycanSearchQuery({ 913 agent: agent || undefined, 914 isAuthed: status === "signedIn", 915 pdsUrl: identity?.pds, 916 feedServiceDid: "did:web:" + lycanurl, 917 query: options.query, 918 type: options.type, 919 }); 920 921 return { 922 ...useInfiniteQuery({ 923 queryKey, 924 queryFn, 925 initialPageParam: undefined as never, 926 getNextPageParam: (lastPage) => lastPage?.cursor as null | undefined, 927 //staleTime: Infinity, 928 refetchOnWindowFocus: false, 929 // enabled: 930 // !!options.feedUri && 931 // (options.isAuthed 932 // ? ((!!options.agent && !!options.pdsUrl) || 933 // !!options.unauthedfeedurl) && 934 // !!options.feedServiceDid 935 // : true), 936 }), 937 queryKey: queryKey, 938 }; 939} 940 941export function constructLycanSearchQuery(options: { 942 agent?: ATPAPI.Agent; 943 isAuthed: boolean; 944 pdsUrl?: string; 945 feedServiceDid?: string; 946 type: "likes" | "pins" | "reposts" | "quotes"; 947 query: string; 948}) { 949 const { agent, isAuthed, pdsUrl, feedServiceDid, type, query } = options; 950 951 return infiniteQueryOptions({ 952 queryKey: ["lycanSearch", query, type, { isAuthed, did: agent?.did }], 953 954 queryFn: async ({ 955 pageParam, 956 }: QueryFunctionContext): Promise<LycanSearchPage | undefined> => { 957 if (isAuthed && agent && pdsUrl && feedServiceDid) { 958 const url = `${pdsUrl}/xrpc/blue.feeds.lycan.searchPosts?query=${query}&collection=${type}${pageParam ? `&cursor=${pageParam}` : ""}`; 959 const res = await agent.fetchHandler(url, { 960 method: "GET", 961 headers: { 962 "atproto-proxy": `${feedServiceDid}#lycan`, 963 "Content-Type": "application/json", 964 }, 965 }); 966 if (!res.ok) 967 throw new Error( 968 `Authenticated lycan status fetch failed: ${res.statusText}`, 969 ); 970 return (await res.json()) as LycanSearchPage; 971 } 972 return undefined; 973 }, 974 initialPageParam: undefined as never, 975 getNextPageParam: (lastPage) => lastPage?.cursor as null | undefined, 976 }); 977}