@atproto/bsky 0.0.117 → 0.0.118

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/api/app/bsky/feed/getQuotes.js +1 -1
  3. package/dist/api/app/bsky/feed/getQuotes.js.map +1 -1
  4. package/dist/api/app/bsky/notification/listNotifications.js +1 -1
  5. package/dist/api/app/bsky/notification/listNotifications.js.map +1 -1
  6. package/dist/data-plane/server/db/migrations/20250207T174822012Z-add-label-exp.d.ts +4 -0
  7. package/dist/data-plane/server/db/migrations/20250207T174822012Z-add-label-exp.d.ts.map +1 -0
  8. package/dist/data-plane/server/db/migrations/20250207T174822012Z-add-label-exp.js +11 -0
  9. package/dist/data-plane/server/db/migrations/20250207T174822012Z-add-label-exp.js.map +1 -0
  10. package/dist/data-plane/server/db/migrations/index.d.ts +1 -0
  11. package/dist/data-plane/server/db/migrations/index.d.ts.map +1 -1
  12. package/dist/data-plane/server/db/migrations/index.js +2 -1
  13. package/dist/data-plane/server/db/migrations/index.js.map +1 -1
  14. package/dist/data-plane/server/db/tables/label.d.ts +1 -0
  15. package/dist/data-plane/server/db/tables/label.d.ts.map +1 -1
  16. package/dist/data-plane/server/routes/labels.d.ts.map +1 -1
  17. package/dist/data-plane/server/routes/labels.js +3 -0
  18. package/dist/data-plane/server/routes/labels.js.map +1 -1
  19. package/dist/views/index.d.ts +4 -1
  20. package/dist/views/index.d.ts.map +1 -1
  21. package/dist/views/index.js +12 -5
  22. package/dist/views/index.js.map +1 -1
  23. package/package.json +5 -5
  24. package/src/api/app/bsky/feed/getQuotes.ts +1 -1
  25. package/src/api/app/bsky/notification/listNotifications.ts +1 -1
  26. package/src/data-plane/server/db/migrations/20250207T174822012Z-add-label-exp.ts +9 -0
  27. package/src/data-plane/server/db/migrations/index.ts +1 -0
  28. package/src/data-plane/server/db/tables/label.ts +1 -0
  29. package/src/data-plane/server/routes/labels.ts +6 -1
  30. package/src/views/index.ts +17 -7
  31. package/tests/label-hydration.test.ts +56 -8
  32. package/tests/query-labels.test.ts +1 -0
  33. package/tests/views/labels-needs-review.test.ts +252 -106
  34. package/tests/views/labels-takedown.test.ts +1 -0
  35. package/tests/views/timeline.test.ts +1 -0
  36. package/tsconfig.build.tsbuildinfo +1 -1
@@ -176,13 +176,23 @@ export class Views {
176
176
  return uri
177
177
  }
178
178
 
