@atproto/bsky 0.0.96 → 0.0.97
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/actor/getProfile.js +3 -1
- package/dist/api/app/bsky/actor/getProfile.js.map +1 -1
- package/dist/api/app/bsky/feed/getLikes.d.ts.map +1 -1
- package/dist/api/app/bsky/feed/getLikes.js +14 -7
- package/dist/api/app/bsky/feed/getLikes.js.map +1 -1
- package/dist/auth-verifier.d.ts.map +1 -1
- package/dist/auth-verifier.js.map +1 -1
- package/dist/hydration/hydrator.d.ts +6 -1
- package/dist/hydration/hydrator.d.ts.map +1 -1
- package/dist/hydration/hydrator.js +28 -4
- package/dist/hydration/hydrator.js.map +1 -1
- package/dist/lexicon/lexicons.d.ts +17779 -7841
- package/dist/lexicon/lexicons.d.ts.map +1 -1
- package/dist/lexicon/lexicons.js.map +1 -1
- package/package.json +5 -5
- package/src/api/app/bsky/actor/getProfile.ts +3 -1
- package/src/api/app/bsky/feed/getLikes.ts +24 -12
- package/src/auth-verifier.ts +1 -8
- package/src/hydration/hydrator.ts +33 -4
- package/src/lexicon/lexicons.ts +3 -2
- package/tests/views/labels-takedown.test.ts +31 -0
- package/tests/views/likes.test.ts +76 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/bsky",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.97",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Reference implementation of app.bsky App View (Bluesky API)",
|
|
6
6
|
"keywords": [
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"structured-headers": "^1.0.1",
|
|
43
43
|
"typed-emitter": "^2.1.0",
|
|
44
44
|
"uint8arrays": "3.0.0",
|
|
45
|
-
"@atproto/api": "^0.13.
|
|
46
|
-
"@atproto/common": "^0.4.4",
|
|
45
|
+
"@atproto/api": "^0.13.19",
|
|
47
46
|
"@atproto/crypto": "^0.4.2",
|
|
48
47
|
"@atproto/identity": "^0.4.3",
|
|
48
|
+
"@atproto/common": "^0.4.4",
|
|
49
49
|
"@atproto/lexicon": "^0.4.3",
|
|
50
50
|
"@atproto/repo": "^0.5.5",
|
|
51
51
|
"@atproto/sync": "^0.1.6",
|
|
@@ -66,9 +66,9 @@
|
|
|
66
66
|
"jest": "^28.1.2",
|
|
67
67
|
"ts-node": "^10.8.2",
|
|
68
68
|
"typescript": "^5.6.3",
|
|
69
|
-
"@atproto/api": "^0.13.
|
|
69
|
+
"@atproto/api": "^0.13.19",
|
|
70
|
+
"@atproto/pds": "^0.4.75",
|
|
70
71
|
"@atproto/lex-cli": "^0.5.2",
|
|
71
|
-
"@atproto/pds": "^0.4.73",
|
|
72
72
|
"@atproto/xrpc": "^0.6.4"
|
|
73
73
|
},
|
|
74
74
|
"scripts": {
|
|
@@ -60,7 +60,9 @@ const hydration = async (input: {
|
|
|
60
60
|
const { ctx, params, skeleton } = input
|
|
61
61
|
return ctx.hydrator.hydrateProfilesDetailed(
|
|
62
62
|
[skeleton.did],
|
|
63
|
-
params.hydrateCtx.copy({
|
|
63
|
+
params.hydrateCtx.copy({
|
|
64
|
+
includeActorTakedowns: true,
|
|
65
|
+
}),
|
|
64
66
|
)
|
|
65
67
|
}
|
|
66
68
|
|
|
@@ -3,11 +3,12 @@ import { normalizeDatetimeAlways } from '@atproto/syntax'
|
|
|
3
3
|
import { Server } from '../../../../lexicon'
|
|
4
4
|
import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getLikes'
|
|
5
5
|
import AppContext from '../../../../context'
|
|
6
|
-
import { createPipeline } from '../../../../pipeline'
|
|
6
|
+
import { createPipeline, RulesFnInput } from '../../../../pipeline'
|
|
7
7
|
import {
|
|
8
8
|
HydrateCtx,
|
|
9
9
|
HydrationState,
|
|
10
10
|
Hydrator,
|
|
11
|
+
mergeStates,
|
|
11
12
|
} from '../../../../hydration/hydrator'
|
|
12
13
|
import { Views } from '../../../../views'
|
|
13
14
|
import { parseString } from '../../../../hydration/util'
|
|
@@ -43,8 +44,10 @@ const skeleton = async (inputs: {
|
|
|
43
44
|
params: Params
|
|
44
45
|
}): Promise<Skeleton> => {
|
|
45
46
|
const { ctx, params } = inputs
|
|
47
|
+
const authorDid = creatorFromUri(params.uri)
|
|
48
|
+
|
|
46
49
|
if (clearlyBadCursor(params.cursor)) {
|
|
47
|
-
return { likes: [] }
|
|
50
|
+
return { authorDid, likes: [] }
|
|
48
51
|
}
|
|
49
52
|
if (looksLikeNonSortedCursor(params.cursor)) {
|
|
50
53
|
throw new InvalidRequestError(
|
|
@@ -57,6 +60,7 @@ const skeleton = async (inputs: {
|
|
|
57
60
|
limit: params.limit,
|
|
58
61
|
})
|
|
59
62
|
return {
|
|
63
|
+
authorDid,
|
|
60
64
|
likes: likesRes.uris,
|
|
61
65
|
cursor: parseString(likesRes.cursor),
|
|
62
66
|
}
|
|
@@ -68,18 +72,25 @@ const hydration = async (inputs: {
|
|
|
68
72
|
skeleton: Skeleton
|
|
69
73
|
}) => {
|
|
70
74
|
const { ctx, params, skeleton } = inputs
|
|
71
|
-
|
|
75
|
+
const likesState = await ctx.hydrator.hydrateLikes(
|
|
76
|
+
skeleton.authorDid,
|
|
77
|
+
skeleton.likes,
|
|
78
|
+
params.hydrateCtx,
|
|
79
|
+
)
|
|
80
|
+
return likesState
|
|
72
81
|
}
|
|
73
82
|
|
|
74
|
-
const noBlocks = (
|
|
75
|
-
ctx
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
+
const noBlocks = (input: RulesFnInput<Context, Params, Skeleton>) => {
|
|
84
|
+
const { ctx, skeleton, hydration } = input
|
|
85
|
+
|
|
86
|
+
skeleton.likes = skeleton.likes.filter((likeUri) => {
|
|
87
|
+
const like = hydration.likes?.get(likeUri)
|
|
88
|
+
if (!like) return false
|
|
89
|
+
const likerDid = creatorFromUri(likeUri)
|
|
90
|
+
return (
|
|
91
|
+
!hydration.likeBlocks?.get(likeUri) &&
|
|
92
|
+
!ctx.views.viewerBlockExists(likerDid, hydration)
|
|
93
|
+
)
|
|
83
94
|
})
|
|
84
95
|
return skeleton
|
|
85
96
|
}
|
|
@@ -123,6 +134,7 @@ type Context = {
|
|
|
123
134
|
type Params = QueryParams & { hydrateCtx: HydrateCtx }
|
|
124
135
|
|
|
125
136
|
type Skeleton = {
|
|
137
|
+
authorDid: string
|
|
126
138
|
likes: string[]
|
|
127
139
|
cursor?: string
|
|
128
140
|
}
|
package/src/auth-verifier.ts
CHANGED
|
@@ -18,14 +18,7 @@ import {
|
|
|
18
18
|
unpackIdentityKeys,
|
|
19
19
|
} from './data-plane'
|
|
20
20
|
import { GetIdentityByDidResponse } from './proto/bsky_pb'
|
|
21
|
-
import {
|
|
22
|
-
extractMultikey,
|
|
23
|
-
extractPrefixedBytes,
|
|
24
|
-
hasPrefix,
|
|
25
|
-
parseDidKey,
|
|
26
|
-
SECP256K1_DID_PREFIX,
|
|
27
|
-
SECP256K1_JWT_ALG,
|
|
28
|
-
} from '@atproto/crypto'
|
|
21
|
+
import { parseDidKey, SECP256K1_JWT_ALG } from '@atproto/crypto'
|
|
29
22
|
|
|
30
23
|
type ReqCtx = {
|
|
31
24
|
req: express.Request
|
|
@@ -65,6 +65,7 @@ export class HydrateCtx {
|
|
|
65
65
|
labelers = this.vals.labelers
|
|
66
66
|
viewer = this.vals.viewer !== null ? serviceRefToDid(this.vals.viewer) : null
|
|
67
67
|
includeTakedowns = this.vals.includeTakedowns
|
|
68
|
+
includeActorTakedowns = this.vals.includeActorTakedowns
|
|
68
69
|
include3pBlocks = this.vals.include3pBlocks
|
|
69
70
|
constructor(private vals: HydrateCtxVals) {}
|
|
70
71
|
copy<V extends Partial<HydrateCtxVals>>(vals?: V): HydrateCtx & V {
|
|
@@ -76,6 +77,7 @@ export type HydrateCtxVals = {
|
|
|
76
77
|
labelers: ParsedLabelers
|
|
77
78
|
viewer: string | null
|
|
78
79
|
includeTakedowns?: boolean
|
|
80
|
+
includeActorTakedowns?: boolean
|
|
79
81
|
include3pBlocks?: boolean
|
|
80
82
|
}
|
|
81
83
|
|
|
@@ -98,6 +100,7 @@ export type HydrationState = {
|
|
|
98
100
|
listViewers?: ListViewerStates
|
|
99
101
|
listItems?: ListItems
|
|
100
102
|
likes?: Likes
|
|
103
|
+
likeBlocks?: LikeBlocks
|
|
101
104
|
labels?: Labels
|
|
102
105
|
feedgens?: FeedGens
|
|
103
106
|
feedgenViewers?: FeedGenViewerStates
|
|
@@ -119,6 +122,9 @@ type PostBlockPairs = {
|
|
|
119
122
|
root?: RelationshipPair
|
|
120
123
|
}
|
|
121
124
|
|
|
125
|
+
export type LikeBlock = boolean
|
|
126
|
+
export type LikeBlocks = HydrationMap<LikeBlock>
|
|
127
|
+
|
|
122
128
|
export type FollowBlock = boolean
|
|
123
129
|
export type FollowBlocks = HydrationMap<FollowBlock>
|
|
124
130
|
|
|
@@ -179,12 +185,13 @@ export class Hydrator {
|
|
|
179
185
|
dids: string[],
|
|
180
186
|
ctx: HydrateCtx,
|
|
181
187
|
): Promise<HydrationState> {
|
|
188
|
+
const includeTakedowns = ctx.includeTakedowns || ctx.includeActorTakedowns
|
|
182
189
|
const [actors, labels, profileViewersState] = await Promise.all([
|
|
183
|
-
this.actor.getActors(dids,
|
|
190
|
+
this.actor.getActors(dids, includeTakedowns),
|
|
184
191
|
this.label.getLabelsForSubjects(labelSubjectsForDid(dids), ctx.labelers),
|
|
185
192
|
this.hydrateProfileViewers(dids, ctx),
|
|
186
193
|
])
|
|
187
|
-
if (!
|
|
194
|
+
if (!includeTakedowns) {
|
|
188
195
|
actionTakedownLabels(dids, actors, labels)
|
|
189
196
|
}
|
|
190
197
|
return mergeStates(profileViewersState ?? {}, {
|
|
@@ -743,12 +750,33 @@ export class Hydrator {
|
|
|
743
750
|
// - like
|
|
744
751
|
// - profile
|
|
745
752
|
// - list basic
|
|
746
|
-
async hydrateLikes(
|
|
753
|
+
async hydrateLikes(
|
|
754
|
+
authorDid: string,
|
|
755
|
+
uris: string[],
|
|
756
|
+
ctx: HydrateCtx,
|
|
757
|
+
): Promise<HydrationState> {
|
|
747
758
|
const [likes, profileState] = await Promise.all([
|
|
748
759
|
this.feed.getLikes(uris, ctx.includeTakedowns),
|
|
749
760
|
this.hydrateProfiles(uris.map(didFromUri), ctx),
|
|
750
761
|
])
|
|
751
|
-
|
|
762
|
+
|
|
763
|
+
const pairs: RelationshipPair[] = []
|
|
764
|
+
for (const [uri, like] of likes) {
|
|
765
|
+
if (like) {
|
|
766
|
+
pairs.push([authorDid, didFromUri(uri)])
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
const blocks = await this.graph.getBidirectionalBlocks(pairs)
|
|
770
|
+
const likeBlocks = new HydrationMap<LikeBlock>()
|
|
771
|
+
for (const [uri, like] of likes) {
|
|
772
|
+
if (like) {
|
|
773
|
+
likeBlocks.set(uri, blocks.isBlocked(authorDid, didFromUri(uri)))
|
|
774
|
+
} else {
|
|
775
|
+
likeBlocks.set(uri, null)
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return mergeStates(profileState, { likes, likeBlocks, ctx })
|
|
752
780
|
}
|
|
753
781
|
|
|
754
782
|
// app.bsky.feed.getRepostedBy#repostedBy
|
|
@@ -1151,6 +1179,7 @@ export const mergeStates = (
|
|
|
1151
1179
|
listViewers: mergeMaps(stateA.listViewers, stateB.listViewers),
|
|
1152
1180
|
listItems: mergeMaps(stateA.listItems, stateB.listItems),
|
|
1153
1181
|
likes: mergeMaps(stateA.likes, stateB.likes),
|
|
1182
|
+
likeBlocks: mergeMaps(stateA.likeBlocks, stateB.likeBlocks),
|
|
1154
1183
|
labels: mergeMaps(stateA.labels, stateB.labels),
|
|
1155
1184
|
feedgens: mergeMaps(stateA.feedgens, stateB.feedgens),
|
|
1156
1185
|
feedgenAggs: mergeMaps(stateA.feedgenAggs, stateB.feedgenAggs),
|
package/src/lexicon/lexicons.ts
CHANGED
|
@@ -10728,8 +10728,9 @@ export const schemaDict = {
|
|
|
10728
10728
|
},
|
|
10729
10729
|
},
|
|
10730
10730
|
},
|
|
10731
|
-
}
|
|
10732
|
-
|
|
10731
|
+
} as const satisfies Record<string, LexiconDoc>
|
|
10732
|
+
|
|
10733
|
+
export const schemas = Object.values(schemaDict)
|
|
10733
10734
|
export const lexicons: Lexicons = new Lexicons(schemas)
|
|
10734
10735
|
export const ids = {
|
|
10735
10736
|
ComAtprotoAdminDefs: 'com.atproto.admin.defs',
|
|
@@ -5,6 +5,7 @@ import { ids } from '../../src/lexicon/lexicons'
|
|
|
5
5
|
describe('bsky takedown labels', () => {
|
|
6
6
|
let network: TestNetwork
|
|
7
7
|
let agent: AtpAgent
|
|
8
|
+
let pdsAgent: AtpAgent
|
|
8
9
|
let sc: SeedClient
|
|
9
10
|
|
|
10
11
|
let takendownSubjects: string[]
|
|
@@ -20,10 +21,21 @@ describe('bsky takedown labels', () => {
|
|
|
20
21
|
dbPostgresSchema: 'bsky_views_takedown_labels',
|
|
21
22
|
})
|
|
22
23
|
agent = network.bsky.getClient()
|
|
24
|
+
pdsAgent = network.pds.getClient()
|
|
23
25
|
sc = network.getSeedClient()
|
|
24
26
|
await basicSeed(sc)
|
|
25
27
|
|
|
26
28
|
aliceListRef = await sc.createList(sc.dids.alice, 'alice list', 'mod')
|
|
29
|
+
// carol blocks dan via alice's (takendown) list
|
|
30
|
+
await sc.addToList(sc.dids.alice, sc.dids.dan, aliceListRef)
|
|
31
|
+
await pdsAgent.app.bsky.graph.listblock.create(
|
|
32
|
+
{ repo: sc.dids.carol },
|
|
33
|
+
{
|
|
34
|
+
subject: aliceListRef.uriStr,
|
|
35
|
+
createdAt: new Date().toISOString(),
|
|
36
|
+
},
|
|
37
|
+
sc.getHeaders(sc.dids.carol),
|
|
38
|
+
)
|
|
27
39
|
carolListRef = await sc.createList(sc.dids.carol, 'carol list', 'mod')
|
|
28
40
|
aliceGenRef = await sc.createFeedGen(
|
|
29
41
|
sc.dids.alice,
|
|
@@ -159,6 +171,25 @@ describe('bsky takedown labels', () => {
|
|
|
159
171
|
await expect(attempt2).rejects.toThrow('List not found')
|
|
160
172
|
})
|
|
161
173
|
|
|
174
|
+
it('halts application of mod lists', async () => {
|
|
175
|
+
const { data: profile } = await agent.app.bsky.actor.getProfile(
|
|
176
|
+
{
|
|
177
|
+
actor: sc.dids.dan, // blocked via alice's takendown list
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
headers: await network.serviceHeaders(
|
|
181
|
+
sc.dids.carol,
|
|
182
|
+
ids.AppBskyActorGetProfile,
|
|
183
|
+
),
|
|
184
|
+
},
|
|
185
|
+
)
|
|
186
|
+
expect(profile.did).toBe(sc.dids.dan)
|
|
187
|
+
expect(profile.viewer).not.toBeUndefined()
|
|
188
|
+
expect(profile.viewer?.blockedBy).toBe(false)
|
|
189
|
+
expect(profile.viewer?.blocking).toBeUndefined()
|
|
190
|
+
expect(profile.viewer?.blockingByList).toBeUndefined()
|
|
191
|
+
})
|
|
192
|
+
|
|
162
193
|
it('takesdown feed generators', async () => {
|
|
163
194
|
const res = await agent.api.app.bsky.feed.getFeedGenerators({
|
|
164
195
|
feeds: [aliceGenRef.uriStr, bobGenRef.uriStr, carolGenRef.uriStr],
|
|
@@ -11,6 +11,8 @@ describe('pds like views', () => {
|
|
|
11
11
|
// account dids, for convenience
|
|
12
12
|
let alice: string
|
|
13
13
|
let bob: string
|
|
14
|
+
let carol: string
|
|
15
|
+
let frankie: string
|
|
14
16
|
|
|
15
17
|
beforeAll(async () => {
|
|
16
18
|
network = await TestNetwork.create({
|
|
@@ -19,9 +21,17 @@ describe('pds like views', () => {
|
|
|
19
21
|
agent = network.bsky.getClient()
|
|
20
22
|
sc = network.getSeedClient()
|
|
21
23
|
await likesSeed(sc)
|
|
24
|
+
await sc.createAccount('frankie', {
|
|
25
|
+
handle: 'frankie.test',
|
|
26
|
+
email: 'frankie@frankie.com',
|
|
27
|
+
password: 'password',
|
|
28
|
+
})
|
|
22
29
|
await network.processAll()
|
|
30
|
+
|
|
23
31
|
alice = sc.dids.alice
|
|
24
32
|
bob = sc.dids.bob
|
|
33
|
+
carol = sc.dids.carol
|
|
34
|
+
frankie = sc.dids.frankie
|
|
25
35
|
})
|
|
26
36
|
|
|
27
37
|
afterAll(async () => {
|
|
@@ -108,4 +118,70 @@ describe('pds like views', () => {
|
|
|
108
118
|
}),
|
|
109
119
|
)
|
|
110
120
|
})
|
|
121
|
+
|
|
122
|
+
it(`author viewer doesn't see likes by user the author blocked`, async () => {
|
|
123
|
+
await sc.like(frankie, sc.posts[alice][1].ref)
|
|
124
|
+
await network.processAll()
|
|
125
|
+
|
|
126
|
+
const beforeBlock = await agent.app.bsky.feed.getLikes(
|
|
127
|
+
{ uri: sc.posts[alice][1].ref.uriStr },
|
|
128
|
+
{ headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetLikes) },
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
expect(beforeBlock.data.likes.map((like) => like.actor.did)).toStrictEqual([
|
|
132
|
+
sc.dids.frankie,
|
|
133
|
+
sc.dids.eve,
|
|
134
|
+
sc.dids.dan,
|
|
135
|
+
sc.dids.carol,
|
|
136
|
+
sc.dids.bob,
|
|
137
|
+
])
|
|
138
|
+
|
|
139
|
+
await sc.block(alice, frankie)
|
|
140
|
+
await network.processAll()
|
|
141
|
+
|
|
142
|
+
const afterBlock = await agent.app.bsky.feed.getLikes(
|
|
143
|
+
{ uri: sc.posts[alice][1].ref.uriStr },
|
|
144
|
+
{ headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetLikes) },
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
expect(afterBlock.data.likes.map((like) => like.actor.did)).toStrictEqual([
|
|
148
|
+
sc.dids.eve,
|
|
149
|
+
sc.dids.dan,
|
|
150
|
+
sc.dids.carol,
|
|
151
|
+
sc.dids.bob,
|
|
152
|
+
])
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it(`non-author viewer doesn't see likes by user the author blocked and by user the viewer blocked `, async () => {
|
|
156
|
+
await sc.unblock(alice, frankie)
|
|
157
|
+
await network.processAll()
|
|
158
|
+
|
|
159
|
+
const beforeBlock = await agent.app.bsky.feed.getLikes(
|
|
160
|
+
{ uri: sc.posts[alice][1].ref.uriStr },
|
|
161
|
+
{ headers: await network.serviceHeaders(bob, ids.AppBskyFeedGetLikes) },
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
expect(beforeBlock.data.likes.map((like) => like.actor.did)).toStrictEqual([
|
|
165
|
+
sc.dids.frankie,
|
|
166
|
+
sc.dids.eve,
|
|
167
|
+
sc.dids.dan,
|
|
168
|
+
sc.dids.carol,
|
|
169
|
+
sc.dids.bob,
|
|
170
|
+
])
|
|
171
|
+
|
|
172
|
+
await sc.block(alice, frankie)
|
|
173
|
+
await sc.block(bob, carol)
|
|
174
|
+
await network.processAll()
|
|
175
|
+
|
|
176
|
+
const afterBlock = await agent.app.bsky.feed.getLikes(
|
|
177
|
+
{ uri: sc.posts[alice][1].ref.uriStr },
|
|
178
|
+
{ headers: await network.serviceHeaders(bob, ids.AppBskyFeedGetLikes) },
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
expect(afterBlock.data.likes.map((like) => like.actor.did)).toStrictEqual([
|
|
182
|
+
sc.dids.eve,
|
|
183
|
+
sc.dids.dan,
|
|
184
|
+
sc.dids.bob,
|
|
185
|
+
])
|
|
186
|
+
})
|
|
111
187
|
})
|