@@ -117,35 +117,6 @@ export async function fetchConstellationCounts(
117117 }
118118}
119119
120- /**
121- * Detect if error is AppView-related (suspended user, not found, etc.)
122- *
123- * IMPORTANT: This determines whether fallback should be triggered.
124- * We should NOT trigger fallback for intentional blocking/privacy errors.
125- */
126- export function isAppViewError ( error : any ) : boolean {
127- if ( ! error ) return false
128-
129- const msg = error . message ?. toLowerCase ( ) || ''
130-
131- // Do NOT trigger fallback for intentional blocking
132- // "Requester has blocked actor" means the user intentionally blocked someone
133- // This is NOT an AppView outage - it's privacy enforcement
134- if ( msg . includes ( 'blocked actor' ) ) return false
135- if ( msg . includes ( 'requester has blocked' ) ) return false
136- if ( msg . includes ( 'blocking' ) ) return false
137-
138- // Check HTTP status codes
139- if ( error . status === 400 || error . status === 404 ) return true
140-
141- // Check error messages for actual AppView issues
142- if ( msg . includes ( 'not found' ) ) return true
143- if ( msg . includes ( 'suspended' ) ) return true
144- if ( msg . includes ( 'could not locate' ) ) return true
145-
146- return false
147- }
148-
149120/**
150121 * Build viewer state for fallback profiles
151122 * Checks local block/mute cache to populate viewer relationship fields
@@ -180,22 +151,6 @@ function buildViewerState(
180151 return viewer
181152}
182153
183- /**
184- * Build a BlockedPost stub to match AppView behavior
185- * Returns the same structure as app.bsky.feed.defs#blockedPost
186- */
187- function buildBlockedPost ( uri : string ) : any {
188- return {
189- $type : 'app.bsky.feed.defs#blockedPost' ,
190- uri,
191- blocked : true ,
192- author : {
193- did : new AtUri ( uri ) . host ,
194- handle : '' ,
195- } ,
196- }
197- }
198-
199154/**
200155 * Build a BlockedProfile stub to match AppView behavior
201156 * Returns a minimal profile view indicating the profile is blocked
@@ -272,60 +227,6 @@ export async function buildSyntheticProfileView(
272227 }
273228}
274229
275- /**
276- * Build synthetic PostView from PDS + Constellation data
277- *
278- * SECURITY: Inherits block/mute checking from buildSyntheticProfileView.
279- * If the author is blocked, returns BlockedPost stub to match AppView behavior.
280- */
281- export async function buildSyntheticPostView (
282- queryClient : QueryClient ,
283- atUri : string ,
284- authorDid : string ,
285- authorHandle : string ,
286- ) : Promise < any > {
287- // Check if author is blocked first, before fetching any data
288- const viewer = buildViewerState ( queryClient , authorDid )
289- if ( viewer . blocking ) {
290- console . log ( '[Fallback] Returning blocked post stub for' , atUri )
291- return buildBlockedPost ( atUri )
292- }
293-
294- const record = await fetchRecordViaSlingshot ( atUri )
295- if ( ! record ) return null
296-
297- const counts = await fetchConstellationCounts ( atUri )
298- // Build profile view (will return basic info since not blocked)
299- const profileView = await buildSyntheticProfileView (
300- queryClient ,
301- authorDid ,
302- authorHandle ,
303- )
304-
305- // Get viewer state for the post itself (like, repost status)
306- // For now we just use empty viewer as we can't determine these from PDS
307- const postViewer = { }
308-
309- const embeds = resolveRecordEmbeds ( authorDid , record . value )
310-
311- return {
312- $type : 'app.bsky.feed.defs#postView' ,
313- uri : atUri ,
314- cid : record . cid ,
315- author : profileView ,
316- record : record . value ,
317- embed : embeds . length > 0 ? embeds [ 0 ] : undefined ,
318- indexedAt : record . value ?. createdAt || new Date ( ) . toISOString ( ) ,
319- likeCount : counts . likeCount ,
320- repostCount : counts . repostCount ,
321- replyCount : counts . replyCount ,
322- quoteCount : counts . quoteCount ,
323- viewer : postViewer , // Post-level viewer state (likes, reposts, etc)
324- labels : [ ] ,
325- __fallbackMode : true , // Mark as fallback data
326- }
327- }
328-
329230// Blacksky moderation account DID - labels from this account are authoritative
330231const BLACKSKY_MOD_DID = 'did:plc:d2mkddsbmnrgr3domzg5qexf'
331232
@@ -581,94 +482,3 @@ export async function buildSyntheticEmbedViewRecord(
581482 __fallbackMode : true ,
582483 }
583484}
584-
585- /**
586- * Build synthetic feed page from PDS data
587- * This is used for infinite queries that need paginated results
588- *
589- * IMPORTANT: This function bypasses Slingshot and fetches directly from the user's PDS
590- * because Slingshot does not support the `com.atproto.repo.listRecords` endpoint needed
591- * for bulk record fetching.
592- *
593- * Trade-off: No caching benefit from Slingshot, but we can still provide author feed
594- * functionality for AppView-suspended users.
595- *
596- * Each post in the feed will trigger:
597- * - 1 record fetch via Slingshot (for the full post data, cached)
598- * - 1 Constellation request (for engagement counts)
599- * - Profile fetch (cached after first request)
600- *
601- * SECURITY: Respects block/mute relationships. If author is blocked, the feed will be empty.
602- */
603- export async function buildSyntheticFeedPage (
604- queryClient : QueryClient ,
605- did : string ,
606- pdsUrl : string ,
607- cursor ?: string ,
608- ) : Promise < any > {
609- // Check if this author is blocked before fetching any posts
610- const viewer = buildViewerState ( queryClient , did )
611- if ( viewer . blocking ) {
612- console . log ( '[Fallback] Author is blocked, returning empty feed for' , did )
613- // Return empty feed to prevent viewing blocked user's posts via fallback
614- return {
615- feed : [ ] ,
616- cursor : undefined ,
617- __fallbackMode : true ,
618- __blocked : true ,
619- }
620- }
621-
622- try {
623- const limit = 25
624- const cursorParam = cursor ? `&cursor=${ encodeURIComponent ( cursor ) } ` : ''
625-
626- // Fetch posts directly from PDS using com.atproto.repo.listRecords
627- // NOTE: This bypasses Slingshot because listRecords is not available there
628- const url = `${ pdsUrl } /xrpc/com.atproto.repo.listRecords?repo=${ did } &collection=app.bsky.feed.post&limit=${ limit } ${ cursorParam } `
629- const res = await fetch ( url )
630-
631- if ( ! res . ok ) {
632- console . error (
633- '[Fallback] Failed to fetch author feed from PDS:' ,
634- res . statusText ,
635- )
636- return null
637- }
638-
639- const data = await res . json ( )
640-
641- // Build FeedViewPost array from records
642- const feed = await Promise . all (
643- data . records . map ( async ( record : any ) => {
644- const postView = await buildSyntheticPostView (
645- queryClient ,
646- record . uri ,
647- did ,
648- '' , // Handle will be resolved in buildSyntheticPostView
649- )
650-
651- if ( ! postView ) return null
652-
653- // Wrap in FeedViewPost format
654- return {
655- $type : 'app.bsky.feed.defs#feedViewPost' ,
656- post : postView ,
657- feedContext : undefined ,
658- }
659- } ) ,
660- )
661-
662- // Filter out null results
663- const validFeed = feed . filter ( item => item !== null )
664-
665- return {
666- feed : validFeed ,
667- cursor : data . cursor ,
668- __fallbackMode : true , // Mark as fallback data
669- }
670- } catch ( e ) {
671- console . error ( '[Fallback] Failed to build synthetic feed page:' , e )
672- return null
673- }
674- }
0 commit comments