@atproto/bsky 0.0.19 → 0.0.21

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.
@@ -6880,6 +6880,10 @@ export declare const schemaDict: {
6880
6880
  ref: string;
6881
6881
  };
6882
6882
  };
6883
+ seenAt: {
6884
+ type: string;
6885
+ format: string;
6886
+ };
6883
6887
  };
6884
6888
  };
6885
6889
  };
@@ -12,6 +12,7 @@ export declare type InputSchema = undefined;
12
12
  export interface OutputSchema {
13
13
  cursor?: string;
14
14
  notifications: Notification[];
15
+ seenAt?: string;
15
16
  [k: string]: unknown;
16
17
  }
17
18
  export declare type HandlerInput = undefined;
@@ -89,6 +89,7 @@ export declare class ModerationService {
89
89
  legacyRefId: number | null;
90
90
  } | null | undefined>;
91
91
  getSubjectsDueForReversal(): Promise<ModerationSubjectStatusRow[]>;
92
+ isSubjectSuspended(did: string): Promise<boolean>;
92
93
  revertState({ createdBy, createdAt, comment, action, subject, }: ReversibleModerationEvent): Promise<{
93
94
  result: ModerationEventRow;
94
95
  restored?: TakedownSubjects;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/bsky",
3
- "version": "0.0.19",
3
+ "version": "0.0.21",
4
4
  "license": "MIT",
5
5
  "description": "Reference implementation of app.bsky App View (Bluesky API)",
6
6
  "keywords": [
@@ -35,7 +35,7 @@
35
35
  "sharp": "^0.32.6",
36
36
  "typed-emitter": "^2.1.0",
37
37
  "uint8arrays": "3.0.0",
38
- "@atproto/api": "^0.7.2",
38
+ "@atproto/api": "^0.7.4",
39
39
  "@atproto/common": "^0.3.3",
40
40
  "@atproto/crypto": "^0.3.0",
41
41
  "@atproto/syntax": "^0.1.5",
@@ -52,10 +52,10 @@
52
52
  "@types/pg": "^8.6.6",
53
53
  "@types/qs": "^6.9.7",
54
54
  "axios": "^0.27.2",
55
- "@atproto/api": "^0.7.2",
56
- "@atproto/dev-env": "^0.2.19",
55
+ "@atproto/api": "^0.7.4",
56
+ "@atproto/dev-env": "^0.2.21",
57
57
  "@atproto/lex-cli": "^0.2.5",
58
- "@atproto/pds": "^0.3.7",
58
+ "@atproto/pds": "^0.3.9",
59
59
  "@atproto/xrpc": "^0.4.1"
60
60
  },
61
61
  "scripts": {
@@ -11,6 +11,7 @@ import {
11
11
  } from '../../../../services/actor'
12
12
  import { setRepoRev } from '../../../util'
13
13
  import { createPipeline, noRules } from '../../../../pipeline'
14
+ import { ModerationService } from '../../../../services/moderation'
14
15
 
15
16
  export default function (server: Server, ctx: AppContext) {
16
17
  const getProfile = createPipeline(skeleton, hydration, noRules, presentation)
@@ -19,6 +20,7 @@ export default function (server: Server, ctx: AppContext) {
19
20
  handler: async ({ auth, params, res }) => {
20
21
  const db = ctx.db.getReplica()
21
22
  const actorService = ctx.services.actor(db)
23
+ const modService = ctx.services.moderation(ctx.db.getPrimary())
22
24
  const viewer = 'did' in auth.credentials ? auth.credentials.did : null
23
25
  const canViewTakendownProfile =
24
26
  auth.credentials.type === 'role' && auth.credentials.triage
@@ -26,7 +28,7 @@ export default function (server: Server, ctx: AppContext) {
26
28
  const [result, repoRev] = await Promise.allSettled([
27
29
  getProfile(
28
30
  { ...params, viewer, canViewTakendownProfile },
29
- { db, actorService },
31
+ { db, actorService, modService },
30
32
  ),
31
33
  actorService.getRepoRev(viewer),
32
34
  ])
@@ -50,17 +52,25 @@ const skeleton = async (
50
52
  params: Params,
51
53
  ctx: Context,
52
54
  ): Promise<SkeletonState> => {
53
- const { actorService } = ctx
55
+ const { actorService, modService } = ctx
54
56
  const { canViewTakendownProfile } = params
55
57
  const actor = await actorService.getActor(params.actor, true)
56
58
  if (!actor) {
57
59
  throw new InvalidRequestError('Profile not found')
58
60
  }
59
61
  if (!canViewTakendownProfile && softDeleted(actor)) {
60
- throw new InvalidRequestError(
61
- 'Account has been taken down',
62
- 'AccountTakedown',
63
- )
62
+ const isSuspended = await modService.isSubjectSuspended(actor.did)
63
+ if (isSuspended) {
64
+ throw new InvalidRequestError(
65
+ 'Account has been temporarily suspended',
66
+ 'AccountTakedown',
67
+ )
68
+ } else {
69
+ throw new InvalidRequestError(
70
+ 'Account has been taken down',
71
+ 'AccountTakedown',
72
+ )
73
+ }
64
74
  }
65
75
  return { params, actor }
66
76
  }
@@ -95,6 +105,7 @@ const presentation = (state: HydrationState, ctx: Context) => {
95
105
  type Context = {
96
106
  db: Database
97
107
  actorService: ActorService
108
+ modService: ModerationService
98
109
  }
99
110
 
100
111
  type Params = QueryParams & {
@@ -148,7 +148,7 @@ const presentation = (state: HydrationState) => {
148
148
  labels: [...recordLabels, ...recordSelfLabels],
149
149
  }
150
150
  })
151
- return { notifications, cursor }
151
+ return { notifications, cursor, seenAt: lastSeenNotifs }
152
152
  }
153
153
 
154
154
  const getRecordMap = async (
@@ -7325,6 +7325,10 @@ export const schemaDict = {
7325
7325
  ref: 'lex:app.bsky.notification.listNotifications#notification',
7326
7326
  },
7327
7327
  },
7328
+ seenAt: {
7329
+ type: 'string',
7330
+ format: 'datetime',
7331
+ },
7328
7332
  },
7329
7333
  },
7330
7334
  },
@@ -21,6 +21,7 @@ export type InputSchema = undefined
21
21
  export interface OutputSchema {
22
22
  cursor?: string
23
23
  notifications: Notification[]
24
+ seenAt?: string
24
25
  [k: string]: unknown
25
26
  }
26
27
 
@@ -323,6 +323,18 @@ export class ModerationService {
323
323
  return subjectsDueForReversal
324
324
  }
325
325
 
326
+ async isSubjectSuspended(did: string): Promise<boolean> {
327
+ const res = await this.db.db
328
+ .selectFrom('moderation_subject_status')
329
+ .where('did', '=', did)
330
+ .where('recordPath', '=', '')
331
+ .where('suspendUntil', '>', new Date().toISOString())
332
+ .select('did')
333
+ .limit(1)
334
+ .executeTakeFirst()
335
+ return !!res
336
+ }
337
+
326
338
  async revertState({
327
339
  createdBy,
328
340
  createdAt,
@@ -176,6 +176,13 @@ describe('notification views', () => {
176
176
  encoding: 'application/json',
177
177
  },
178
178
  )
179
+ const full2 = await agent.api.app.bsky.notification.listNotifications(
180
+ {},
181
+ { headers: await network.serviceHeaders(alice) },
182
+ )
183
+ expect(full2.data.notifications.length).toBe(full.data.notifications.length)
184
+ expect(full2.data.seenAt).toEqual(seenAt)
185
+
179
186
  const notifCount = await agent.api.app.bsky.notification.getUnreadCount(
180
187
  {},
181
188
  { headers: await network.serviceHeaders(alice) },
@@ -224,6 +224,52 @@ describe('pds profile views', () => {
224
224
  )
225
225
  })
226
226
 
227
+ it('blocked by actor suspension', async () => {
228
+ await agent.api.com.atproto.admin.emitModerationEvent(
229
+ {
230
+ event: {
231
+ $type: 'com.atproto.admin.defs#modEventTakedown',
232
+ durationInHours: 1,
233
+ },
234
+ subject: {
235
+ $type: 'com.atproto.admin.defs#repoRef',
236
+ did: alice,
237
+ },
238
+ createdBy: 'did:example:admin',
239
+ reason: 'Y',
240
+ },
241
+ {
242
+ encoding: 'application/json',
243
+ headers: network.pds.adminAuthHeaders(),
244
+ },
245
+ )
246
+ const promise = agent.api.app.bsky.actor.getProfile(
247
+ { actor: alice },
248
+ { headers: await network.serviceHeaders(bob) },
249
+ )
250
+
251
+ await expect(promise).rejects.toThrow(
252
+ 'Account has been temporarily suspended',
253
+ )
254
+
255
+ // Cleanup
256
+ await agent.api.com.atproto.admin.emitModerationEvent(
257
+ {
258
+ event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' },
259
+ subject: {
260
+ $type: 'com.atproto.admin.defs#repoRef',
261
+ did: alice,
262
+ },
263
+ createdBy: 'did:example:admin',
264
+ reason: 'Y',
265
+ },
266
+ {
267
+ encoding: 'application/json',
268
+ headers: network.pds.adminAuthHeaders(),
269
+ },
270
+ )
271
+ })
272
+
227
273
  async function updateProfile(did: string, record: Record<string, unknown>) {
228
274
  return await pdsAgent.api.com.atproto.repo.putRecord(
229
275
  {