@atproto/ozone 0.1.44 → 0.1.46

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 (59) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/api/moderation/emitEvent.d.ts.map +1 -1
  3. package/dist/api/moderation/emitEvent.js +3 -0
  4. package/dist/api/moderation/emitEvent.js.map +1 -1
  5. package/dist/api/moderation/queryStatuses.d.ts.map +1 -1
  6. package/dist/api/moderation/queryStatuses.js +2 -1
  7. package/dist/api/moderation/queryStatuses.js.map +1 -1
  8. package/dist/api/util.d.ts.map +1 -1
  9. package/dist/api/util.js +11 -7
  10. package/dist/api/util.js.map +1 -1
  11. package/dist/lexicon/lexicons.d.ts +77 -0
  12. package/dist/lexicon/lexicons.d.ts.map +1 -1
  13. package/dist/lexicon/lexicons.js +86 -3
  14. package/dist/lexicon/lexicons.js.map +1 -1
  15. package/dist/lexicon/types/app/bsky/actor/defs.d.ts +16 -0
  16. package/dist/lexicon/types/app/bsky/actor/defs.d.ts.map +1 -1
  17. package/dist/lexicon/types/app/bsky/actor/defs.js +9 -1
  18. package/dist/lexicon/types/app/bsky/actor/defs.js.map +1 -1
  19. package/dist/lexicon/types/app/bsky/actor/profile.d.ts +1 -0
  20. package/dist/lexicon/types/app/bsky/actor/profile.d.ts.map +1 -1
  21. package/dist/lexicon/types/app/bsky/actor/profile.js.map +1 -1
  22. package/dist/lexicon/types/app/bsky/feed/defs.d.ts +13 -2
  23. package/dist/lexicon/types/app/bsky/feed/defs.d.ts.map +1 -1
  24. package/dist/lexicon/types/app/bsky/feed/defs.js +21 -1
  25. package/dist/lexicon/types/app/bsky/feed/defs.js.map +1 -1
  26. package/dist/lexicon/types/app/bsky/feed/getAuthorFeed.d.ts +1 -0
  27. package/dist/lexicon/types/app/bsky/feed/getAuthorFeed.d.ts.map +1 -1
  28. package/dist/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.d.ts +2 -0
  29. package/dist/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.d.ts.map +1 -1
  30. package/dist/lexicon/types/app/bsky/unspecced/getSuggestionsSkeleton.d.ts +2 -0
  31. package/dist/lexicon/types/app/bsky/unspecced/getSuggestionsSkeleton.d.ts.map +1 -1
  32. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts +2 -0
  33. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts.map +1 -1
  34. package/dist/lexicon/types/tools/ozone/moderation/defs.js.map +1 -1
  35. package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.d.ts +3 -0
  36. package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.d.ts.map +1 -1
  37. package/dist/mod-service/index.d.ts +3 -1
  38. package/dist/mod-service/index.d.ts.map +1 -1
  39. package/dist/mod-service/index.js +48 -6
  40. package/dist/mod-service/index.js.map +1 -1
  41. package/dist/mod-service/views.d.ts.map +1 -1
  42. package/dist/mod-service/views.js +7 -0
  43. package/dist/mod-service/views.js.map +1 -1
  44. package/package.json +9 -9
  45. package/src/api/moderation/emitEvent.ts +4 -0
  46. package/src/api/moderation/queryStatuses.ts +2 -0
  47. package/src/api/util.ts +23 -7
  48. package/src/lexicon/lexicons.ts +92 -3
  49. package/src/lexicon/types/app/bsky/actor/defs.ts +25 -0
  50. package/src/lexicon/types/app/bsky/actor/profile.ts +1 -0
  51. package/src/lexicon/types/app/bsky/feed/defs.ts +38 -2
  52. package/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts +1 -0
  53. package/src/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.ts +2 -0
  54. package/src/lexicon/types/app/bsky/unspecced/getSuggestionsSkeleton.ts +2 -0
  55. package/src/lexicon/types/tools/ozone/moderation/defs.ts +2 -0
  56. package/src/lexicon/types/tools/ozone/moderation/queryStatuses.ts +3 -0
  57. package/src/mod-service/index.ts +64 -4
  58. package/src/mod-service/views.ts +10 -0
  59. package/tests/ack-all-subjects-of-account.test.ts +131 -0
