an appview-less Bluesky client using Constellation and PDS Queries reddwarf.app
frontend spa bluesky reddwarf microcosm
at main 27 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}) { 243 // : QueryOptions< 244 // | linksRecordsResponse 245 // | linksDidsResponse 246 // | linksCountResponse 247 // | linksAllResponse 248 // | undefined, 249 // Error 250 // > 251 return queryOptions({ 252 queryKey: [ 253 "constellation", 254 query?.method, 255 query?.target, 256 query?.collection, 257 query?.path, 258 query?.cursor, 259 query?.dids, 260 ] as const, 261 queryFn: async () => { 262 if (!query || query.method === "undefined") return undefined as undefined; 263 const method = query.method; 264 const target = query.target; 265 const collection = query?.collection; 266 const path = query?.path; 267 const cursor = query.cursor; 268 const dids = query?.dids; 269 const res = await fetch( 270 `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("") : ""}` 271 ); 272 if (!res.ok) throw new Error("Failed to fetch post"); 273 try { 274 switch (method) { 275 case "/links": 276 return (await res.json()) as linksRecordsResponse; 277 case "/links/distinct-dids": 278 return (await res.json()) as linksDidsResponse; 279 case "/links/count": 280 return (await res.json()) as linksCountResponse; 281 case "/links/count/distinct-dids": 282 return (await res.json()) as linksCountResponse; 283 case "/links/all": 284 return (await res.json()) as linksAllResponse; 285 default: 286 return undefined; 287 } 288 } catch (_e) { 289 return undefined; 290 } 291 }, 292 // enforce short lifespan 293 staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes 294 gcTime: /*0//*/ 5 * 60 * 1000, 295 }); 296} 297// todo do more of these instead of overloads since overloads sucks so much apparently 298export function useQueryConstellationLinksCountDistinctDids(query?: { 299 method: "/links/count/distinct-dids"; 300 target: string; 301 collection: string; 302 path: string; 303 cursor?: string; 304}): UseQueryResult<linksCountResponse, Error> | undefined { 305 //if (!query) return; 306 const [constellationurl] = useAtom(constellationURLAtom); 307 const queryres = useQuery( 308 constructConstellationQuery( 309 query && { constellation: constellationurl, ...query } 310 ) 311 ) as unknown as UseQueryResult<linksCountResponse, Error>; 312 if (!query) { 313 return undefined as undefined; 314 } 315 return queryres as UseQueryResult<linksCountResponse, Error>; 316} 317 318export function useQueryConstellation(query: { 319 method: "/links"; 320 target: string; 321 collection: string; 322 path: string; 323 cursor?: string; 324 dids?: string[]; 325}): UseQueryResult<linksRecordsResponse, Error>; 326export function useQueryConstellation(query: { 327 method: "/links/distinct-dids"; 328 target: string; 329 collection: string; 330 path: string; 331 cursor?: string; 332}): UseQueryResult<linksDidsResponse, Error>; 333export function useQueryConstellation(query: { 334 method: "/links/count"; 335 target: string; 336 collection: string; 337 path: string; 338 cursor?: string; 339}): UseQueryResult<linksCountResponse, Error>; 340export function useQueryConstellation(query: { 341 method: "/links/count/distinct-dids"; 342 target: string; 343 collection: string; 344 path: string; 345 cursor?: string; 346}): UseQueryResult<linksCountResponse, Error>; 347export function useQueryConstellation(query: { 348 method: "/links/all"; 349 target: string; 350}): UseQueryResult<linksAllResponse, Error>; 351export function useQueryConstellation(): undefined; 352export function useQueryConstellation(query: { 353 method: "undefined"; 354 target: string; 355}): undefined; 356export function useQueryConstellation(query?: { 357 method: 358 | "/links" 359 | "/links/distinct-dids" 360 | "/links/count" 361 | "/links/count/distinct-dids" 362 | "/links/all" 363 | "undefined"; 364 target: string; 365 collection?: string; 366 path?: string; 367 cursor?: string; 368 dids?: string[]; 369}): 370 | UseQueryResult< 371 | linksRecordsResponse 372 | linksDidsResponse 373 | linksCountResponse 374 | linksAllResponse 375 | undefined, 376 Error 377 > 378 | undefined { 379 //if (!query) return; 380 const [constellationurl] = useAtom(constellationURLAtom); 381 return useQuery( 382 constructConstellationQuery( 383 query && { constellation: constellationurl, ...query } 384 ) 385 ); 386} 387 388export type linksRecord = { 389 did: string; 390 collection: string; 391 rkey: string; 392}; 393export type linksRecordsResponse = { 394 total: string; 395 linking_records: linksRecord[]; 396 cursor?: string; 397}; 398type linksDidsResponse = { 399 total: string; 400 linking_dids: string[]; 401 cursor?: string; 402}; 403type linksCountResponse = { 404 total: string; 405}; 406export type linksAllResponse = { 407 links: Record< 408 string, 409 Record< 410 string, 411 { 412 records: number; 413 distinct_dids: number; 414 } 415 > 416 >; 417}; 418 419export function constructFeedSkeletonQuery(options?: { 420 feedUri: string; 421 agent?: ATPAPI.Agent; 422 isAuthed: boolean; 423 pdsUrl?: string; 424 feedServiceDid?: string; 425}) { 426 return queryOptions({ 427 // The query key includes all dependencies to ensure it refetches when they change 428 queryKey: [ 429 "feedSkeleton", 430 options?.feedUri, 431 { isAuthed: options?.isAuthed, did: options?.agent?.did }, 432 ], 433 queryFn: async () => { 434 if (!options) return undefined as undefined; 435 const { feedUri, agent, isAuthed, pdsUrl, feedServiceDid } = options; 436 if (isAuthed) { 437 // Authenticated flow 438 if (!agent || !pdsUrl || !feedServiceDid) { 439 throw new Error( 440 "Missing required info for authenticated feed fetch." 441 ); 442 } 443 const url = `${pdsUrl}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}`; 444 const res = await agent.fetchHandler(url, { 445 method: "GET", 446 headers: { 447 "atproto-proxy": `${feedServiceDid}#bsky_fg`, 448 "Content-Type": "application/json", 449 }, 450 }); 451 if (!res.ok) 452 throw new Error(`Authenticated feed fetch failed: ${res.statusText}`); 453 return (await res.json()) as ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema; 454 } else { 455 // Unauthenticated flow (using a public PDS/AppView) 456 const url = `https://discover.bsky.app/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}`; 457 const res = await fetch(url); 458 if (!res.ok) 459 throw new Error(`Public feed fetch failed: ${res.statusText}`); 460 return (await res.json()) as ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema; 461 } 462 }, 463 //enabled: !!feedUri && (isAuthed ? !!agent && !!pdsUrl && !!feedServiceDid : true), 464 }); 465} 466 467export function useQueryFeedSkeleton(options?: { 468 feedUri: string; 469 agent?: ATPAPI.Agent; 470 isAuthed: boolean; 471 pdsUrl?: string; 472 feedServiceDid?: string; 473}) { 474 return useQuery(constructFeedSkeletonQuery(options)); 475} 476 477export function constructPreferencesQuery( 478 agent?: ATPAPI.Agent | undefined, 479 pdsUrl?: string | undefined 480) { 481 return queryOptions({ 482 queryKey: ["preferences", agent?.did], 483 queryFn: async () => { 484 if (!agent || !pdsUrl) throw new Error("Agent or PDS URL not available"); 485 const url = `${pdsUrl}/xrpc/app.bsky.actor.getPreferences`; 486 const res = await agent.fetchHandler(url, { method: "GET" }); 487 if (!res.ok) throw new Error("Failed to fetch preferences"); 488 return res.json(); 489 }, 490 }); 491} 492export function useQueryPreferences(options: { 493 agent?: ATPAPI.Agent | undefined; 494 pdsUrl?: string | undefined; 495}) { 496 return useQuery(constructPreferencesQuery(options.agent, options.pdsUrl)); 497} 498 499export function constructArbitraryQuery(uri?: string, slingshoturl?: string) { 500 return queryOptions({ 501 queryKey: ["arbitrary", uri], 502 queryFn: async () => { 503 if (!uri) return undefined as undefined; 504 const res = await fetch( 505 `https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 506 ); 507 let data: any; 508 try { 509 data = await res.json(); 510 } catch { 511 return undefined; 512 } 513 if (res.status === 400) return undefined; 514 if ( 515 data?.error === "InvalidRequest" && 516 data.message?.includes("Could not find repo") 517 ) { 518 return undefined; // cache “not found” 519 } 520 try { 521 if (!res.ok) throw new Error("Failed to fetch post"); 522 return data as { 523 uri: string; 524 cid: string; 525 value: any; 526 }; 527 } catch (_e) { 528 return undefined; 529 } 530 }, 531 retry: (failureCount, error) => { 532 // dont retry 400 errors 533 if ((error as any)?.message?.includes("400")) return false; 534 return failureCount < 2; 535 }, 536 staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes 537 gcTime: /*0//*/ 5 * 60 * 1000, 538 }); 539} 540export function useQueryArbitrary(uri: string): UseQueryResult< 541 { 542 uri: string; 543 cid: string; 544 value: any; 545 }, 546 Error 547>; 548export function useQueryArbitrary(): UseQueryResult<undefined, Error>; 549export function useQueryArbitrary(uri?: string): UseQueryResult< 550 | { 551 uri: string; 552 cid: string; 553 value: any; 554 } 555 | undefined, 556 Error 557>; 558export function useQueryArbitrary(uri?: string) { 559 const [slingshoturl] = useAtom(slingshotURLAtom); 560 return useQuery(constructArbitraryQuery(uri, slingshoturl)); 561} 562 563export function constructFallbackNothingQuery() { 564 return queryOptions({ 565 queryKey: ["nothing"], 566 queryFn: async () => { 567 return undefined; 568 }, 569 }); 570} 571 572type ListRecordsResponse = { 573 cursor?: string; 574 records: { 575 uri: string; 576 cid: string; 577 value: ATPAPI.AppBskyFeedPost.Record; 578 }[]; 579}; 580 581export function constructAuthorFeedQuery( 582 did: string, 583 pdsUrl: string, 584 collection: string = "app.bsky.feed.post" 585) { 586 return queryOptions({ 587 queryKey: ["authorFeed", did, collection], 588 queryFn: async ({ pageParam }: QueryFunctionContext) => { 589 const limit = 25; 590 591 const cursor = pageParam as string | undefined; 592 const cursorParam = cursor ? `&cursor=${cursor}` : ""; 593 594 const url = `${pdsUrl}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=${collection}&limit=${limit}${cursorParam}`; 595 596 const res = await fetch(url); 597 if (!res.ok) throw new Error("Failed to fetch author's posts"); 598 599 return res.json() as Promise<ListRecordsResponse>; 600 }, 601 }); 602} 603 604export function useInfiniteQueryAuthorFeed( 605 did: string | undefined, 606 pdsUrl: string | undefined, 607 collection?: string 608) { 609 const { queryKey, queryFn } = constructAuthorFeedQuery( 610 did!, 611 pdsUrl!, 612 collection 613 ); 614 615 return useInfiniteQuery({ 616 queryKey, 617 queryFn, 618 initialPageParam: undefined as never, // ???? what is this shit 619 getNextPageParam: (lastPage) => lastPage.cursor as null | undefined, 620 enabled: !!did && !!pdsUrl, 621 }); 622} 623 624type FeedSkeletonPage = ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema; 625 626export function constructInfiniteFeedSkeletonQuery(options: { 627 feedUri: string; 628 agent?: ATPAPI.Agent; 629 isAuthed: boolean; 630 pdsUrl?: string; 631 feedServiceDid?: string; 632 // todo the hell is a unauthedfeedurl 633 unauthedfeedurl?: string; 634}) { 635 const { feedUri, agent, isAuthed, pdsUrl, feedServiceDid, unauthedfeedurl } = 636 options; 637 638 return queryOptions({ 639 queryKey: ["feedSkeleton", feedUri, { isAuthed, did: agent?.did }], 640 641 queryFn: async ({ 642 pageParam, 643 }: QueryFunctionContext): Promise<FeedSkeletonPage> => { 644 const cursorParam = pageParam ? `&cursor=${pageParam}` : ""; 645 646 if (isAuthed && !unauthedfeedurl) { 647 if (!agent || !pdsUrl || !feedServiceDid) { 648 throw new Error( 649 "Missing required info for authenticated feed fetch." 650 ); 651 } 652 const url = `${pdsUrl}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}${cursorParam}`; 653 const res = await agent.fetchHandler(url, { 654 method: "GET", 655 headers: { 656 "atproto-proxy": `${feedServiceDid}#bsky_fg`, 657 "Content-Type": "application/json", 658 }, 659 }); 660 if (!res.ok) 661 throw new Error(`Authenticated feed fetch failed: ${res.statusText}`); 662 return (await res.json()) as FeedSkeletonPage; 663 } else { 664 const url = `https://${unauthedfeedurl ? unauthedfeedurl : "discover.bsky.app"}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}${cursorParam}`; 665 const res = await fetch(url); 666 if (!res.ok) 667 throw new Error(`Public feed fetch failed: ${res.statusText}`); 668 return (await res.json()) as FeedSkeletonPage; 669 } 670 }, 671 }); 672} 673 674export function useInfiniteQueryFeedSkeleton(options: { 675 feedUri: string; 676 agent?: ATPAPI.Agent; 677 isAuthed: boolean; 678 pdsUrl?: string; 679 feedServiceDid?: string; 680 unauthedfeedurl?: string; 681}) { 682 const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery(options); 683 684 return { 685 ...useInfiniteQuery({ 686 queryKey, 687 queryFn, 688 initialPageParam: undefined as never, 689 getNextPageParam: (lastPage) => lastPage.cursor as null | undefined, 690 staleTime: Infinity, 691 refetchOnWindowFocus: false, 692 enabled: 693 !!options.feedUri && 694 (options.isAuthed 695 ? ((!!options.agent && !!options.pdsUrl) || 696 !!options.unauthedfeedurl) && 697 !!options.feedServiceDid 698 : true), 699 }), 700 queryKey: queryKey, 701 }; 702} 703 704export function yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(query?: { 705 constellation: string; 706 method: "/links"; 707 target?: string; 708 collection: string; 709 path: string; 710 staleMult?: number; 711}) { 712 const safemult = query?.staleMult ?? 1; 713 // console.log( 714 // 'yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks', 715 // query, 716 // ) 717 718 return infiniteQueryOptions({ 719 enabled: !!query?.target, 720 queryKey: [ 721 "reddwarf_constellation", 722 query?.method, 723 query?.target, 724 query?.collection, 725 query?.path, 726 ] as const, 727 728 queryFn: async ({ pageParam }: { pageParam?: string }) => { 729 if (!query || !query?.target) return undefined; 730 731 const method = query.method; 732 const target = query.target; 733 const collection = query.collection; 734 const path = query.path; 735 const cursor = pageParam; 736 737 const res = await fetch( 738 `https://${query.constellation}${method}?target=${encodeURIComponent(target)}${ 739 collection ? `&collection=${encodeURIComponent(collection)}` : "" 740 }${path ? `&path=${encodeURIComponent(path)}` : ""}${ 741 cursor ? `&cursor=${encodeURIComponent(cursor)}` : "" 742 }` 743 ); 744 745 if (!res.ok) throw new Error("Failed to fetch"); 746 747 return (await res.json()) as linksRecordsResponse; 748 }, 749 750 getNextPageParam: (lastPage) => { 751 return (lastPage as any)?.cursor ?? undefined; 752 }, 753 initialPageParam: undefined, 754 staleTime: 5 * 60 * 1000 * safemult, 755 gcTime: 5 * 60 * 1000 * safemult, 756 }); 757} 758 759export function useQueryLycanStatus() { 760 const [lycanurl] = useAtom(lycanURLAtom); 761 const { agent, status } = useAuth(); 762 const { data: identity } = useQueryIdentity(agent?.did); 763 return useQuery( 764 constructLycanStatusCheckQuery({ 765 agent: agent || undefined, 766 isAuthed: status === "signedIn", 767 pdsUrl: identity?.pds, 768 feedServiceDid: "did:web:"+lycanurl, 769 }) 770 ); 771} 772 773export function constructLycanStatusCheckQuery(options: { 774 agent?: ATPAPI.Agent; 775 isAuthed: boolean; 776 pdsUrl?: string; 777 feedServiceDid?: string; 778}) { 779 const { agent, isAuthed, pdsUrl, feedServiceDid } = options; 780 781 return queryOptions({ 782 queryKey: ["lycanStatus", { isAuthed, did: agent?.did }], 783 784 queryFn: async () => { 785 if (isAuthed && agent && pdsUrl && feedServiceDid) { 786 const url = `${pdsUrl}/xrpc/blue.feeds.lycan.getImportStatus`; 787 const res = await agent.fetchHandler(url, { 788 method: "GET", 789 headers: { 790 "atproto-proxy": `${feedServiceDid}#lycan`, 791 "Content-Type": "application/json", 792 }, 793 }); 794 if (!res.ok) 795 throw new Error( 796 `Authenticated lycan status fetch failed: ${res.statusText}` 797 ); 798 return (await res.json()) as statuschek; 799 } 800 return undefined; 801 }, 802 }); 803} 804 805type statuschek = { 806 [key: string]: unknown; 807 error?: "MethodNotImplemented"; 808 message?: "Method Not Implemented"; 809 status?: "finished" | "in_progress"; 810 position?: string, 811 progress?: number, 812 813}; 814 815//{"status":"in_progress","position":"2025-08-30T06:53:18Z","progress":0.0878319661441268} 816type importtype = { 817 message?: "Import has already started" | "Import has been scheduled" 818} 819 820export function constructLycanRequestIndexQuery(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: ["lycanIndex", { isAuthed, did: agent?.did }], 830 831 queryFn: async () => { 832 if (isAuthed && agent && pdsUrl && feedServiceDid) { 833 const url = `${pdsUrl}/xrpc/blue.feeds.lycan.startImport`; 834 const res = await agent.fetchHandler(url, { 835 method: "POST", 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 importtype; 846 } 847 return undefined; 848 }, 849 }); 850} 851 852type LycanSearchPage = { 853 terms: string[]; 854 posts: string[]; 855 cursor?: string; 856}; 857 858 859export function useInfiniteQueryLycanSearch(options: { query: string, type: "likes" | "pins" | "reposts" | "quotes"}) { 860 861 862 const [lycanurl] = useAtom(lycanURLAtom); 863 const { agent, status } = useAuth(); 864 const { data: identity } = useQueryIdentity(agent?.did); 865 866 const { queryKey, queryFn } = constructLycanSearchQuery({ 867 agent: agent || undefined, 868 isAuthed: status === "signedIn", 869 pdsUrl: identity?.pds, 870 feedServiceDid: "did:web:"+lycanurl, 871 query: options.query, 872 type: options.type, 873 }) 874 875 return { 876 ...useInfiniteQuery({ 877 queryKey, 878 queryFn, 879 initialPageParam: undefined as never, 880 getNextPageParam: (lastPage) => lastPage?.cursor as null | undefined, 881 //staleTime: Infinity, 882 refetchOnWindowFocus: false, 883 // enabled: 884 // !!options.feedUri && 885 // (options.isAuthed 886 // ? ((!!options.agent && !!options.pdsUrl) || 887 // !!options.unauthedfeedurl) && 888 // !!options.feedServiceDid 889 // : true), 890 }), 891 queryKey: queryKey, 892 }; 893} 894 895 896export function constructLycanSearchQuery(options: { 897 agent?: ATPAPI.Agent; 898 isAuthed: boolean; 899 pdsUrl?: string; 900 feedServiceDid?: string; 901 type: "likes" | "pins" | "reposts" | "quotes"; 902 query: string; 903}) { 904 const { agent, isAuthed, pdsUrl, feedServiceDid, type, query } = options; 905 906 return infiniteQueryOptions({ 907 queryKey: ["lycanSearch", query, type, { isAuthed, did: agent?.did }], 908 909 queryFn: async ({ 910 pageParam, 911 }: QueryFunctionContext): Promise<LycanSearchPage | undefined> => { 912 if (isAuthed && agent && pdsUrl && feedServiceDid) { 913 const url = `${pdsUrl}/xrpc/blue.feeds.lycan.searchPosts?query=${query}&collection=${type}${pageParam ? `&cursor=${pageParam}` : ""}`; 914 const res = await agent.fetchHandler(url, { 915 method: "GET", 916 headers: { 917 "atproto-proxy": `${feedServiceDid}#lycan`, 918 "Content-Type": "application/json", 919 }, 920 }); 921 if (!res.ok) 922 throw new Error( 923 `Authenticated lycan status fetch failed: ${res.statusText}` 924 ); 925 return (await res.json()) as LycanSearchPage; 926 } 927 return undefined; 928 }, 929 initialPageParam: undefined as never, 930 getNextPageParam: (lastPage) => lastPage?.cursor as null | undefined, 931 }); 932}