@atproto/bsky 0.0.66 → 0.0.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/api/app/bsky/feed/getLikes.d.ts.map +1 -1
- package/dist/api/app/bsky/feed/getLikes.js +6 -2
- package/dist/api/app/bsky/feed/getLikes.js.map +1 -1
- package/dist/api/app/bsky/feed/getRepostedBy.d.ts.map +1 -1
- package/dist/api/app/bsky/feed/getRepostedBy.js +6 -2
- package/dist/api/app/bsky/feed/getRepostedBy.js.map +1 -1
- package/dist/api/app/bsky/graph/getLists.d.ts.map +1 -1
- package/dist/api/app/bsky/graph/getLists.js +10 -1
- package/dist/api/app/bsky/graph/getLists.js.map +1 -1
- package/dist/api/app/bsky/graph/getSuggestedFollowsByActor.d.ts.map +1 -1
- package/dist/api/app/bsky/graph/getSuggestedFollowsByActor.js +37 -12
- package/dist/api/app/bsky/graph/getSuggestedFollowsByActor.js.map +1 -1
- package/dist/api/com/atproto/repo/getRecord.d.ts.map +1 -1
- package/dist/api/com/atproto/repo/getRecord.js +27 -19
- package/dist/api/com/atproto/repo/getRecord.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +14 -0
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +3 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +3 -0
- package/dist/context.js.map +1 -1
- package/dist/feature-gates.d.ts +25 -0
- package/dist/feature-gates.d.ts.map +1 -0
- package/dist/feature-gates.js +82 -0
- package/dist/feature-gates.js.map +1 -0
- package/dist/hydration/hydrator.d.ts +3 -0
- package/dist/hydration/hydrator.d.ts.map +1 -1
- package/dist/hydration/hydrator.js +67 -44
- package/dist/hydration/hydrator.js.map +1 -1
- package/dist/hydration/util.d.ts +3 -0
- package/dist/hydration/util.d.ts.map +1 -1
- package/dist/hydration/util.js +25 -1
- package/dist/hydration/util.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/lexicon/lexicons.d.ts +5 -0
- package/dist/lexicon/lexicons.d.ts.map +1 -1
- package/dist/lexicon/lexicons.js +7 -0
- package/dist/lexicon/lexicons.js.map +1 -1
- package/dist/lexicon/types/app/bsky/actor/defs.d.ts +1 -1
- package/dist/lexicon/types/app/bsky/actor/defs.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/actor/defs.js.map +1 -1
- package/dist/lexicon/types/app/bsky/embed/record.d.ts +1 -1
- package/dist/lexicon/types/app/bsky/embed/record.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/embed/record.js.map +1 -1
- package/dist/lexicon/types/app/bsky/unspecced/getSuggestionsSkeleton.d.ts +2 -0
- package/dist/lexicon/types/app/bsky/unspecced/getSuggestionsSkeleton.d.ts.map +1 -1
- package/dist/logger.d.ts +1 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +2 -1
- package/dist/logger.js.map +1 -1
- package/dist/views/index.d.ts +2 -3
- package/dist/views/index.d.ts.map +1 -1
- package/dist/views/index.js +21 -13
- package/dist/views/index.js.map +1 -1
- package/package.json +5 -4
- package/src/api/app/bsky/feed/getLikes.ts +6 -2
- package/src/api/app/bsky/feed/getRepostedBy.ts +6 -2
- package/src/api/app/bsky/graph/getLists.ts +19 -2
- package/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts +55 -15
- package/src/api/com/atproto/repo/getRecord.ts +28 -19
- package/src/config.ts +20 -0
- package/src/context.ts +6 -0
- package/src/feature-gates.ts +66 -0
- package/src/hydration/hydrator.ts +58 -16
- package/src/hydration/util.ts +28 -0
- package/src/index.ts +9 -0
- package/src/lexicon/lexicons.ts +8 -0
- package/src/lexicon/types/app/bsky/actor/defs.ts +1 -0
- package/src/lexicon/types/app/bsky/embed/record.ts +1 -0
- package/src/lexicon/types/app/bsky/unspecced/getSuggestionsSkeleton.ts +2 -0
- package/src/logger.ts +2 -0
- package/src/views/index.ts +18 -17
- package/tests/__snapshots__/feed-generation.test.ts.snap +136 -0
- package/tests/feed-generation.test.ts +80 -0
- package/tests/hydration/util.test.ts +82 -0
- package/tests/seed/known-followers.ts +110 -0
- package/tests/views/__snapshots__/lists.test.ts.snap +42 -0
- package/tests/views/known-followers.test.ts +160 -0
- package/tests/views/lists.test.ts +62 -0
- package/tests/views/profile.test.ts +0 -35
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { mapDefined } from '@atproto/common'
|
|
1
|
+
import { mapDefined, noUndefinedVals } from '@atproto/common'
|
|
2
2
|
import { InvalidRequestError } from '@atproto/xrpc-server'
|
|
3
|
+
import AtpAgent from '@atproto/api'
|
|
3
4
|
import { Server } from '../../../../lexicon'
|
|
4
5
|
import { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getSuggestedFollowsByActor'
|
|
5
6
|
import AppContext from '../../../../context'
|
|
@@ -27,14 +28,27 @@ export default function (server: Server, ctx: AppContext) {
|
|
|
27
28
|
const viewer = auth.credentials.iss
|
|
28
29
|
const labelers = ctx.reqLabelers(req)
|
|
29
30
|
const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer })
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
const headers = noUndefinedVals({
|
|
32
|
+
'accept-language': req.headers['accept-language'],
|
|
33
|
+
'x-bsky-topics': Array.isArray(req.headers['x-bsky-topics'])
|
|
34
|
+
? req.headers['x-bsky-topics'].join(',')
|
|
35
|
+
: req.headers['x-bsky-topics'],
|
|
36
|
+
})
|
|
37
|
+
const { headers: resultHeaders, ...result } =
|
|
38
|
+
await getSuggestedFollowsByActor(
|
|
39
|
+
{ ...params, hydrateCtx: hydrateCtx.copy({ viewer }), headers },
|
|
40
|
+
ctx,
|
|
41
|
+
)
|
|
42
|
+
const responseHeaders = noUndefinedVals({
|
|
43
|
+
'content-language': resultHeaders?.['content-language'],
|
|
44
|
+
})
|
|
34
45
|
return {
|
|
35
46
|
encoding: 'application/json',
|
|
36
47
|
body: result,
|
|
37
|
-
headers:
|
|
48
|
+
headers: {
|
|
49
|
+
...responseHeaders,
|
|
50
|
+
...resHeaders({ labelers: hydrateCtx.labelers }),
|
|
51
|
+
},
|
|
38
52
|
}
|
|
39
53
|
},
|
|
40
54
|
})
|
|
@@ -42,17 +56,39 @@ export default function (server: Server, ctx: AppContext) {
|
|
|
42
56
|
|
|
43
57
|
const skeleton = async (input: SkeletonFnInput<Context, Params>) => {
|
|
44
58
|
const { params, ctx } = input
|
|
59
|
+
const gates = ctx.featureGates
|
|
45
60
|
const [relativeToDid] = await ctx.hydrator.actor.getDids([params.actor])
|
|
46
61
|
if (!relativeToDid) {
|
|
47
62
|
throw new InvalidRequestError('Actor not found')
|
|
48
63
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
|
|
65
|
+
if (
|
|
66
|
+
ctx.suggestionsAgent &&
|
|
67
|
+
gates.check(
|
|
68
|
+
await gates.user({ did: params.hydrateCtx.viewer }),
|
|
69
|
+
gates.ids.NewSuggestedFollowsByActor,
|
|
70
|
+
)
|
|
71
|
+
) {
|
|
72
|
+
const res =
|
|
73
|
+
await ctx.suggestionsAgent.api.app.bsky.unspecced.getSuggestionsSkeleton(
|
|
74
|
+
{
|
|
75
|
+
viewer: params.hydrateCtx.viewer ?? undefined,
|
|
76
|
+
relativeToDid,
|
|
77
|
+
},
|
|
78
|
+
{ headers: params.headers },
|
|
79
|
+
)
|
|
80
|
+
return {
|
|
81
|
+
suggestedDids: res.data.actors.map((a) => a.did),
|
|
82
|
+
headers: res.headers,
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
const { dids } = await ctx.hydrator.dataplane.getFollowSuggestions({
|
|
86
|
+
actorDid: params.hydrateCtx.viewer,
|
|
87
|
+
relativeToDid,
|
|
88
|
+
})
|
|
89
|
+
return {
|
|
90
|
+
suggestedDids: dids,
|
|
91
|
+
}
|
|
56
92
|
}
|
|
57
93
|
}
|
|
58
94
|
|
|
@@ -80,22 +116,26 @@ const presentation = (
|
|
|
80
116
|
input: PresentationFnInput<Context, Params, SkeletonState>,
|
|
81
117
|
) => {
|
|
82
118
|
const { ctx, hydration, skeleton } = input
|
|
83
|
-
const { suggestedDids } = skeleton
|
|
119
|
+
const { suggestedDids, headers } = skeleton
|
|
84
120
|
const suggestions = mapDefined(suggestedDids, (did) =>
|
|
85
121
|
ctx.views.profileDetailed(did, hydration),
|
|
86
122
|
)
|
|
87
|
-
return { suggestions }
|
|
123
|
+
return { suggestions, headers }
|
|
88
124
|
}
|
|
89
125
|
|
|
90
126
|
type Context = {
|
|
91
127
|
hydrator: Hydrator
|
|
92
128
|
views: Views
|
|
129
|
+
suggestionsAgent: AtpAgent | undefined
|
|
130
|
+
featureGates: AppContext['featureGates']
|
|
93
131
|
}
|
|
94
132
|
|
|
95
133
|
type Params = QueryParams & {
|
|
96
134
|
hydrateCtx: HydrateCtx & { viewer: string }
|
|
135
|
+
headers: Record<string, string>
|
|
97
136
|
}
|
|
98
137
|
|
|
99
138
|
type SkeletonState = {
|
|
100
139
|
suggestedDids: string[]
|
|
140
|
+
headers?: Record<string, string>
|
|
101
141
|
}
|
|
@@ -4,27 +4,36 @@ import { Server } from '../../../../lexicon'
|
|
|
4
4
|
import AppContext from '../../../../context'
|
|
5
5
|
|
|
6
6
|
export default function (server: Server, ctx: AppContext) {
|
|
7
|
-
server.com.atproto.repo.getRecord(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
server.com.atproto.repo.getRecord({
|
|
8
|
+
auth: ctx.authVerifier.optionalStandardOrRole,
|
|
9
|
+
handler: async ({ auth, params }) => {
|
|
10
|
+
const { repo, collection, rkey, cid } = params
|
|
11
|
+
const { includeTakedowns } = ctx.authVerifier.parseCreds(auth)
|
|
12
|
+
const [did] = await ctx.hydrator.actor.getDids([repo])
|
|
13
|
+
if (!did) {
|
|
14
|
+
throw new InvalidRequestError(`Could not find repo: ${repo}`)
|
|
15
|
+
}
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
const actors = await ctx.hydrator.actor.getActors([did], includeTakedowns)
|
|
18
|
+
if (!actors.get(did)) {
|
|
19
|
+
throw new InvalidRequestError(`Could not find repo: ${repo}`)
|
|
20
|
+
}
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
22
|
+
const uri = AtUri.make(did, collection, rkey).toString()
|
|
23
|
+
const result = await ctx.hydrator.getRecord(uri, includeTakedowns)
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
if (!result || (cid && result.cid !== cid)) {
|
|
26
|
+
throw new InvalidRequestError(`Could not locate record: ${uri}`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
encoding: 'application/json' as const,
|
|
31
|
+
body: {
|
|
32
|
+
uri: uri,
|
|
33
|
+
cid: result.cid,
|
|
34
|
+
value: result.record,
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
},
|
|
29
38
|
})
|
|
30
39
|
}
|
package/src/config.ts
CHANGED
|
@@ -35,6 +35,8 @@ export interface ServerConfigValues {
|
|
|
35
35
|
labelsFromIssuerDids?: string[]
|
|
36
36
|
// misc/dev
|
|
37
37
|
blobCacheLocation?: string
|
|
38
|
+
statsigKey?: string
|
|
39
|
+
statsigEnv?: string
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
export class ServerConfig {
|
|
@@ -102,6 +104,14 @@ export class ServerConfig {
|
|
|
102
104
|
assert(modServiceDid)
|
|
103
105
|
assert(dataplaneUrls.length)
|
|
104
106
|
assert(dataplaneHttpVersion === '1.1' || dataplaneHttpVersion === '2')
|
|
107
|
+
const statsigKey =
|
|
108
|
+
process.env.NODE_ENV === 'test'
|
|
109
|
+
? 'secret-key'
|
|
110
|
+
: process.env.BSKY_STATSIG_KEY || undefined
|
|
111
|
+
const statsigEnv =
|
|
112
|
+
process.env.NODE_ENV === 'test'
|
|
113
|
+
? 'test'
|
|
114
|
+
: process.env.BSKY_STATSIG_ENV || 'development'
|
|
105
115
|
return new ServerConfig({
|
|
106
116
|
version,
|
|
107
117
|
debugMode,
|
|
@@ -132,6 +142,8 @@ export class ServerConfig {
|
|
|
132
142
|
blobRateLimitBypassHostname,
|
|
133
143
|
adminPasswords,
|
|
134
144
|
modServiceDid,
|
|
145
|
+
statsigKey,
|
|
146
|
+
statsigEnv,
|
|
135
147
|
...stripUndefineds(overrides ?? {}),
|
|
136
148
|
})
|
|
137
149
|
}
|
|
@@ -264,6 +276,14 @@ export class ServerConfig {
|
|
|
264
276
|
get blobCacheLocation() {
|
|
265
277
|
return this.cfg.blobCacheLocation
|
|
266
278
|
}
|
|
279
|
+
|
|
280
|
+
get statsigKey() {
|
|
281
|
+
return this.cfg.statsigKey
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
get statsigEnv() {
|
|
285
|
+
return this.cfg.statsigEnv
|
|
286
|
+
}
|
|
267
287
|
}
|
|
268
288
|
|
|
269
289
|
function stripUndefineds(
|
package/src/context.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { Views } from './views'
|
|
|
11
11
|
import { AuthVerifier } from './auth-verifier'
|
|
12
12
|
import { BsyncClient } from './bsync'
|
|
13
13
|
import { CourierClient } from './courier'
|
|
14
|
+
import { FeatureGates } from './feature-gates'
|
|
14
15
|
import {
|
|
15
16
|
ParsedLabelers,
|
|
16
17
|
defaultLabelerHeader,
|
|
@@ -32,6 +33,7 @@ export class AppContext {
|
|
|
32
33
|
bsyncClient: BsyncClient
|
|
33
34
|
courierClient: CourierClient
|
|
34
35
|
authVerifier: AuthVerifier
|
|
36
|
+
featureGates: FeatureGates
|
|
35
37
|
},
|
|
36
38
|
) {}
|
|
37
39
|
|
|
@@ -83,6 +85,10 @@ export class AppContext {
|
|
|
83
85
|
return this.opts.authVerifier
|
|
84
86
|
}
|
|
85
87
|
|
|
88
|
+
get featureGates(): FeatureGates {
|
|
89
|
+
return this.opts.featureGates
|
|
90
|
+
}
|
|
91
|
+
|
|
86
92
|
async serviceAuthJwt(aud: string) {
|
|
87
93
|
const iss = this.cfg.serverDid
|
|
88
94
|
return createServiceJwt({
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Statsig, StatsigUser } from 'statsig-node'
|
|
2
|
+
import { sha256Hex } from '@atproto/crypto'
|
|
3
|
+
|
|
4
|
+
import { featureGatesLogger } from './logger'
|
|
5
|
+
import type { ServerConfig } from './config'
|
|
6
|
+
|
|
7
|
+
export type Config = {
|
|
8
|
+
apiKey?: string
|
|
9
|
+
env?: 'development' | 'staging' | 'production' | string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export enum GateID {
|
|
13
|
+
NewSuggestedFollowsByActor = 'new_sugg_foll_by_actor',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @see https://docs.statsig.com/server/nodejsServerSDK
|
|
18
|
+
*/
|
|
19
|
+
export class FeatureGates {
|
|
20
|
+
ready = false
|
|
21
|
+
private statsig = Statsig
|
|
22
|
+
ids = GateID
|
|
23
|
+
|
|
24
|
+
constructor(private config: Config) {}
|
|
25
|
+
|
|
26
|
+
async start() {
|
|
27
|
+
try {
|
|
28
|
+
if (this.config.apiKey) {
|
|
29
|
+
/**
|
|
30
|
+
* Special handling in test mode, see {@link ServerConfig}
|
|
31
|
+
*
|
|
32
|
+
* {@link https://docs.statsig.com/server/nodejsServerSDK#local-overrides}
|
|
33
|
+
*/
|
|
34
|
+
await this.statsig.initialize(this.config.apiKey, {
|
|
35
|
+
localMode: this.config.env === 'test',
|
|
36
|
+
environment: {
|
|
37
|
+
tier: this.config.env || 'development',
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
this.ready = true
|
|
41
|
+
}
|
|
42
|
+
} catch (err) {
|
|
43
|
+
featureGatesLogger.error({ err }, 'Failed to initialize StatSig')
|
|
44
|
+
this.ready = false
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
destroy() {
|
|
49
|
+
if (this.ready) {
|
|
50
|
+
this.ready = false
|
|
51
|
+
this.statsig.shutdown()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async user({ did }: { did: string }): Promise<StatsigUser> {
|
|
56
|
+
const userID = await sha256Hex(did)
|
|
57
|
+
return {
|
|
58
|
+
userID,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
check(user: StatsigUser, gate: GateID) {
|
|
63
|
+
if (!this.ready) return false
|
|
64
|
+
return this.statsig.checkGateSync(user, gate)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -36,11 +36,13 @@ import {
|
|
|
36
36
|
} from './label'
|
|
37
37
|
import {
|
|
38
38
|
HydrationMap,
|
|
39
|
-
Merges,
|
|
40
39
|
RecordInfo,
|
|
41
40
|
ItemRef,
|
|
42
41
|
didFromUri,
|
|
43
42
|
urisByCollection,
|
|
43
|
+
mergeMaps,
|
|
44
|
+
mergeNestedMaps,
|
|
45
|
+
mergeManyMaps,
|
|
44
46
|
} from './util'
|
|
45
47
|
import {
|
|
46
48
|
FeedGenAggs,
|
|
@@ -57,6 +59,7 @@ import {
|
|
|
57
59
|
FeedItem,
|
|
58
60
|
} from './feed'
|
|
59
61
|
import { ParsedLabelers } from '../util'
|
|
62
|
+
import starterPack from '../data-plane/server/indexing/plugins/starter-pack'
|
|
60
63
|
|
|
61
64
|
export class HydrateCtx {
|
|
62
65
|
labelers = this.vals.labelers
|
|
@@ -102,6 +105,7 @@ export type HydrationState = {
|
|
|
102
105
|
labelerViewers?: LabelerViewerStates
|
|
103
106
|
labelerAggs?: LabelerAggs
|
|
104
107
|
knownFollowers?: KnownFollowers
|
|
108
|
+
bidirectionalBlocks?: BidirectionalBlocks
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
export type PostBlock = { embed: boolean; reply: boolean }
|
|
@@ -111,6 +115,8 @@ type PostBlockPairs = { embed?: RelationshipPair; reply?: RelationshipPair }
|
|
|
111
115
|
export type FollowBlock = boolean
|
|
112
116
|
export type FollowBlocks = HydrationMap<FollowBlock>
|
|
113
117
|
|
|
118
|
+
export type BidirectionalBlocks = HydrationMap<HydrationMap<boolean>>
|
|
119
|
+
|
|
114
120
|
export class Hydrator {
|
|
115
121
|
actor: ActorHydrator
|
|
116
122
|
feed: FeedHydrator
|
|
@@ -215,13 +221,23 @@ export class Hydrator {
|
|
|
215
221
|
)
|
|
216
222
|
}
|
|
217
223
|
|
|
218
|
-
const
|
|
224
|
+
const subjectsToKnownFollowersMap = Array.from(
|
|
225
|
+
knownFollowers.keys(),
|
|
226
|
+
).reduce((acc, did) => {
|
|
227
|
+
const known = knownFollowers.get(did)
|
|
228
|
+
if (known) {
|
|
229
|
+
acc.set(did, known.followers)
|
|
230
|
+
}
|
|
231
|
+
return acc
|
|
232
|
+
}, new Map<string, string[]>())
|
|
233
|
+
const allKnownFollowerDids = Array.from(knownFollowers.values())
|
|
219
234
|
.filter(Boolean)
|
|
220
235
|
.flatMap((f) => f!.followers)
|
|
221
|
-
const allDids = Array.from(new Set(dids.concat(
|
|
222
|
-
const [state, profileAggs] = await Promise.all([
|
|
236
|
+
const allDids = Array.from(new Set(dids.concat(allKnownFollowerDids)))
|
|
237
|
+
const [state, profileAggs, bidirectionalBlocks] = await Promise.all([
|
|
223
238
|
this.hydrateProfiles(allDids, ctx),
|
|
224
239
|
this.actor.getProfileAggregates(dids),
|
|
240
|
+
this.hydrateBidirectionalBlocks(subjectsToKnownFollowersMap),
|
|
225
241
|
])
|
|
226
242
|
const starterPackUriSet = new Set<string>()
|
|
227
243
|
state.actors?.forEach((actor) => {
|
|
@@ -237,6 +253,7 @@ export class Hydrator {
|
|
|
237
253
|
profileAggs,
|
|
238
254
|
knownFollowers,
|
|
239
255
|
ctx,
|
|
256
|
+
bidirectionalBlocks,
|
|
240
257
|
})
|
|
241
258
|
}
|
|
242
259
|
|
|
@@ -352,6 +369,10 @@ export class Hydrator {
|
|
|
352
369
|
...(urisLayer1ByCollection.get(ids.AppBskyLabelerService) ?? []),
|
|
353
370
|
...(urisLayer2ByCollection.get(ids.AppBskyLabelerService) ?? []),
|
|
354
371
|
].map((uri) => new AtUri(uri).hostname)
|
|
372
|
+
const nestedStarterPackUris = [
|
|
373
|
+
...(urisLayer1ByCollection.get(ids.AppBskyGraphStarterpack) ?? []),
|
|
374
|
+
...(urisLayer2ByCollection.get(ids.AppBskyGraphStarterpack) ?? []),
|
|
375
|
+
]
|
|
355
376
|
const posts =
|
|
356
377
|
mergeManyMaps(postsLayer0, postsLayer1, postsLayer2) ?? postsLayer0
|
|
357
378
|
const allPostUris = [...posts.keys()]
|
|
@@ -374,6 +395,7 @@ export class Hydrator {
|
|
|
374
395
|
listState,
|
|
375
396
|
feedGenState,
|
|
376
397
|
labelerState,
|
|
398
|
+
starterPackState,
|
|
377
399
|
] = await Promise.all([
|
|
378
400
|
this.feed.getPostAggregates(allRefs),
|
|
379
401
|
ctx.viewer
|
|
@@ -385,6 +407,7 @@ export class Hydrator {
|
|
|
385
407
|
this.hydrateLists([...nestedListUris, ...gateListUris], ctx),
|
|
386
408
|
this.hydrateFeedGens(nestedFeedGenUris, ctx),
|
|
387
409
|
this.hydrateLabelers(nestedLabelerDids, ctx),
|
|
410
|
+
this.hydrateStarterPacksBasic(nestedStarterPackUris, ctx),
|
|
388
411
|
])
|
|
389
412
|
if (!ctx.includeTakedowns) {
|
|
390
413
|
actionTakedownLabels(allPostUris, posts, labels)
|
|
@@ -395,6 +418,7 @@ export class Hydrator {
|
|
|
395
418
|
listState,
|
|
396
419
|
feedGenState,
|
|
397
420
|
labelerState,
|
|
421
|
+
starterPackState,
|
|
398
422
|
{
|
|
399
423
|
posts,
|
|
400
424
|
postAggs,
|
|
@@ -661,7 +685,7 @@ export class Hydrator {
|
|
|
661
685
|
// - list basic
|
|
662
686
|
async hydrateLikes(uris: string[], ctx: HydrateCtx): Promise<HydrationState> {
|
|
663
687
|
const [likes, profileState] = await Promise.all([
|
|
664
|
-
this.feed.getLikes(uris),
|
|
688
|
+
this.feed.getLikes(uris, ctx.includeTakedowns),
|
|
665
689
|
this.hydrateProfiles(uris.map(didFromUri), ctx),
|
|
666
690
|
])
|
|
667
691
|
return mergeStates(profileState, { likes, ctx })
|
|
@@ -673,7 +697,7 @@ export class Hydrator {
|
|
|
673
697
|
// - list basic
|
|
674
698
|
async hydrateReposts(uris: string[], ctx: HydrateCtx) {
|
|
675
699
|
const [reposts, profileState] = await Promise.all([
|
|
676
|
-
this.feed.getReposts(uris),
|
|
700
|
+
this.feed.getReposts(uris, ctx.includeTakedowns),
|
|
677
701
|
this.hydrateProfiles(uris.map(didFromUri), ctx),
|
|
678
702
|
])
|
|
679
703
|
return mergeStates(profileState, { reposts, ctx })
|
|
@@ -737,6 +761,30 @@ export class Hydrator {
|
|
|
737
761
|
return { follows, followBlocks }
|
|
738
762
|
}
|
|
739
763
|
|
|
764
|
+
async hydrateBidirectionalBlocks(
|
|
765
|
+
didMap: Map<string, string[]>, // DID -> DID[]
|
|
766
|
+
): Promise<BidirectionalBlocks> {
|
|
767
|
+
const pairs: RelationshipPair[] = []
|
|
768
|
+
for (const [source, targets] of didMap) {
|
|
769
|
+
for (const target of targets) {
|
|
770
|
+
pairs.push([source, target])
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const result = new HydrationMap<HydrationMap<boolean>>()
|
|
775
|
+
const blocks = await this.graph.getBidirectionalBlocks(pairs)
|
|
776
|
+
|
|
777
|
+
for (const [source, targets] of didMap) {
|
|
778
|
+
const didBlocks = new HydrationMap<boolean>()
|
|
779
|
+
for (const target of targets) {
|
|
780
|
+
didBlocks.set(target, blocks.isBlocked(source, target))
|
|
781
|
+
}
|
|
782
|
+
result.set(source, didBlocks)
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return result
|
|
786
|
+
}
|
|
787
|
+
|
|
740
788
|
// app.bsky.labeler.def#labelerViewDetailed
|
|
741
789
|
// - labeler
|
|
742
790
|
// - profile
|
|
@@ -1022,23 +1070,17 @@ export const mergeStates = (
|
|
|
1022
1070
|
labelerAggs: mergeMaps(stateA.labelerAggs, stateB.labelerAggs),
|
|
1023
1071
|
labelerViewers: mergeMaps(stateA.labelerViewers, stateB.labelerViewers),
|
|
1024
1072
|
knownFollowers: mergeMaps(stateA.knownFollowers, stateB.knownFollowers),
|
|
1073
|
+
bidirectionalBlocks: mergeNestedMaps(
|
|
1074
|
+
stateA.bidirectionalBlocks,
|
|
1075
|
+
stateB.bidirectionalBlocks,
|
|
1076
|
+
),
|
|
1025
1077
|
}
|
|
1026
1078
|
}
|
|
1027
1079
|
|
|
1028
|
-
const mergeMaps = <M extends Merges>(mapA?: M, mapB?: M): M | undefined => {
|
|
1029
|
-
if (!mapA) return mapB
|
|
1030
|
-
if (!mapB) return mapA
|
|
1031
|
-
return mapA.merge(mapB)
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
1080
|
const mergeManyStates = (...states: HydrationState[]) => {
|
|
1035
1081
|
return states.reduce(mergeStates, {} as HydrationState)
|
|
1036
1082
|
}
|
|
1037
1083
|
|
|
1038
|
-
const mergeManyMaps = <T>(...maps: HydrationMap<T>[]) => {
|
|
1039
|
-
return maps.reduce(mergeMaps, undefined as HydrationMap<T> | undefined)
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
1084
|
const actionTakedownLabels = <T>(
|
|
1043
1085
|
keys: string[],
|
|
1044
1086
|
hydrationMap: HydrationMap<T>,
|
package/src/hydration/util.ts
CHANGED
|
@@ -25,6 +25,34 @@ export type RecordInfo<T> = {
|
|
|
25
25
|
takedownRef: string | undefined
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
export const mergeMaps = <V, M extends HydrationMap<V>>(
|
|
29
|
+
mapA?: M,
|
|
30
|
+
mapB?: M,
|
|
31
|
+
): M | undefined => {
|
|
32
|
+
if (!mapA) return mapB
|
|
33
|
+
if (!mapB) return mapA
|
|
34
|
+
return mapA.merge(mapB)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const mergeNestedMaps = <V, M extends HydrationMap<HydrationMap<V>>>(
|
|
38
|
+
mapA?: M,
|
|
39
|
+
mapB?: M,
|
|
40
|
+
): M | undefined => {
|
|
41
|
+
if (!mapA) return mapB
|
|
42
|
+
if (!mapB) return mapA
|
|
43
|
+
|
|
44
|
+
for (const [key, map] of mapB) {
|
|
45
|
+
const merged = mergeMaps(mapA.get(key) ?? undefined, map ?? undefined)
|
|
46
|
+
mapA.set(key, merged ?? null)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return mapA
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const mergeManyMaps = <T>(...maps: HydrationMap<T>[]) => {
|
|
53
|
+
return maps.reduce(mergeMaps, undefined as HydrationMap<T> | undefined)
|
|
54
|
+
}
|
|
55
|
+
|
|
28
56
|
export type ItemRef = { uri: string; cid?: string }
|
|
29
57
|
|
|
30
58
|
export const parseRecord = <T>(
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { Views } from './views'
|
|
|
23
23
|
import { AuthVerifier } from './auth-verifier'
|
|
24
24
|
import { authWithApiKey as bsyncAuth, createBsyncClient } from './bsync'
|
|
25
25
|
import { authWithApiKey as courierAuth, createCourierClient } from './courier'
|
|
26
|
+
import { FeatureGates } from './feature-gates'
|
|
26
27
|
|
|
27
28
|
export * from './data-plane'
|
|
28
29
|
export type { ServerConfigValues } from './config'
|
|
@@ -116,6 +117,11 @@ export class BskyAppView {
|
|
|
116
117
|
adminPasses: config.adminPasswords,
|
|
117
118
|
})
|
|
118
119
|
|
|
120
|
+
const featureGates = new FeatureGates({
|
|
121
|
+
apiKey: config.statsigKey,
|
|
122
|
+
env: config.statsigEnv,
|
|
123
|
+
})
|
|
124
|
+
|
|
119
125
|
const ctx = new AppContext({
|
|
120
126
|
cfg: config,
|
|
121
127
|
dataplane,
|
|
@@ -128,6 +134,7 @@ export class BskyAppView {
|
|
|
128
134
|
bsyncClient,
|
|
129
135
|
courierClient,
|
|
130
136
|
authVerifier,
|
|
137
|
+
featureGates,
|
|
131
138
|
})
|
|
132
139
|
|
|
133
140
|
let server = createServer({
|
|
@@ -154,6 +161,7 @@ export class BskyAppView {
|
|
|
154
161
|
}
|
|
155
162
|
|
|
156
163
|
async start(): Promise<http.Server> {
|
|
164
|
+
await this.ctx.featureGates.start()
|
|
157
165
|
const server = this.app.listen(this.ctx.cfg.port)
|
|
158
166
|
this.server = server
|
|
159
167
|
server.keepAliveTimeout = 90000
|
|
@@ -166,6 +174,7 @@ export class BskyAppView {
|
|
|
166
174
|
|
|
167
175
|
async destroy(): Promise<void> {
|
|
168
176
|
await this.terminator?.terminate()
|
|
177
|
+
this.ctx.featureGates.destroy()
|
|
169
178
|
}
|
|
170
179
|
}
|
|
171
180
|
|
package/src/lexicon/lexicons.ts
CHANGED
|
@@ -4167,6 +4167,7 @@ export const schemaDict = {
|
|
|
4167
4167
|
'lex:app.bsky.actor.defs#mutedWordsPref',
|
|
4168
4168
|
'lex:app.bsky.actor.defs#hiddenPostsPref',
|
|
4169
4169
|
'lex:app.bsky.actor.defs#bskyAppStatePref',
|
|
4170
|
+
'lex:app.bsky.actor.defs#labelersPref',
|
|
4170
4171
|
],
|
|
4171
4172
|
},
|
|
4172
4173
|
},
|
|
@@ -4957,6 +4958,7 @@ export const schemaDict = {
|
|
|
4957
4958
|
'lex:app.bsky.feed.defs#generatorView',
|
|
4958
4959
|
'lex:app.bsky.graph.defs#listView',
|
|
4959
4960
|
'lex:app.bsky.labeler.defs#labelerView',
|
|
4961
|
+
'lex:app.bsky.graph.defs#starterPackViewBasic',
|
|
4960
4962
|
],
|
|
4961
4963
|
},
|
|
4962
4964
|
},
|
|
@@ -8710,6 +8712,12 @@ export const schemaDict = {
|
|
|
8710
8712
|
cursor: {
|
|
8711
8713
|
type: 'string',
|
|
8712
8714
|
},
|
|
8715
|
+
relativeToDid: {
|
|
8716
|
+
type: 'string',
|
|
8717
|
+
format: 'did',
|
|
8718
|
+
description:
|
|
8719
|
+
'DID of the account to get suggestions relative to. If not provided, suggestions will be based on the viewer.',
|
|
8720
|
+
},
|
|
8713
8721
|
},
|
|
8714
8722
|
},
|
|
8715
8723
|
output: {
|
|
@@ -14,6 +14,8 @@ export interface QueryParams {
|
|
|
14
14
|
viewer?: string
|
|
15
15
|
limit: number
|
|
16
16
|
cursor?: string
|
|
17
|
+
/** DID of the account to get suggestions relative to. If not provided, suggestions will be based on the viewer. */
|
|
18
|
+
relativeToDid?: string
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export type InputSchema = undefined
|
package/src/logger.ts
CHANGED
|
@@ -12,6 +12,8 @@ export const labelerLogger: ReturnType<typeof subsystemLogger> =
|
|
|
12
12
|
subsystemLogger('bsky:labeler')
|
|
13
13
|
export const hydrationLogger: ReturnType<typeof subsystemLogger> =
|
|
14
14
|
subsystemLogger('bsky:hydration')
|
|
15
|
+
export const featureGatesLogger: ReturnType<typeof subsystemLogger> =
|
|
16
|
+
subsystemLogger('bsky:featuregates')
|
|
15
17
|
export const httpLogger: ReturnType<typeof subsystemLogger> =
|
|
16
18
|
subsystemLogger('bsky')
|
|
17
19
|
|