See the best posts from any Bluesky account
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Fetch viewer like/repost state for authenticated users on profile pages

Injects AtprotoOAuthService into ProfileController and uses the viewer's
authenticated agent to call getPosts, building a viewer map (postUri →
likeUri/repostUri) and CID map passed to the template. Falls back silently
to viewer=null on auth errors. Sets Cache-Control to private when viewer
data is present.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+36 -4
+36 -4
app/controllers/profile_controller.ts
··· 5 5 import { AtprotoClient, BlueskyRateLimitedError } from '#lib/atproto/index' 6 6 import type { Facet, FacetLink, FacetMention } from '#lib/atproto/index' 7 7 import { ClickHouseStore } from '#lib/clickhouse/index' 8 + import AtprotoOAuthService from '#services/atproto_oauth' 8 9 import { runBackfillStream, type SseWriter } from '#lib/backfill_stream' 9 10 import TrackedProfile from '#models/tracked_profile' 10 11 import BackfillJobRow from '#models/backfill_job' ··· 18 19 constructor( 19 20 private readonly handleResolver: HandleResolver, 20 21 private readonly clickHouseStore: ClickHouseStore, 21 - private readonly atprotoClient: AtprotoClient 22 + private readonly atprotoClient: AtprotoClient, 23 + private readonly atprotoOAuthService: AtprotoOAuthService 22 24 ) {} 23 25 24 26 // --------------------------------------------------------------------------- ··· 305 307 } 306 308 } 307 309 308 - // Add bsky.app URL to each post 310 + // 6. Fetch viewer state (like/repost status) if the user is authenticated 311 + let viewer: Record<string, { likeUri: string | null; repostUri: string | null }> | null = null 312 + const cidMap = new Map<string, string>() 313 + 314 + const isAuthenticated = await ctx.auth.check() 315 + if (isAuthenticated) { 316 + try { 317 + const viewerDid = ctx.auth.user!.did 318 + const agent = await this.atprotoOAuthService.getAgent(viewerDid) 319 + const postUris = posts.map((p) => p.postUri) 320 + if (postUris.length > 0) { 321 + const res = await agent.app.bsky.feed.getPosts({ uris: postUris }) 322 + viewer = {} 323 + for (const post of res.data.posts) { 324 + viewer[post.uri] = { 325 + likeUri: post.viewer?.like ?? null, 326 + repostUri: post.viewer?.repost ?? null, 327 + } 328 + cidMap.set(post.uri, post.cid) 329 + } 330 + } 331 + } catch (err) { 332 + // Silently fall back — don't let auth issues break the page 333 + logger.debug({ err }, 'Failed to fetch viewer state for profile page') 334 + viewer = null 335 + } 336 + } 337 + 338 + // 7. Add bsky.app URL and postCid to each post 309 339 const postsWithUrl = posts.map((p) => ({ 310 340 ...p, 311 341 embed: p.embed, 312 342 bskyUrl: atUriToBskyUrl(p.postUri), 313 343 replyParentBskyUrl: p.replyParentUri ? atUriToBskyUrl(p.replyParentUri) : null, 314 344 postTextSafe: renderRichText(p.postText, p.facets).replace(/\n/g, '<br>'), 345 + postCid: cidMap.get(p.postUri) ?? '', 315 346 })) 316 347 317 - // 6. Render profile page with Cache-Control 348 + // 8. Render profile page with Cache-Control 318 349 const appUrl = env.get('APP_URL') 319 350 const daysQs = daysWindow ? `?days=${daysWindow}` : '' 320 351 const canonicalUrl = `${appUrl}/profile/${canonicalHandle}/${kind}${daysQs}` 321 352 322 - response.header('Cache-Control', 'public, max-age=60') 353 + response.header('Cache-Control', isAuthenticated ? 'private, max-age=60' : 'public, max-age=60') 323 354 return view.render('pages/profile/show', { 324 355 handle: canonicalHandle, 325 356 displayName: profile.displayName, ··· 329 360 posts: postsWithUrl, 330 361 canonicalUrl, 331 362 indexedSince, 363 + viewer, 332 364 }) 333 365 } 334 366