@atproto/ozone 0.1.68 → 0.1.70

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 (135) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/api/moderation/queryStatuses.d.ts.map +1 -1
  3. package/dist/api/moderation/queryStatuses.js +1 -33
  4. package/dist/api/moderation/queryStatuses.js.map +1 -1
  5. package/dist/background.d.ts +49 -6
  6. package/dist/background.d.ts.map +1 -1
  7. package/dist/background.js +149 -14
  8. package/dist/background.js.map +1 -1
  9. package/dist/config/config.d.ts +1 -0
  10. package/dist/config/config.d.ts.map +1 -1
  11. package/dist/config/config.js +1 -0
  12. package/dist/config/config.js.map +1 -1
  13. package/dist/config/env.d.ts +1 -0
  14. package/dist/config/env.d.ts.map +1 -1
  15. package/dist/config/env.js +1 -0
  16. package/dist/config/env.js.map +1 -1
  17. package/dist/daemon/context.d.ts +9 -3
  18. package/dist/daemon/context.d.ts.map +1 -1
  19. package/dist/daemon/context.js +33 -3
  20. package/dist/daemon/context.js.map +1 -1
  21. package/dist/daemon/index.d.ts.map +1 -1
  22. package/dist/daemon/index.js +3 -6
  23. package/dist/daemon/index.js.map +1 -1
  24. package/dist/daemon/materialized-view-refresher.d.ts +5 -0
  25. package/dist/daemon/materialized-view-refresher.d.ts.map +1 -0
  26. package/dist/daemon/materialized-view-refresher.js +29 -0
  27. package/dist/daemon/materialized-view-refresher.js.map +1 -0
  28. package/dist/db/migrations/20241220T144630860Z-stats-materialized-views.d.ts +5 -0
  29. package/dist/db/migrations/20241220T144630860Z-stats-materialized-views.d.ts.map +1 -0
  30. package/dist/db/migrations/20241220T144630860Z-stats-materialized-views.js +158 -0
  31. package/dist/db/migrations/20241220T144630860Z-stats-materialized-views.js.map +1 -0
  32. package/dist/db/migrations/index.d.ts +1 -0
  33. package/dist/db/migrations/index.d.ts.map +1 -1
  34. package/dist/db/migrations/index.js +2 -1
  35. package/dist/db/migrations/index.js.map +1 -1
  36. package/dist/db/schema/account_events_stats.d.ts +15 -0
  37. package/dist/db/schema/account_events_stats.d.ts.map +1 -0
  38. package/dist/db/schema/account_events_stats.js +5 -0
  39. package/dist/db/schema/account_events_stats.js.map +1 -0
  40. package/dist/db/schema/account_record_events_stats.d.ts +15 -0
  41. package/dist/db/schema/account_record_events_stats.d.ts.map +1 -0
  42. package/dist/db/schema/account_record_events_stats.js +5 -0
  43. package/dist/db/schema/account_record_events_stats.js.map +1 -0
  44. package/dist/db/schema/account_record_status_stats.d.ts +15 -0
  45. package/dist/db/schema/account_record_status_stats.d.ts.map +1 -0
  46. package/dist/db/schema/account_record_status_stats.js +5 -0
  47. package/dist/db/schema/account_record_status_stats.js.map +1 -0
  48. package/dist/db/schema/index.d.ts +5 -1
  49. package/dist/db/schema/index.d.ts.map +1 -1
  50. package/dist/db/schema/record_events_stats.d.ts +14 -0
  51. package/dist/db/schema/record_events_stats.d.ts.map +1 -0
  52. package/dist/db/schema/record_events_stats.js +5 -0
  53. package/dist/db/schema/record_events_stats.js.map +1 -0
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +1 -4
  56. package/dist/index.js.map +1 -1
  57. package/dist/lexicon/index.d.ts +2 -0
  58. package/dist/lexicon/index.d.ts.map +1 -1
  59. package/dist/lexicon/index.js +2 -0
  60. package/dist/lexicon/index.js.map +1 -1
  61. package/dist/lexicon/lexicons.d.ts +230 -2
  62. package/dist/lexicon/lexicons.d.ts.map +1 -1
  63. package/dist/lexicon/lexicons.js +126 -1
  64. package/dist/lexicon/lexicons.js.map +1 -1
  65. package/dist/lexicon/types/app/bsky/feed/defs.d.ts +5 -0
  66. package/dist/lexicon/types/app/bsky/feed/defs.d.ts.map +1 -1
  67. package/dist/lexicon/types/app/bsky/feed/defs.js +5 -1
  68. package/dist/lexicon/types/app/bsky/feed/defs.js.map +1 -1
  69. package/dist/lexicon/types/app/bsky/feed/generator.d.ts +1 -0
  70. package/dist/lexicon/types/app/bsky/feed/generator.d.ts.map +1 -1
  71. package/dist/lexicon/types/app/bsky/feed/generator.js.map +1 -1
  72. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts +40 -0
  73. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts.map +1 -1
  74. package/dist/lexicon/types/tools/ozone/moderation/defs.js +20 -0
  75. package/dist/lexicon/types/tools/ozone/moderation/defs.js.map +1 -1
  76. package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.d.ts +7 -1
  77. package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.d.ts.map +1 -1
  78. package/dist/mod-service/index.d.ts +4 -62
  79. package/dist/mod-service/index.d.ts.map +1 -1
  80. package/dist/mod-service/index.js +80 -74
  81. package/dist/mod-service/index.js.map +1 -1
  82. package/dist/mod-service/status.d.ts +115 -4
  83. package/dist/mod-service/status.d.ts.map +1 -1
  84. package/dist/mod-service/status.js +51 -34
  85. package/dist/mod-service/status.js.map +1 -1
  86. package/dist/mod-service/types.d.ts +16 -1
  87. package/dist/mod-service/types.d.ts.map +1 -1
  88. package/dist/mod-service/views.d.ts.map +1 -1
  89. package/dist/mod-service/views.js +49 -41
  90. package/dist/mod-service/views.js.map +1 -1
  91. package/dist/util.d.ts +34 -0
  92. package/dist/util.d.ts.map +1 -1
  93. package/dist/util.js +132 -0
  94. package/dist/util.js.map +1 -1
  95. package/package.json +3 -3
  96. package/src/api/moderation/queryStatuses.ts +1 -63
  97. package/src/background.ts +140 -14
  98. package/src/config/config.ts +2 -0
  99. package/src/config/env.ts +4 -0
  100. package/src/daemon/context.ts +43 -5
  101. package/src/daemon/index.ts +3 -6
  102. package/src/daemon/materialized-view-refresher.ts +27 -0
  103. package/src/db/migrations/20241220T144630860Z-stats-materialized-views.ts +218 -0
  104. package/src/db/migrations/index.ts +1 -0
  105. package/src/db/schema/account_events_stats.ts +16 -0
  106. package/src/db/schema/account_record_events_stats.ts +15 -0
  107. package/src/db/schema/account_record_status_stats.ts +15 -0
  108. package/src/db/schema/index.ts +10 -1
  109. package/src/db/schema/record_events_stats.ts +15 -0
  110. package/src/index.ts +1 -7
  111. package/src/lexicon/index.ts +2 -0
  112. package/src/lexicon/lexicons.ts +138 -1
  113. package/src/lexicon/types/app/bsky/feed/defs.ts +9 -0
  114. package/src/lexicon/types/app/bsky/feed/generator.ts +4 -0
  115. package/src/lexicon/types/tools/ozone/moderation/defs.ts +62 -0
  116. package/src/lexicon/types/tools/ozone/moderation/queryStatuses.ts +11 -1
  117. package/src/mod-service/index.ts +181 -118
  118. package/src/mod-service/status.ts +55 -28
  119. package/src/mod-service/types.ts +22 -1
  120. package/src/mod-service/views.ts +64 -50
  121. package/src/util.ts +145 -0
  122. package/tests/__snapshots__/get-record.test.ts.snap +28 -0
  123. package/tests/__snapshots__/get-records.test.ts.snap +14 -0
  124. package/tests/__snapshots__/get-repo.test.ts.snap +11 -0
  125. package/tests/__snapshots__/get-repos.test.ts.snap +11 -0
  126. package/tests/__snapshots__/moderation-events.test.ts.snap +19 -0
  127. package/tests/__snapshots__/moderation-statuses.test.ts.snap +114 -0
  128. package/tests/get-record.test.ts +4 -0
  129. package/tests/get-records.test.ts +4 -0
  130. package/tests/get-repo.test.ts +4 -0
  131. package/tests/get-repos.test.ts +4 -0
  132. package/tests/moderation-events.test.ts +4 -0
  133. package/tests/moderation-statuses.test.ts +4 -0
  134. package/tests/query-labels.test.ts +1 -0
  135. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,10 +1,6 @@