@@ -7,6 +7,7 @@ import { isObj, hasProp } from '../../../../util'
7
7
  import { CID } from 'multiformats/cid'
8
8
  import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs'
9
9
  import * as AppBskyGraphDefs from '../graph/defs'
10
+ import * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef'
10
11
 
11
12
  export interface ProfileViewBasic {
12
13
  did: string
@@ -74,6 +75,7 @@ export interface ProfileViewDetailed {
74
75
  createdAt?: string
75
76
  viewer?: ViewerState
76
77
  labels?: ComAtprotoLabelDefs.Label[]
78
+ pinnedPost?: ComAtprotoRepoStrongRef.Main
77
79
  [k: string]: unknown
78
80
  }
79
81
 
@@ -469,6 +471,8 @@ export interface BskyAppStatePref {
469
471
  activeProgressGuide?: BskyAppProgressGuide
470
472
  /** An array of tokens which identify nudges (modals, popups, tours, highlight dots) that should be shown to the user. */
471
473
  queuedNudges?: string[]
474
+ /** Storage for NUXs the user has encountered. */
475
+ nuxs?: Nux[]
472
476
  [k: string]: unknown
473
477
  }
474
478
 
@@ -501,3 +505,24 @@ export function isBskyAppProgressGuide(v: unknown): v is BskyAppProgressGuide {
501
505
  export function validateBskyAppProgressGuide(v: unknown): ValidationResult {
502
506
  return lexicons.validate('app.bsky.actor.defs#bskyAppProgressGuide', v)
503
507
  }
508
+
509
+ /** A new user experiences (NUX) storage object */
510
+ export interface Nux {
511
+ id: string
512
+ completed: boolean
513
+ /** Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters. */
514
+ data?: string
515
+ /** The date and time at which the NUX will expire and should be considered completed. */
516
+ expiresAt?: string
517
+ [k: string]: unknown
518
+ }
519
+
520
+ export function isNux(v: unknown): v is Nux {
521
+ return (
522
+ isObj(v) && hasProp(v, '$type') && v.$type === 'app.bsky.actor.defs#nux'
523
+ )
524
+ }
525
+
526
+ export function validateNux(v: unknown): ValidationResult {
527
+ return lexicons.validate('app.bsky.actor.defs#nux', v)
528
+ }
@@ -20,6 +20,7 @@ export interface Record {
20
20
  | ComAtprotoLabelDefs.SelfLabels
21
21
  | { $type: string; [k: string]: unknown }
22
22
  joinedViaStarterPack?: ComAtprotoRepoStrongRef.Main
23
+ pinnedPost?: ComAtprotoRepoStrongRef.Main
23
24
  createdAt?: string
24
25
  [k: string]: unknown
25
26
  }
@@ -55,6 +55,7 @@ export interface ViewerState {
55
55
  threadMuted?: boolean
56
56
  replyDisabled?: boolean
57
57
  embeddingDisabled?: boolean
58
+ pinned?: boolean
58
59
  [k: string]: unknown
59
60
  }
60
61
 
@@ -73,7 +74,7 @@ export function validateViewerState(v: unknown): ValidationResult {
73
74
  export interface FeedViewPost {
74
75
  post: PostView
75
76
  reply?: ReplyRef
76
- reason?: ReasonRepost | { $type: string; [k: string]: unknown }
77
+ reason?: ReasonRepost | ReasonPin | { $type: string; [k: string]: unknown }
77
78
  /** Context provided by feed generator that may be passed back alongside interactions. */
78
79
  feedContext?: string
79
80
  [k: string]: unknown
@@ -134,6 +135,22 @@ export function validateReasonRepost(v: unknown): ValidationResult {
134
135
  return lexicons.validate('app.bsky.feed.defs#reasonRepost', v)
135
136
  }
136
137
 
138
+ export interface ReasonPin {
139
+ [k: string]: unknown
140
+ }
141
+
142
+ export function isReasonPin(v: unknown): v is ReasonPin {
143
+ return (
144
+ isObj(v) &&
145
+ hasProp(v, '$type') &&
146
+ v.$type === 'app.bsky.feed.defs#reasonPin'
147
+ )
148
+ }
149
+
150
+ export function validateReasonPin(v: unknown): ValidationResult {
151
+ return lexicons.validate('app.bsky.feed.defs#reasonPin', v)
152
+ }
153
+
137
154
  export interface ThreadViewPost {
138
155
  post: PostView
139
156
  parent?:
@@ -265,7 +282,10 @@ export function validateGeneratorViewerState(v: unknown): ValidationResult {
265
282
 
266
283
  export interface SkeletonFeedPost {
267
284
  post: string
268
- reason?: SkeletonReasonRepost | { $type: string; [k: string]: unknown }
285
+ reason?:
286
+ | SkeletonReasonRepost
287
+ | SkeletonReasonPin
288
+ | { $type: string; [k: string]: unknown }
269
289
  /** Context that will be passed through to client and may be passed to feed generator back alongside interactions. */
270
290
  feedContext?: string
271
291
  [k: string]: unknown
@@ -300,6 +320,22 @@ export function validateSkeletonReasonRepost(v: unknown): ValidationResult {
300
320
  return lexicons.validate('app.bsky.feed.defs#skeletonReasonRepost', v)
301
321
  }
302
322
 
323
+ export interface SkeletonReasonPin {
324
+ [k: string]: unknown
325
+ }
326
+
327
+ export function isSkeletonReasonPin(v: unknown): v is SkeletonReasonPin {
328
+ return (
329
+ isObj(v) &&
330
+ hasProp(v, '$type') &&
331
+ v.$type === 'app.bsky.feed.defs#skeletonReasonPin'
332
+ )
333
+ }
334
+
335
+ export function validateSkeletonReasonPin(v: unknown): ValidationResult {
336
+ return lexicons.validate('app.bsky.feed.defs#skeletonReasonPin', v)
337
+ }
338
+
303
339
  export interface ThreadgateView {
304
340
  uri?: string
305
341
  cid?: string
@@ -20,6 +20,7 @@ export interface QueryParams {
20
20
  | 'posts_with_media'
21
21
  | 'posts_and_author_threads'
22
22
  | (string & {})
23
+ includePins: boolean
23
24
  }
24
25
 
25
26
  export type InputSchema = undefined
@@ -17,6 +17,8 @@ export type InputSchema = undefined
17
17
 
18
18
  export interface OutputSchema {
19
19
  suggestions: AppBskyActorDefs.ProfileView[]
20
+ /** If true, response has fallen-back to generic results, and is not scoped using relativeToDid */
21
+ isFallback?: boolean
20
22
  [k: string]: unknown
21
23
  }
22
24
 
@@ -23,6 +23,8 @@ export type InputSchema = undefined
23
23
  export interface OutputSchema {
24
24
  cursor?: string
25
25
  actors: AppBskyUnspeccedDefs.SkeletonSearchActor[]
26
+ /** DID of the account these suggestions are relative to. If this is returned undefined, suggestions are based on the viewer. */
27
+ relativeToDid?: string
26
28
  [k: string]: unknown
27
29
  }
28
30
 
@@ -162,6 +162,8 @@ export interface ModEventTakedown {
162
162
  comment?: string
163
163
  /** Indicates how long the takedown should be in effect before automatically expiring. */
164
164
  durationInHours?: number
165
+ /** If true, all other reports on content authored by this account will be resolved (acknowledged). */
166
+ acknowledgeAccountSubjects?: boolean
165
167
  [k: string]: unknown
166
168
  }
167
169
 
@@ -10,6 +10,9 @@ import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server'
10
10
  import * as ToolsOzoneModerationDefs from './defs'
11
11
 
12
12
  export interface QueryParams {
13
+ /** All subjects belonging to the account specified in the 'subject' param will be returned. */
14
+ includeAllUserRecords?: boolean
15
+ /** The subject to get the status for. */
13
16
  subject?: string
14
17
  /** Search subjects by keyword from comments */
15
18
  comment?: string
@@ -3,7 +3,7 @@ import { Insertable, sql } from 'kysely'
3
3
  import { CID } from 'multiformats/cid'
4
4
  import { AtUri, INVALID_HANDLE } from '@atproto/syntax'
5
5
  import { InvalidRequestError } from '@atproto/xrpc-server'
6
- import { addHoursToDate } from '@atproto/common'
6
+ import { addHoursToDate, chunkArray } from '@atproto/common'
7
7
  import { Keypair } from '@atproto/crypto'
8
8
  import { IdResolver } from '@atproto/identity'
9
9
  import { AtpAgent } from '@atproto/api'
@@ -18,6 +18,8 @@ import {
18
18
  isModEventTakedown,
19
19
  isModEventEmail,
20
20
  isModEventTag,
21
+ REVIEWESCALATED,
22
+ REVIEWOPEN,
21
23
  } from '../lexicon/types/tools/ozone/moderation/defs'
22
24
  import { RepoRef, RepoBlobRef } from '../lexicon/types/com/atproto/admin/defs'
23
25
  import {
@@ -279,6 +281,52 @@ export class ModerationService {
279
281
  return await builder.execute()
280
282
  }
281
283
 
284
+ async resolveSubjectsForAccount(did: string, createdBy: string) {
285
+ const subjectsToBeResolved = await this.db.db
286
+ .selectFrom('moderation_subject_status')
287
+ .where('did', '=', did)
288
+ .where('recordPath', '!=', '')
289
+ .where('reviewState', 'in', [REVIEWESCALATED, REVIEWOPEN])
290
+ .selectAll()
291
+ .execute()
292
+
293
+ if (subjectsToBeResolved.length === 0) {
294
+ return
295
+ }
296
+
297
+ // Process subjects in chunks of 100 since each of these will trigger multiple db queries
298
+ for (const subjects of chunkArray(subjectsToBeResolved, 100)) {
299
+ await Promise.all(
300
+ subjects.map(async (subject) => {
301
+ const eventData = {
302
+ createdBy,
303
+ subject: subjectFromStatusRow(subject),
304
+ }
305
+ // For consistency's sake, when acknowledging appealed subjects, we should first resolve the appeal
306
+ if (subject.appealed) {
307
+ await this.logEvent({
308
+ event: {
309
+ $type: 'tools.ozone.moderation.defs#modEventResolveAppeal',
310
+ comment:
311
+ '[AUTO_RESOLVE_FOR_TAKENDOWN_ACCOUNT]: Automatically resolving all appealed content for a takendown account',
312
+ },
313
+ ...eventData,
314
+ })
315
+ }
316
+
317
+ await this.logEvent({
318
+ event: {
319
+ $type: 'tools.ozone.moderation.defs#modEventAcknowledge',
320
+ comment:
321
+ '[AUTO_RESOLVE_FOR_TAKENDOWN_ACCOUNT]: Automatically resolving all reported content for a takendown account',
322
+ },
323
+ ...eventData,
324
+ })
325
+ }),
326
+ )
327
+ }
328
+ }
329
+
282
330
  async logEvent(info: {
283
331
  event: ModEventType
284
332
  subject: ModSubject
@@ -320,6 +368,10 @@ export class ModerationService {
320
368
  }
321
369
  }
322
370
 
371
+ if (isModEventTakedown(event) && event.acknowledgeAccountSubjects) {
372
+ meta.acknowledgeAccountSubjects = true
373
+ }
374
+
323
375
  // Keep trace of reports that came in while the reporter was in muted stated
324
376
  if (isModEventReport(event)) {
325
377
  const isReportingMuted = await this.isReportingMutedForSubject(createdBy)
@@ -677,6 +729,7 @@ export class ModerationService {
677
729
  }
678
730
 
679
731
  async getSubjectStatuses({
732
+ includeAllUserRecords,
680
733
  cursor,
681
734
  limit = 50,
682
735
  takendown,
@@ -696,6 +749,7 @@ export class ModerationService {
696
749
  tags,
697
750
  excludeTags,
698
751
  }: {
752
+ includeAllUserRecords?: boolean
699
753
  cursor?: string
700
754
  limit?: number
701
755
  takendown?: boolean
@@ -720,13 +774,19 @@ export class ModerationService {
720
774
 
721
775
  if (subject) {
722
776
  const subjectInfo = getStatusIdentifierFromSubject(subject)
723
- builder = builder
724
- .where('moderation_subject_status.did', '=', subjectInfo.did)
725
- .where((qb) =>
777
+ builder = builder.where(
778
+ 'moderation_subject_status.did',
779
+ '=',
780
+ subjectInfo.did,
781
+ )
782
+
783
+ if (!includeAllUserRecords) {
784
+ builder = builder.where((qb) =>
726
785
  subjectInfo.recordPath
727
786
  ? qb.where('recordPath', '=', subjectInfo.recordPath)
728
787
  : qb.where('recordPath', '=', ''),
729
788
  )
789
+ }
730
790
  }
731
791
 
732
792
  if (ignoreSubjects?.length) {
@@ -122,6 +122,16 @@ export class ModerationViews {
122
122
  }
123
123
  }
124
124
 
125
+ if (
126
+ event.action === 'tools.ozone.moderation.defs#modEventTakedown' &&
127
+ event.meta?.acknowledgeAccountSubjects
128
+ ) {
129
+ eventView.event = {
130
+ ...eventView.event,
131
+ acknowledgeAccountSubjects: event.meta?.acknowledgeAccountSubjects,
132
+ }
133
+ }
134
+
125
135
  if (event.action === 'tools.ozone.moderation.defs#modEventLabel') {
126
136
  eventView.event = {
127
137
  ...eventView.event,
@@ -0,0 +1,131 @@
1
+ import {
2
+ TestNetwork,
3
+ RecordRef,
4
+ SeedClient,
5
+ basicSeed,
6
+ ModeratorClient,
7
+ } from '@atproto/dev-env'
8
+ import {
9
+ REASONAPPEAL,
10
+ REASONOTHER,
11
+ REASONSPAM,
12
+ } from '../src/lexicon/types/com/atproto/moderation/defs'
13
+ import {
14
+ REVIEWCLOSED,
15
+ REVIEWESCALATED,
16
+ REVIEWOPEN,
17
+ SubjectStatusView,
18
+ } from '../src/lexicon/types/tools/ozone/moderation/defs'
19
+ import { isRepoRef } from '../src/lexicon/types/com/atproto/admin/defs'
20
+ import { ComAtprotoRepoStrongRef } from '@atproto/api'
21
+
22
+ describe('moderation', () => {
23
+ let network: TestNetwork
24
+ let sc: SeedClient
25
+ let modClient: ModeratorClient
26
+
27
+ const repoSubject = (did: string) => ({
28
+ $type: 'com.atproto.admin.defs#repoRef',
29
+ did,
30
+ })
31
+
32
+ const recordSubject = (ref: RecordRef) => ({
33
+ $type: 'com.atproto.repo.strongRef',
34
+ uri: ref.uriStr,
35
+ cid: ref.cidStr,
36
+ })
37
+
38
+ beforeAll(async () => {
39
+ network = await TestNetwork.create({
40
+ dbPostgresSchema: 'ozone_ack_all_subjects_of_account',
41
+ })
42
+ sc = network.getSeedClient()
43
+ modClient = network.ozone.getModClient()
44
+ await basicSeed(sc)
45
+ await network.processAll()
46
+ })
47
+
48
+ afterAll(async () => {
49
+ await network.close()
50
+ })
51
+
52
+ it('acknowledges all open/escalated review subjects.', async () => {
53
+ const postOne = sc.posts[sc.dids.bob][0].ref
54
+ const postTwo = sc.posts[sc.dids.bob][1].ref
55
+ await Promise.all([
56
+ sc.createReport({
57
+ reasonType: REASONSPAM,
58
+ subject: repoSubject(sc.dids.bob),
59
+ reportedBy: sc.dids.alice,
60
+ }),
61
+ sc.createReport({
62
+ reasonType: REASONOTHER,
63
+ reason: 'defamation',
64
+ subject: recordSubject(postOne),
65
+ reportedBy: sc.dids.carol,
66
+ }),
67
+ sc.createReport({
68
+ reasonType: REASONOTHER,
69
+ reason: 'defamation',
70
+ subject: recordSubject(postTwo),
71
+ reportedBy: sc.dids.carol,
72
+ }),
73
+ ])
74
+
75
+ await modClient.emitEvent({
76
+ event: {
77
+ $type: 'tools.ozone.moderation.defs#modEventReport',
78
+ reportType: REASONAPPEAL,
79
+ },
80
+ subject: recordSubject(postTwo),
81
+ })
82
+
83
+ const { subjectStatuses: statusesBefore } = await modClient.queryStatuses({
84
+ subject: sc.dids.bob,
85
+ includeAllUserRecords: true,
86
+ })
87
+
88
+ await modClient.performTakedown({
89
+ subject: repoSubject(sc.dids.bob),
90
+ acknowledgeAccountSubjects: true,
91
+ })
92
+
93
+ const { subjectStatuses: statusesAfter } = await modClient.queryStatuses({
94
+ subject: sc.dids.bob,
95
+ includeAllUserRecords: true,
96
+ })
97
+
98
+ const getReviewStateBySubject = (subjects: SubjectStatusView[]) => {
99
+ const states = new Map<string, SubjectStatusView>()
100
+
101
+ subjects.forEach((item) => {
102
+ if (ComAtprotoRepoStrongRef.isMain(item.subject)) {
103
+ states.set(item.subject.uri, item)
104
+ } else if (isRepoRef(item.subject)) {
105
+ states.set(item.subject.did, item)
106
+ }
107
+ })
108
+
109
+ return states
110
+ }
111
+
112
+ const reviewStatesBefore = getReviewStateBySubject(statusesBefore)
113
+ const reviewStatesAfter = getReviewStateBySubject(statusesAfter)
114
+
115
+ // Check that review states before were different for different subjects
116
+ expect(reviewStatesBefore.get(postOne.uriStr)?.reviewState).toBe(REVIEWOPEN)
117
+ expect(reviewStatesBefore.get(postTwo.uriStr)?.reviewState).toBe(
118
+ REVIEWESCALATED,
119
+ )
120
+ expect(reviewStatesBefore.get(sc.dids.bob)?.reviewState).toBe(REVIEWOPEN)
121
+
122
+ // Check that review states after are all closed
123
+ expect(reviewStatesAfter.get(postOne.uriStr)?.reviewState).toBe(
124
+ REVIEWCLOSED,
125
+ )
126
+ expect(reviewStatesAfter.get(postTwo.uriStr)?.reviewState).toBe(
127
+ REVIEWCLOSED,
128
+ )
129
+ expect(reviewStatesAfter.get(sc.dids.bob)?.reviewState).toBe(REVIEWCLOSED)
130
+ })
131
+ })