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