1
1
  import { sql } from 'kysely'
2
2
  import { AtUri, INVALID_HANDLE, normalizeDatetimeAlways } from '@atproto/syntax'
3
- import {
4
- AtpAgent,
5
- AppBskyFeedDefs,
6
- ToolsOzoneModerationDefs,
7
- } from '@atproto/api'
3
+ import { AtpAgent, AppBskyFeedDefs } from '@atproto/api'
8
4
  import { dedupeStrs } from '@atproto/common'
9
5
  import { BlobRef } from '@atproto/lexicon'
10
6
  import { Keypair } from '@atproto/crypto'
@@ -12,7 +8,6 @@ import { Database } from '../db'
12
8
  import {
13
9
  ModEventView,
14
10
  RepoView,
15
- RepoViewDetail,
16
11
  RecordView,
17
12
  RecordViewDetail,
18
13
  BlobView,
@@ -34,6 +29,7 @@ import { dbLogger } from '../logger'
34
29
  import { httpLogger } from '../logger'
35
30
  import { ParsedLabelers } from '../util'
36
31
  import { ids } from '../lexicon/lexicons'
32
+ import { moderationSubjectStatusQueryBuilder } from './status'
37
33
 
38
34
  export type AuthHeaders = {
39
35
  headers: {
@@ -481,8 +477,9 @@ export class ModerationViews {
481
477
  async blob(blobs: BlobRef[]): Promise<BlobView[]> {
482
478
  if (!blobs.length) return []
483
479
  const { ref } = this.db.db.dynamic
484
- const modStatusResults = await this.db.db
485
- .selectFrom('moderation_subject_status')
480
+ const modStatusResults = await moderationSubjectStatusQueryBuilder(
481
+ this.db.db,
482
+ )
486
483
  .where(
487
484
  sql<string>`${ref(
488
485
  'moderation_subject_status.blobCids',
@@ -529,10 +526,10 @@ export class ModerationViews {
529
526
  await Promise.all(
530
527
  res.map(async (labelRow) => {
531
528
  const signedLabel = await this.formatLabelAndEnsureSig(labelRow)
532
- if (!labels.has(labelRow.uri)) {
533
- labels.set(labelRow.uri, [])
534
- }
535
- labels.get(labelRow.uri)?.push(signedLabel)
529
+
530
+ const current = labels.get(labelRow.uri)
531
+ if (current) current.push(signedLabel)
532
+ else labels.set(labelRow.uri, [signedLabel])
536
533
  }),
537
534
  )
538
535
  return labels
@@ -559,51 +556,39 @@ export class ModerationViews {
559
556
  async getSubjectStatus(
560
557
  subjects: string[],
561
558
  ): Promise<Map<string, ModerationSubjectStatusRowWithHandle>> {
562
- const parsedSubjects = subjects.map((subject) => parseSubjectId(subject))
563
- const filterForSubject = (did: string, recordPath?: string) => {
564
- return (clause: any) => {
565
- clause = clause
566
- .where('moderation_subject_status.did', '=', did)
567
- .where('moderation_subject_status.recordPath', '=', recordPath || '')
568
- return clause
569
- }
570
- // TODO: Fix the typing here?
571
- }
572
-
573
- const builder = this.db.db
574
- .selectFrom('moderation_subject_status')
575
- .where((clause) => {
576
- parsedSubjects.forEach((subject, i) => {
577
- const applySubjectFilter = filterForSubject(
578
- subject.did,
579
- subject.recordPath,
559
+ if (!subjects.length) return new Map()
560
+
561
+ const parsedSubjects = subjects.map(parseSubjectId)
562
+
563
+ const builder = moderationSubjectStatusQueryBuilder(this.db.db)
564
+ //
565
+ .where((qb) => {
566
+ for (const sub of parsedSubjects) {
567
+ qb = qb.orWhere((qb) =>
568
+ qb
569
+ .where('moderation_subject_status.did', '=', sub.did)
570
+ .where(
571
+ 'moderation_subject_status.recordPath',
572
+ '=',
573
+ sub.recordPath ?? '',
574
+ ),
580
575
  )
581
- if (i === 0) {
582
- clause = clause.where(applySubjectFilter)
583
- } else {
584
- clause = clause.orWhere(applySubjectFilter)
585
- }
586
- })
587
-
588
- return clause
576
+ }
577
+ return qb
589
578
  })
590
- .selectAll()
591
579
 
592
580
  const [statusRes, accountsByDid] = await Promise.all([
593
581
  builder.execute(),
594
582
  this.getAccoutInfosByDid(parsedSubjects.map((s) => s.did)),
595
583
  ])
596
584
 
597
- return statusRes.reduce((acc, cur) => {
598
- const subject = cur.recordPath
599
- ? formatSubjectId(cur.did, cur.recordPath)
600
- : cur.did
601
- const handle = accountsByDid.get(cur.did)?.handle
602
- return acc.set(subject, {
603
- ...cur,
604
- handle: handle ?? INVALID_HANDLE,
605
- })
606
- }, new Map<string, ModerationSubjectStatusRowWithHandle>())
585
+ return new Map(
586
+ statusRes.map((row): [string, ModerationSubjectStatusRowWithHandle] => {
587
+ const subjectId = formatSubjectId(row.did, row.recordPath)
588
+ const handle = accountsByDid.get(row.did)?.handle ?? INVALID_HANDLE
589
+ return [subjectId, { ...row, handle }]
590
+ }),
591
+ )
607
592
  }
608
593
 
609
594
  formatSubjectStatus(
@@ -628,6 +613,35 @@ export class ModerationViews {
628
613
  subjectBlobCids: status.blobCids || [],
629
614
  tags: status.tags || [],
630
615
  subject: subjectFromStatusRow(status).lex(),
616
+
617
+ accountStats: {
618
+ // Explicitly typing to allow for easy manipulation (e.g. to strip from tests snapshots)
619
+ $type: 'tools.ozone.moderation.defs#accountStats',
620
+
621
+ // account_events_stats
622
+ reportCount: status.reportCount ?? undefined,
623
+ appealCount: status.appealCount ?? undefined,
624
+ suspendCount: status.suspendCount ?? undefined,
625
+ takedownCount: status.takedownCount ?? undefined,
626
+ escalateCount: status.escalateCount ?? undefined,
627
+ },
628
+
629
+ recordsStats: {
630
+ // Explicitly typing to allow for easy manipulation (e.g. to strip from tests snapshots)
631
+ $type: 'tools.ozone.moderation.defs#recordStats',
632
+
633
+ // account_record_events_stats
634
+ totalReports: status.totalReports ?? undefined,
635
+ reportedCount: status.reportedCount ?? undefined,
636
+ escalatedCount: status.escalatedCount ?? undefined,
637
+ appealedCount: status.appealedCount ?? undefined,
638
+
639
+ // account_record_status_stats
640
+ subjectCount: status.subjectCount ?? undefined,
641
+ pendingCount: status.pendingCount ?? undefined,
642
+ processedCount: status.processedCount ?? undefined,
643
+ takendownCount: status.takendownCount ?? undefined,
644
+ },
631
645
  }
632
646
 
633
647
  if (status.recordPath !== '') {
@@ -677,7 +691,7 @@ type RecordInfo = {
677
691
  indexedAt: string
678
692
  }
679
693
 
680
- function parseSubjectId(subject: string) {
694
+ function parseSubjectId(subject: string): { did: string; recordPath?: string } {
681
695
  if (subject.startsWith('did:')) {
682
696
  return { did: subject }
683
697
  }
package/src/util.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import assert from 'node:assert'
1
2
  import { createRetryable } from '@atproto/common'
2
3
  import { ResponseType, XRPCError } from '@atproto/xrpc'
3
4
  import { parseList } from 'structured-headers'
@@ -83,3 +84,147 @@ export const formatLabelerHeader = (parsed: ParsedLabelers): string => {
83
84
  )
84
85
  return parts.join(',')
85
86
  }
87
+
88
+ /**
89
+ * Utility function similar to `setInterval()`. The main difference is that the
90
+ * execution is controlled through a signal and that the function will wait for
91
+ * `interval` milliseconds *between* the end of the previous execution and the
92
+ * start of the next one (instead of starting the execution every `interval`
93
+ * milliseconds), ensuring that the function is not running concurrently.
94
+ *
95
+ * @param fn The function to execute. That function must not throw any error
96
+ * other than {@link signal}'s {@link AbortSignal.reason} or an {@link Error}
97
+ * that has the {@link signal}'s {@link AbortSignal.reason} as its
98
+ * {@link Error.cause}.
99
+ *
100
+ * @returns A promise that resolves when the signal is aborted, and the last
101
+ * execution is done.
102
+ *
103
+ * @throws {AbortSignal['reason']} if the {@link signal} is already aborted.
104
+ * @throws {TypeError} if {@link fn} throws an unexpected error (with the
105
+ * unexpected error as the {@link Error.cause}).
106
+ */
107
+ export async function startInterval(
108
+ fn: (signal: AbortSignal) => void | Promise<void>,
109
+ interval: number,
110
+ signal: AbortSignal,
111
+ runImmediately = false,
112
+ ) {
113
+ signal.throwIfAborted()
114
+
115
+ // Renaming for clarity
116
+ const inputSignal = signal
117
+
118
+ const intervalController = new AbortController()
119
+ const intervalSignal = intervalController.signal
120
+
121
+ return new Promise<void>((resolve, reject) => {
122
+ let timer: NodeJS.Timeout | undefined
123
+
124
+ const run = async () => {
125
+ // Cloning the signal for this particular run to prevent memory leaks
126
+ const runController = boundAbortController(intervalSignal)
127
+ const runSignal = runController.signal
128
+
129
+ try {
130
+ await fn(runSignal)
131
+ } catch (err) {
132
+ if (err != null && isCausedBySignal(err, runSignal)) {
133
+ // Silently ignore the error if it is caused by the signal. At this
134
+ // point, the interval controller was aborted, which will cause the
135
+ // promise to resolve in the "finally" block bellow.
136
+ } else {
137
+ // Invalid behavior: stop the interval and reject the promise.
138
+ const error = new TypeError('Unexpected error', { cause: err })
139
+
140
+ // Rejecting here will make `resolve()` in the "finally" block to be a
141
+ // no-op. Rejecting before aborting the controller to ensure the
142
+ // promise does not get resolved by the `abort` event listeners.
143
+ reject(error)
144
+
145
+ // Using `error` as abort reason to avoid creating an AbortError.
146
+ intervalController.abort(error)
147
+ }
148
+ } finally {
149
+ // Cleanup the listeners added by `boundAbortController`
150
+ runController.abort()
151
+
152
+ if (intervalSignal.aborted) resolve()
153
+ else schedule()
154
+ }
155
+ }
156
+
157
+ const schedule = () => {
158
+ assert(timer === undefined, 'unexpected state')
159
+ timer = setTimeout(() => {
160
+ timer = undefined // "running" state
161
+ void run()
162
+ }, interval)
163
+ }
164
+
165
+ inputSignal.addEventListener(
166
+ 'abort',
167
+ // This function will only be called if the `inputSignal` is aborted
168
+ // before the interval controller is aborted.
169
+ () => {
170
+ // Stop the interval, using the input signal's reason
171
+ intervalController.abort(inputSignal.reason)
172
+
173
+ if (timer === undefined) {
174
+ // `fn` is currently running; `run`'s finally block will resolve the
175
+ // promise.
176
+ } else {
177
+ // The execution was scheduled but not started yet. Clear the timer
178
+ // and resolve the promise.
179
+ clearTimeout(timer)
180
+ resolve()
181
+ }
182
+ },
183
+ // Remove the listener whenever the interval is aborted.
184
+ { signal: intervalSignal },
185
+ )
186
+
187
+ if (runImmediately) void run()
188
+ else schedule()
189
+ })
190
+ }
191
+
192
+ /**
193
+ * Determines whether the cause of an error is a signal's reason
194
+ */
195
+ export function isCausedBySignal(err: unknown, signal: AbortSignal) {
196
+ if (!signal.aborted) return false
197
+ if (signal.reason == null) return false // Ignore nullish reasons
198
+ return (
199
+ err === signal.reason ||
200
+ (err instanceof Error && err.cause === signal.reason)
201
+ )
202
+ }
203
+
204
+ /**
205
+ * Creates an AbortController that will be aborted when any of the given signals
206
+ * is aborted.
207
+ *
208
+ * @note Make sure to call `abortController.abort()` when you are done with
209
+ * the controller to avoid memory leaks.
210
+ *
211
+ * @throws if any of the input signals is already aborted.
212
+ */
213
+ export function boundAbortController(
214
+ ...signals: readonly (AbortSignal | undefined | null)[]
215
+ ): AbortController {
216
+ for (const signal of signals) {
217
+ signal?.throwIfAborted()
218
+ }
219
+
220
+ const abortController = new AbortController()
221
+ const abort = function (event: Event) {
222
+ abortController.abort((event.target as AbortSignal)?.reason)
223
+ }
224
+
225
+ for (const signal of signals) {
226
+ signal?.addEventListener('abort', abort, { signal: abortController.signal })
227
+ }
228
+
229
+ return abortController
230
+ }
@@ -28,6 +28,9 @@ Object {
28
28
  ],
29
29
  "moderation": Object {
30
30
  "subjectStatus": Object {
31
+ "accountStats": Object {
32
+ "$type": "tools.ozone.moderation.defs#accountStats",
33
+ },
31
34
  "createdAt": "1970-01-01T00:00:00.000Z",
32
35
  "hosting": Object {
33
36
  "$type": "tools.ozone.moderation.defs#recordHosting",
@@ -37,6 +40,17 @@ Object {
37
40
  "lastReportedAt": "1970-01-01T00:00:00.000Z",
38
41
  "lastReviewedAt": "1970-01-01T00:00:00.000Z",
39
42
  "lastReviewedBy": "user(1)",
43
+ "recordsStats": Object {
44
+ "$type": "tools.ozone.moderation.defs#recordStats",
45
+ "appealedCount": 0,
46
+ "escalatedCount": 0,
47
+ "pendingCount": 0,
48
+ "processedCount": 1,
49
+ "reportedCount": 1,
50
+ "subjectCount": 1,
51
+ "takendownCount": 1,
52
+ "totalReports": 2,
53
+ },
40
54
  "reviewState": "tools.ozone.moderation.defs#reviewClosed",
41
55
  "subject": Object {
42
56
  "$type": "com.atproto.repo.strongRef",
@@ -134,6 +148,9 @@ Object {
134
148
  ],
135
149
  "moderation": Object {
136
150
  "subjectStatus": Object {
151
+ "accountStats": Object {
152
+ "$type": "tools.ozone.moderation.defs#accountStats",
153
+ },
137
154
  "createdAt": "1970-01-01T00:00:00.000Z",
138
155
  "hosting": Object {
139
156
  "$type": "tools.ozone.moderation.defs#recordHosting",
@@ -143,6 +160,17 @@ Object {
143
160
  "lastReportedAt": "1970-01-01T00:00:00.000Z",
144
161
  "lastReviewedAt": "1970-01-01T00:00:00.000Z",
145
162
  "lastReviewedBy": "user(1)",
163
+ "recordsStats": Object {
164
+ "$type": "tools.ozone.moderation.defs#recordStats",
165
+ "appealedCount": 0,
166
+ "escalatedCount": 0,
167
+ "pendingCount": 0,
168
+ "processedCount": 1,
169
+ "reportedCount": 1,
170
+ "subjectCount": 1,
171
+ "takendownCount": 1,
172
+ "totalReports": 2,
173
+ },
146
174
  "reviewState": "tools.ozone.moderation.defs#reviewClosed",
147
175
  "subject": Object {
148
176
  "$type": "com.atproto.repo.strongRef",
@@ -31,6 +31,9 @@ Object {
31
31
  ],
32
32
  "moderation": Object {
33
33
  "subjectStatus": Object {
34
+ "accountStats": Object {
35
+ "$type": "tools.ozone.moderation.defs#accountStats",
36
+ },
34
37
  "createdAt": "1970-01-01T00:00:00.000Z",
35
38
  "hosting": Object {
36
39
  "$type": "tools.ozone.moderation.defs#recordHosting",
@@ -40,6 +43,17 @@ Object {
40
43
  "lastReportedAt": "1970-01-01T00:00:00.000Z",
41
44
  "lastReviewedAt": "1970-01-01T00:00:00.000Z",
42
45
  "lastReviewedBy": "user(1)",
46
+ "recordsStats": Object {
47
+ "$type": "tools.ozone.moderation.defs#recordStats",
48
+ "appealedCount": 0,
49
+ "escalatedCount": 0,
50
+ "pendingCount": 0,
51
+ "processedCount": 1,
52
+ "reportedCount": 1,
53
+ "subjectCount": 1,
54
+ "takendownCount": 1,
55
+ "totalReports": 2,
56
+ },
43
57
  "reviewState": "tools.ozone.moderation.defs#reviewClosed",
44
58
  "subject": Object {
45
59
  "$type": "com.atproto.repo.strongRef",
@@ -22,6 +22,14 @@ Object {
22
22
  ],
23
23
  "moderation": Object {
24
24
  "subjectStatus": Object {
25
+ "accountStats": Object {
26
+ "$type": "tools.ozone.moderation.defs#accountStats",
27
+ "appealCount": 0,
28
+ "escalateCount": 0,
29
+ "reportCount": 2,
30
+ "suspendCount": 0,
31
+ "takedownCount": 1,
32
+ },
25
33
  "createdAt": "1970-01-01T00:00:00.000Z",
26
34
  "hosting": Object {
27
35
  "$type": "tools.ozone.moderation.defs#accountHosting",
@@ -31,6 +39,9 @@ Object {
31
39
  "lastReportedAt": "1970-01-01T00:00:00.000Z",
32
40
  "lastReviewedAt": "1970-01-01T00:00:00.000Z",
33
41
  "lastReviewedBy": "user(1)",
42
+ "recordsStats": Object {
43
+ "$type": "tools.ozone.moderation.defs#recordStats",
44
+ },
34
45
  "reviewState": "tools.ozone.moderation.defs#reviewClosed",
35
46
  "subject": Object {
36
47
  "$type": "com.atproto.admin.defs#repoRef",
@@ -25,6 +25,14 @@ Object {
25
25
  ],
26
26
  "moderation": Object {
27
27
  "subjectStatus": Object {
28
+ "accountStats": Object {
29
+ "$type": "tools.ozone.moderation.defs#accountStats",
30
+ "appealCount": 0,
31
+ "escalateCount": 0,
32
+ "reportCount": 2,
33
+ "suspendCount": 0,
34
+ "takedownCount": 1,
35
+ },
28
36
  "createdAt": "1970-01-01T00:00:00.000Z",
29
37
  "hosting": Object {
30
38
  "$type": "tools.ozone.moderation.defs#accountHosting",
@@ -34,6 +42,9 @@ Object {
34
42
  "lastReportedAt": "1970-01-01T00:00:00.000Z",
35
43
  "lastReviewedAt": "1970-01-01T00:00:00.000Z",
36
44
  "lastReviewedBy": "user(1)",
45
+ "recordsStats": Object {
46
+ "$type": "tools.ozone.moderation.defs#recordStats",
47
+ },
37
48
  "reviewState": "tools.ozone.moderation.defs#reviewClosed",
38
49
  "subject": Object {
39
50
  "$type": "com.atproto.admin.defs#repoRef",
@@ -18,6 +18,14 @@ Object {
18
18
  "indexedAt": "1970-01-01T00:00:00.000Z",
19
19
  "moderation": Object {
20
20
  "subjectStatus": Object {
21
+ "accountStats": Object {
22
+ "$type": "tools.ozone.moderation.defs#accountStats",
23
+ "appealCount": 0,
24
+ "escalateCount": 1,
25
+ "reportCount": 4,
26
+ "suspendCount": 0,
27
+ "takedownCount": 0,
28
+ },
21
29
  "createdAt": "1970-01-01T00:00:00.000Z",
22
30
  "hosting": Object {
23
31
  "$type": "tools.ozone.moderation.defs#accountHosting",
@@ -27,6 +35,17 @@ Object {
27
35
  "lastReportedAt": "1970-01-01T00:00:00.000Z",
28
36
  "lastReviewedAt": "1970-01-01T00:00:00.000Z",
29
37
  "lastReviewedBy": "user(1)",
38
+ "recordsStats": Object {
39
+ "$type": "tools.ozone.moderation.defs#recordStats",
40
+ "appealedCount": 0,
41
+ "escalatedCount": 0,
42
+ "pendingCount": 2,
43
+ "processedCount": 0,
44
+ "reportedCount": 2,
45
+ "subjectCount": 2,
46
+ "takendownCount": 0,
47
+ "totalReports": 3,
48
+ },
30
49
  "reviewState": "tools.ozone.moderation.defs#reviewEscalated",
31
50
  "subject": Object {
32
51
  "$type": "com.atproto.admin.defs#repoRef",
@@ -3,6 +3,14 @@
3
3
  exports[`moderation-statuses query statuses returns statuses filtered by subject language 1`] = `
4
4
  Array [
5
5
  Object {
6
+ "accountStats": Object {
7
+ "$type": "tools.ozone.moderation.defs#accountStats",
8
+ "appealCount": 0,
9
+ "escalateCount": 0,
10
+ "reportCount": 2,
11
+ "suspendCount": 0,
12
+ "takedownCount": 0,
13
+ },
6
14
  "createdAt": "1970-01-01T00:00:00.000Z",
7
15
  "hosting": Object {
8
16
  "$type": "tools.ozone.moderation.defs#recordHosting",
@@ -10,6 +18,17 @@ Array [
10
18
  },
11
19
  "id": 7,
12
20
  "lastReportedAt": "1970-01-01T00:00:00.000Z",
21
+ "recordsStats": Object {
22
+ "$type": "tools.ozone.moderation.defs#recordStats",
23
+ "appealedCount": 0,
24
+ "escalatedCount": 0,
25
+ "pendingCount": 1,
26
+ "processedCount": 0,
27
+ "reportedCount": 1,
28
+ "subjectCount": 1,
29
+ "takendownCount": 0,
30
+ "totalReports": 2,
31
+ },
13
32
  "reviewState": "tools.ozone.moderation.defs#reviewOpen",
14
33
  "subject": Object {
15
34
  "$type": "com.atproto.repo.strongRef",
@@ -27,6 +46,14 @@ Array [
27
46
  "updatedAt": "1970-01-01T00:00:00.000Z",
28
47
  },
29
48
  Object {
49
+ "accountStats": Object {
50
+ "$type": "tools.ozone.moderation.defs#accountStats",
51
+ "appealCount": 0,
52
+ "escalateCount": 0,
53
+ "reportCount": 2,
54
+ "suspendCount": 0,
55
+ "takedownCount": 0,
56
+ },
30
57
  "createdAt": "1970-01-01T00:00:00.000Z",
31
58
  "hosting": Object {
32
59
  "$type": "tools.ozone.moderation.defs#accountHosting",
@@ -34,6 +61,17 @@ Array [
34
61
  },
35
62
  "id": 5,
36
63
  "lastReportedAt": "1970-01-01T00:00:00.000Z",
64
+ "recordsStats": Object {
65
+ "$type": "tools.ozone.moderation.defs#recordStats",
66
+ "appealedCount": 0,
67
+ "escalatedCount": 0,
68
+ "pendingCount": 1,
69
+ "processedCount": 0,
70
+ "reportedCount": 1,
71
+ "subjectCount": 1,
72
+ "takendownCount": 0,
73
+ "totalReports": 2,
74
+ },
37
75
  "reviewState": "tools.ozone.moderation.defs#reviewOpen",
38
76
  "subject": Object {
39
77
  "$type": "com.atproto.admin.defs#repoRef",
@@ -55,6 +93,14 @@ Array [
55
93
  exports[`moderation-statuses query statuses returns statuses for subjects that received moderation events 1`] = `
56
94
  Array [
57
95
  Object {
96
+ "accountStats": Object {
97
+ "$type": "tools.ozone.moderation.defs#accountStats",
98
+ "appealCount": 0,
99
+ "escalateCount": 0,
100
+ "reportCount": 2,
101
+ "suspendCount": 0,
102
+ "takedownCount": 0,
103
+ },
58
104
  "createdAt": "1970-01-01T00:00:00.000Z",
59
105
  "hosting": Object {
60
106
  "$type": "tools.ozone.moderation.defs#recordHosting",
@@ -62,6 +108,17 @@ Array [
62
108
  },
63
109
  "id": 7,
64
110
  "lastReportedAt": "1970-01-01T00:00:00.000Z",
111
+ "recordsStats": Object {
112
+ "$type": "tools.ozone.moderation.defs#recordStats",
113
+ "appealedCount": 0,
114
+ "escalatedCount": 0,
115
+ "pendingCount": 1,
116
+ "processedCount": 0,
117
+ "reportedCount": 1,
118
+ "subjectCount": 1,
119
+ "takendownCount": 0,
120
+ "totalReports": 2,
121
+ },
65
122
  "reviewState": "tools.ozone.moderation.defs#reviewOpen",
66
123
  "subject": Object {
67
124
  "$type": "com.atproto.repo.strongRef",
@@ -79,6 +136,14 @@ Array [
79
136
  "updatedAt": "1970-01-01T00:00:00.000Z",
80
137
  },
81
138
  Object {
139
+ "accountStats": Object {
140
+ "$type": "tools.ozone.moderation.defs#accountStats",
141
+ "appealCount": 0,
142
+ "escalateCount": 0,
143
+ "reportCount": 2,
144
+ "suspendCount": 0,
145
+ "takedownCount": 0,
146
+ },
82
147
  "createdAt": "1970-01-01T00:00:00.000Z",
83
148
  "hosting": Object {
84
149
  "$type": "tools.ozone.moderation.defs#accountHosting",
@@ -86,6 +151,17 @@ Array [
86
151
  },
87
152
  "id": 5,
88
153
  "lastReportedAt": "1970-01-01T00:00:00.000Z",
154
+ "recordsStats": Object {
155
+ "$type": "tools.ozone.moderation.defs#recordStats",
156
+ "appealedCount": 0,
157
+ "escalatedCount": 0,
158
+ "pendingCount": 1,
159
+ "processedCount": 0,
160
+ "reportedCount": 1,
161
+ "subjectCount": 1,
162
+ "takendownCount": 0,
163
+ "totalReports": 2,
164
+ },
89
165
  "reviewState": "tools.ozone.moderation.defs#reviewOpen",
90
166
  "subject": Object {
91
167
  "$type": "com.atproto.admin.defs#repoRef",
@@ -102,6 +178,14 @@ Array [
102
178
  "updatedAt": "1970-01-01T00:00:00.000Z",
103
179
  },
104
180
  Object {
181
+ "accountStats": Object {
182
+ "$type": "tools.ozone.moderation.defs#accountStats",
183
+ "appealCount": 0,
184
+ "escalateCount": 0,
185
+ "reportCount": 2,
186
+ "suspendCount": 0,
187
+ "takedownCount": 0,
188
+ },
105
189
  "createdAt": "1970-01-01T00:00:00.000Z",
106
190
  "hosting": Object {
107
191
  "$type": "tools.ozone.moderation.defs#recordHosting",
@@ -109,6 +193,17 @@ Array [
109
193
  },
110
194
  "id": 3,
111
195
  "lastReportedAt": "1970-01-01T00:00:00.000Z",
196
+ "recordsStats": Object {
197
+ "$type": "tools.ozone.moderation.defs#recordStats",
198
+ "appealedCount": 0,
199
+ "escalatedCount": 0,
200
+ "pendingCount": 1,
201
+ "processedCount": 0,
202
+ "reportedCount": 1,
203
+ "subjectCount": 1,
204
+ "takendownCount": 0,
205
+ "totalReports": 2,
206
+ },
112
207
  "reviewState": "tools.ozone.moderation.defs#reviewOpen",
113
208
  "subject": Object {
114
209
  "$type": "com.atproto.repo.strongRef",
@@ -125,6 +220,14 @@ Array [
125
220
  "updatedAt": "1970-01-01T00:00:00.000Z",
126
221
  },
127
222
  Object {
223
+ "accountStats": Object {
224
+ "$type": "tools.ozone.moderation.defs#accountStats",
225
+ "appealCount": 0,
226
+ "escalateCount": 0,
227
+ "reportCount": 2,
228
+ "suspendCount": 0,
229
+ "takedownCount": 0,
230
+ },
128
231
  "createdAt": "1970-01-01T00:00:00.000Z",
129
232
  "hosting": Object {
130
233
  "$type": "tools.ozone.moderation.defs#accountHosting",
@@ -132,6 +235,17 @@ Array [
132
235
  },
133
236
  "id": 1,
134
237
  "lastReportedAt": "1970-01-01T00:00:00.000Z",
238
+ "recordsStats": Object {
239
+ "$type": "tools.ozone.moderation.defs#recordStats",
240
+ "appealedCount": 0,
241
+ "escalatedCount": 0,
242
+ "pendingCount": 1,
243
+ "processedCount": 0,
244
+ "reportedCount": 1,
245
+ "subjectCount": 1,
246
+ "takendownCount": 0,
247
+ "totalReports": 2,
248
+ },
135
249
  "reviewState": "tools.ozone.moderation.defs#reviewOpen",
136
250
  "subject": Object {
137
251
  "$type": "com.atproto.admin.defs#repoRef",