Bluesky app fork with some witchin' additions 馃挮
at post-text-option 163 lines 4.2 kB view raw
1import { 2 type AppBskyFeedDefs, 3 type AppBskyFeedGetFeed as GetCustomFeed, 4 BskyAgent, 5 jsonStringToLex, 6} from '@atproto/api' 7 8import {PUBLIC_BSKY_SERVICE} from '#/lib/constants' 9import { 10 getAppLanguageAsContentLanguage, 11 getContentLanguages, 12} from '#/state/preferences/languages' 13import {type FeedAPI, type FeedAPIResponse} from './types' 14import {createBskyTopicsHeader, isBlueskyOwnedFeed} from './utils' 15 16export class CustomFeedAPI implements FeedAPI { 17 agent: BskyAgent 18 params: GetCustomFeed.QueryParams 19 userInterests?: string 20 21 constructor({ 22 agent, 23 feedParams, 24 userInterests, 25 }: { 26 agent: BskyAgent 27 feedParams: GetCustomFeed.QueryParams 28 userInterests?: string 29 }) { 30 this.agent = agent 31 this.params = feedParams 32 this.userInterests = userInterests 33 } 34 35 async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> { 36 const contentLangs = getContentLanguages().join(',') 37 const res = await this.agent.app.bsky.feed.getFeed( 38 { 39 ...this.params, 40 limit: 1, 41 }, 42 {headers: {'Accept-Language': contentLangs}}, 43 ) 44 return res.data.feed[0] 45 } 46 47 async fetch({ 48 cursor, 49 limit, 50 }: { 51 cursor: string | undefined 52 limit: number 53 }): Promise<FeedAPIResponse> { 54 const contentLangs = getContentLanguages().join(',') 55 const agent = this.agent 56 const isBlueskyOwned = isBlueskyOwnedFeed(this.params.feed) 57 58 const res = agent.did 59 ? await this.agent.app.bsky.feed.getFeed( 60 { 61 ...this.params, 62 cursor, 63 limit, 64 }, 65 { 66 headers: { 67 ...(isBlueskyOwned 68 ? createBskyTopicsHeader(this.userInterests) 69 : {}), 70 'Accept-Language': contentLangs, 71 }, 72 }, 73 ) 74 : await loggedOutFetch({...this.params, cursor, limit}) 75 if (res.success) { 76 // NOTE 77 // some custom feeds fail to enforce the pagination limit 78 // so we manually truncate here 79 // -prf 80 if (res.data.feed.length > limit) { 81 res.data.feed = res.data.feed.slice(0, limit) 82 } 83 return { 84 cursor: res.data.feed.length ? res.data.cursor : undefined, 85 feed: res.data.feed, 86 } 87 } 88 return { 89 feed: [], 90 } 91 } 92} 93 94// HACK 95// we want feeds to give language-specific results immediately when a 96// logged-out user changes their language. this comes with two problems: 97// 1. not all languages have content, and 98// 2. our public caching layer isnt correctly busting against the accept-language header 99// for now we handle both of these with a manual workaround 100// -prf 101async function loggedOutFetch({ 102 feed, 103 limit, 104 cursor, 105}: { 106 feed: string 107 limit: number 108 cursor?: string 109}) { 110 let contentLangs = getAppLanguageAsContentLanguage() 111 112 /** 113 * Copied from our root `Agent` class 114 * @see https://github.com/bluesky-social/atproto/blob/60df3fc652b00cdff71dd9235d98a7a4bb828f05/packages/api/src/agent.ts#L120 115 */ 116 const labelersHeader = { 117 'atproto-accept-labelers': BskyAgent.appLabelers 118 .map(l => `${l};redact`) 119 .join(', '), 120 } 121 122 // manually construct fetch call so we can add the `lang` cache-busting param 123 let res = await fetch( 124 `${PUBLIC_BSKY_SERVICE}/xrpc/app.bsky.feed.getFeed?feed=${feed}${ 125 cursor ? `&cursor=${cursor}` : '' 126 }&limit=${limit}&lang=${contentLangs}`, 127 { 128 method: 'GET', 129 headers: {'Accept-Language': contentLangs, ...labelersHeader}, 130 }, 131 ) 132 let data = res.ok 133 ? (jsonStringToLex(await res.text()) as GetCustomFeed.OutputSchema) 134 : null 135 if (data?.feed?.length) { 136 return { 137 success: true, 138 data, 139 } 140 } 141 142 // no data, try again with language headers removed 143 res = await fetch( 144 `${PUBLIC_BSKY_SERVICE}/xrpc/app.bsky.feed.getFeed?feed=${feed}${ 145 cursor ? `&cursor=${cursor}` : '' 146 }&limit=${limit}`, 147 {method: 'GET', headers: {'Accept-Language': '', ...labelersHeader}}, 148 ) 149 data = res.ok 150 ? (jsonStringToLex(await res.text()) as GetCustomFeed.OutputSchema) 151 : null 152 if (data?.feed?.length) { 153 return { 154 success: true, 155 data, 156 } 157 } 158 159 return { 160 success: false, 161 data: {feed: []}, 162 } 163}