1+ import { Client } from '@atproto/lex'
2+ import * as app from '#shared/types/lexicons/app'
13import type { AtprotoProfile } from '#shared/types/atproto'
24
35import {
4- ONE_THOUSAND_NPMX_USER_ACCOUNTS_XRPC ,
5- BSKY_APP_VIEW_USER_PROFILES_XRPC ,
6+ BLUESKY_API ,
67 ERROR_PDS_FETCH_FAILED ,
78} from '#shared/utils/constants'
89
@@ -11,11 +12,25 @@ interface GraphLink {
1112 target : string
1213}
1314
15+ const NPMX_PDS_HOST = 'https://npmx.social'
16+ const LIST_REPOS_LIMIT = 1000
1417const USER_BATCH_AMOUNT = 25
1518
16- export default defineCachedEventHandler (
17- async ( ) : Promise < { nodes : AtprotoProfile [ ] ; links : GraphLink [ ] } > => {
18- const response = await fetch ( ONE_THOUSAND_NPMX_USER_ACCOUNTS_XRPC )
19+ const blueskyClient = new Client ( { service : BLUESKY_API } )
20+
21+ /**
22+ * Paginate through all repos on the npmx PDS via com.atproto.sync.listRepos.
23+ */
24+ async function fetchAllDids ( ) : Promise < string [ ] > {
25+ const dids : string [ ] = [ ]
26+ let cursor : string | undefined
27+
28+ do {
29+ const url = new URL ( `${ NPMX_PDS_HOST } /xrpc/com.atproto.sync.listRepos` )
30+ url . searchParams . set ( 'limit' , String ( LIST_REPOS_LIMIT ) )
31+ if ( cursor ) url . searchParams . set ( 'cursor' , cursor )
32+
33+ const response = await fetch ( url . toString ( ) )
1934
2035 if ( ! response . ok ) {
2136 throw createError ( {
@@ -24,61 +39,75 @@ export default defineCachedEventHandler(
2439 } )
2540 }
2641
27- const listRepos = ( await response . json ( ) ) as { repos : { did : string } [ ] }
28- const dids = listRepos . repos . map ( repo => repo . did )
42+ const data = ( await response . json ( ) ) as {
43+ repos : { did : string } [ ]
44+ cursor ?: string
45+ }
46+
47+ dids . push ( ...data . repos . map ( repo => repo . did ) )
48+ cursor = data . cursor
49+ } while ( cursor )
50+
51+ return dids
52+ }
53+
54+ export default defineCachedEventHandler (
55+ async ( ) : Promise < { nodes : AtprotoProfile [ ] ; links : GraphLink [ ] } > => {
56+ const dids = await fetchAllDids ( )
2957 const localDids = new Set ( dids )
3058
3159 const nodes : AtprotoProfile [ ] = [ ]
3260 const links : GraphLink [ ] = [ ]
3361
62+ // Fetch profiles in batches using the @atproto /lex client
3463 for ( let i = 0 ; i < dids . length ; i += USER_BATCH_AMOUNT ) {
3564 const batch = dids . slice ( i , i + USER_BATCH_AMOUNT )
3665
37- const url = new URL ( BSKY_APP_VIEW_USER_PROFILES_XRPC )
38- for ( const did of batch ) {
39- url . searchParams . append ( 'actors' , did )
40- }
41-
4266 try {
43- const profilesResponse = await fetch ( url . toString ( ) )
44-
45- if ( ! profilesResponse . ok ) {
46- console . warn ( `Failed to fetch atproto profiles: ${ profilesResponse . status } ` )
47- continue
48- }
49-
50- const profilesData = ( await profilesResponse . json ( ) ) as { profiles : AtprotoProfile [ ] }
51-
52- if ( profilesData . profiles ) {
53- nodes . push ( ... profilesData . profiles )
54- }
67+ const data = await blueskyClient . call ( app . bsky . actor . getProfiles , {
68+ actors : batch ,
69+ } )
70+
71+ nodes . push (
72+ ... data . profiles . map ( profile => ( {
73+ did : profile . did ,
74+ handle : profile . handle ,
75+ displayName : profile . displayName ,
76+ avatar : profile . avatar ,
77+ } ) ) ,
78+ )
5579 } catch ( error ) {
5680 console . warn ( 'Failed to fetch atproto profiles:' , error )
5781 }
5882 }
5983
84+ // Fetch follow graphs (no lexicon type for getFollows, using raw fetch)
6085 for ( const did of dids ) {
61- const followResponse = await fetch (
62- `https://public.api.bsky.app/xrpc/app.bsky.graph.getFollows?actor=${ did } ` ,
63- )
86+ try {
87+ const followResponse = await fetch (
88+ `https://public.api.bsky.app/xrpc/app.bsky.graph.getFollows?actor=${ did } ` ,
89+ )
6490
65- if ( ! followResponse . ok ) {
66- console . warn ( `Failed to fetch follows: ${ followResponse . status } ` )
67- continue
68- }
91+ if ( ! followResponse . ok ) {
92+ console . warn ( `Failed to fetch follows: ${ followResponse . status } ` )
93+ continue
94+ }
6995
70- const followData = await followResponse . json ( )
96+ const followData = ( await followResponse . json ( ) ) as {
97+ follows : { did : string } [ ]
98+ }
7199
72- for ( const followedUser of followData . follows ) {
73- if ( localDids . has ( followedUser . did ) ) {
74- links . push ( { source : did , target : followedUser . did } )
100+ for ( const followedUser of followData . follows ) {
101+ if ( localDids . has ( followedUser . did ) ) {
102+ links . push ( { source : did , target : followedUser . did } )
103+ }
75104 }
105+ } catch ( error ) {
106+ console . warn ( 'Failed to fetch follows:' , error )
76107 }
77108 }
78- return {
79- nodes,
80- links,
81- }
109+
110+ return { nodes, links }
82111 } ,
83112 {
84113 maxAge : 3600 ,
0 commit comments