179
- viewerSeesNeedsReview(did: string, state: HydrationState): boolean {
179
+ viewerSeesNeedsReview(
180
+ { did, uri }: { did?: string; uri?: string },
181
+ state: HydrationState,
182
+ ): boolean {
180
183
  const { labels, profileViewers, ctx } = state
181
- return (
182
- !labels?.get(did)?.needsReview ||
183
- ctx?.viewer === did ||
184
- !!profileViewers?.get(did)?.following
185
- )
184
+ did = did || (uri && uriToDid(uri))
185
+ if (!did) {
186
+ return true
187
+ }
188
+ if (
189
+ labels?.get(did)?.needsReview ||
190
+ (uri && labels?.get(uri)?.needsReview)
191
+ ) {
192
+ // content marked as needs review
193
+ return ctx?.viewer === did || !!profileViewers?.get(did)?.following
194
+ }
195
+ return true
186
196
  }
187
197
 
188
198
  replyIsHiddenByThreadgate(
@@ -972,7 +982,7 @@ export class Views {
972
982
  if (this.viewerBlockExists(post.author.did, state)) {
973
983
  return this.blockedPost(uri, post.author.did, state)
974
984
  }
975
- if (!this.viewerSeesNeedsReview(post.author.did, state)) {
985
+ if (!this.viewerSeesNeedsReview({ uri, did: post.author.did }, state)) {
976
986
  return undefined
977
987
  }
978
988
  return {
@@ -1,4 +1,6 @@
1
+ import assert from 'node:assert'
1
2
  import { AtpAgent } from '@atproto/api'
3
+ import { MINUTE } from '@atproto/common'
2
4
  import { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'
3
5
 
4
6
  describe('label hydration', () => {
@@ -10,6 +12,7 @@ describe('label hydration', () => {
10
12
  let bob: string
11
13
  let carol: string
12
14
  let labelerDid: string
15
+ let labeler2Did: string
13
16
 
14
17
  beforeAll(async () => {
15
18
  network = await TestNetwork.create({
@@ -22,6 +25,7 @@ describe('label hydration', () => {
22
25
  bob = sc.dids.bob
23
26
  carol = sc.dids.carol
24
27
  labelerDid = network.bsky.ctx.cfg.labelsFromIssuerDids[0]
28
+ labeler2Did = network.bsky.ctx.cfg.labelsFromIssuerDids[1]
25
29
  await createLabel({ src: alice, uri: carol, cid: '', val: 'spam' })
26
30
  await createLabel({ src: bob, uri: carol, cid: '', val: 'impersonation' })
27
31
  await createLabel({
@@ -30,6 +34,20 @@ describe('label hydration', () => {
30
34
  cid: '',
31
35
  val: 'misleading',
32
36
  })
37
+ await createLabel({
38
+ src: labeler2Did,
39
+ uri: carol,
40
+ cid: '',
41
+ val: 'expired',
42
+ exp: new Date(Date.now() - MINUTE).toISOString(),
43
+ })
44
+ await createLabel({
45
+ src: labeler2Did,
46
+ uri: carol,
47
+ cid: '',
48
+ val: 'not-expired',
49
+ exp: new Date(Date.now() + MINUTE).toISOString(),
50
+ })
33
51
  await network.processAll()
34
52
  })
35
53
 
@@ -39,17 +57,33 @@ describe('label hydration', () => {
39
57
 
40
58
  it('hydrates labels based on a supplied labeler header', async () => {
41
59
  AtpAgent.configure({ appLabelers: [alice] })
42
- pdsAgent.configureLabelers([])
60
+ pdsAgent.configureLabelers([labeler2Did])
43
61
  const res = await pdsAgent.api.app.bsky.actor.getProfile(
44
62
  { actor: carol },
45
63
  {
46
64
  headers: sc.getHeaders(bob),
47
65
  },
48
66
  )
49
- expect(res.data.labels?.length).toBe(1)
50
- expect(res.data.labels?.[0].src).toBe(alice)
51
- expect(res.data.labels?.[0].val).toBe('spam')
52
- expect(res.headers['atproto-content-labelers']).toEqual(`${alice};redact`)
67
+ expect(res.data.labels?.length).toBe(2)
68
+ assert(res.data.labels)
69
+
70
+ const sortedLabels = res.data.labels.sort((a, b) =>
71
+ a.src.localeCompare(b.src),
72
+ )
73
+ const sortedExpected = [
74
+ { src: labeler2Did, val: 'not-expired' },
75
+ { src: alice, val: 'spam' },
76
+ ].sort((a, b) => a.src.localeCompare(b.src))
77
+
78
+ expect(sortedLabels[0].src).toBe(sortedExpected[0].src)
79
+ expect(sortedLabels[0].val).toBe(sortedExpected[0].val)
80
+
81
+ expect(sortedLabels[1].src).toBe(sortedExpected[1].src)
82
+ expect(sortedLabels[1].val).toBe(sortedExpected[1].val)
83
+
84
+ expect(res.headers['atproto-content-labelers']).toEqual(
85
+ `${alice};redact,${labeler2Did}`,
86
+ )
53
87
  })
54
88
 
55
89
  it('hydrates labels based on multiple a supplied labelers', async () => {
@@ -88,9 +122,21 @@ describe('label hydration', () => {
88
122
  { headers: sc.getHeaders(bob) },
89
123
  )
90
124
  const data = await res.json()
91
- expect(data.labels?.length).toBe(1)
92
- expect(data.labels?.[0].src).toBe(labelerDid)
93
- expect(data.labels?.[0].val).toBe('misleading')
125
+
126
+ expect(data.labels?.length).toBe(2)
127
+ assert(data.labels)
128
+
129
+ const sortedLabels = data.labels.sort((a, b) => a.src.localeCompare(b.src))
130
+ const sortedExpected = [
131
+ { src: labeler2Did, val: 'not-expired' },
132
+ { src: labelerDid, val: 'misleading' },
133
+ ].sort((a, b) => a.src.localeCompare(b.src))
134
+
135
+ expect(sortedLabels[0].src).toBe(sortedExpected[0].src)
136
+ expect(sortedLabels[0].val).toBe(sortedExpected[0].val)
137
+
138
+ expect(sortedLabels[1].src).toBe(sortedExpected[1].src)
139
+ expect(sortedLabels[1].val).toBe(sortedExpected[1].val)
94
140
 
95
141
  expect(res.headers.get('atproto-content-labelers')).toEqual(
96
142
  network.bsky.ctx.cfg.labelsFromIssuerDids
@@ -181,6 +227,7 @@ describe('label hydration', () => {
181
227
  uri: string
182
228
  cid: string
183
229
  val: string
230
+ exp?: string
184
231
  }) => {
185
232
  await network.bsky.db.db
186
233
  .insertInto('label')
@@ -189,6 +236,7 @@ describe('label hydration', () => {
189
236
  cid: opts.cid,
190
237
  val: opts.val,
191
238
  cts: new Date().toISOString(),
239
+ exp: opts.exp ?? null,
192
240
  neg: false,
193
241
  src: opts.src ?? labelerDid,
194
242
  })
@@ -76,6 +76,7 @@ describe('label hydration', () => {
76
76
  cid: opts.cid,
77
77
  val: opts.val,
78
78
  cts: new Date().toISOString(),
79
+ exp: null,
79
80
  neg: false,
80
81
  src: opts.src ?? labelerDid,
81
82
  })
@@ -43,126 +43,272 @@ describe('bsky needs-review labels', () => {
43
43
  await network.processAll()
44
44
 
45
45
  AtpAgent.configure({ appLabelers: [network.ozone.ctx.cfg.service.did] })
46
- await network.bsky.db.db
47
- .insertInto('label')
48
- .values({
49
- src: network.ozone.ctx.cfg.service.did,
50
- uri: sc.dids.geoff,
51
- cid: '',
52
- val: 'needs-review',
53
- neg: false,
54
- cts: new Date().toISOString(),
55
- })
56
- .execute()
57
46
  })
58
47
 
59
48
  afterAll(async () => {
60
49
  await network.close()
61
50
  })
62
51
 
63
- it('applies to thread replies.', async () => {
64
- const {
65
- data: { thread },
66
- } = await agent.app.bsky.feed.getPostThread({
67
- uri: sc.posts[sc.dids.alice][0].ref.uriStr,
52
+ describe('account-level', () => {
53
+ beforeAll(async () => {
54
+ await network.bsky.db.db
55
+ .insertInto('label')
56
+ .values({
57
+ src: network.ozone.ctx.cfg.service.did,
58
+ uri: sc.dids.geoff,
59
+ cid: '',
60
+ val: 'needs-review',
61
+ exp: null,
62
+ neg: false,
63
+ cts: new Date().toISOString(),
64
+ })
65
+ .execute()
68
66
  })
69
- assert(isThreadViewPost(thread))
70
- expect(
71
- thread.replies?.some((reply) => {
72
- return (
73
- isThreadViewPost(reply) && reply.post.author.did === sc.dids.geoff
74
- )
75
- }),
76
- ).toBe(false)
77
- })
78
67
 
79
- it('applies to quote lists.', async () => {
80
- const {
81
- data: { posts },
82
- } = await agent.app.bsky.feed.getQuotes({
83
- uri: sc.posts[sc.dids.alice][0].ref.uriStr,
68
+ afterAll(async () => {
69
+ await network.bsky.db.db
70
+ .deleteFrom('label')
71
+ .where('src', '=', network.ozone.ctx.cfg.service.did)
72
+ .execute()
84
73
  })
85
- expect(
86
- posts.some((post) => {
87
- return post.author.did === sc.dids.geoff
88
- }),
89
- ).toBe(false)
90
- })
91
74
 
92
- it('applies to reply, quote, and mention notifications.', async () => {
93
- const {
94
- data: { notifications },
95
- } = await agent.app.bsky.notification.listNotifications(
96
- {},
97
- {
98
- headers: await network.serviceHeaders(
99
- sc.dids.alice,
100
- ids.AppBskyNotificationListNotifications,
101
- ),
102
- },
103
- )
104
- expect(
105
- notifications.some((notif) => {
106
- return notif.reason === 'reply' && notif.author.did === sc.dids.geoff
107
- }),
108
- ).toBe(false)
109
- expect(
110
- notifications.some((notif) => {
111
- return notif.reason === 'quote' && notif.author.did === sc.dids.geoff
112
- }),
113
- ).toBe(false)
114
- expect(
115
- notifications.some((notif) => {
116
- return notif.reason === 'mention' && notif.author.did === sc.dids.geoff
117
- }),
118
- ).toBe(false)
119
- })
75
+ it('applies to thread replies.', async () => {
76
+ const {
77
+ data: { thread },
78
+ } = await agent.app.bsky.feed.getPostThread({
79
+ uri: sc.posts[sc.dids.alice][0].ref.uriStr,
80
+ })
81
+ assert(isThreadViewPost(thread))
82
+ expect(
83
+ thread.replies?.some((reply) => {
84
+ return (
85
+ isThreadViewPost(reply) && reply.post.author.did === sc.dids.geoff
86
+ )
87
+ }),
88
+ ).toBe(false)
89
+ })
120
90
 
121
- it('does not apply to self.', async () => {
122
- const {
123
- data: { thread },
124
- } = await agent.app.bsky.feed.getPostThread(
125
- {
91
+ it('applies to quote lists.', async () => {
92
+ const {
93
+ data: { posts },
94
+ } = await agent.app.bsky.feed.getQuotes({
126
95
  uri: sc.posts[sc.dids.alice][0].ref.uriStr,
127
- },
128
- {
129
- headers: await network.serviceHeaders(
130
- sc.dids.geoff,
131
- ids.AppBskyFeedGetPostThread,
132
- ),
133
- },
134
- )
135
- assert(isThreadViewPost(thread))
136
- expect(
137
- thread.replies?.some((reply) => {
138
- return (
139
- isThreadViewPost(reply) && reply.post.author.did === sc.dids.geoff
140
- )
141
- }),
142
- ).toBe(true)
96
+ })
97
+ expect(
98
+ posts.some((post) => {
99
+ return post.author.did === sc.dids.geoff
100
+ }),
101
+ ).toBe(false)
102
+ })
103
+
104
+ it('applies to reply, quote, and mention notifications.', async () => {
105
+ const {
106
+ data: { notifications },
107
+ } = await agent.app.bsky.notification.listNotifications(
108
+ {},
109
+ {
110
+ headers: await network.serviceHeaders(
111
+ sc.dids.alice,
112
+ ids.AppBskyNotificationListNotifications,
113
+ ),
114
+ },
115
+ )
116
+ expect(
117
+ notifications.some((notif) => {
118
+ return notif.reason === 'reply' && notif.author.did === sc.dids.geoff
119
+ }),
120
+ ).toBe(false)
121
+ expect(
122
+ notifications.some((notif) => {
123
+ return notif.reason === 'quote' && notif.author.did === sc.dids.geoff
124
+ }),
125
+ ).toBe(false)
126
+ expect(
127
+ notifications.some((notif) => {
128
+ return (
129
+ notif.reason === 'mention' && notif.author.did === sc.dids.geoff
130
+ )
131
+ }),
132
+ ).toBe(false)
133
+ })
134
+
135
+ it('does not apply to self.', async () => {
136
+ const {
137
+ data: { thread },
138
+ } = await agent.app.bsky.feed.getPostThread(
139
+ {
140
+ uri: sc.posts[sc.dids.alice][0].ref.uriStr,
141
+ },
142
+ {
143
+ headers: await network.serviceHeaders(
144
+ sc.dids.geoff,
145
+ ids.AppBskyFeedGetPostThread,
146
+ ),
147
+ },
148
+ )
149
+ assert(isThreadViewPost(thread))
150
+ expect(
151
+ thread.replies?.some((reply) => {
152
+ return (
153
+ isThreadViewPost(reply) && reply.post.author.did === sc.dids.geoff
154
+ )
155
+ }),
156
+ ).toBe(true)
157
+ })
158
+
159
+ it('does not apply to followers.', async () => {
160
+ const {
161
+ data: { thread },
162
+ } = await agent.app.bsky.feed.getPostThread(
163
+ {
164
+ uri: sc.posts[sc.dids.alice][0].ref.uriStr,
165
+ },
166
+ {
167
+ headers: await network.serviceHeaders(
168
+ sc.dids.bob, // follows geoff
169
+ ids.AppBskyFeedGetPostThread,
170
+ ),
171
+ },
172
+ )
173
+ assert(isThreadViewPost(thread))
174
+ expect(
175
+ thread.replies?.some((reply) => {
176
+ return (
177
+ isThreadViewPost(reply) && reply.post.author.did === sc.dids.geoff
178
+ )
179
+ }),
180
+ ).toBe(true)
181
+ })
143
182
  })
144
183
 
145
- it('does not apply to followers.', async () => {
146
- const {
147
- data: { thread },
148
- } = await agent.app.bsky.feed.getPostThread(
149
- {
150
- uri: sc.posts[sc.dids.alice][0].ref.uriStr,
151
- },
152
- {
153
- headers: await network.serviceHeaders(
154
- sc.dids.bob, // follows geoff
155
- ids.AppBskyFeedGetPostThread,
156
- ),
157
- },
158
- )
159
- assert(isThreadViewPost(thread))
160
- expect(
161
- thread.replies?.some((reply) => {
162
- return (
163
- isThreadViewPost(reply) && reply.post.author.did === sc.dids.geoff
184
+ describe('record-level', () => {
185
+ beforeAll(async () => {
186
+ const geoffPostUris = [
187
+ ...sc.posts[sc.dids.geoff],
188
+ ...sc.replies[sc.dids.geoff],
189
+ ].map((post) => post.ref.uriStr)
190
+ await network.bsky.db.db
191
+ .insertInto('label')
192
+ .values(
193
+ geoffPostUris.map((uri) => ({
194
+ src: network.ozone.ctx.cfg.service.did,
195
+ uri,
196
+ cid: '',
197
+ val: 'needs-review',
198
+ exp: null,
199
+ neg: false,
200
+ cts: new Date().toISOString(),
201
+ })),
164
202
  )
165
- }),
166
- ).toBe(true)
203
+ .execute()
204
+ })
205
+
206
+ it('applies to thread replies.', async () => {
207
+ const {
208
+ data: { thread },
209
+ } = await agent.app.bsky.feed.getPostThread({
210
+ uri: sc.posts[sc.dids.alice][0].ref.uriStr,
211
+ })
212
+ assert(isThreadViewPost(thread))
213
+ expect(
214
+ thread.replies?.some((reply) => {
215
+ return (
216
+ isThreadViewPost(reply) && reply.post.author.did === sc.dids.geoff
217
+ )
218
+ }),
219
+ ).toBe(false)
220
+ })
221
+
222
+ it('applies to quote lists.', async () => {
223
+ const {
224
+ data: { posts },
225
+ } = await agent.app.bsky.feed.getQuotes({
226
+ uri: sc.posts[sc.dids.alice][0].ref.uriStr,
227
+ })
228
+ expect(
229
+ posts.some((post) => {
230
+ return post.author.did === sc.dids.geoff
231
+ }),
232
+ ).toBe(false)
233
+ })
234
+
235
+ it('applies to reply, quote, and mention notifications.', async () => {
236
+ const {
237
+ data: { notifications },
238
+ } = await agent.app.bsky.notification.listNotifications(
239
+ {},
240
+ {
241
+ headers: await network.serviceHeaders(
242
+ sc.dids.alice,
243
+ ids.AppBskyNotificationListNotifications,
244
+ ),
245
+ },
246
+ )
247
+ expect(
248
+ notifications.some((notif) => {
249
+ return notif.reason === 'reply' && notif.author.did === sc.dids.geoff
250
+ }),
251
+ ).toBe(false)
252
+ expect(
253
+ notifications.some((notif) => {
254
+ return notif.reason === 'quote' && notif.author.did === sc.dids.geoff
255
+ }),
256
+ ).toBe(false)
257
+ expect(
258
+ notifications.some((notif) => {
259
+ return (
260
+ notif.reason === 'mention' && notif.author.did === sc.dids.geoff
261
+ )
262
+ }),
263
+ ).toBe(false)
264
+ })
265
+
266
+ it('does not apply to self.', async () => {
267
+ const {
268
+ data: { thread },
269
+ } = await agent.app.bsky.feed.getPostThread(
270
+ {
271
+ uri: sc.posts[sc.dids.alice][0].ref.uriStr,
272
+ },
273
+ {
274
+ headers: await network.serviceHeaders(
275
+ sc.dids.geoff,
276
+ ids.AppBskyFeedGetPostThread,
277
+ ),
278
+ },
279
+ )
280
+ assert(isThreadViewPost(thread))
281
+ expect(
282
+ thread.replies?.some((reply) => {
283
+ return (
284
+ isThreadViewPost(reply) && reply.post.author.did === sc.dids.geoff
285
+ )
286
+ }),
287
+ ).toBe(true)
288
+ })
289
+
290
+ it('does not apply to followers.', async () => {
291
+ const {
292
+ data: { thread },
293
+ } = await agent.app.bsky.feed.getPostThread(
294
+ {
295
+ uri: sc.posts[sc.dids.alice][0].ref.uriStr,
296
+ },
297
+ {
298
+ headers: await network.serviceHeaders(
299
+ sc.dids.bob, // follows geoff
300
+ ids.AppBskyFeedGetPostThread,
301
+ ),
302
+ },
303
+ )
304
+ assert(isThreadViewPost(thread))
305
+ expect(
306
+ thread.replies?.some((reply) => {
307
+ return (
308
+ isThreadViewPost(reply) && reply.post.author.did === sc.dids.geoff
309
+ )
310
+ }),
311
+ ).toBe(true)
312
+ })
167
313
  })
168
314
  })
@@ -122,6 +122,7 @@ describe('bsky takedown labels', () => {
122
122
  uri,
123
123
  cid: '',
124
124
  val: '!takedown',
125
+ exp: null,
125
126
  neg: false,
126
127
  cts,
127
128
  }))
@@ -315,6 +315,7 @@ const createLabel = async (
315
315
  cid: opts.cid,
316
316
  val: opts.val,
317
317
  cts: new Date().toISOString(),
318
+ exp: null,
318
319
  neg: false,
319
320
  src: EXAMPLE_LABELER,
320
321
  })