@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
|
@@ -61,7 +61,6 @@ export class AutoModerator {
|
|
|
61
61
|
'moderation service not properly configured',
|
|
62
62
|
)
|
|
63
63
|
}
|
|
64
|
-
|
|
65
64
|
this.imgLabeler = hiveApiKey ? new HiveLabeler(hiveApiKey, ctx) : undefined
|
|
66
65
|
this.textLabeler = new KeywordLabeler(ctx.cfg.labelerKeywords)
|
|
67
66
|
if (abyssEndpoint && abyssPassword) {
|
|
@@ -157,18 +156,22 @@ export class AutoModerator {
|
|
|
157
156
|
if (!this.textFlagger) return
|
|
158
157
|
const matches = this.textFlagger.getMatches(text)
|
|
159
158
|
if (matches.length < 1) return
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
159
|
+
await this.ctx.db.transaction(async (dbTxn) => {
|
|
160
|
+
if (!this.services.moderation) {
|
|
161
|
+
log.error(
|
|
162
|
+
{ subject, text, matches },
|
|
163
|
+
'no moderation service setup to flag record text',
|
|
164
|
+
)
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
return this.services.moderation(dbTxn).report({
|
|
168
|
+
reasonType: REASONOTHER,
|
|
169
|
+
reason: `Automatically flagged for possible slurs: ${matches.join(
|
|
170
|
+
', ',
|
|
171
|
+
)}`,
|
|
172
|
+
subject,
|
|
173
|
+
reportedBy: this.ctx.cfg.labelerDid,
|
|
174
|
+
})
|
|
172
175
|
})
|
|
173
176
|
}
|
|
174
177
|
|
|
@@ -244,15 +247,17 @@ export class AutoModerator {
|
|
|
244
247
|
}
|
|
245
248
|
|
|
246
249
|
if (this.pushAgent) {
|
|
247
|
-
await this.pushAgent.com.atproto.admin.
|
|
248
|
-
|
|
250
|
+
await this.pushAgent.com.atproto.admin.emitModerationEvent({
|
|
251
|
+
event: {
|
|
252
|
+
$type: 'com.atproto.admin.defs#modEventTakedown',
|
|
253
|
+
comment: takedownReason,
|
|
254
|
+
},
|
|
249
255
|
subject: {
|
|
250
256
|
$type: 'com.atproto.repo.strongRef',
|
|
251
257
|
uri: uri.toString(),
|
|
252
258
|
cid: recordCid.toString(),
|
|
253
259
|
},
|
|
254
260
|
subjectBlobCids: takedownCids.map((c) => c.toString()),
|
|
255
|
-
reason: takedownReason,
|
|
256
261
|
createdBy: this.ctx.cfg.labelerDid,
|
|
257
262
|
})
|
|
258
263
|
} else {
|
|
@@ -261,11 +266,13 @@ export class AutoModerator {
|
|
|
261
266
|
throw new Error('no mod push agent or uri invalidator setup')
|
|
262
267
|
}
|
|
263
268
|
const modSrvc = this.services.moderation(dbTxn)
|
|
264
|
-
const action = await modSrvc.
|
|
265
|
-
|
|
269
|
+
const action = await modSrvc.logEvent({
|
|
270
|
+
event: {
|
|
271
|
+
$type: 'com.atproto.admin.defs#modEventTakedown',
|
|
272
|
+
comment: takedownReason,
|
|
273
|
+
},
|
|
266
274
|
subject: { uri, cid: recordCid },
|
|
267
275
|
subjectBlobCids: takedownCids,
|
|
268
|
-
reason: takedownReason,
|
|
269
276
|
createdBy: this.ctx.cfg.labelerDid,
|
|
270
277
|
})
|
|
271
278
|
await modSrvc.takedownRecord({
|
package/src/context.ts
CHANGED
|
@@ -94,6 +94,10 @@ export class AppContext {
|
|
|
94
94
|
return auth.authVerifier(this.idResolver, { aud: null })
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
get authOptionalVerifierAnyAudience() {
|
|
98
|
+
return auth.authOptionalVerifier(this.idResolver, { aud: null })
|
|
99
|
+
}
|
|
100
|
+
|
|
97
101
|
get authOptionalVerifier() {
|
|
98
102
|
return auth.authOptionalVerifier(this.idResolver, {
|
|
99
103
|
aud: this.cfg.serverDid,
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Kysely } from 'kysely'
|
|
2
|
+
|
|
3
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
4
|
+
await db.schema
|
|
5
|
+
.createTable('moderation_event')
|
|
6
|
+
.addColumn('id', 'serial', (col) => col.primaryKey())
|
|
7
|
+
.addColumn('action', 'varchar', (col) => col.notNull())
|
|
8
|
+
.addColumn('subjectType', 'varchar', (col) => col.notNull())
|
|
9
|
+
.addColumn('subjectDid', 'varchar', (col) => col.notNull())
|
|
10
|
+
.addColumn('subjectUri', 'varchar')
|
|
11
|
+
.addColumn('subjectCid', 'varchar')
|
|
12
|
+
.addColumn('comment', 'text')
|
|
13
|
+
.addColumn('meta', 'jsonb')
|
|
14
|
+
.addColumn('createdAt', 'varchar', (col) => col.notNull())
|
|
15
|
+
.addColumn('createdBy', 'varchar', (col) => col.notNull())
|
|
16
|
+
.addColumn('reversedAt', 'varchar')
|
|
17
|
+
.addColumn('reversedBy', 'varchar')
|
|
18
|
+
.addColumn('durationInHours', 'integer')
|
|
19
|
+
.addColumn('expiresAt', 'varchar')
|
|
20
|
+
.addColumn('reversedReason', 'text')
|
|
21
|
+
.addColumn('createLabelVals', 'varchar')
|
|
22
|
+
.addColumn('negateLabelVals', 'varchar')
|
|
23
|
+
.addColumn('legacyRefId', 'integer')
|
|
24
|
+
.execute()
|
|
25
|
+
await db.schema
|
|
26
|
+
.createTable('moderation_subject_status')
|
|
27
|
+
.addColumn('id', 'serial', (col) => col.primaryKey())
|
|
28
|
+
|
|
29
|
+
// Identifiers
|
|
30
|
+
.addColumn('did', 'varchar', (col) => col.notNull())
|
|
31
|
+
// Default to '' so that we can apply unique constraints on did and recordPath columns
|
|
32
|
+
.addColumn('recordPath', 'varchar', (col) => col.notNull().defaultTo(''))
|
|
33
|
+
.addColumn('blobCids', 'jsonb')
|
|
34
|
+
.addColumn('recordCid', 'varchar')
|
|
35
|
+
|
|
36
|
+
// human review team state
|
|
37
|
+
.addColumn('reviewState', 'varchar', (col) => col.notNull())
|
|
38
|
+
.addColumn('comment', 'varchar')
|
|
39
|
+
.addColumn('muteUntil', 'varchar')
|
|
40
|
+
.addColumn('lastReviewedAt', 'varchar')
|
|
41
|
+
.addColumn('lastReviewedBy', 'varchar')
|
|
42
|
+
|
|
43
|
+
// report state
|
|
44
|
+
.addColumn('lastReportedAt', 'varchar')
|
|
45
|
+
|
|
46
|
+
// visibility/intervention state
|
|
47
|
+
.addColumn('takendown', 'boolean', (col) => col.defaultTo(false).notNull())
|
|
48
|
+
.addColumn('suspendUntil', 'varchar')
|
|
49
|
+
|
|
50
|
+
// timestamps
|
|
51
|
+
.addColumn('createdAt', 'varchar', (col) => col.notNull())
|
|
52
|
+
.addColumn('updatedAt', 'varchar', (col) => col.notNull())
|
|
53
|
+
.addUniqueConstraint('moderation_status_unique_idx', ['did', 'recordPath'])
|
|
54
|
+
.execute()
|
|
55
|
+
|
|
56
|
+
await db.schema
|
|
57
|
+
.createIndex('moderation_subject_status_blob_cids_idx')
|
|
58
|
+
.on('moderation_subject_status')
|
|
59
|
+
.using('gin')
|
|
60
|
+
.column('blobCids')
|
|
61
|
+
.execute()
|
|
62
|
+
|
|
63
|
+
// Move foreign keys from moderation_action to moderation_event
|
|
64
|
+
await db.schema
|
|
65
|
+
.alterTable('record')
|
|
66
|
+
.dropConstraint('record_takedown_id_fkey')
|
|
67
|
+
.execute()
|
|
68
|
+
await db.schema
|
|
69
|
+
.alterTable('actor')
|
|
70
|
+
.dropConstraint('actor_takedown_id_fkey')
|
|
71
|
+
.execute()
|
|
72
|
+
await db.schema
|
|
73
|
+
.alterTable('actor')
|
|
74
|
+
.addForeignKeyConstraint(
|
|
75
|
+
'actor_takedown_id_fkey',
|
|
76
|
+
['takedownId'],
|
|
77
|
+
'moderation_event',
|
|
78
|
+
['id'],
|
|
79
|
+
)
|
|
80
|
+
.execute()
|
|
81
|
+
await db.schema
|
|
82
|
+
.alterTable('record')
|
|
83
|
+
.addForeignKeyConstraint(
|
|
84
|
+
'record_takedown_id_fkey',
|
|
85
|
+
['takedownId'],
|
|
86
|
+
'moderation_event',
|
|
87
|
+
['id'],
|
|
88
|
+
)
|
|
89
|
+
.execute()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
93
|
+
await db.schema.dropTable('moderation_event').execute()
|
|
94
|
+
await db.schema.dropTable('moderation_subject_status').execute()
|
|
95
|
+
|
|
96
|
+
// Revert foreign key constraints
|
|
97
|
+
await db.schema
|
|
98
|
+
.alterTable('record')
|
|
99
|
+
.dropConstraint('record_takedown_id_fkey')
|
|
100
|
+
.execute()
|
|
101
|
+
await db.schema
|
|
102
|
+
.alterTable('actor')
|
|
103
|
+
.dropConstraint('actor_takedown_id_fkey')
|
|
104
|
+
.execute()
|
|
105
|
+
await db.schema
|
|
106
|
+
.alterTable('actor')
|
|
107
|
+
.addForeignKeyConstraint(
|
|
108
|
+
'actor_takedown_id_fkey',
|
|
109
|
+
['takedownId'],
|
|
110
|
+
'moderation_action',
|
|
111
|
+
['id'],
|
|
112
|
+
)
|
|
113
|
+
.execute()
|
|
114
|
+
await db.schema
|
|
115
|
+
.alterTable('record')
|
|
116
|
+
.addForeignKeyConstraint(
|
|
117
|
+
'record_takedown_id_fkey',
|
|
118
|
+
['takedownId'],
|
|
119
|
+
'moderation_action',
|
|
120
|
+
['id'],
|
|
121
|
+
)
|
|
122
|
+
.execute()
|
|
123
|
+
}
|
|
@@ -30,3 +30,4 @@ export * as _20230904T211011773Z from './20230904T211011773Z-block-lists'
|
|
|
30
30
|
export * as _20230906T222220386Z from './20230906T222220386Z-thread-gating'
|
|
31
31
|
export * as _20230920T213858047Z from './20230920T213858047Z-add-tags-to-post'
|
|
32
32
|
export * as _20230929T192920807Z from './20230929T192920807Z-record-cursor-indexes'
|
|
33
|
+
export * as _20231003T202833377Z from './20231003T202833377Z-create-moderation-subject-status'
|
package/src/db/pagination.ts
CHANGED
|
@@ -117,13 +117,36 @@ export const paginate = <
|
|
|
117
117
|
direction?: 'asc' | 'desc'
|
|
118
118
|
keyset: K
|
|
119
119
|
tryIndex?: boolean
|
|
120
|
+
// By default, pg does nullsFirst
|
|
121
|
+
nullsLast?: boolean
|
|
120
122
|
},
|
|
121
123
|
): QB => {
|
|
122
|
-
const {
|
|
124
|
+
const {
|
|
125
|
+
limit,
|
|
126
|
+
cursor,
|
|
127
|
+
keyset,
|
|
128
|
+
direction = 'desc',
|
|
129
|
+
tryIndex,
|
|
130
|
+
nullsLast,
|
|
131
|
+
} = opts
|
|
123
132
|
const keysetSql = keyset.getSql(keyset.unpack(cursor), direction, tryIndex)
|
|
124
133
|
return qb
|
|
125
134
|
.if(!!limit, (q) => q.limit(limit as number))
|
|
126
|
-
.
|
|
127
|
-
|
|
135
|
+
.if(!nullsLast, (q) =>
|
|
136
|
+
q.orderBy(keyset.primary, direction).orderBy(keyset.secondary, direction),
|
|
137
|
+
)
|
|
138
|
+
.if(!!nullsLast, (q) =>
|
|
139
|
+
q
|
|
140
|
+
.orderBy(
|
|
141
|
+
direction === 'asc'
|
|
142
|
+
? sql`${keyset.primary} asc nulls last`
|
|
143
|
+
: sql`${keyset.primary} desc nulls last`,
|
|
144
|
+
)
|
|
145
|
+
.orderBy(
|
|
146
|
+
direction === 'asc'
|
|
147
|
+
? sql`${keyset.secondary} asc nulls last`
|
|
148
|
+
: sql`${keyset.secondary} desc nulls last`,
|
|
149
|
+
),
|
|
150
|
+
)
|
|
128
151
|
.if(!!keysetSql, (qb) => (keysetSql ? qb.where(keysetSql) : qb)) as QB
|
|
129
152
|
}
|
package/src/db/{periodic-moderation-action-reversal.ts → periodic-moderation-event-reversal.ts}
RENAMED
|
@@ -2,13 +2,15 @@ import { wait } from '@atproto/common'
|
|
|
2
2
|
import { Leader } from './leader'
|
|
3
3
|
import { dbLogger } from '../logger'
|
|
4
4
|
import AppContext from '../context'
|
|
5
|
+
import { AtUri } from '@atproto/api'
|
|
6
|
+
import { ModerationSubjectStatusRow } from '../services/moderation/types'
|
|
7
|
+
import { CID } from 'multiformats/cid'
|
|
5
8
|
import AtpAgent from '@atproto/api'
|
|
6
|
-
import {
|
|
7
|
-
import { ModerationActionRow } from '../services/moderation'
|
|
9
|
+
import { retryHttp } from '../util/retry'
|
|
8
10
|
|
|
9
11
|
export const MODERATION_ACTION_REVERSAL_ID = 1011
|
|
10
12
|
|
|
11
|
-
export class
|
|
13
|
+
export class PeriodicModerationEventReversal {
|
|
12
14
|
leader = new Leader(
|
|
13
15
|
MODERATION_ACTION_REVERSAL_ID,
|
|
14
16
|
this.appContext.db.getPrimary(),
|
|
@@ -20,48 +22,50 @@ export class PeriodicModerationActionReversal {
|
|
|
20
22
|
this.pushAgent = appContext.moderationPushAgent
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
async reverseLabels(labelTxn: LabelService, actionRow: ModerationActionRow) {
|
|
25
|
-
let uri: string
|
|
26
|
-
let cid: string | null = null
|
|
27
|
-
|
|
28
|
-
if (actionRow.subjectUri && actionRow.subjectCid) {
|
|
29
|
-
uri = actionRow.subjectUri
|
|
30
|
-
cid = actionRow.subjectCid
|
|
31
|
-
} else {
|
|
32
|
-
uri = actionRow.subjectDid
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
await labelTxn.formatAndCreate(this.appContext.cfg.labelerDid, uri, cid, {
|
|
36
|
-
create: actionRow.negateLabelVals
|
|
37
|
-
? actionRow.negateLabelVals.split(' ')
|
|
38
|
-
: undefined,
|
|
39
|
-
negate: actionRow.createLabelVals
|
|
40
|
-
? actionRow.createLabelVals.split(' ')
|
|
41
|
-
: undefined,
|
|
42
|
-
})
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async revertAction(actionRow: ModerationActionRow) {
|
|
46
|
-
const reverseAction = {
|
|
47
|
-
id: actionRow.id,
|
|
48
|
-
createdBy: actionRow.createdBy,
|
|
49
|
-
createdAt: new Date(),
|
|
50
|
-
reason: `[SCHEDULED_REVERSAL] Reverting action as originally scheduled`,
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (this.pushAgent) {
|
|
54
|
-
await this.pushAgent.com.atproto.admin.reverseModerationAction(
|
|
55
|
-
reverseAction,
|
|
56
|
-
)
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
|
|
25
|
+
async revertState(eventRow: ModerationSubjectStatusRow) {
|
|
60
26
|
await this.appContext.db.getPrimary().transaction(async (dbTxn) => {
|
|
61
27
|
const moderationTxn = this.appContext.services.moderation(dbTxn)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
28
|
+
const originalEvent =
|
|
29
|
+
await moderationTxn.getLastReversibleEventForSubject(eventRow)
|
|
30
|
+
if (originalEvent) {
|
|
31
|
+
const { restored } = await moderationTxn.revertState({
|
|
32
|
+
action: originalEvent.action,
|
|
33
|
+
createdBy: originalEvent.createdBy,
|
|
34
|
+
comment:
|
|
35
|
+
'[SCHEDULED_REVERSAL] Reverting action as originally scheduled',
|
|
36
|
+
subject:
|
|
37
|
+
eventRow.recordPath && eventRow.recordCid
|
|
38
|
+
? {
|
|
39
|
+
uri: AtUri.make(
|
|
40
|
+
eventRow.did,
|
|
41
|
+
...eventRow.recordPath.split('/'),
|
|
42
|
+
),
|
|
43
|
+
cid: CID.parse(eventRow.recordCid),
|
|
44
|
+
}
|
|
45
|
+
: { did: eventRow.did },
|
|
46
|
+
createdAt: new Date(),
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const { pushAgent } = this
|
|
50
|
+
if (
|
|
51
|
+
originalEvent.action === 'com.atproto.admin.defs#modEventTakedown' &&
|
|
52
|
+
restored?.subjects?.length &&
|
|
53
|
+
pushAgent
|
|
54
|
+
) {
|
|
55
|
+
await Promise.allSettled(
|
|
56
|
+
restored.subjects.map((subject) =>
|
|
57
|
+
retryHttp(() =>
|
|
58
|
+
pushAgent.api.com.atproto.admin.updateSubjectStatus({
|
|
59
|
+
subject,
|
|
60
|
+
takedown: {
|
|
61
|
+
applied: false,
|
|
62
|
+
},
|
|
63
|
+
}),
|
|
64
|
+
),
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
65
69
|
})
|
|
66
70
|
}
|
|
67
71
|
|
|
@@ -69,12 +73,12 @@ export class PeriodicModerationActionReversal {
|
|
|
69
73
|
const moderationService = this.appContext.services.moderation(
|
|
70
74
|
this.appContext.db.getPrimary(),
|
|
71
75
|
)
|
|
72
|
-
const
|
|
73
|
-
await moderationService.
|
|
76
|
+
const subjectsDueForReversal =
|
|
77
|
+
await moderationService.getSubjectsDueForReversal()
|
|
74
78
|
|
|
75
79
|
// We shouldn't have too many actions due for reversal at any given time, so running in parallel is probably fine
|
|
76
80
|
// Internally, each reversal runs within its own transaction
|
|
77
|
-
await Promise.all(
|
|
81
|
+
await Promise.all(subjectsDueForReversal.map(this.revertState.bind(this)))
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
async run() {
|
|
@@ -1,76 +1,59 @@
|
|
|
1
1
|
import { Generated } from 'kysely'
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
ESCALATE,
|
|
3
|
+
REVIEWCLOSED,
|
|
4
|
+
REVIEWOPEN,
|
|
5
|
+
REVIEWESCALATED,
|
|
7
6
|
} from '../../lexicon/types/com/atproto/admin/defs'
|
|
8
|
-
import {
|
|
9
|
-
REASONOTHER,
|
|
10
|
-
REASONSPAM,
|
|
11
|
-
REASONMISLEADING,
|
|
12
|
-
REASONRUDE,
|
|
13
|
-
REASONSEXUAL,
|
|
14
|
-
REASONVIOLATION,
|
|
15
|
-
} from '../../lexicon/types/com/atproto/moderation/defs'
|
|
16
7
|
|
|
17
|
-
export const
|
|
18
|
-
export const
|
|
19
|
-
export const reportTableName = 'moderation_report'
|
|
20
|
-
export const reportResolutionTableName = 'moderation_report_resolution'
|
|
8
|
+
export const eventTableName = 'moderation_event'
|
|
9
|
+
export const subjectStatusTableName = 'moderation_subject_status'
|
|
21
10
|
|
|
22
|
-
export interface
|
|
11
|
+
export interface ModerationEvent {
|
|
23
12
|
id: Generated<number>
|
|
24
|
-
action:
|
|
13
|
+
action:
|
|
14
|
+
| 'com.atproto.admin.defs#modEventTakedown'
|
|
15
|
+
| 'com.atproto.admin.defs#modEventAcknowledge'
|
|
16
|
+
| 'com.atproto.admin.defs#modEventEscalate'
|
|
17
|
+
| 'com.atproto.admin.defs#modEventComment'
|
|
18
|
+
| 'com.atproto.admin.defs#modEventLabel'
|
|
19
|
+
| 'com.atproto.admin.defs#modEventReport'
|
|
20
|
+
| 'com.atproto.admin.defs#modEventMute'
|
|
21
|
+
| 'com.atproto.admin.defs#modEventReverseTakedown'
|
|
22
|
+
| 'com.atproto.admin.defs#modEventEmail'
|
|
25
23
|
subjectType: 'com.atproto.admin.defs#repoRef' | 'com.atproto.repo.strongRef'
|
|
26
24
|
subjectDid: string
|
|
27
25
|
subjectUri: string | null
|
|
28
26
|
subjectCid: string | null
|
|
29
27
|
createLabelVals: string | null
|
|
30
28
|
negateLabelVals: string | null
|
|
31
|
-
|
|
29
|
+
comment: string | null
|
|
32
30
|
createdAt: string
|
|
33
31
|
createdBy: string
|
|
34
|
-
reversedAt: string | null
|
|
35
|
-
reversedBy: string | null
|
|
36
|
-
reversedReason: string | null
|
|
37
32
|
durationInHours: number | null
|
|
38
33
|
expiresAt: string | null
|
|
34
|
+
meta: Record<string, string | boolean> | null
|
|
35
|
+
legacyRefId: number | null
|
|
39
36
|
}
|
|
40
37
|
|
|
41
|
-
export interface
|
|
42
|
-
actionId: number
|
|
43
|
-
cid: string
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface ModerationReport {
|
|
38
|
+
export interface ModerationSubjectStatus {
|
|
47
39
|
id: Generated<number>
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
| typeof REASONSPAM
|
|
54
|
-
| typeof REASONOTHER
|
|
55
|
-
| typeof REASONMISLEADING
|
|
56
|
-
| typeof REASONRUDE
|
|
57
|
-
| typeof REASONSEXUAL
|
|
58
|
-
| typeof REASONVIOLATION
|
|
59
|
-
reason: string | null
|
|
60
|
-
reportedByDid: string
|
|
40
|
+
did: string
|
|
41
|
+
recordPath: string
|
|
42
|
+
recordCid: string | null
|
|
43
|
+
blobCids: string[] | null
|
|
44
|
+
reviewState: typeof REVIEWCLOSED | typeof REVIEWOPEN | typeof REVIEWESCALATED
|
|
61
45
|
createdAt: string
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
46
|
+
updatedAt: string
|
|
47
|
+
lastReviewedBy: string | null
|
|
48
|
+
lastReviewedAt: string | null
|
|
49
|
+
lastReportedAt: string | null
|
|
50
|
+
muteUntil: string | null
|
|
51
|
+
suspendUntil: string | null
|
|
52
|
+
takendown: boolean
|
|
53
|
+
comment: string | null
|
|
69
54
|
}
|
|
70
55
|
|
|
71
56
|
export type PartialDB = {
|
|
72
|
-
[
|
|
73
|
-
[
|
|
74
|
-
[reportTableName]: ModerationReport
|
|
75
|
-
[reportResolutionTableName]: ModerationReportResolution
|
|
57
|
+
[eventTableName]: ModerationEvent
|
|
58
|
+
[subjectStatusTableName]: ModerationSubjectStatus
|
|
76
59
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { InvalidRequestError } from '@atproto/xrpc-server'
|
|
1
|
+
import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'
|
|
2
2
|
import { QueryParams as SkeletonParams } from '../lexicon/types/app/bsky/feed/getFeedSkeleton'
|
|
3
3
|
import { AlgoHandler, AlgoResponse } from './types'
|
|
4
4
|
import { GenericKeyset, paginate } from '../db/pagination'
|
|
@@ -7,12 +7,15 @@ import AppContext from '../context'
|
|
|
7
7
|
const handler: AlgoHandler = async (
|
|
8
8
|
ctx: AppContext,
|
|
9
9
|
params: SkeletonParams,
|
|
10
|
-
viewer: string,
|
|
10
|
+
viewer: string | null,
|
|
11
11
|
): Promise<AlgoResponse> => {
|
|
12
|
+
if (!viewer) {
|
|
13
|
+
throw new AuthRequiredError('This feed requires being logged-in')
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
const { limit, cursor } = params
|
|
13
17
|
const db = ctx.db.getReplica('feed')
|
|
14
18
|
const feedService = ctx.services.feed(db)
|
|
15
|
-
|
|
16
19
|
const { ref } = db.db.dynamic
|
|
17
20
|
|
|
18
21
|
// candidates are ranked within a materialized view by like count, depreciated over time.
|
|
@@ -14,7 +14,7 @@ const BSKY_TEAM: NotEmptyArray<string> = [
|
|
|
14
14
|
const handler: AlgoHandler = async (
|
|
15
15
|
ctx: AppContext,
|
|
16
16
|
params: SkeletonParams,
|
|
17
|
-
_viewer: string,
|
|
17
|
+
_viewer: string | null,
|
|
18
18
|
): Promise<AlgoResponse> => {
|
|
19
19
|
const { limit = 50, cursor } = params
|
|
20
20
|
const db = ctx.db.getReplica('feed')
|
|
@@ -11,7 +11,7 @@ const NO_WHATS_HOT_LABELS: NotEmptyArray<string> = ['!no-promote']
|
|
|
11
11
|
const handler: AlgoHandler = async (
|
|
12
12
|
ctx: AppContext,
|
|
13
13
|
params: SkeletonParams,
|
|
14
|
-
_viewer: string,
|
|
14
|
+
_viewer: string | null,
|
|
15
15
|
): Promise<AlgoResponse> => {
|
|
16
16
|
const { limit = 50, cursor } = params
|
|
17
17
|
const db = ctx.db.getReplica('feed')
|
package/src/feed-gen/mutuals.ts
CHANGED
|
@@ -3,16 +3,20 @@ import AppContext from '../context'
|
|
|
3
3
|
import { paginate } from '../db/pagination'
|
|
4
4
|
import { AlgoHandler, AlgoResponse } from './types'
|
|
5
5
|
import { FeedKeyset, getFeedDateThreshold } from '../api/app/bsky/util/feed'
|
|
6
|
+
import { AuthRequiredError } from '@atproto/xrpc-server'
|
|
6
7
|
|
|
7
8
|
const handler: AlgoHandler = async (
|
|
8
9
|
ctx: AppContext,
|
|
9
10
|
params: SkeletonParams,
|
|
10
|
-
viewer: string,
|
|
11
|
+
viewer: string | null,
|
|
11
12
|
): Promise<AlgoResponse> => {
|
|
13
|
+
if (!viewer) {
|
|
14
|
+
throw new AuthRequiredError('This feed requires being logged-in')
|
|
15
|
+
}
|
|
16
|
+
|
|
12
17
|
const { limit = 50, cursor } = params
|
|
13
18
|
const db = ctx.db.getReplica('feed')
|
|
14
19
|
const feedService = ctx.services.feed(db)
|
|
15
|
-
|
|
16
20
|
const { ref } = db.db.dynamic
|
|
17
21
|
|
|
18
22
|
const mutualsSubquery = db.db
|
package/src/feed-gen/types.ts
CHANGED
|
@@ -21,7 +21,7 @@ const NO_WHATS_HOT_LABELS: NotEmptyArray<string> = [
|
|
|
21
21
|
const handler: AlgoHandler = async (
|
|
22
22
|
ctx: AppContext,
|
|
23
23
|
params: SkeletonParams,
|
|
24
|
-
_viewer: string,
|
|
24
|
+
_viewer: string | null,
|
|
25
25
|
): Promise<AlgoResponse> => {
|
|
26
26
|
const { limit, cursor } = params
|
|
27
27
|
const db = ctx.db.getReplica('feed')
|
|
@@ -3,16 +3,20 @@ import { QueryParams as SkeletonParams } from '../lexicon/types/app/bsky/feed/ge
|
|
|
3
3
|
import { paginate } from '../db/pagination'
|
|
4
4
|
import { AlgoHandler, AlgoResponse } from './types'
|
|
5
5
|
import { FeedKeyset, getFeedDateThreshold } from '../api/app/bsky/util/feed'
|
|
6
|
+
import { AuthRequiredError } from '@atproto/xrpc-server'
|
|
6
7
|
|
|
7
8
|
const handler: AlgoHandler = async (
|
|
8
9
|
ctx: AppContext,
|
|
9
10
|
params: SkeletonParams,
|
|
10
|
-
|
|
11
|
+
viewer: string | null,
|
|
11
12
|
): Promise<AlgoResponse> => {
|
|
13
|
+
if (!viewer) {
|
|
14
|
+
throw new AuthRequiredError('This feed requires being logged-in')
|
|
15
|
+
}
|
|
16
|
+
|
|
12
17
|
const { cursor, limit = 50 } = params
|
|
13
18
|
const db = ctx.db.getReplica('feed')
|
|
14
19
|
const feedService = ctx.services.feed(db)
|
|
15
|
-
|
|
16
20
|
const { ref } = db.db.dynamic
|
|
17
21
|
|
|
18
22
|
const keyset = new FeedKeyset(ref('post.sortAt'), ref('post.cid'))
|
|
@@ -23,7 +27,7 @@ const handler: AlgoHandler = async (
|
|
|
23
27
|
.innerJoin('follow', 'follow.subjectDid', 'post.creator')
|
|
24
28
|
.innerJoin('post_agg', 'post_agg.uri', 'post.uri')
|
|
25
29
|
.where('post_agg.likeCount', '>=', 5)
|
|
26
|
-
.where('follow.creator', '=',
|
|
30
|
+
.where('follow.creator', '=', viewer)
|
|
27
31
|
.where('post.sortAt', '>', getFeedDateThreshold(sortFrom))
|
|
28
32
|
|
|
29
33
|
postsQb = paginate(postsQb, { limit, cursor, keyset, tryIndex: true })
|
package/src/index.ts
CHANGED
|
@@ -32,13 +32,14 @@ export type { ServerConfigValues } from './config'
|
|
|
32
32
|
export type { MountedAlgos } from './feed-gen/types'
|
|
33
33
|
export { ServerConfig } from './config'
|
|
34
34
|
export { Database, PrimaryDatabase, DatabaseCoordinator } from './db'
|
|
35
|
-
export {
|
|
35
|
+
export { PeriodicModerationEventReversal } from './db/periodic-moderation-event-reversal'
|
|
36
36
|
export { Redis } from './redis'
|
|
37
37
|
export { ViewMaintainer } from './db/views'
|
|
38
38
|
export { AppContext } from './context'
|
|
39
39
|
export { makeAlgos } from './feed-gen'
|
|
40
40
|
export * from './indexer'
|
|
41
41
|
export * from './ingester'
|
|
42
|
+
export { MigrateModerationData } from './migrate-moderation-data'
|
|
42
43
|
|
|
43
44
|
export class BskyAppView {
|
|
44
45
|
public ctx: AppContext
|