@atproto/bsky 0.0.15 → 0.0.16
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 +11 -0
- package/dist/api/com/atproto/moderation/util.d.ts +4 -3
- package/dist/context.d.ts +15 -0
- package/dist/db/index.js +26 -1
- package/dist/db/index.js.map +3 -3
- package/dist/db/migrations/20231003T202833377Z-create-moderation-subject-status.d.ts +3 -0
- package/dist/db/migrations/index.d.ts +1 -0
- package/dist/db/pagination.d.ts +2 -1
- package/dist/db/{periodic-moderation-action-reversal.d.ts → periodic-moderation-event-reversal.d.ts} +3 -5
- package/dist/db/tables/moderation.d.ts +24 -34
- package/dist/feed-gen/types.d.ts +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2750 -2121
- package/dist/index.js.map +3 -3
- package/dist/lexicon/index.d.ts +11 -18
- package/dist/lexicon/lexicons.d.ts +414 -399
- package/dist/lexicon/types/app/bsky/feed/defs.d.ts +1 -7
- package/dist/lexicon/types/app/bsky/graph/defs.d.ts +1 -0
- package/dist/lexicon/types/com/atproto/admin/defs.d.ts +114 -48
- package/dist/lexicon/types/com/atproto/admin/{takeModerationAction.d.ts → emitModerationEvent.d.ts} +5 -6
- package/dist/lexicon/types/com/atproto/admin/{getModerationAction.d.ts → getModerationEvent.d.ts} +1 -1
- package/dist/lexicon/types/com/atproto/admin/{getModerationActions.d.ts → queryModerationEvents.d.ts} +5 -1
- package/dist/lexicon/types/com/atproto/admin/{getModerationReports.d.ts → queryModerationStatuses.d.ts} +12 -6
- package/dist/lexicon/types/com/atproto/admin/sendEmail.d.ts +1 -0
- package/dist/migrate-moderation-data.d.ts +1 -0
- package/dist/services/actor/views.d.ts +2 -5
- package/dist/services/feed/index.d.ts +1 -0
- package/dist/services/feed/util.d.ts +9 -1
- package/dist/services/feed/views.d.ts +6 -17
- package/dist/services/graph/index.d.ts +5 -29
- package/dist/services/graph/types.d.ts +1 -0
- package/dist/services/moderation/index.d.ts +135 -72
- package/dist/services/moderation/pagination.d.ts +36 -0
- package/dist/services/moderation/status.d.ts +13 -0
- package/dist/services/moderation/types.d.ts +35 -0
- package/dist/services/moderation/views.d.ts +18 -14
- package/dist/util/debug.d.ts +1 -1
- package/package.json +11 -11
- package/src/api/app/bsky/feed/getActorFeeds.ts +2 -1
- package/src/api/app/bsky/feed/getActorLikes.ts +1 -3
- package/src/api/app/bsky/feed/getAuthorFeed.ts +1 -3
- package/src/api/app/bsky/feed/getFeed.ts +9 -9
- package/src/api/app/bsky/feed/getFeedGenerator.ts +3 -0
- package/src/api/app/bsky/feed/getFeedGenerators.ts +2 -1
- package/src/api/app/bsky/feed/getListFeed.ts +1 -3
- package/src/api/app/bsky/feed/getPostThread.ts +15 -54
- package/src/api/app/bsky/feed/getPosts.ts +21 -18
- package/src/api/app/bsky/feed/getSuggestedFeeds.ts +2 -1
- package/src/api/app/bsky/feed/getTimeline.ts +1 -3
- package/src/api/app/bsky/feed/searchPosts.ts +20 -17
- package/src/api/app/bsky/graph/getList.ts +6 -3
- package/src/api/app/bsky/graph/getListBlocks.ts +3 -2
- package/src/api/app/bsky/graph/getListMutes.ts +2 -1
- package/src/api/app/bsky/graph/getLists.ts +2 -1
- package/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +3 -1
- package/src/api/blob-resolver.ts +6 -11
- package/src/api/com/atproto/admin/emitModerationEvent.ts +220 -0
- package/src/api/com/atproto/admin/{getModerationActions.ts → getModerationEvent.ts} +5 -11
- package/src/api/com/atproto/admin/getRecord.ts +1 -0
- package/src/api/com/atproto/admin/{getModerationReports.ts → queryModerationEvents.ts} +13 -16
- package/src/api/com/atproto/admin/queryModerationStatuses.ts +55 -0
- package/src/api/com/atproto/moderation/createReport.ts +9 -7
- package/src/api/com/atproto/moderation/util.ts +38 -20
- package/src/api/index.ts +8 -14
- package/src/auth.ts +29 -21
- package/src/auto-moderator/index.ts +26 -19
- package/src/context.ts +4 -0
- package/src/db/migrations/20231003T202833377Z-create-moderation-subject-status.ts +123 -0
- package/src/db/migrations/index.ts +1 -0
- package/src/db/pagination.ts +26 -3
- package/src/db/{periodic-moderation-action-reversal.ts → periodic-moderation-event-reversal.ts} +50 -46
- package/src/db/tables/moderation.ts +35 -52
- package/src/feed-gen/best-of-follows.ts +6 -3
- package/src/feed-gen/bsky-team.ts +1 -1
- package/src/feed-gen/hot-classic.ts +1 -1
- package/src/feed-gen/mutuals.ts +6 -2
- package/src/feed-gen/types.ts +1 -1
- package/src/feed-gen/whats-hot.ts +1 -1
- package/src/feed-gen/with-friends.ts +7 -3
- package/src/index.ts +2 -1
- package/src/lexicon/index.ts +30 -67
- package/src/lexicon/lexicons.ts +526 -491
- package/src/lexicon/types/app/bsky/feed/defs.ts +1 -18
- package/src/lexicon/types/app/bsky/graph/defs.ts +1 -0
- package/src/lexicon/types/com/atproto/admin/defs.ts +276 -84
- package/src/lexicon/types/com/atproto/admin/{takeModerationAction.ts → emitModerationEvent.ts} +13 -11
- package/src/lexicon/types/com/atproto/admin/{getModerationReport.ts → getModerationEvent.ts} +1 -1
- package/src/lexicon/types/com/atproto/admin/{getModerationActions.ts → queryModerationEvents.ts} +8 -1
- package/src/lexicon/types/com/atproto/admin/{getModerationReports.ts → queryModerationStatuses.ts} +21 -14
- package/src/lexicon/types/com/atproto/admin/sendEmail.ts +1 -0
- package/src/migrate-moderation-data.ts +414 -0
- package/src/services/actor/views.ts +5 -14
- package/src/services/feed/index.ts +26 -7
- package/src/services/feed/util.ts +47 -19
- package/src/services/feed/views.ts +68 -4
- package/src/services/graph/index.ts +21 -3
- package/src/services/graph/types.ts +1 -0
- package/src/services/indexing/plugins/block.ts +2 -3
- package/src/services/indexing/plugins/feed-generator.ts +2 -3
- package/src/services/indexing/plugins/follow.ts +2 -3
- package/src/services/indexing/plugins/like.ts +2 -3
- package/src/services/indexing/plugins/list-block.ts +2 -3
- package/src/services/indexing/plugins/list-item.ts +2 -3
- package/src/services/indexing/plugins/list.ts +2 -3
- package/src/services/indexing/plugins/post.ts +3 -4
- package/src/services/indexing/plugins/repost.ts +2 -3
- package/src/services/indexing/plugins/thread-gate.ts +2 -3
- package/src/services/label/index.ts +2 -3
- package/src/services/moderation/index.ts +380 -395
- package/src/services/moderation/pagination.ts +96 -0
- package/src/services/moderation/status.ts +244 -0
- package/src/services/moderation/types.ts +49 -0
- package/src/services/moderation/views.ts +278 -329
- package/src/util/debug.ts +2 -2
- package/tests/__snapshots__/feed-generation.test.ts.snap +322 -6
- package/tests/__snapshots__/indexing.test.ts.snap +0 -6
- package/tests/admin/__snapshots__/get-record.test.ts.snap +30 -132
- package/tests/admin/__snapshots__/get-repo.test.ts.snap +14 -60
- package/tests/admin/__snapshots__/moderation-events.test.ts.snap +146 -0
- package/tests/admin/__snapshots__/moderation-statuses.test.ts.snap +64 -0
- package/tests/admin/__snapshots__/moderation.test.ts.snap +0 -125
- package/tests/admin/get-record.test.ts +5 -9
- package/tests/admin/get-repo.test.ts +5 -9
- package/tests/admin/moderation-events.test.ts +221 -0
- package/tests/admin/moderation-statuses.test.ts +145 -0
- package/tests/admin/moderation.test.ts +512 -860
- package/tests/admin/repo-search.test.ts +2 -3
- package/tests/auto-moderator/fuzzy-matcher.test.ts +2 -1
- package/tests/auto-moderator/takedowns.test.ts +45 -18
- package/tests/feed-generation.test.ts +57 -9
- package/tests/views/__snapshots__/block-lists.test.ts.snap +3 -9
- package/tests/views/__snapshots__/blocks.test.ts.snap +0 -9
- package/tests/views/__snapshots__/mute-lists.test.ts.snap +5 -5
- package/tests/views/__snapshots__/mutes.test.ts.snap +0 -3
- package/tests/views/__snapshots__/thread.test.ts.snap +0 -30
- package/tests/views/actor-search.test.ts +2 -3
- package/tests/views/author-feed.test.ts +42 -36
- package/tests/views/follows.test.ts +40 -35
- package/tests/views/list-feed.test.ts +17 -9
- package/tests/views/notifications.test.ts +13 -9
- package/tests/views/profile.test.ts +20 -18
- package/tests/views/thread.test.ts +54 -26
- package/tests/views/threadgating.test.ts +51 -19
- package/tests/views/timeline.test.ts +21 -13
- package/dist/api/com/atproto/admin/resolveModerationReports.d.ts +0 -3
- package/dist/api/com/atproto/admin/reverseModerationAction.d.ts +0 -3
- package/dist/api/com/atproto/admin/takeModerationAction.d.ts +0 -3
- package/dist/lexicon/types/com/atproto/admin/getModerationReport.d.ts +0 -29
- package/dist/lexicon/types/com/atproto/admin/resolveModerationReports.d.ts +0 -36
- package/dist/lexicon/types/com/atproto/admin/reverseModerationAction.d.ts +0 -36
- package/src/api/com/atproto/admin/getModerationAction.ts +0 -44
- package/src/api/com/atproto/admin/getModerationReport.ts +0 -43
- package/src/api/com/atproto/admin/resolveModerationReports.ts +0 -24
- package/src/api/com/atproto/admin/reverseModerationAction.ts +0 -115
- package/src/api/com/atproto/admin/takeModerationAction.ts +0 -156
- package/src/lexicon/types/com/atproto/admin/getModerationAction.ts +0 -41
- package/src/lexicon/types/com/atproto/admin/resolveModerationReports.ts +0 -49
- package/src/lexicon/types/com/atproto/admin/reverseModerationAction.ts +0 -49
- package/tests/admin/__snapshots__/get-moderation-action.test.ts.snap +0 -172
- package/tests/admin/__snapshots__/get-moderation-actions.test.ts.snap +0 -178
- package/tests/admin/__snapshots__/get-moderation-report.test.ts.snap +0 -177
- package/tests/admin/__snapshots__/get-moderation-reports.test.ts.snap +0 -307
- package/tests/admin/get-moderation-action.test.ts +0 -100
- package/tests/admin/get-moderation-actions.test.ts +0 -164
- package/tests/admin/get-moderation-report.test.ts +0 -100
- package/tests/admin/get-moderation-reports.test.ts +0 -332
- /package/dist/api/com/atproto/admin/{getModerationAction.d.ts → emitModerationEvent.d.ts} +0 -0
- /package/dist/api/com/atproto/admin/{getModerationActions.d.ts → getModerationEvent.d.ts} +0 -0
- /package/dist/api/com/atproto/admin/{getModerationReport.d.ts → queryModerationEvents.d.ts} +0 -0
- /package/dist/api/com/atproto/admin/{getModerationReports.d.ts → queryModerationStatuses.d.ts} +0 -0
|
@@ -1,19 +1,37 @@
|
|
|
1
|
-
import { Selectable, sql } from 'kysely'
|
|
2
1
|
import { CID } from 'multiformats/cid'
|
|
3
2
|
import { AtUri } from '@atproto/syntax'
|
|
4
3
|
import { InvalidRequestError } from '@atproto/xrpc-server'
|
|
5
4
|
import { PrimaryDatabase } from '../../db'
|
|
6
|
-
import { ModerationAction, ModerationReport } from '../../db/tables/moderation'
|
|
7
5
|
import { ModerationViews } from './views'
|
|
8
6
|
import { ImageUriBuilder } from '../../image/uri'
|
|
7
|
+
import { Main as StrongRef } from '../../lexicon/types/com/atproto/repo/strongRef'
|
|
9
8
|
import { ImageInvalidator } from '../../image/invalidator'
|
|
10
9
|
import {
|
|
10
|
+
isModEventComment,
|
|
11
|
+
isModEventLabel,
|
|
12
|
+
isModEventMute,
|
|
13
|
+
isModEventReport,
|
|
14
|
+
isModEventTakedown,
|
|
15
|
+
isModEventEmail,
|
|
11
16
|
RepoRef,
|
|
12
17
|
RepoBlobRef,
|
|
13
|
-
TAKEDOWN,
|
|
14
18
|
} from '../../lexicon/types/com/atproto/admin/defs'
|
|
15
|
-
import { Main as StrongRef } from '../../lexicon/types/com/atproto/repo/strongRef'
|
|
16
19
|
import { addHoursToDate } from '../../util/date'
|
|
20
|
+
import {
|
|
21
|
+
adjustModerationSubjectStatus,
|
|
22
|
+
getStatusIdentifierFromSubject,
|
|
23
|
+
} from './status'
|
|
24
|
+
import {
|
|
25
|
+
ModEventType,
|
|
26
|
+
ModerationEventRow,
|
|
27
|
+
ModerationEventRowWithHandle,
|
|
28
|
+
ModerationSubjectStatusRow,
|
|
29
|
+
ReversibleModerationEvent,
|
|
30
|
+
SubjectInfo,
|
|
31
|
+
} from './types'
|
|
32
|
+
import { ModerationEvent } from '../../db/tables/moderation'
|
|
33
|
+
import { paginate } from '../../db/pagination'
|
|
34
|
+
import { StatusKeyset, TimeIdKeyset } from './pagination'
|
|
17
35
|
|
|
18
36
|
export class ModerationService {
|
|
19
37
|
constructor(
|
|
@@ -32,350 +50,311 @@ export class ModerationService {
|
|
|
32
50
|
|
|
33
51
|
views = new ModerationViews(this.db)
|
|
34
52
|
|
|
35
|
-
async
|
|
53
|
+
async getEvent(id: number): Promise<ModerationEventRow | undefined> {
|
|
36
54
|
return await this.db.db
|
|
37
|
-
.selectFrom('
|
|
55
|
+
.selectFrom('moderation_event')
|
|
38
56
|
.selectAll()
|
|
39
57
|
.where('id', '=', id)
|
|
40
58
|
.executeTakeFirst()
|
|
41
59
|
}
|
|
42
60
|
|
|
43
|
-
async
|
|
44
|
-
const
|
|
45
|
-
if (!
|
|
46
|
-
return
|
|
61
|
+
async getEventOrThrow(id: number): Promise<ModerationEventRow> {
|
|
62
|
+
const event = await this.getEvent(id)
|
|
63
|
+
if (!event) throw new InvalidRequestError('Moderation event not found')
|
|
64
|
+
return event
|
|
47
65
|
}
|
|
48
66
|
|
|
49
|
-
async
|
|
67
|
+
async getEvents(opts: {
|
|
50
68
|
subject?: string
|
|
69
|
+
createdBy?: string
|
|
51
70
|
limit: number
|
|
52
71
|
cursor?: string
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
builder = builder.where((qb) => {
|
|
58
|
-
return qb
|
|
59
|
-
.where('subjectDid', '=', subject)
|
|
60
|
-
.orWhere('subjectUri', '=', subject)
|
|
61
|
-
})
|
|
62
|
-
}
|
|
63
|
-
if (cursor) {
|
|
64
|
-
const cursorNumeric = parseInt(cursor, 10)
|
|
65
|
-
if (isNaN(cursorNumeric)) {
|
|
66
|
-
throw new InvalidRequestError('Malformed cursor')
|
|
67
|
-
}
|
|
68
|
-
builder = builder.where('id', '<', cursorNumeric)
|
|
69
|
-
}
|
|
70
|
-
return await builder
|
|
71
|
-
.selectAll()
|
|
72
|
-
.orderBy('id', 'desc')
|
|
73
|
-
.limit(limit)
|
|
74
|
-
.execute()
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async getReport(id: number): Promise<ModerationReportRow | undefined> {
|
|
78
|
-
return await this.db.db
|
|
79
|
-
.selectFrom('moderation_report')
|
|
80
|
-
.selectAll()
|
|
81
|
-
.where('id', '=', id)
|
|
82
|
-
.executeTakeFirst()
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async getReports(opts: {
|
|
86
|
-
subject?: string
|
|
87
|
-
resolved?: boolean
|
|
88
|
-
actionType?: string
|
|
89
|
-
limit: number
|
|
90
|
-
cursor?: string
|
|
91
|
-
ignoreSubjects?: string[]
|
|
92
|
-
reverse?: boolean
|
|
93
|
-
reporters?: string[]
|
|
94
|
-
actionedBy?: string
|
|
95
|
-
}): Promise<ModerationReportRowWithHandle[]> {
|
|
72
|
+
includeAllUserRecords: boolean
|
|
73
|
+
types: ModerationEvent['action'][]
|
|
74
|
+
sortDirection?: 'asc' | 'desc'
|
|
75
|
+
}): Promise<{ cursor?: string; events: ModerationEventRowWithHandle[] }> {
|
|
96
76
|
const {
|
|
97
77
|
subject,
|
|
98
|
-
|
|
99
|
-
actionType,
|
|
78
|
+
createdBy,
|
|
100
79
|
limit,
|
|
101
80
|
cursor,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
actionedBy,
|
|
81
|
+
includeAllUserRecords,
|
|
82
|
+
sortDirection = 'desc',
|
|
83
|
+
types,
|
|
106
84
|
} = opts
|
|
107
|
-
|
|
108
|
-
|
|
85
|
+
let builder = this.db.db
|
|
86
|
+
.selectFrom('moderation_event')
|
|
87
|
+
.leftJoin(
|
|
88
|
+
'actor as creatorActor',
|
|
89
|
+
'creatorActor.did',
|
|
90
|
+
'moderation_event.createdBy',
|
|
91
|
+
)
|
|
92
|
+
.leftJoin(
|
|
93
|
+
'actor as subjectActor',
|
|
94
|
+
'subjectActor.did',
|
|
95
|
+
'moderation_event.subjectDid',
|
|
96
|
+
)
|
|
109
97
|
if (subject) {
|
|
110
98
|
builder = builder.where((qb) => {
|
|
99
|
+
if (includeAllUserRecords) {
|
|
100
|
+
// If subject is an at-uri, we need to extract the DID from the at-uri
|
|
101
|
+
// otherwise, subject is probably a DID already
|
|
102
|
+
if (subject.startsWith('at://')) {
|
|
103
|
+
const uri = new AtUri(subject)
|
|
104
|
+
return qb.where('subjectDid', '=', uri.hostname)
|
|
105
|
+
}
|
|
106
|
+
return qb.where('subjectDid', '=', subject)
|
|
107
|
+
}
|
|
111
108
|
return qb
|
|
112
|
-
.where(
|
|
109
|
+
.where((subQb) =>
|
|
110
|
+
subQb
|
|
111
|
+
.where('subjectDid', '=', subject)
|
|
112
|
+
.where('subjectUri', 'is', null),
|
|
113
|
+
)
|
|
113
114
|
.orWhere('subjectUri', '=', subject)
|
|
114
115
|
})
|
|
115
116
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
ignoreSubjects.forEach((subject) => {
|
|
122
|
-
if (subject.startsWith('at://')) {
|
|
123
|
-
ignoreUris.push(subject)
|
|
124
|
-
} else if (subject.startsWith('did:')) {
|
|
125
|
-
ignoreDids.push(subject)
|
|
117
|
+
if (types.length) {
|
|
118
|
+
builder = builder.where((qb) => {
|
|
119
|
+
if (types.length === 1) {
|
|
120
|
+
return qb.where('action', '=', types[0])
|
|
126
121
|
}
|
|
127
|
-
})
|
|
128
122
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
if (ignoreUris.length) {
|
|
133
|
-
builder = builder.where((qb) => {
|
|
134
|
-
// Without the null condition, postgres will ignore all reports where `subjectUri` is null
|
|
135
|
-
// which will make all the account reports be ignored as well
|
|
136
|
-
return qb
|
|
137
|
-
.where('subjectUri', 'not in', ignoreUris)
|
|
138
|
-
.orWhere('subjectUri', 'is', null)
|
|
139
|
-
})
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (reporters?.length) {
|
|
144
|
-
builder = builder.where('reportedByDid', 'in', reporters)
|
|
123
|
+
return qb.where('action', 'in', types)
|
|
124
|
+
})
|
|
145
125
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const resolutionsQuery = this.db.db
|
|
149
|
-
.selectFrom('moderation_report_resolution')
|
|
150
|
-
.selectAll()
|
|
151
|
-
.whereRef(
|
|
152
|
-
'moderation_report_resolution.reportId',
|
|
153
|
-
'=',
|
|
154
|
-
ref('moderation_report.id'),
|
|
155
|
-
)
|
|
156
|
-
builder = resolved
|
|
157
|
-
? builder.whereExists(resolutionsQuery)
|
|
158
|
-
: builder.whereNotExists(resolutionsQuery)
|
|
126
|
+
if (createdBy) {
|
|
127
|
+
builder = builder.where('createdBy', '=', createdBy)
|
|
159
128
|
}
|
|
160
|
-
if (actionType !== undefined || actionedBy !== undefined) {
|
|
161
|
-
let resolutionActionsQuery = this.db.db
|
|
162
|
-
.selectFrom('moderation_report_resolution')
|
|
163
|
-
.innerJoin(
|
|
164
|
-
'moderation_action',
|
|
165
|
-
'moderation_action.id',
|
|
166
|
-
'moderation_report_resolution.actionId',
|
|
167
|
-
)
|
|
168
|
-
.whereRef(
|
|
169
|
-
'moderation_report_resolution.reportId',
|
|
170
|
-
'=',
|
|
171
|
-
ref('moderation_report.id'),
|
|
172
|
-
)
|
|
173
129
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
130
|
+
const { ref } = this.db.db.dynamic
|
|
131
|
+
const keyset = new TimeIdKeyset(
|
|
132
|
+
ref(`moderation_event.createdAt`),
|
|
133
|
+
ref('moderation_event.id'),
|
|
134
|
+
)
|
|
135
|
+
const paginatedBuilder = paginate(builder, {
|
|
136
|
+
limit,
|
|
137
|
+
cursor,
|
|
138
|
+
keyset,
|
|
139
|
+
direction: sortDirection,
|
|
140
|
+
tryIndex: true,
|
|
141
|
+
})
|
|
187
142
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
builder = builder.where('id', reverse ? '>' : '<', cursorNumeric)
|
|
196
|
-
}
|
|
197
|
-
return await builder
|
|
198
|
-
.leftJoin('actor', 'actor.did', 'moderation_report.subjectDid')
|
|
199
|
-
.selectAll(['moderation_report', 'actor'])
|
|
200
|
-
.orderBy('id', reverse ? 'asc' : 'desc')
|
|
201
|
-
.limit(limit)
|
|
143
|
+
const result = await paginatedBuilder
|
|
144
|
+
.selectAll(['moderation_event'])
|
|
145
|
+
.select([
|
|
146
|
+
'subjectActor.handle as subjectHandle',
|
|
147
|
+
'creatorActor.handle as creatorHandle',
|
|
148
|
+
])
|
|
202
149
|
.execute()
|
|
150
|
+
|
|
151
|
+
return { cursor: keyset.packFromResult(result), events: result }
|
|
203
152
|
}
|
|
204
153
|
|
|
205
|
-
async
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
154
|
+
async getReport(id: number): Promise<ModerationEventRow | undefined> {
|
|
155
|
+
return await this.db.db
|
|
156
|
+
.selectFrom('moderation_event')
|
|
157
|
+
.where('action', '=', 'com.atproto.admin.defs#modEventReport')
|
|
158
|
+
.selectAll()
|
|
159
|
+
.where('id', '=', id)
|
|
160
|
+
.executeTakeFirst()
|
|
209
161
|
}
|
|
210
162
|
|
|
211
|
-
async
|
|
163
|
+
async getCurrentStatus(
|
|
212
164
|
subject: { did: string } | { uri: AtUri } | { cids: CID[] },
|
|
213
165
|
) {
|
|
214
|
-
|
|
215
|
-
let builder = this.db.db
|
|
216
|
-
.selectFrom('moderation_action')
|
|
217
|
-
.selectAll()
|
|
218
|
-
.where('reversedAt', 'is', null)
|
|
166
|
+
let builder = this.db.db.selectFrom('moderation_subject_status').selectAll()
|
|
219
167
|
if ('did' in subject) {
|
|
220
|
-
builder = builder
|
|
221
|
-
.where('subjectType', '=', 'com.atproto.admin.defs#repoRef')
|
|
222
|
-
.where('subjectDid', '=', subject.did)
|
|
168
|
+
builder = builder.where('did', '=', subject.did)
|
|
223
169
|
} else if ('uri' in subject) {
|
|
224
|
-
builder = builder
|
|
225
|
-
.where('subjectType', '=', 'com.atproto.repo.strongRef')
|
|
226
|
-
.where('subjectUri', '=', subject.uri.toString())
|
|
227
|
-
} else {
|
|
228
|
-
const blobsForAction = this.db.db
|
|
229
|
-
.selectFrom('moderation_action_subject_blob')
|
|
230
|
-
.selectAll()
|
|
231
|
-
.whereRef('actionId', '=', ref('moderation_action.id'))
|
|
232
|
-
.where(
|
|
233
|
-
'cid',
|
|
234
|
-
'in',
|
|
235
|
-
subject.cids.map((cid) => cid.toString()),
|
|
236
|
-
)
|
|
237
|
-
builder = builder.whereExists(blobsForAction)
|
|
170
|
+
builder = builder.where('recordPath', '=', subject.uri.toString())
|
|
238
171
|
}
|
|
172
|
+
// TODO: Handle the cid status
|
|
239
173
|
return await builder.execute()
|
|
240
174
|
}
|
|
241
175
|
|
|
242
|
-
|
|
243
|
-
|
|
176
|
+
buildSubjectInfo(
|
|
177
|
+
subject: { did: string } | { uri: AtUri; cid: CID },
|
|
178
|
+
subjectBlobCids?: CID[],
|
|
179
|
+
): SubjectInfo {
|
|
180
|
+
if ('did' in subject) {
|
|
181
|
+
if (subjectBlobCids?.length) {
|
|
182
|
+
throw new InvalidRequestError('Blobs do not apply to repo subjects')
|
|
183
|
+
}
|
|
184
|
+
// Allowing dids that may not exist: may have been deleted but needs to remain actionable.
|
|
185
|
+
return {
|
|
186
|
+
subjectType: 'com.atproto.admin.defs#repoRef',
|
|
187
|
+
subjectDid: subject.did,
|
|
188
|
+
subjectUri: null,
|
|
189
|
+
subjectCid: null,
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Allowing records/blobs that may not exist: may have been deleted but needs to remain actionable.
|
|
194
|
+
return {
|
|
195
|
+
subjectType: 'com.atproto.repo.strongRef',
|
|
196
|
+
subjectDid: subject.uri.host,
|
|
197
|
+
subjectUri: subject.uri.toString(),
|
|
198
|
+
subjectCid: subject.cid.toString(),
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async logEvent(info: {
|
|
203
|
+
event: ModEventType
|
|
244
204
|
subject: { did: string } | { uri: AtUri; cid: CID }
|
|
245
205
|
subjectBlobCids?: CID[]
|
|
246
|
-
reason: string
|
|
247
|
-
createLabelVals?: string[]
|
|
248
|
-
negateLabelVals?: string[]
|
|
249
206
|
createdBy: string
|
|
250
207
|
createdAt?: Date
|
|
251
|
-
|
|
252
|
-
}): Promise<ModerationActionRow> {
|
|
208
|
+
}): Promise<ModerationEventRow> {
|
|
253
209
|
this.db.assertTransaction()
|
|
254
210
|
const {
|
|
255
|
-
|
|
211
|
+
event,
|
|
256
212
|
createdBy,
|
|
257
|
-
reason,
|
|
258
213
|
subject,
|
|
259
214
|
subjectBlobCids,
|
|
260
|
-
durationInHours,
|
|
261
215
|
createdAt = new Date(),
|
|
262
216
|
} = info
|
|
217
|
+
|
|
218
|
+
// Resolve subject info
|
|
219
|
+
const subjectInfo = this.buildSubjectInfo(subject, subjectBlobCids)
|
|
220
|
+
|
|
263
221
|
const createLabelVals =
|
|
264
|
-
|
|
265
|
-
?
|
|
222
|
+
isModEventLabel(event) && event.createLabelVals.length > 0
|
|
223
|
+
? event.createLabelVals.join(' ')
|
|
266
224
|
: undefined
|
|
267
225
|
const negateLabelVals =
|
|
268
|
-
|
|
269
|
-
?
|
|
226
|
+
isModEventLabel(event) && event.negateLabelVals.length > 0
|
|
227
|
+
? event.negateLabelVals.join(' ')
|
|
270
228
|
: undefined
|
|
271
229
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (
|
|
275
|
-
|
|
276
|
-
subjectInfo = {
|
|
277
|
-
subjectType: 'com.atproto.admin.defs#repoRef',
|
|
278
|
-
subjectDid: subject.did,
|
|
279
|
-
subjectUri: null,
|
|
280
|
-
subjectCid: null,
|
|
281
|
-
}
|
|
282
|
-
if (subjectBlobCids?.length) {
|
|
283
|
-
throw new InvalidRequestError('Blobs do not apply to repo subjects')
|
|
284
|
-
}
|
|
285
|
-
} else {
|
|
286
|
-
// Allowing records/blobs that may not exist: may have been deleted but needs to remain actionable.
|
|
287
|
-
subjectInfo = {
|
|
288
|
-
subjectType: 'com.atproto.repo.strongRef',
|
|
289
|
-
subjectDid: subject.uri.host,
|
|
290
|
-
subjectUri: subject.uri.toString(),
|
|
291
|
-
subjectCid: subject.cid.toString(),
|
|
292
|
-
}
|
|
230
|
+
const meta: Record<string, string | boolean> = {}
|
|
231
|
+
|
|
232
|
+
if (isModEventReport(event)) {
|
|
233
|
+
meta.reportType = event.reportType
|
|
293
234
|
}
|
|
294
235
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
236
|
+
if (isModEventComment(event) && event.sticky) {
|
|
237
|
+
meta.sticky = event.sticky
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (isModEventEmail(event)) {
|
|
241
|
+
meta.subjectLine = event.subjectLine
|
|
301
242
|
}
|
|
302
|
-
|
|
303
|
-
|
|
243
|
+
|
|
244
|
+
const modEvent = await this.db.db
|
|
245
|
+
.insertInto('moderation_event')
|
|
304
246
|
.values({
|
|
305
|
-
|
|
306
|
-
|
|
247
|
+
comment: event.comment ? `${event.comment}` : null,
|
|
248
|
+
action: event.$type as ModerationEvent['action'],
|
|
307
249
|
createdAt: createdAt.toISOString(),
|
|
308
250
|
createdBy,
|
|
309
251
|
createLabelVals,
|
|
310
252
|
negateLabelVals,
|
|
311
|
-
durationInHours
|
|
253
|
+
durationInHours: event.durationInHours
|
|
254
|
+
? Number(event.durationInHours)
|
|
255
|
+
: null,
|
|
256
|
+
meta,
|
|
312
257
|
expiresAt:
|
|
313
|
-
|
|
314
|
-
|
|
258
|
+
(isModEventTakedown(event) || isModEventMute(event)) &&
|
|
259
|
+
event.durationInHours
|
|
260
|
+
? addHoursToDate(event.durationInHours, createdAt).toISOString()
|
|
315
261
|
: undefined,
|
|
316
262
|
...subjectInfo,
|
|
317
263
|
})
|
|
318
264
|
.returningAll()
|
|
319
265
|
.executeTakeFirstOrThrow()
|
|
320
266
|
|
|
321
|
-
|
|
322
|
-
const blobActions = await this.getCurrentActions({
|
|
323
|
-
cids: subjectBlobCids,
|
|
324
|
-
})
|
|
325
|
-
if (blobActions.length) {
|
|
326
|
-
throw new InvalidRequestError(
|
|
327
|
-
`Blob already has an active action: #${blobActions[0].id}`,
|
|
328
|
-
'SubjectHasAction',
|
|
329
|
-
)
|
|
330
|
-
}
|
|
267
|
+
await adjustModerationSubjectStatus(this.db, modEvent, subjectBlobCids)
|
|
331
268
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
269
|
+
return modEvent
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async getLastReversibleEventForSubject({
|
|
273
|
+
did,
|
|
274
|
+
muteUntil,
|
|
275
|
+
recordPath,
|
|
276
|
+
suspendUntil,
|
|
277
|
+
}: ModerationSubjectStatusRow) {
|
|
278
|
+
const isSuspended = suspendUntil && new Date(suspendUntil) < new Date()
|
|
279
|
+
const isMuted = muteUntil && new Date(muteUntil) < new Date()
|
|
280
|
+
|
|
281
|
+
// If the subject is neither suspended nor muted don't bother finding the last reversible event
|
|
282
|
+
// Ideally, this should never happen because the caller of this method should only call this
|
|
283
|
+
// after ensuring that the suspended or muted subjects are being reversed
|
|
284
|
+
if (!isSuspended && !isMuted) {
|
|
285
|
+
return null
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let builder = this.db.db
|
|
289
|
+
.selectFrom('moderation_event')
|
|
290
|
+
.where('subjectDid', '=', did)
|
|
291
|
+
|
|
292
|
+
if (recordPath) {
|
|
293
|
+
builder = builder.where('subjectUri', 'like', `%${recordPath}%`)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Means the subject was suspended and needs to be unsuspended
|
|
297
|
+
if (isSuspended) {
|
|
298
|
+
builder = builder
|
|
299
|
+
.where('action', '=', 'com.atproto.admin.defs#modEventTakedown')
|
|
300
|
+
.where('durationInHours', 'is not', null)
|
|
301
|
+
}
|
|
302
|
+
if (isMuted) {
|
|
303
|
+
builder = builder
|
|
304
|
+
.where('action', '=', 'com.atproto.admin.defs#modEventMute')
|
|
305
|
+
.where('durationInHours', 'is not', null)
|
|
341
306
|
}
|
|
342
307
|
|
|
343
|
-
return
|
|
308
|
+
return await builder
|
|
309
|
+
.orderBy('id', 'desc')
|
|
310
|
+
.selectAll()
|
|
311
|
+
.limit(1)
|
|
312
|
+
.executeTakeFirst()
|
|
344
313
|
}
|
|
345
314
|
|
|
346
|
-
async
|
|
347
|
-
const
|
|
348
|
-
.selectFrom('
|
|
349
|
-
.where('
|
|
350
|
-
.
|
|
351
|
-
.where('reversedAt', 'is', null)
|
|
315
|
+
async getSubjectsDueForReversal(): Promise<ModerationSubjectStatusRow[]> {
|
|
316
|
+
const subjectsDueForReversal = await this.db.db
|
|
317
|
+
.selectFrom('moderation_subject_status')
|
|
318
|
+
.where('suspendUntil', '<', new Date().toISOString())
|
|
319
|
+
.orWhere('muteUntil', '<', new Date().toISOString())
|
|
352
320
|
.selectAll()
|
|
353
321
|
.execute()
|
|
354
322
|
|
|
355
|
-
return
|
|
323
|
+
return subjectsDueForReversal
|
|
356
324
|
}
|
|
357
325
|
|
|
358
|
-
async
|
|
359
|
-
id,
|
|
326
|
+
async revertState({
|
|
360
327
|
createdBy,
|
|
361
328
|
createdAt,
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
329
|
+
comment,
|
|
330
|
+
action,
|
|
331
|
+
subject,
|
|
332
|
+
}: ReversibleModerationEvent): Promise<{
|
|
333
|
+
result: ModerationEventRow
|
|
365
334
|
restored?: TakedownSubjects
|
|
366
335
|
}> {
|
|
336
|
+
const isRevertingTakedown =
|
|
337
|
+
action === 'com.atproto.admin.defs#modEventTakedown'
|
|
367
338
|
this.db.assertTransaction()
|
|
368
|
-
const result = await this.
|
|
369
|
-
|
|
339
|
+
const result = await this.logEvent({
|
|
340
|
+
event: {
|
|
341
|
+
$type: isRevertingTakedown
|
|
342
|
+
? 'com.atproto.admin.defs#modEventReverseTakedown'
|
|
343
|
+
: 'com.atproto.admin.defs#modEventUnmute',
|
|
344
|
+
comment: comment ?? undefined,
|
|
345
|
+
},
|
|
370
346
|
createdAt,
|
|
371
347
|
createdBy,
|
|
372
|
-
|
|
348
|
+
subject,
|
|
373
349
|
})
|
|
374
350
|
|
|
375
351
|
let restored: TakedownSubjects | undefined
|
|
376
352
|
|
|
353
|
+
if (!isRevertingTakedown) {
|
|
354
|
+
return { result, restored }
|
|
355
|
+
}
|
|
356
|
+
|
|
377
357
|
if (
|
|
378
|
-
result.action === TAKEDOWN &&
|
|
379
358
|
result.subjectType === 'com.atproto.admin.defs#repoRef' &&
|
|
380
359
|
result.subjectDid
|
|
381
360
|
) {
|
|
@@ -394,7 +373,6 @@ export class ModerationService {
|
|
|
394
373
|
}
|
|
395
374
|
|
|
396
375
|
if (
|
|
397
|
-
result.action === TAKEDOWN &&
|
|
398
376
|
result.subjectType === 'com.atproto.repo.strongRef' &&
|
|
399
377
|
result.subjectUri
|
|
400
378
|
) {
|
|
@@ -403,11 +381,14 @@ export class ModerationService {
|
|
|
403
381
|
uri,
|
|
404
382
|
})
|
|
405
383
|
const did = uri.hostname
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
.
|
|
409
|
-
.
|
|
410
|
-
.
|
|
384
|
+
// TODO: MOD_EVENT This bit needs testing
|
|
385
|
+
const subjectStatus = await this.db.db
|
|
386
|
+
.selectFrom('moderation_subject_status')
|
|
387
|
+
.where('did', '=', uri.host)
|
|
388
|
+
.where('recordPath', '=', `${uri.collection}/${uri.rkey}`)
|
|
389
|
+
.select('blobCids')
|
|
390
|
+
.executeTakeFirst()
|
|
391
|
+
const blobCids = subjectStatus?.blobCids || []
|
|
411
392
|
restored = {
|
|
412
393
|
did,
|
|
413
394
|
subjects: [
|
|
@@ -416,10 +397,10 @@ export class ModerationService {
|
|
|
416
397
|
uri: result.subjectUri,
|
|
417
398
|
cid: result.subjectCid ?? '',
|
|
418
399
|
},
|
|
419
|
-
...
|
|
400
|
+
...blobCids.map((cid) => ({
|
|
420
401
|
$type: 'com.atproto.admin.defs#repoBlobRef',
|
|
421
402
|
did,
|
|
422
|
-
cid
|
|
403
|
+
cid,
|
|
423
404
|
recordUri: result.subjectUri,
|
|
424
405
|
})),
|
|
425
406
|
],
|
|
@@ -429,29 +410,6 @@ export class ModerationService {
|
|
|
429
410
|
return { result, restored }
|
|
430
411
|
}
|
|
431
412
|
|
|
432
|
-
async logReverseAction(
|
|
433
|
-
info: ReversibleModerationAction,
|
|
434
|
-
): Promise<ModerationActionRow> {
|
|
435
|
-
const { id, createdBy, reason, createdAt = new Date() } = info
|
|
436
|
-
|
|
437
|
-
const result = await this.db.db
|
|
438
|
-
.updateTable('moderation_action')
|
|
439
|
-
.where('id', '=', id)
|
|
440
|
-
.set({
|
|
441
|
-
reversedAt: createdAt.toISOString(),
|
|
442
|
-
reversedBy: createdBy,
|
|
443
|
-
reversedReason: reason,
|
|
444
|
-
})
|
|
445
|
-
.returningAll()
|
|
446
|
-
.executeTakeFirst()
|
|
447
|
-
|
|
448
|
-
if (!result) {
|
|
449
|
-
throw new InvalidRequestError('Moderation action not found')
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
return result
|
|
453
|
-
}
|
|
454
|
-
|
|
455
413
|
async takedownRepo(info: {
|
|
456
414
|
takedownId: number
|
|
457
415
|
did: string
|
|
@@ -536,64 +494,13 @@ export class ModerationService {
|
|
|
536
494
|
.execute()
|
|
537
495
|
}
|
|
538
496
|
|
|
539
|
-
async resolveReports(info: {
|
|
540
|
-
reportIds: number[]
|
|
541
|
-
actionId: number
|
|
542
|
-
createdBy: string
|
|
543
|
-
createdAt?: Date
|
|
544
|
-
}): Promise<void> {
|
|
545
|
-
const { reportIds, actionId, createdBy, createdAt = new Date() } = info
|
|
546
|
-
const action = await this.getActionOrThrow(actionId)
|
|
547
|
-
|
|
548
|
-
if (!reportIds.length) return
|
|
549
|
-
const reports = await this.db.db
|
|
550
|
-
.selectFrom('moderation_report')
|
|
551
|
-
.where('id', 'in', reportIds)
|
|
552
|
-
.select(['id', 'subjectType', 'subjectDid', 'subjectUri'])
|
|
553
|
-
.execute()
|
|
554
|
-
|
|
555
|
-
reportIds.forEach((reportId) => {
|
|
556
|
-
const report = reports.find((r) => r.id === reportId)
|
|
557
|
-
if (!report) throw new InvalidRequestError('Report not found')
|
|
558
|
-
if (action.subjectDid !== report.subjectDid) {
|
|
559
|
-
// Report and action always must target repo or record from the same did
|
|
560
|
-
throw new InvalidRequestError(
|
|
561
|
-
`Report ${report.id} cannot be resolved by action`,
|
|
562
|
-
)
|
|
563
|
-
}
|
|
564
|
-
if (
|
|
565
|
-
action.subjectType === 'com.atproto.repo.strongRef' &&
|
|
566
|
-
report.subjectType === 'com.atproto.repo.strongRef' &&
|
|
567
|
-
report.subjectUri !== action.subjectUri
|
|
568
|
-
) {
|
|
569
|
-
// If report and action are both for a record, they must be for the same record
|
|
570
|
-
throw new InvalidRequestError(
|
|
571
|
-
`Report ${report.id} cannot be resolved by action`,
|
|
572
|
-
)
|
|
573
|
-
}
|
|
574
|
-
})
|
|
575
|
-
|
|
576
|
-
await this.db.db
|
|
577
|
-
.insertInto('moderation_report_resolution')
|
|
578
|
-
.values(
|
|
579
|
-
reportIds.map((reportId) => ({
|
|
580
|
-
reportId,
|
|
581
|
-
actionId,
|
|
582
|
-
createdAt: createdAt.toISOString(),
|
|
583
|
-
createdBy,
|
|
584
|
-
})),
|
|
585
|
-
)
|
|
586
|
-
.onConflict((oc) => oc.doNothing())
|
|
587
|
-
.execute()
|
|
588
|
-
}
|
|
589
|
-
|
|
590
497
|
async report(info: {
|
|
591
|
-
reasonType:
|
|
498
|
+
reasonType: NonNullable<ModerationEventRow['meta']>['reportType']
|
|
592
499
|
reason?: string
|
|
593
500
|
subject: { did: string } | { uri: AtUri; cid: CID }
|
|
594
501
|
reportedBy: string
|
|
595
502
|
createdAt?: Date
|
|
596
|
-
}): Promise<
|
|
503
|
+
}): Promise<ModerationEventRow> {
|
|
597
504
|
const {
|
|
598
505
|
reasonType,
|
|
599
506
|
reason,
|
|
@@ -602,39 +509,144 @@ export class ModerationService {
|
|
|
602
509
|
subject,
|
|
603
510
|
} = info
|
|
604
511
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
512
|
+
const event = await this.logEvent({
|
|
513
|
+
event: {
|
|
514
|
+
$type: 'com.atproto.admin.defs#modEventReport',
|
|
515
|
+
reportType: reasonType,
|
|
516
|
+
comment: reason,
|
|
517
|
+
},
|
|
518
|
+
createdBy: reportedBy,
|
|
519
|
+
subject,
|
|
520
|
+
createdAt,
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
return event
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async getSubjectStatuses({
|
|
527
|
+
cursor,
|
|
528
|
+
limit = 50,
|
|
529
|
+
takendown,
|
|
530
|
+
reviewState,
|
|
531
|
+
reviewedAfter,
|
|
532
|
+
reviewedBefore,
|
|
533
|
+
reportedAfter,
|
|
534
|
+
reportedBefore,
|
|
535
|
+
includeMuted,
|
|
536
|
+
ignoreSubjects,
|
|
537
|
+
sortDirection,
|
|
538
|
+
lastReviewedBy,
|
|
539
|
+
sortField,
|
|
540
|
+
subject,
|
|
541
|
+
}: {
|
|
542
|
+
cursor?: string
|
|
543
|
+
limit?: number
|
|
544
|
+
takendown?: boolean
|
|
545
|
+
reviewedBefore?: string
|
|
546
|
+
reviewState?: ModerationSubjectStatusRow['reviewState']
|
|
547
|
+
reviewedAfter?: string
|
|
548
|
+
reportedAfter?: string
|
|
549
|
+
reportedBefore?: string
|
|
550
|
+
includeMuted?: boolean
|
|
551
|
+
subject?: string
|
|
552
|
+
ignoreSubjects?: string[]
|
|
553
|
+
sortDirection: 'asc' | 'desc'
|
|
554
|
+
lastReviewedBy?: string
|
|
555
|
+
sortField: 'lastReviewedAt' | 'lastReportedAt'
|
|
556
|
+
}) {
|
|
557
|
+
let builder = this.db.db
|
|
558
|
+
.selectFrom('moderation_subject_status')
|
|
559
|
+
.leftJoin('actor', 'actor.did', 'moderation_subject_status.did')
|
|
560
|
+
|
|
561
|
+
if (subject) {
|
|
562
|
+
const subjectInfo = getStatusIdentifierFromSubject(subject)
|
|
563
|
+
builder = builder
|
|
564
|
+
.where('moderation_subject_status.did', '=', subjectInfo.did)
|
|
565
|
+
.where((qb) =>
|
|
566
|
+
subjectInfo.recordPath
|
|
567
|
+
? qb.where('recordPath', '=', subjectInfo.recordPath)
|
|
568
|
+
: qb.where('recordPath', '=', ''),
|
|
569
|
+
)
|
|
623
570
|
}
|
|
624
571
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
572
|
+
if (ignoreSubjects?.length) {
|
|
573
|
+
builder = builder
|
|
574
|
+
.where('moderation_subject_status.did', 'not in', ignoreSubjects)
|
|
575
|
+
.where('recordPath', 'not in', ignoreSubjects)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (reviewState) {
|
|
579
|
+
builder = builder.where('reviewState', '=', reviewState)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (lastReviewedBy) {
|
|
583
|
+
builder = builder.where('lastReviewedBy', '=', lastReviewedBy)
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (reviewedAfter) {
|
|
587
|
+
builder = builder.where('lastReviewedAt', '>', reviewedAfter)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (reviewedBefore) {
|
|
591
|
+
builder = builder.where('lastReviewedAt', '<', reviewedBefore)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (reportedAfter) {
|
|
595
|
+
builder = builder.where('lastReviewedAt', '>', reportedAfter)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (reportedBefore) {
|
|
599
|
+
builder = builder.where('lastReportedAt', '<', reportedBefore)
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (takendown) {
|
|
603
|
+
builder = builder.where('takendown', '=', true)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (!includeMuted) {
|
|
607
|
+
builder = builder.where((qb) =>
|
|
608
|
+
qb
|
|
609
|
+
.where('muteUntil', '<', new Date().toISOString())
|
|
610
|
+
.orWhere('muteUntil', 'is', null),
|
|
611
|
+
)
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const { ref } = this.db.db.dynamic
|
|
615
|
+
const keyset = new StatusKeyset(
|
|
616
|
+
ref(`moderation_subject_status.${sortField}`),
|
|
617
|
+
ref('moderation_subject_status.id'),
|
|
618
|
+
)
|
|
619
|
+
const paginatedBuilder = paginate(builder, {
|
|
620
|
+
limit,
|
|
621
|
+
cursor,
|
|
622
|
+
keyset,
|
|
623
|
+
direction: sortDirection,
|
|
624
|
+
tryIndex: true,
|
|
625
|
+
nullsLast: true,
|
|
626
|
+
})
|
|
627
|
+
|
|
628
|
+
const results = await paginatedBuilder
|
|
629
|
+
.select('actor.handle as handle')
|
|
630
|
+
.selectAll('moderation_subject_status')
|
|
631
|
+
.execute()
|
|
632
|
+
|
|
633
|
+
return { statuses: results, cursor: keyset.packFromResult(results) }
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async isSubjectTakendown(
|
|
637
|
+
subject: { did: string } | { uri: AtUri },
|
|
638
|
+
): Promise<boolean> {
|
|
639
|
+
const { did, recordPath } = getStatusIdentifierFromSubject(
|
|
640
|
+
'did' in subject ? subject.did : subject.uri,
|
|
641
|
+
)
|
|
642
|
+
let builder = this.db.db
|
|
643
|
+
.selectFrom('moderation_subject_status')
|
|
644
|
+
.where('did', '=', did)
|
|
645
|
+
.where('recordPath', '=', recordPath || '')
|
|
646
|
+
|
|
647
|
+
const result = await builder.select('takendown').executeTakeFirst()
|
|
636
648
|
|
|
637
|
-
return
|
|
649
|
+
return !!result?.takendown
|
|
638
650
|
}
|
|
639
651
|
}
|
|
640
652
|
|
|
@@ -642,30 +654,3 @@ export type TakedownSubjects = {
|
|
|
642
654
|
did: string
|
|
643
655
|
subjects: (RepoRef | RepoBlobRef | StrongRef)[]
|
|
644
656
|
}
|
|
645
|
-
|
|
646
|
-
export type ModerationActionRow = Selectable<ModerationAction>
|
|
647
|
-
export type ReversibleModerationAction = Pick<
|
|
648
|
-
ModerationActionRow,
|
|
649
|
-
'id' | 'createdBy' | 'reason'
|
|
650
|
-
> & {
|
|
651
|
-
createdAt?: Date
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
export type ModerationReportRow = Selectable<ModerationReport>
|
|
655
|
-
export type ModerationReportRowWithHandle = ModerationReportRow & {
|
|
656
|
-
handle?: string | null
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
export type SubjectInfo =
|
|
660
|
-
| {
|
|
661
|
-
subjectType: 'com.atproto.admin.defs#repoRef'
|
|
662
|
-
subjectDid: string
|
|
663
|
-
subjectUri: null
|
|
664
|
-
subjectCid: null
|
|
665
|
-
}
|
|
666
|
-
| {
|
|
667
|
-
subjectType: 'com.atproto.repo.strongRef'
|
|
668
|
-
subjectDid: string
|
|
669
|
-
subjectUri: string
|
|
670
|
-
subjectCid: string
|
|
671
|
-
}
|