an appview-less Bluesky client using Constellation and PDS Queries
reddwarf.app
frontend
spa
bluesky
reddwarf
microcosm
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}