@atproto/ozone 0.1.69 → 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.
- package/CHANGELOG.md +9 -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 +3 -3
- 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
package/src/lexicon/lexicons.ts
CHANGED
|
@@ -11322,6 +11322,85 @@ export const schemaDict = {
|
|
|
11322
11322
|
type: 'string',
|
|
11323
11323
|
},
|
|
11324
11324
|
},
|
|
11325
|
+
accountStats: {
|
|
11326
|
+
description: 'Statistics related to the account subject',
|
|
11327
|
+
type: 'ref',
|
|
11328
|
+
ref: 'lex:tools.ozone.moderation.defs#accountStats',
|
|
11329
|
+
},
|
|
11330
|
+
recordsStats: {
|
|
11331
|
+
description:
|
|
11332
|
+
"Statistics related to the record subjects authored by the subject's account",
|
|
11333
|
+
type: 'ref',
|
|
11334
|
+
ref: 'lex:tools.ozone.moderation.defs#recordsStats',
|
|
11335
|
+
},
|
|
11336
|
+
},
|
|
11337
|
+
},
|
|
11338
|
+
accountStats: {
|
|
11339
|
+
description: 'Statistics about a particular account subject',
|
|
11340
|
+
type: 'object',
|
|
11341
|
+
properties: {
|
|
11342
|
+
reportCount: {
|
|
11343
|
+
description: 'Total number of reports on the account',
|
|
11344
|
+
type: 'integer',
|
|
11345
|
+
},
|
|
11346
|
+
appealCount: {
|
|
11347
|
+
description:
|
|
11348
|
+
'Total number of appeals against a moderation action on the account',
|
|
11349
|
+
type: 'integer',
|
|
11350
|
+
},
|
|
11351
|
+
suspendCount: {
|
|
11352
|
+
description: 'Number of times the account was suspended',
|
|
11353
|
+
type: 'integer',
|
|
11354
|
+
},
|
|
11355
|
+
escalateCount: {
|
|
11356
|
+
description: 'Number of times the account was escalated',
|
|
11357
|
+
type: 'integer',
|
|
11358
|
+
},
|
|
11359
|
+
takedownCount: {
|
|
11360
|
+
description: 'Number of times the account was taken down',
|
|
11361
|
+
type: 'integer',
|
|
11362
|
+
},
|
|
11363
|
+
},
|
|
11364
|
+
},
|
|
11365
|
+
recordsStats: {
|
|
11366
|
+
description: 'Statistics about a set of record subject items',
|
|
11367
|
+
type: 'object',
|
|
11368
|
+
properties: {
|
|
11369
|
+
totalReports: {
|
|
11370
|
+
description:
|
|
11371
|
+
'Cumulative sum of the number of reports on the items in the set',
|
|
11372
|
+
type: 'integer',
|
|
11373
|
+
},
|
|
11374
|
+
reportedCount: {
|
|
11375
|
+
description: 'Number of items that were reported at least once',
|
|
11376
|
+
type: 'integer',
|
|
11377
|
+
},
|
|
11378
|
+
escalatedCount: {
|
|
11379
|
+
description: 'Number of items that were escalated at least once',
|
|
11380
|
+
type: 'integer',
|
|
11381
|
+
},
|
|
11382
|
+
appealedCount: {
|
|
11383
|
+
description: 'Number of items that were appealed at least once',
|
|
11384
|
+
type: 'integer',
|
|
11385
|
+
},
|
|
11386
|
+
subjectCount: {
|
|
11387
|
+
description: 'Total number of item in the set',
|
|
11388
|
+
type: 'integer',
|
|
11389
|
+
},
|
|
11390
|
+
pendingCount: {
|
|
11391
|
+
description:
|
|
11392
|
+
'Number of item currently in "reviewOpen" or "reviewEscalated" state',
|
|
11393
|
+
type: 'integer',
|
|
11394
|
+
},
|
|
11395
|
+
processedCount: {
|
|
11396
|
+
description:
|
|
11397
|
+
'Number of item currently in "reviewNone" or "reviewClosed" state',
|
|
11398
|
+
type: 'integer',
|
|
11399
|
+
},
|
|
11400
|
+
takendownCount: {
|
|
11401
|
+
description: 'Number of item currently taken down',
|
|
11402
|
+
type: 'integer',
|
|
11403
|
+
},
|
|
11325
11404
|
},
|
|
11326
11405
|
},
|
|
11327
11406
|
subjectReviewState: {
|
|
@@ -12570,7 +12649,12 @@ export const schemaDict = {
|
|
|
12570
12649
|
sortField: {
|
|
12571
12650
|
type: 'string',
|
|
12572
12651
|
default: 'lastReportedAt',
|
|
12573
|
-
enum: [
|
|
12652
|
+
enum: [
|
|
12653
|
+
'lastReviewedAt',
|
|
12654
|
+
'lastReportedAt',
|
|
12655
|
+
'reportedRecordsCount',
|
|
12656
|
+
'takendownRecordsCount',
|
|
12657
|
+
],
|
|
12574
12658
|
},
|
|
12575
12659
|
sortDirection: {
|
|
12576
12660
|
type: 'string',
|
|
@@ -12625,6 +12709,21 @@ export const schemaDict = {
|
|
|
12625
12709
|
"If specified, subjects of the given type (account or record) will be returned. When this is set to 'account' the 'collections' parameter will be ignored. When includeAllUserRecords or subject is set, this will be ignored.",
|
|
12626
12710
|
knownValues: ['account', 'record'],
|
|
12627
12711
|
},
|
|
12712
|
+
minAccountSuspendCount: {
|
|
12713
|
+
type: 'integer',
|
|
12714
|
+
description:
|
|
12715
|
+
'If specified, only subjects that belong to an account that has at least this many suspensions will be returned.',
|
|
12716
|
+
},
|
|
12717
|
+
minReportedRecordsCount: {
|
|
12718
|
+
type: 'integer',
|
|
12719
|
+
description:
|
|
12720
|
+
'If specified, only subjects that belong to an account that has at least this many reported records will be returned.',
|
|
12721
|
+
},
|
|
12722
|
+
minTakendownRecordsCount: {
|
|
12723
|
+
type: 'integer',
|
|
12724
|
+
description:
|
|
12725
|
+
'If specified, only subjects that belong to an account that has at least this many taken down records will be returned.',
|
|
12726
|
+
},
|
|
12628
12727
|
},
|
|
12629
12728
|
},
|
|
12630
12729
|
output: {
|
|
@@ -136,6 +136,8 @@ export interface SubjectStatusView {
|
|
|
136
136
|
appealed?: boolean
|
|
137
137
|
suspendUntil?: string
|
|
138
138
|
tags?: string[]
|
|
139
|
+
accountStats?: AccountStats
|
|
140
|
+
recordsStats?: RecordsStats
|
|
139
141
|
[k: string]: unknown
|
|
140
142
|
}
|
|
141
143
|
|
|
@@ -151,6 +153,66 @@ export function validateSubjectStatusView(v: unknown): ValidationResult {
|
|
|
151
153
|
return lexicons.validate('tools.ozone.moderation.defs#subjectStatusView', v)
|
|
152
154
|
}
|
|
153
155
|
|
|
156
|
+
/** Statistics about a particular account subject */
|
|
157
|
+
export interface AccountStats {
|
|
158
|
+
/** Total number of reports on the account */
|
|
159
|
+
reportCount?: number
|
|
160
|
+
/** Total number of appeals against a moderation action on the account */
|
|
161
|
+
appealCount?: number
|
|
162
|
+
/** Number of times the account was suspended */
|
|
163
|
+
suspendCount?: number
|
|
164
|
+
/** Number of times the account was escalated */
|
|
165
|
+
escalateCount?: number
|
|
166
|
+
/** Number of times the account was taken down */
|
|
167
|
+
takedownCount?: number
|
|
168
|
+
[k: string]: unknown
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function isAccountStats(v: unknown): v is AccountStats {
|
|
172
|
+
return (
|
|
173
|
+
isObj(v) &&
|
|
174
|
+
hasProp(v, '$type') &&
|
|
175
|
+
v.$type === 'tools.ozone.moderation.defs#accountStats'
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function validateAccountStats(v: unknown): ValidationResult {
|
|
180
|
+
return lexicons.validate('tools.ozone.moderation.defs#accountStats', v)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Statistics about a set of record subject items */
|
|
184
|
+
export interface RecordsStats {
|
|
185
|
+
/** Cumulative sum of the number of reports on the items in the set */
|
|
186
|
+
totalReports?: number
|
|
187
|
+
/** Number of items that were reported at least once */
|
|
188
|
+
reportedCount?: number
|
|
189
|
+
/** Number of items that were escalated at least once */
|
|
190
|
+
escalatedCount?: number
|
|
191
|
+
/** Number of items that were appealed at least once */
|
|
192
|
+
appealedCount?: number
|
|
193
|
+
/** Total number of item in the set */
|
|
194
|
+
subjectCount?: number
|
|
195
|
+
/** Number of item currently in "reviewOpen" or "reviewEscalated" state */
|
|
196
|
+
pendingCount?: number
|
|
197
|
+
/** Number of item currently in "reviewNone" or "reviewClosed" state */
|
|
198
|
+
processedCount?: number
|
|
199
|
+
/** Number of item currently taken down */
|
|
200
|
+
takendownCount?: number
|
|
201
|
+
[k: string]: unknown
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function isRecordsStats(v: unknown): v is RecordsStats {
|
|
205
|
+
return (
|
|
206
|
+
isObj(v) &&
|
|
207
|
+
hasProp(v, '$type') &&
|
|
208
|
+
v.$type === 'tools.ozone.moderation.defs#recordsStats'
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function validateRecordsStats(v: unknown): ValidationResult {
|
|
213
|
+
return lexicons.validate('tools.ozone.moderation.defs#recordsStats', v)
|
|
214
|
+
}
|
|
215
|
+
|
|
154
216
|
export type SubjectReviewState =
|
|
155
217
|
| 'lex:tools.ozone.moderation.defs#reviewOpen'
|
|
156
218
|
| 'lex:tools.ozone.moderation.defs#reviewEscalated'
|
|
@@ -49,7 +49,11 @@ export interface QueryParams {
|
|
|
49
49
|
ignoreSubjects?: string[]
|
|
50
50
|
/** Get all subject statuses that were reviewed by a specific moderator */
|
|
51
51
|
lastReviewedBy?: string
|
|
52
|
-
sortField:
|
|
52
|
+
sortField:
|
|
53
|
+
| 'lastReviewedAt'
|
|
54
|
+
| 'lastReportedAt'
|
|
55
|
+
| 'reportedRecordsCount'
|
|
56
|
+
| 'takendownRecordsCount'
|
|
53
57
|
sortDirection: 'asc' | 'desc'
|
|
54
58
|
/** Get subjects that were taken down */
|
|
55
59
|
takendown?: boolean
|
|
@@ -63,6 +67,12 @@ export interface QueryParams {
|
|
|
63
67
|
collections?: string[]
|
|
64
68
|
/** If specified, subjects of the given type (account or record) will be returned. When this is set to 'account' the 'collections' parameter will be ignored. When includeAllUserRecords or subject is set, this will be ignored. */
|
|
65
69
|
subjectType?: 'account' | 'record' | (string & {})
|
|
70
|
+
/** If specified, only subjects that belong to an account that has at least this many suspensions will be returned. */
|
|
71
|
+
minAccountSuspendCount?: number
|
|
72
|
+
/** If specified, only subjects that belong to an account that has at least this many reported records will be returned. */
|
|
73
|
+
minReportedRecordsCount?: number
|
|
74
|
+
/** If specified, only subjects that belong to an account that has at least this many taken down records will be returned. */
|
|
75
|
+
minTakendownRecordsCount?: number
|
|
66
76
|
}
|
|
67
77
|
|
|
68
78
|
export type InputSchema = undefined
|
package/src/mod-service/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import net from 'node:net'
|
|
2
|
-
import { Insertable,
|
|
2
|
+
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'
|
|
@@ -29,16 +29,19 @@ import { RepoRef, RepoBlobRef } from '../lexicon/types/com/atproto/admin/defs'
|
|
|
29
29
|
import {
|
|
30
30
|
adjustModerationSubjectStatus,
|
|
31
31
|
getStatusIdentifierFromSubject,
|
|
32
|
+
moderationSubjectStatusQueryBuilder,
|
|
32
33
|
} from './status'
|
|
33
34
|
import {
|
|
34
35
|
ModEventType,
|
|
35
36
|
ModerationEventRow,
|
|
36
37
|
ModerationSubjectStatusRow,
|
|
38
|
+
ModerationSubjectStatusRowWithHandle,
|
|
37
39
|
ReversibleModerationEvent,
|
|
38
40
|
} from './types'
|
|
39
41
|
import { ModerationEvent } from '../db/schema/moderation_event'
|
|
40
42
|
import { StatusKeyset, TimeIdKeyset, paginate } from '../db/pagination'
|
|
41
43
|
import { Label } from '../lexicon/types/com/atproto/label/defs'
|
|
44
|
+
import { QueryParams as QueryStatusParams } from '../lexicon/types/tools/ozone/moderation/queryStatuses'
|
|
42
45
|
import {
|
|
43
46
|
ModSubject,
|
|
44
47
|
RecordSubject,
|
|
@@ -56,6 +59,7 @@ import { httpLogger as log } from '../logger'
|
|
|
56
59
|
import { OzoneConfig } from '../config'
|
|
57
60
|
import { LABELER_HEADER_NAME, ParsedLabelers } from '../util'
|
|
58
61
|
import { ids } from '../lexicon/lexicons'
|
|
62
|
+
import { getReviewState } from '../api/util'
|
|
59
63
|
|
|
60
64
|
export type ModerationServiceCreator = (db: Database) => ModerationService
|
|
61
65
|
|
|
@@ -805,45 +809,6 @@ export class ModerationService {
|
|
|
805
809
|
return result
|
|
806
810
|
}
|
|
807
811
|
|
|
808
|
-
applyTagFilter = (
|
|
809
|
-
builder: SelectQueryBuilder<any, any, any>,
|
|
810
|
-
tags: string[],
|
|
811
|
-
) => {
|
|
812
|
-
const { ref } = this.db.db.dynamic
|
|
813
|
-
// Build an array of conditions
|
|
814
|
-
const conditions = tags
|
|
815
|
-
.map((tag) => {
|
|
816
|
-
if (tag.includes('&&')) {
|
|
817
|
-
// Split by '&&' for AND logic
|
|
818
|
-
const subTags = tag
|
|
819
|
-
.split('&&')
|
|
820
|
-
// Make sure spaces on either sides of '&&' are trimmed
|
|
821
|
-
.map((subTag) => subTag.trim())
|
|
822
|
-
// Remove empty strings after trimming is applied
|
|
823
|
-
.filter(Boolean)
|
|
824
|
-
|
|
825
|
-
if (!subTags.length) return null
|
|
826
|
-
|
|
827
|
-
return sql`(${sql.join(
|
|
828
|
-
subTags.map(
|
|
829
|
-
(subTag) =>
|
|
830
|
-
sql`${ref('moderation_subject_status.tags')} ? ${subTag}`,
|
|
831
|
-
),
|
|
832
|
-
sql` AND `,
|
|
833
|
-
)})`
|
|
834
|
-
} else {
|
|
835
|
-
// Single tag condition
|
|
836
|
-
return sql`${ref('moderation_subject_status.tags')} ? ${tag}`
|
|
837
|
-
}
|
|
838
|
-
})
|
|
839
|
-
.filter(Boolean)
|
|
840
|
-
|
|
841
|
-
if (!conditions.length) return builder
|
|
842
|
-
|
|
843
|
-
// Combine all conditions with OR
|
|
844
|
-
return builder.where(sql`(${sql.join(conditions, sql` OR `)})`)
|
|
845
|
-
}
|
|
846
|
-
|
|
847
812
|
async getSubjectStatuses({
|
|
848
813
|
queueCount,
|
|
849
814
|
queueIndex,
|
|
@@ -858,54 +823,31 @@ export class ModerationService {
|
|
|
858
823
|
reviewedBefore,
|
|
859
824
|
reportedAfter,
|
|
860
825
|
reportedBefore,
|
|
861
|
-
includeMuted,
|
|
826
|
+
includeMuted = false,
|
|
862
827
|
hostingDeletedBefore,
|
|
863
828
|
hostingDeletedAfter,
|
|
864
829
|
hostingUpdatedBefore,
|
|
865
830
|
hostingUpdatedAfter,
|
|
866
831
|
hostingStatuses,
|
|
867
|
-
onlyMuted,
|
|
832
|
+
onlyMuted = false,
|
|
868
833
|
ignoreSubjects,
|
|
869
|
-
sortDirection,
|
|
834
|
+
sortDirection = 'desc',
|
|
870
835
|
lastReviewedBy,
|
|
871
|
-
sortField,
|
|
836
|
+
sortField = 'lastReportedAt',
|
|
872
837
|
subject,
|
|
873
838
|
tags,
|
|
874
839
|
excludeTags,
|
|
875
840
|
collections,
|
|
876
841
|
subjectType,
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
842
|
+
minAccountSuspendCount,
|
|
843
|
+
minReportedRecordsCount,
|
|
844
|
+
minTakendownRecordsCount,
|
|
845
|
+
}: QueryStatusParams): Promise<{
|
|
846
|
+
statuses: ModerationSubjectStatusRowWithHandle[]
|
|
882
847
|
cursor?: string
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
reviewedBefore?: string
|
|
887
|
-
reviewState?: ModerationSubjectStatusRow['reviewState']
|
|
888
|
-
reviewedAfter?: string
|
|
889
|
-
reportedAfter?: string
|
|
890
|
-
reportedBefore?: string
|
|
891
|
-
includeMuted?: boolean
|
|
892
|
-
hostingDeletedBefore?: string
|
|
893
|
-
hostingDeletedAfter?: string
|
|
894
|
-
hostingUpdatedBefore?: string
|
|
895
|
-
hostingUpdatedAfter?: string
|
|
896
|
-
hostingStatuses?: string[]
|
|
897
|
-
onlyMuted?: boolean
|
|
898
|
-
subject?: string
|
|
899
|
-
ignoreSubjects?: string[]
|
|
900
|
-
sortDirection: 'asc' | 'desc'
|
|
901
|
-
lastReviewedBy?: string
|
|
902
|
-
sortField: 'lastReviewedAt' | 'lastReportedAt'
|
|
903
|
-
tags: string[]
|
|
904
|
-
excludeTags: string[]
|
|
905
|
-
collections: string[]
|
|
906
|
-
subjectType?: string
|
|
907
|
-
}) {
|
|
908
|
-
let builder = this.db.db.selectFrom('moderation_subject_status').selectAll()
|
|
848
|
+
}> {
|
|
849
|
+
let builder = moderationSubjectStatusQueryBuilder(this.db.db)
|
|
850
|
+
|
|
909
851
|
const { ref } = this.db.db.dynamic
|
|
910
852
|
|
|
911
853
|
if (subject) {
|
|
@@ -919,14 +861,18 @@ export class ModerationService {
|
|
|
919
861
|
if (!includeAllUserRecords) {
|
|
920
862
|
builder = builder.where((qb) =>
|
|
921
863
|
subjectInfo.recordPath
|
|
922
|
-
? qb.where(
|
|
923
|
-
|
|
864
|
+
? qb.where(
|
|
865
|
+
'moderation_subject_status.recordPath',
|
|
866
|
+
'=',
|
|
867
|
+
subjectInfo.recordPath,
|
|
868
|
+
)
|
|
869
|
+
: qb.where('moderation_subject_status.recordPath', '=', ''),
|
|
924
870
|
)
|
|
925
871
|
}
|
|
926
872
|
} else if (subjectType === 'account') {
|
|
927
|
-
builder = builder.where('recordPath', '=', '')
|
|
873
|
+
builder = builder.where('moderation_subject_status.recordPath', '=', '')
|
|
928
874
|
} else if (subjectType === 'record') {
|
|
929
|
-
builder = builder.where('recordPath', '!=', '')
|
|
875
|
+
builder = builder.where('moderation_subject_status.recordPath', '!=', '')
|
|
930
876
|
}
|
|
931
877
|
|
|
932
878
|
// Only fetch items that belongs to the specified queue when specified
|
|
@@ -948,110 +894,216 @@ export class ModerationService {
|
|
|
948
894
|
}
|
|
949
895
|
|
|
950
896
|
// If subjectType is set to 'account' let that take priority and ignore collections filter
|
|
951
|
-
if (
|
|
952
|
-
builder = builder
|
|
953
|
-
|
|
954
|
-
|
|
897
|
+
if (subjectType !== 'account' && collections?.length) {
|
|
898
|
+
builder = builder
|
|
899
|
+
.where('moderation_subject_status.recordPath', '!=', '')
|
|
900
|
+
.where((qb) => {
|
|
901
|
+
for (const collection of collections) {
|
|
902
|
+
qb = qb.orWhere(
|
|
903
|
+
'moderation_subject_status.recordPath',
|
|
904
|
+
'like',
|
|
905
|
+
`${collection}/%`,
|
|
906
|
+
)
|
|
907
|
+
}
|
|
908
|
+
return qb
|
|
955
909
|
})
|
|
956
|
-
return qb
|
|
957
|
-
})
|
|
958
910
|
}
|
|
959
911
|
|
|
960
912
|
if (ignoreSubjects?.length) {
|
|
961
913
|
builder = builder
|
|
962
|
-
.where('did', 'not in', ignoreSubjects)
|
|
963
|
-
.where('recordPath', 'not in', ignoreSubjects)
|
|
914
|
+
.where('moderation_subject_status.did', 'not in', ignoreSubjects)
|
|
915
|
+
.where('moderation_subject_status.recordPath', 'not in', ignoreSubjects)
|
|
964
916
|
}
|
|
965
917
|
|
|
966
|
-
|
|
967
|
-
|
|
918
|
+
const reviewStateNormalized = getReviewState(reviewState)
|
|
919
|
+
if (reviewStateNormalized) {
|
|
920
|
+
builder = builder.where(
|
|
921
|
+
'moderation_subject_status.reviewState',
|
|
922
|
+
'=',
|
|
923
|
+
reviewStateNormalized,
|
|
924
|
+
)
|
|
968
925
|
}
|
|
969
926
|
|
|
970
927
|
if (lastReviewedBy) {
|
|
971
|
-
builder = builder.where(
|
|
928
|
+
builder = builder.where(
|
|
929
|
+
'moderation_subject_status.lastReviewedBy',
|
|
930
|
+
'=',
|
|
931
|
+
lastReviewedBy,
|
|
932
|
+
)
|
|
972
933
|
}
|
|
973
934
|
|
|
974
935
|
if (reviewedAfter) {
|
|
975
|
-
builder = builder.where(
|
|
936
|
+
builder = builder.where(
|
|
937
|
+
'moderation_subject_status.lastReviewedAt',
|
|
938
|
+
'>',
|
|
939
|
+
reviewedAfter,
|
|
940
|
+
)
|
|
976
941
|
}
|
|
977
942
|
|
|
978
943
|
if (reviewedBefore) {
|
|
979
|
-
builder = builder.where(
|
|
944
|
+
builder = builder.where(
|
|
945
|
+
'moderation_subject_status.lastReviewedAt',
|
|
946
|
+
'<',
|
|
947
|
+
reviewedBefore,
|
|
948
|
+
)
|
|
980
949
|
}
|
|
981
950
|
|
|
982
951
|
if (hostingUpdatedAfter) {
|
|
983
|
-
builder = builder.where(
|
|
952
|
+
builder = builder.where(
|
|
953
|
+
'moderation_subject_status.hostingUpdatedAt',
|
|
954
|
+
'>',
|
|
955
|
+
hostingUpdatedAfter,
|
|
956
|
+
)
|
|
984
957
|
}
|
|
985
958
|
|
|
986
959
|
if (hostingUpdatedBefore) {
|
|
987
|
-
builder = builder.where(
|
|
960
|
+
builder = builder.where(
|
|
961
|
+
'moderation_subject_status.hostingUpdatedAt',
|
|
962
|
+
'<',
|
|
963
|
+
hostingUpdatedBefore,
|
|
964
|
+
)
|
|
988
965
|
}
|
|
989
966
|
|
|
990
967
|
if (hostingDeletedAfter) {
|
|
991
|
-
builder = builder.where(
|
|
968
|
+
builder = builder.where(
|
|
969
|
+
'moderation_subject_status.hostingDeletedAt',
|
|
970
|
+
'>',
|
|
971
|
+
hostingDeletedAfter,
|
|
972
|
+
)
|
|
992
973
|
}
|
|
993
974
|
|
|
994
975
|
if (hostingDeletedBefore) {
|
|
995
|
-
builder = builder.where(
|
|
976
|
+
builder = builder.where(
|
|
977
|
+
'moderation_subject_status.hostingDeletedAt',
|
|
978
|
+
'<',
|
|
979
|
+
hostingDeletedBefore,
|
|
980
|
+
)
|
|
996
981
|
}
|
|
997
982
|
|
|
998
983
|
if (hostingStatuses?.length) {
|
|
999
|
-
builder = builder.where(
|
|
984
|
+
builder = builder.where(
|
|
985
|
+
'moderation_subject_status.hostingStatus',
|
|
986
|
+
'in',
|
|
987
|
+
hostingStatuses,
|
|
988
|
+
)
|
|
1000
989
|
}
|
|
1001
990
|
|
|
1002
991
|
if (reportedAfter) {
|
|
1003
|
-
builder = builder.where(
|
|
992
|
+
builder = builder.where(
|
|
993
|
+
'moderation_subject_status.lastReviewedAt',
|
|
994
|
+
'>',
|
|
995
|
+
reportedAfter,
|
|
996
|
+
)
|
|
1004
997
|
}
|
|
1005
998
|
|
|
1006
999
|
if (reportedBefore) {
|
|
1007
|
-
builder = builder.where(
|
|
1000
|
+
builder = builder.where(
|
|
1001
|
+
'moderation_subject_status.lastReportedAt',
|
|
1002
|
+
'<',
|
|
1003
|
+
reportedBefore,
|
|
1004
|
+
)
|
|
1008
1005
|
}
|
|
1009
1006
|
|
|
1010
1007
|
if (takendown) {
|
|
1011
|
-
builder = builder.where('takendown', '=', true)
|
|
1008
|
+
builder = builder.where('moderation_subject_status.takendown', '=', true)
|
|
1012
1009
|
}
|
|
1013
1010
|
|
|
1014
1011
|
if (appealed !== undefined) {
|
|
1015
1012
|
builder =
|
|
1016
1013
|
appealed === false
|
|
1017
|
-
? builder.where('appealed', 'is', null)
|
|
1018
|
-
: builder.where('appealed', '=', appealed)
|
|
1014
|
+
? builder.where('moderation_subject_status.appealed', 'is', null)
|
|
1015
|
+
: builder.where('moderation_subject_status.appealed', '=', appealed)
|
|
1019
1016
|
}
|
|
1020
1017
|
|
|
1021
1018
|
if (!includeMuted) {
|
|
1022
1019
|
builder = builder.where((qb) =>
|
|
1023
1020
|
qb
|
|
1024
|
-
.where(
|
|
1025
|
-
|
|
1021
|
+
.where(
|
|
1022
|
+
'moderation_subject_status.muteUntil',
|
|
1023
|
+
'<',
|
|
1024
|
+
new Date().toISOString(),
|
|
1025
|
+
)
|
|
1026
|
+
.orWhere('moderation_subject_status.muteUntil', 'is', null),
|
|
1026
1027
|
)
|
|
1027
1028
|
}
|
|
1028
1029
|
|
|
1029
1030
|
if (onlyMuted) {
|
|
1030
1031
|
builder = builder.where((qb) =>
|
|
1031
1032
|
qb
|
|
1032
|
-
.where(
|
|
1033
|
-
|
|
1033
|
+
.where(
|
|
1034
|
+
'moderation_subject_status.muteUntil',
|
|
1035
|
+
'>',
|
|
1036
|
+
new Date().toISOString(),
|
|
1037
|
+
)
|
|
1038
|
+
.orWhere(
|
|
1039
|
+
'moderation_subject_status.muteReportingUntil',
|
|
1040
|
+
'>',
|
|
1041
|
+
new Date().toISOString(),
|
|
1042
|
+
),
|
|
1034
1043
|
)
|
|
1035
1044
|
}
|
|
1036
1045
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1046
|
+
// ["tag1", "tag2 && tag3", "tag4"] => [["tag1"], ["tag2", "tag3"], ["tag4"]]
|
|
1047
|
+
const conditions = parseTags(tags)
|
|
1048
|
+
if (conditions?.length) {
|
|
1049
|
+
// [["tag1"], ["tag2", "tag3"], ["tag4"]] => (tags ? 'tag1') OR (tags ? 'tag2' AND tags ? 'tag3') OR (tags ? 'tag4')
|
|
1050
|
+
builder = builder.where((qb) => {
|
|
1051
|
+
for (const subTags of conditions) {
|
|
1052
|
+
// OR between every conditions items (subTags)
|
|
1053
|
+
qb = qb.orWhere((qb) => {
|
|
1054
|
+
// AND between every subTags items (subTag)
|
|
1055
|
+
for (const subTag of subTags) {
|
|
1056
|
+
qb = qb.where(
|
|
1057
|
+
sql`${ref('moderation_subject_status.tags')} ? ${subTag}`,
|
|
1058
|
+
)
|
|
1059
|
+
}
|
|
1060
|
+
return qb
|
|
1061
|
+
})
|
|
1062
|
+
}
|
|
1063
|
+
return qb
|
|
1064
|
+
})
|
|
1039
1065
|
}
|
|
1040
1066
|
|
|
1041
|
-
if (excludeTags
|
|
1067
|
+
if (excludeTags?.length) {
|
|
1042
1068
|
builder = builder.where((qb) =>
|
|
1043
1069
|
qb
|
|
1044
1070
|
.where(
|
|
1045
|
-
sql`NOT(${ref(
|
|
1046
|
-
'moderation_subject_status.tags',
|
|
1047
|
-
)} ?| array[${sql.join(excludeTags)}]::TEXT[])`,
|
|
1071
|
+
sql`NOT(${ref('moderation_subject_status.tags')} ?| array[${sql.join(excludeTags)}]::TEXT[])`,
|
|
1048
1072
|
)
|
|
1049
1073
|
.orWhere('tags', 'is', null),
|
|
1050
1074
|
)
|
|
1051
1075
|
}
|
|
1052
1076
|
|
|
1077
|
+
if (minAccountSuspendCount != null && minAccountSuspendCount > 0) {
|
|
1078
|
+
builder = builder.where(
|
|
1079
|
+
'account_events_stats.suspendCount',
|
|
1080
|
+
'>=',
|
|
1081
|
+
minAccountSuspendCount,
|
|
1082
|
+
)
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
if (minTakendownRecordsCount != null && minTakendownRecordsCount > 0) {
|
|
1086
|
+
builder = builder.where(
|
|
1087
|
+
'account_record_status_stats.takendownCount',
|
|
1088
|
+
'>=',
|
|
1089
|
+
minTakendownRecordsCount,
|
|
1090
|
+
)
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
if (minReportedRecordsCount != null && minReportedRecordsCount > 0) {
|
|
1094
|
+
builder = builder.where(
|
|
1095
|
+
'account_record_events_stats.reportedCount',
|
|
1096
|
+
'>=',
|
|
1097
|
+
minReportedRecordsCount,
|
|
1098
|
+
)
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1053
1101
|
const keyset = new StatusKeyset(
|
|
1054
|
-
|
|
1102
|
+
sortField === 'reportedRecordsCount'
|
|
1103
|
+
? ref(`account_record_events_stats.reportedCount`)
|
|
1104
|
+
: sortField === 'takendownRecordsCount'
|
|
1105
|
+
? ref(`account_record_status_stats.takendownCount`)
|
|
1106
|
+
: ref(`moderation_subject_status.${sortField}`),
|
|
1055
1107
|
ref('moderation_subject_status.id'),
|
|
1056
1108
|
)
|
|
1057
1109
|
const paginatedBuilder = paginate(builder, {
|
|
@@ -1067,13 +1119,12 @@ export class ModerationService {
|
|
|
1067
1119
|
const infos = await this.views.getAccoutInfosByDid(
|
|
1068
1120
|
results.map((r) => r.did),
|
|
1069
1121
|
)
|
|
1070
|
-
const resultsWithHandles = results.map((r) => ({
|
|
1071
|
-
...r,
|
|
1072
|
-
handle: infos.get(r.did)?.handle ?? INVALID_HANDLE,
|
|
1073
|
-
}))
|
|
1074
1122
|
|
|
1075
1123
|
return {
|
|
1076
|
-
statuses:
|
|
1124
|
+
statuses: results.map((r) => ({
|
|
1125
|
+
...r,
|
|
1126
|
+
handle: infos.get(r.did)?.handle ?? INVALID_HANDLE,
|
|
1127
|
+
})),
|
|
1077
1128
|
cursor: keyset.packFromResult(results),
|
|
1078
1129
|
}
|
|
1079
1130
|
}
|
|
@@ -1195,6 +1246,18 @@ export class ModerationService {
|
|
|
1195
1246
|
}
|
|
1196
1247
|
}
|
|
1197
1248
|
|
|
1249
|
+
const parseTags = (tags?: string[]) =>
|
|
1250
|
+
tags
|
|
1251
|
+
?.map((tag) =>
|
|
1252
|
+
tag
|
|
1253
|
+
.split(/\s*&&\s*/g)
|
|
1254
|
+
.map((subTag) => subTag.trim())
|
|
1255
|
+
// Ignore invalid syntax ("", "tag1 &&", "&& tag2", "tag1 && && tag2", etc.)
|
|
1256
|
+
.filter(Boolean),
|
|
1257
|
+
)
|
|
1258
|
+
// Ignore invalid items
|
|
1259
|
+
.filter((subTags): subTags is [string, ...string[]] => subTags.length > 0)
|
|
1260
|
+
|
|
1198
1261
|
const isSafeUrl = (url: URL) => {
|
|
1199
1262
|
if (url.protocol !== 'https:') return false
|
|
1200
1263
|
if (!url.hostname || url.hostname === 'localhost') return false
|