forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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}