@atproto/ozone 0.1.69 → 0.1.71
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 +16 -0
- package/dist/api/moderation/queryStatuses.d.ts.map +1 -1
- package/dist/api/moderation/queryStatuses.js +1 -33
- package/dist/api/moderation/queryStatuses.js.map +1 -1
- package/dist/background.d.ts +49 -6
- package/dist/background.d.ts.map +1 -1
- package/dist/background.js +149 -14
- package/dist/background.js.map +1 -1
- package/dist/config/config.d.ts +1 -0
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +1 -0
- package/dist/config/config.js.map +1 -1
- package/dist/config/env.d.ts +1 -0
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +1 -0
- package/dist/config/env.js.map +1 -1
- package/dist/daemon/context.d.ts +9 -3
- package/dist/daemon/context.d.ts.map +1 -1
- package/dist/daemon/context.js +33 -3
- package/dist/daemon/context.js.map +1 -1
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +3 -6
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/materialized-view-refresher.d.ts +5 -0
- package/dist/daemon/materialized-view-refresher.d.ts.map +1 -0
- package/dist/daemon/materialized-view-refresher.js +29 -0
- package/dist/daemon/materialized-view-refresher.js.map +1 -0
- package/dist/db/migrations/20241220T144630860Z-stats-materialized-views.d.ts +5 -0
- package/dist/db/migrations/20241220T144630860Z-stats-materialized-views.d.ts.map +1 -0
- package/dist/db/migrations/20241220T144630860Z-stats-materialized-views.js +158 -0
- package/dist/db/migrations/20241220T144630860Z-stats-materialized-views.js.map +1 -0
- package/dist/db/migrations/index.d.ts +1 -0
- package/dist/db/migrations/index.d.ts.map +1 -1
- package/dist/db/migrations/index.js +2 -1
- package/dist/db/migrations/index.js.map +1 -1
- package/dist/db/schema/account_events_stats.d.ts +15 -0
- package/dist/db/schema/account_events_stats.d.ts.map +1 -0
- package/dist/db/schema/account_events_stats.js +5 -0
- package/dist/db/schema/account_events_stats.js.map +1 -0
- package/dist/db/schema/account_record_events_stats.d.ts +15 -0
- package/dist/db/schema/account_record_events_stats.d.ts.map +1 -0
- package/dist/db/schema/account_record_events_stats.js +5 -0
- package/dist/db/schema/account_record_events_stats.js.map +1 -0
- package/dist/db/schema/account_record_status_stats.d.ts +15 -0
- package/dist/db/schema/account_record_status_stats.d.ts.map +1 -0
- package/dist/db/schema/account_record_status_stats.js +5 -0
- package/dist/db/schema/account_record_status_stats.js.map +1 -0
- package/dist/db/schema/index.d.ts +5 -1
- package/dist/db/schema/index.d.ts.map +1 -1
- package/dist/db/schema/record_events_stats.d.ts +14 -0
- package/dist/db/schema/record_events_stats.d.ts.map +1 -0
- package/dist/db/schema/record_events_stats.js +5 -0
- package/dist/db/schema/record_events_stats.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -4
- package/dist/index.js.map +1 -1
- package/dist/lexicon/lexicons.d.ts +174 -2
- package/dist/lexicon/lexicons.d.ts.map +1 -1
- package/dist/lexicon/lexicons.js +92 -1
- package/dist/lexicon/lexicons.js.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts +40 -0
- package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/defs.js +20 -0
- package/dist/lexicon/types/tools/ozone/moderation/defs.js.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.d.ts +7 -1
- package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.d.ts.map +1 -1
- package/dist/mod-service/index.d.ts +4 -62
- package/dist/mod-service/index.d.ts.map +1 -1
- package/dist/mod-service/index.js +80 -74
- package/dist/mod-service/index.js.map +1 -1
- package/dist/mod-service/status.d.ts +115 -4
- package/dist/mod-service/status.d.ts.map +1 -1
- package/dist/mod-service/status.js +51 -34
- package/dist/mod-service/status.js.map +1 -1
- package/dist/mod-service/types.d.ts +16 -1
- package/dist/mod-service/types.d.ts.map +1 -1
- package/dist/mod-service/views.d.ts.map +1 -1
- package/dist/mod-service/views.js +49 -41
- package/dist/mod-service/views.js.map +1 -1
- package/dist/util.d.ts +34 -0
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +132 -0
- package/dist/util.js.map +1 -1
- package/package.json +5 -5
- package/src/api/moderation/queryStatuses.ts +1 -63
- package/src/background.ts +140 -14
- package/src/config/config.ts +2 -0
- package/src/config/env.ts +4 -0
- package/src/daemon/context.ts +43 -5
- package/src/daemon/index.ts +3 -6
- package/src/daemon/materialized-view-refresher.ts +27 -0
- package/src/db/migrations/20241220T144630860Z-stats-materialized-views.ts +218 -0
- package/src/db/migrations/index.ts +1 -0
- package/src/db/schema/account_events_stats.ts +16 -0
- package/src/db/schema/account_record_events_stats.ts +15 -0
- package/src/db/schema/account_record_status_stats.ts +15 -0
- package/src/db/schema/index.ts +10 -1
- package/src/db/schema/record_events_stats.ts +15 -0
- package/src/index.ts +1 -7
- package/src/lexicon/lexicons.ts +100 -1
- package/src/lexicon/types/tools/ozone/moderation/defs.ts +62 -0
- package/src/lexicon/types/tools/ozone/moderation/queryStatuses.ts +11 -1
- package/src/mod-service/index.ts +181 -118
- package/src/mod-service/status.ts +55 -28
- package/src/mod-service/types.ts +22 -1
- package/src/mod-service/views.ts +64 -50
- package/src/util.ts +145 -0
- package/tests/__snapshots__/get-record.test.ts.snap +28 -0
- package/tests/__snapshots__/get-records.test.ts.snap +14 -0
- package/tests/__snapshots__/get-repo.test.ts.snap +11 -0
- package/tests/__snapshots__/get-repos.test.ts.snap +11 -0
- package/tests/__snapshots__/moderation-events.test.ts.snap +19 -0
- package/tests/__snapshots__/moderation-statuses.test.ts.snap +114 -0
- package/tests/get-record.test.ts +4 -0
- package/tests/get-records.test.ts +4 -0
- package/tests/get-repo.test.ts +4 -0
- package/tests/get-repos.test.ts +4 -0
- package/tests/moderation-events.test.ts +4 -0
- package/tests/moderation-statuses.test.ts +4 -0
- package/tests/query-labels.test.ts +1 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
// This may require better organization but for now, just dumping functions here containing DB queries for moderation status
|
|
2
2
|
|
|
3
|
+
import { HOUR } from '@atproto/common'
|
|
3
4
|
import { AtUri } from '@atproto/syntax'
|
|
4
5
|
import { Database } from '../db'
|
|
5
|
-
import
|
|
6
|
+
import DatabaseSchema from '../db/schema'
|
|
7
|
+
import { jsonb } from '../db/types'
|
|
8
|
+
import { REASONAPPEAL } from '../lexicon/types/com/atproto/moderation/defs'
|
|
6
9
|
import {
|
|
7
|
-
REVIEWOPEN,
|
|
8
10
|
REVIEWCLOSED,
|
|
9
11
|
REVIEWESCALATED,
|
|
10
12
|
REVIEWNONE,
|
|
13
|
+
REVIEWOPEN,
|
|
11
14
|
} from '../lexicon/types/tools/ozone/moderation/defs'
|
|
12
15
|
import { ModerationEventRow, ModerationSubjectStatusRow } from './types'
|
|
13
|
-
import { HOUR } from '@atproto/common'
|
|
14
|
-
import { REASONAPPEAL } from '../lexicon/types/com/atproto/moderation/defs'
|
|
15
|
-
import { jsonb } from '../db/types'
|
|
16
16
|
|
|
17
17
|
const getSubjectStatusForModerationEvent = ({
|
|
18
18
|
currentStatus,
|
|
@@ -203,6 +203,56 @@ const getSubjectStatusForRecordEvent = ({
|
|
|
203
203
|
return {}
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
export const moderationSubjectStatusQueryBuilder = (db: DatabaseSchema) => {
|
|
207
|
+
// @NOTE: Using select() instead of selectAll() below because the materialized
|
|
208
|
+
// views might be incomplete, and we don't want the null `did` columns to
|
|
209
|
+
// interfere with the (never null) `did` column from the
|
|
210
|
+
// `moderation_subject_status` table in the results
|
|
211
|
+
return db
|
|
212
|
+
.selectFrom('moderation_subject_status')
|
|
213
|
+
.selectAll('moderation_subject_status')
|
|
214
|
+
.leftJoin('account_events_stats', (join) =>
|
|
215
|
+
join.onRef(
|
|
216
|
+
'moderation_subject_status.did',
|
|
217
|
+
'=',
|
|
218
|
+
'account_events_stats.subjectDid',
|
|
219
|
+
),
|
|
220
|
+
)
|
|
221
|
+
.select([
|
|
222
|
+
'account_events_stats.takedownCount',
|
|
223
|
+
'account_events_stats.suspendCount',
|
|
224
|
+
'account_events_stats.escalateCount',
|
|
225
|
+
'account_events_stats.reportCount',
|
|
226
|
+
'account_events_stats.appealCount',
|
|
227
|
+
])
|
|
228
|
+
.leftJoin('account_record_events_stats', (join) =>
|
|
229
|
+
join.onRef(
|
|
230
|
+
'moderation_subject_status.did',
|
|
231
|
+
'=',
|
|
232
|
+
'account_record_events_stats.subjectDid',
|
|
233
|
+
),
|
|
234
|
+
)
|
|
235
|
+
.select([
|
|
236
|
+
'account_record_events_stats.totalReports',
|
|
237
|
+
'account_record_events_stats.reportedCount',
|
|
238
|
+
'account_record_events_stats.escalatedCount',
|
|
239
|
+
'account_record_events_stats.appealedCount',
|
|
240
|
+
])
|
|
241
|
+
.leftJoin('account_record_status_stats', (join) =>
|
|
242
|
+
join.onRef(
|
|
243
|
+
'moderation_subject_status.did',
|
|
244
|
+
'=',
|
|
245
|
+
'account_record_status_stats.did',
|
|
246
|
+
),
|
|
247
|
+
)
|
|
248
|
+
.select([
|
|
249
|
+
'account_record_status_stats.subjectCount',
|
|
250
|
+
'account_record_status_stats.pendingCount',
|
|
251
|
+
'account_record_status_stats.processedCount',
|
|
252
|
+
'account_record_status_stats.takendownCount',
|
|
253
|
+
])
|
|
254
|
+
}
|
|
255
|
+
|
|
206
256
|
// Based on a given moderation action event, this function will update the moderation status of the subject
|
|
207
257
|
// If there's no existing status, it will create one
|
|
208
258
|
// If the action event does not affect the status, it will do nothing
|
|
@@ -393,29 +443,6 @@ export const adjustModerationSubjectStatus = async (
|
|
|
393
443
|
return status || null
|
|
394
444
|
}
|
|
395
445
|
|
|
396
|
-
type ModerationSubjectStatusFilter =
|
|
397
|
-
| Pick<ModerationSubjectStatus, 'did'>
|
|
398
|
-
| Pick<ModerationSubjectStatus, 'did' | 'recordPath'>
|
|
399
|
-
| Pick<ModerationSubjectStatus, 'did' | 'recordPath' | 'recordCid'>
|
|
400
|
-
export const getModerationSubjectStatus = async (
|
|
401
|
-
db: Database,
|
|
402
|
-
filters: ModerationSubjectStatusFilter,
|
|
403
|
-
) => {
|
|
404
|
-
let builder = db.db
|
|
405
|
-
.selectFrom('moderation_subject_status')
|
|
406
|
-
// DID will always be passed at the very least
|
|
407
|
-
.where('did', '=', filters.did)
|
|
408
|
-
.where('recordPath', '=', 'recordPath' in filters ? filters.recordPath : '')
|
|
409
|
-
|
|
410
|
-
if ('recordCid' in filters) {
|
|
411
|
-
builder = builder.where('recordCid', '=', filters.recordCid)
|
|
412
|
-
} else {
|
|
413
|
-
builder = builder.where('recordCid', 'is', null)
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
return builder.executeTakeFirst()
|
|
417
|
-
}
|
|
418
|
-
|
|
419
446
|
export const getStatusIdentifierFromSubject = (
|
|
420
447
|
subject: string | AtUri,
|
|
421
448
|
): { did: string; recordPath: string } => {
|
package/src/mod-service/types.ts
CHANGED
|
@@ -18,8 +18,29 @@ export type ModerationEventRowWithHandle = ModerationEventRow & {
|
|
|
18
18
|
creatorHandle?: string | null
|
|
19
19
|
}
|
|
20
20
|
export type ModerationSubjectStatusRow = Selectable<ModerationSubjectStatus>
|
|
21
|
+
export type ModerationSubjectStatusRowWithStats = ModerationSubjectStatusRow & {
|
|
22
|
+
// account_events_stats
|
|
23
|
+
takedownCount: number | null
|
|
24
|
+
suspendCount: number | null
|
|
25
|
+
escalateCount: number | null
|
|
26
|
+
reportCount: number | null
|
|
27
|
+
appealCount: number | null
|
|
28
|
+
|
|
29
|
+
// account_record_events_stats
|
|
30
|
+
totalReports: number | null
|
|
31
|
+
reportedCount: number | null
|
|
32
|
+
escalatedCount: number | null
|
|
33
|
+
appealedCount: number | null
|
|
34
|
+
|
|
35
|
+
// account_record_status_stats
|
|
36
|
+
subjectCount: number | null
|
|
37
|
+
pendingCount: number | null
|
|
38
|
+
processedCount: number | null
|
|
39
|
+
takendownCount: number | null
|
|
40
|
+
}
|
|
41
|
+
|
|
21
42
|
export type ModerationSubjectStatusRowWithHandle =
|
|
22
|
-
|
|
43
|
+
ModerationSubjectStatusRowWithStats & { handle: string | null }
|
|
23
44
|
|
|
24
45
|
export type ModEventType =
|
|
25
46
|
| ToolsOzoneModerationDefs.ModEventTakedown
|
package/src/mod-service/views.ts
CHANGED
|
@@ -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
|
|
485
|
-
.
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
labels.
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
|
|
582
|
-
|
|
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
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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",
|