@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
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { sql } from 'kysely'
|
|
2
|
+
import { DatabaseCoordinator, PrimaryDatabase } from './index'
|
|
3
|
+
import { adjustModerationSubjectStatus } from './services/moderation/status'
|
|
4
|
+
import { ModerationEventRow } from './services/moderation/types'
|
|
5
|
+
|
|
6
|
+
type ModerationActionRow = Omit<ModerationEventRow, 'comment' | 'meta'> & {
|
|
7
|
+
reason: string | null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const getEnv = () => ({
|
|
11
|
+
DB_URL:
|
|
12
|
+
process.env.MODERATION_MIGRATION_DB_URL ||
|
|
13
|
+
'postgresql://pg:password@127.0.0.1:5433/postgres',
|
|
14
|
+
DB_POOL_SIZE: Number(process.env.MODERATION_MIGRATION_DB_POOL_SIZE) || 10,
|
|
15
|
+
DB_SCHEMA: process.env.MODERATION_MIGRATION_DB_SCHEMA || 'bsky',
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const countEntries = async (db: PrimaryDatabase) => {
|
|
19
|
+
const [allActions, allReports] = await Promise.all([
|
|
20
|
+
db.db
|
|
21
|
+
// @ts-ignore
|
|
22
|
+
.selectFrom('moderation_action')
|
|
23
|
+
// @ts-ignore
|
|
24
|
+
.select((eb) => eb.fn.count<number>('id').as('count'))
|
|
25
|
+
.executeTakeFirstOrThrow(),
|
|
26
|
+
db.db
|
|
27
|
+
// @ts-ignore
|
|
28
|
+
.selectFrom('moderation_report')
|
|
29
|
+
// @ts-ignore
|
|
30
|
+
.select((eb) => eb.fn.count<number>('id').as('count'))
|
|
31
|
+
.executeTakeFirstOrThrow(),
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
return { reportsCount: allReports.count, actionsCount: allActions.count }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const countEvents = async (db: PrimaryDatabase) => {
|
|
38
|
+
const events = await db.db
|
|
39
|
+
.selectFrom('moderation_event')
|
|
40
|
+
.select((eb) => eb.fn.count<number>('id').as('count'))
|
|
41
|
+
.executeTakeFirstOrThrow()
|
|
42
|
+
|
|
43
|
+
return events.count
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const getLatestReportLegacyRefId = async (db: PrimaryDatabase) => {
|
|
47
|
+
const events = await db.db
|
|
48
|
+
.selectFrom('moderation_event')
|
|
49
|
+
.select((eb) => eb.fn.max('legacyRefId').as('latestLegacyRefId'))
|
|
50
|
+
.where('action', '=', 'com.atproto.admin.defs#modEventReport')
|
|
51
|
+
.executeTakeFirstOrThrow()
|
|
52
|
+
|
|
53
|
+
return events.latestLegacyRefId
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const countStatuses = async (db: PrimaryDatabase) => {
|
|
57
|
+
const events = await db.db
|
|
58
|
+
.selectFrom('moderation_subject_status')
|
|
59
|
+
.select((eb) => eb.fn.count<number>('id').as('count'))
|
|
60
|
+
.executeTakeFirstOrThrow()
|
|
61
|
+
|
|
62
|
+
return events.count
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const processLegacyReports = async (
|
|
66
|
+
db: PrimaryDatabase,
|
|
67
|
+
legacyIds: number[],
|
|
68
|
+
) => {
|
|
69
|
+
if (!legacyIds.length) {
|
|
70
|
+
console.log('No legacy reports to process')
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
const reports = await db.db
|
|
74
|
+
.selectFrom('moderation_event')
|
|
75
|
+
.where('action', '=', 'com.atproto.admin.defs#modEventReport')
|
|
76
|
+
.where('legacyRefId', 'in', legacyIds)
|
|
77
|
+
.orderBy('legacyRefId', 'asc')
|
|
78
|
+
.selectAll()
|
|
79
|
+
.execute()
|
|
80
|
+
|
|
81
|
+
console.log(`Processing ${reports.length} reports from ${legacyIds.length}`)
|
|
82
|
+
await db.transaction(async (tx) => {
|
|
83
|
+
// This will be slow but we need to run this in sequence
|
|
84
|
+
for (const report of reports) {
|
|
85
|
+
await adjustModerationSubjectStatus(tx, report)
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
console.log(`Completed processing ${reports.length} reports`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const getReportEventsAboveLegacyId = async (
|
|
92
|
+
db: PrimaryDatabase,
|
|
93
|
+
aboveLegacyId: number,
|
|
94
|
+
) => {
|
|
95
|
+
return await db.db
|
|
96
|
+
.selectFrom('moderation_event')
|
|
97
|
+
.where('action', '=', 'com.atproto.admin.defs#modEventReport')
|
|
98
|
+
.where('legacyRefId', '>', aboveLegacyId)
|
|
99
|
+
.select(sql<number>`"legacyRefId"`.as('legacyRefId'))
|
|
100
|
+
.execute()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const createEvents = async (
|
|
104
|
+
db: PrimaryDatabase,
|
|
105
|
+
opts?: { onlyReportsAboveId: number },
|
|
106
|
+
) => {
|
|
107
|
+
const commonColumnsToSelect = [
|
|
108
|
+
'subjectDid',
|
|
109
|
+
'subjectUri',
|
|
110
|
+
'subjectType',
|
|
111
|
+
'subjectCid',
|
|
112
|
+
sql`reason`.as('comment'),
|
|
113
|
+
'createdAt',
|
|
114
|
+
]
|
|
115
|
+
const commonColumnsToInsert = [
|
|
116
|
+
'subjectDid',
|
|
117
|
+
'subjectUri',
|
|
118
|
+
'subjectType',
|
|
119
|
+
'subjectCid',
|
|
120
|
+
'comment',
|
|
121
|
+
'createdAt',
|
|
122
|
+
'action',
|
|
123
|
+
'createdBy',
|
|
124
|
+
] as const
|
|
125
|
+
|
|
126
|
+
let totalActions: number
|
|
127
|
+
if (!opts?.onlyReportsAboveId) {
|
|
128
|
+
await db.db
|
|
129
|
+
.insertInto('moderation_event')
|
|
130
|
+
.columns([
|
|
131
|
+
'id',
|
|
132
|
+
...commonColumnsToInsert,
|
|
133
|
+
'createLabelVals',
|
|
134
|
+
'negateLabelVals',
|
|
135
|
+
'durationInHours',
|
|
136
|
+
'expiresAt',
|
|
137
|
+
])
|
|
138
|
+
.expression((eb) =>
|
|
139
|
+
eb
|
|
140
|
+
// @ts-ignore
|
|
141
|
+
.selectFrom('moderation_action')
|
|
142
|
+
// @ts-ignore
|
|
143
|
+
.select([
|
|
144
|
+
'id',
|
|
145
|
+
...commonColumnsToSelect,
|
|
146
|
+
sql`CONCAT('com.atproto.admin.defs#modEvent', UPPER(SUBSTRING(SPLIT_PART(action, '#', 2) FROM 1 FOR 1)), SUBSTRING(SPLIT_PART(action, '#', 2) FROM 2))`.as(
|
|
147
|
+
'action',
|
|
148
|
+
),
|
|
149
|
+
'createdBy',
|
|
150
|
+
'createLabelVals',
|
|
151
|
+
'negateLabelVals',
|
|
152
|
+
'durationInHours',
|
|
153
|
+
'expiresAt',
|
|
154
|
+
])
|
|
155
|
+
.orderBy('id', 'asc'),
|
|
156
|
+
)
|
|
157
|
+
.execute()
|
|
158
|
+
|
|
159
|
+
totalActions = await countEvents(db)
|
|
160
|
+
console.log(`Created ${totalActions} events from actions`)
|
|
161
|
+
|
|
162
|
+
await sql`SELECT setval(pg_get_serial_sequence('moderation_event', 'id'), (select max(id) from moderation_event))`.execute(
|
|
163
|
+
db.db,
|
|
164
|
+
)
|
|
165
|
+
console.log('Reset the id sequence for moderation_event')
|
|
166
|
+
} else {
|
|
167
|
+
totalActions = await countEvents(db)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
await db.db
|
|
171
|
+
.insertInto('moderation_event')
|
|
172
|
+
.columns([...commonColumnsToInsert, 'meta', 'legacyRefId'])
|
|
173
|
+
.expression((eb) => {
|
|
174
|
+
const builder = eb
|
|
175
|
+
// @ts-ignore
|
|
176
|
+
.selectFrom('moderation_report')
|
|
177
|
+
// @ts-ignore
|
|
178
|
+
.select([
|
|
179
|
+
...commonColumnsToSelect,
|
|
180
|
+
sql`'com.atproto.admin.defs#modEventReport'`.as('action'),
|
|
181
|
+
sql`"reportedByDid"`.as('createdBy'),
|
|
182
|
+
sql`json_build_object('reportType', "reasonType")`.as('meta'),
|
|
183
|
+
sql`id`.as('legacyRefId'),
|
|
184
|
+
])
|
|
185
|
+
|
|
186
|
+
if (opts?.onlyReportsAboveId) {
|
|
187
|
+
// @ts-ignore
|
|
188
|
+
return builder.where('id', '>', opts.onlyReportsAboveId)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return builder
|
|
192
|
+
})
|
|
193
|
+
.execute()
|
|
194
|
+
|
|
195
|
+
const totalEvents = await countEvents(db)
|
|
196
|
+
console.log(`Created ${totalEvents - totalActions} events from reports`)
|
|
197
|
+
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const setReportedAtTimestamp = async (db: PrimaryDatabase) => {
|
|
202
|
+
console.log('Initiating lastReportedAt timestamp sync')
|
|
203
|
+
const didUpdate = await sql`
|
|
204
|
+
UPDATE moderation_subject_status
|
|
205
|
+
SET "lastReportedAt" = reports."createdAt"
|
|
206
|
+
FROM (
|
|
207
|
+
select "subjectDid", "subjectUri", MAX("createdAt") as "createdAt"
|
|
208
|
+
from moderation_report
|
|
209
|
+
where "subjectUri" is null
|
|
210
|
+
group by "subjectDid", "subjectUri"
|
|
211
|
+
) as reports
|
|
212
|
+
WHERE reports."subjectDid" = moderation_subject_status."did"
|
|
213
|
+
AND "recordPath" = ''
|
|
214
|
+
AND ("lastReportedAt" is null OR "lastReportedAt" < reports."createdAt")
|
|
215
|
+
`.execute(db.db)
|
|
216
|
+
|
|
217
|
+
console.log(
|
|
218
|
+
`Updated lastReportedAt for ${didUpdate.numUpdatedOrDeletedRows} did subject`,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
const contentUpdate = await sql`
|
|
222
|
+
UPDATE moderation_subject_status
|
|
223
|
+
SET "lastReportedAt" = reports."createdAt"
|
|
224
|
+
FROM (
|
|
225
|
+
select "subjectDid", "subjectUri", MAX("createdAt") as "createdAt"
|
|
226
|
+
from moderation_report
|
|
227
|
+
where "subjectUri" is not null
|
|
228
|
+
group by "subjectDid", "subjectUri"
|
|
229
|
+
) as reports
|
|
230
|
+
WHERE reports."subjectDid" = moderation_subject_status."did"
|
|
231
|
+
AND "recordPath" is not null
|
|
232
|
+
AND POSITION(moderation_subject_status."recordPath" IN reports."subjectUri") > 0
|
|
233
|
+
AND ("lastReportedAt" is null OR "lastReportedAt" < reports."createdAt")
|
|
234
|
+
`.execute(db.db)
|
|
235
|
+
|
|
236
|
+
console.log(
|
|
237
|
+
`Updated lastReportedAt for ${contentUpdate.numUpdatedOrDeletedRows} subject with uri`,
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const createStatusFromActions = async (db: PrimaryDatabase) => {
|
|
242
|
+
const allEvents = await db.db
|
|
243
|
+
// @ts-ignore
|
|
244
|
+
.selectFrom('moderation_action')
|
|
245
|
+
// @ts-ignore
|
|
246
|
+
.where('reversedAt', 'is', null)
|
|
247
|
+
// @ts-ignore
|
|
248
|
+
.select((eb) => eb.fn.count<number>('id').as('count'))
|
|
249
|
+
.executeTakeFirstOrThrow()
|
|
250
|
+
|
|
251
|
+
const chunkSize = 2500
|
|
252
|
+
const totalChunks = Math.ceil(allEvents.count / chunkSize)
|
|
253
|
+
|
|
254
|
+
console.log(`Processing ${allEvents.count} actions in ${totalChunks} chunks`)
|
|
255
|
+
|
|
256
|
+
await db.transaction(async (tx) => {
|
|
257
|
+
// This is not used for pagination but only for logging purposes
|
|
258
|
+
let currentChunk = 1
|
|
259
|
+
let lastProcessedId: undefined | number = 0
|
|
260
|
+
do {
|
|
261
|
+
const eventsQuery = tx.db
|
|
262
|
+
// @ts-ignore
|
|
263
|
+
.selectFrom('moderation_action')
|
|
264
|
+
// @ts-ignore
|
|
265
|
+
.where('reversedAt', 'is', null)
|
|
266
|
+
// @ts-ignore
|
|
267
|
+
.where('id', '>', lastProcessedId)
|
|
268
|
+
.limit(chunkSize)
|
|
269
|
+
.selectAll()
|
|
270
|
+
const events = (await eventsQuery.execute()) as ModerationActionRow[]
|
|
271
|
+
|
|
272
|
+
for (const event of events) {
|
|
273
|
+
// Remap action to event data type
|
|
274
|
+
const actionParts = event.action.split('#')
|
|
275
|
+
await adjustModerationSubjectStatus(tx, {
|
|
276
|
+
...event,
|
|
277
|
+
action: `com.atproto.admin.defs#modEvent${actionParts[1]
|
|
278
|
+
.charAt(0)
|
|
279
|
+
.toUpperCase()}${actionParts[1].slice(
|
|
280
|
+
1,
|
|
281
|
+
)}` as ModerationEventRow['action'],
|
|
282
|
+
comment: event.reason,
|
|
283
|
+
meta: null,
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
console.log(`Processed events chunk ${currentChunk} of ${totalChunks}`)
|
|
288
|
+
lastProcessedId = events.at(-1)?.id
|
|
289
|
+
currentChunk++
|
|
290
|
+
} while (lastProcessedId !== undefined)
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
console.log(`Events migration complete!`)
|
|
294
|
+
|
|
295
|
+
const totalStatuses = await countStatuses(db)
|
|
296
|
+
console.log(`Created ${totalStatuses} statuses`)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const remapFlagToAcknlowedge = async (db: PrimaryDatabase) => {
|
|
300
|
+
console.log('Initiating flag to ack remap')
|
|
301
|
+
const results = await sql`
|
|
302
|
+
UPDATE moderation_event
|
|
303
|
+
SET "action" = 'com.atproto.admin.defs#modEventAcknowledge'
|
|
304
|
+
WHERE action = 'com.atproto.admin.defs#modEventFlag'
|
|
305
|
+
`.execute(db.db)
|
|
306
|
+
console.log(`Remapped ${results.numUpdatedOrDeletedRows} flag actions to ack`)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const syncBlobCids = async (db: PrimaryDatabase) => {
|
|
310
|
+
console.log('Initiating blob cid sync')
|
|
311
|
+
const results = await sql`
|
|
312
|
+
UPDATE moderation_subject_status
|
|
313
|
+
SET "blobCids" = blob_action."cids"
|
|
314
|
+
FROM (
|
|
315
|
+
SELECT moderation_action."subjectUri", moderation_action."subjectDid", jsonb_agg(moderation_action_subject_blob."cid") as cids
|
|
316
|
+
FROM moderation_action_subject_blob
|
|
317
|
+
JOIN moderation_action
|
|
318
|
+
ON moderation_action.id = moderation_action_subject_blob."actionId"
|
|
319
|
+
WHERE moderation_action."reversedAt" is NULL
|
|
320
|
+
GROUP by moderation_action."subjectUri", moderation_action."subjectDid"
|
|
321
|
+
) as blob_action
|
|
322
|
+
WHERE did = "subjectDid" AND position("recordPath" IN "subjectUri") > 0
|
|
323
|
+
`.execute(db.db)
|
|
324
|
+
console.log(`Updated blob cids on ${results.numUpdatedOrDeletedRows} rows`)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function updateStatusFromUnresolvedReports(db: PrimaryDatabase) {
|
|
328
|
+
const { ref } = db.db.dynamic
|
|
329
|
+
const reports = await db.db
|
|
330
|
+
// @ts-ignore
|
|
331
|
+
.selectFrom('moderation_report')
|
|
332
|
+
.whereNotExists((qb) =>
|
|
333
|
+
qb
|
|
334
|
+
.selectFrom('moderation_report_resolution')
|
|
335
|
+
.selectAll()
|
|
336
|
+
// @ts-ignore
|
|
337
|
+
.whereRef('reportId', '=', ref('moderation_report.id')),
|
|
338
|
+
)
|
|
339
|
+
.select(sql<number>`moderation_report.id`.as('legacyId'))
|
|
340
|
+
.execute()
|
|
341
|
+
|
|
342
|
+
console.log('Updating statuses based on unresolved reports')
|
|
343
|
+
await processLegacyReports(
|
|
344
|
+
db,
|
|
345
|
+
reports.map((report) => report.legacyId),
|
|
346
|
+
)
|
|
347
|
+
console.log('Completed updating statuses based on unresolved reports')
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export async function MigrateModerationData() {
|
|
351
|
+
const env = getEnv()
|
|
352
|
+
const db = new DatabaseCoordinator({
|
|
353
|
+
schema: env.DB_SCHEMA,
|
|
354
|
+
primary: {
|
|
355
|
+
url: env.DB_URL,
|
|
356
|
+
poolSize: env.DB_POOL_SIZE,
|
|
357
|
+
},
|
|
358
|
+
replicas: [],
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
const primaryDb = db.getPrimary()
|
|
362
|
+
|
|
363
|
+
const [counts, existingEventsCount] = await Promise.all([
|
|
364
|
+
countEntries(primaryDb),
|
|
365
|
+
countEvents(primaryDb),
|
|
366
|
+
])
|
|
367
|
+
|
|
368
|
+
// If there are existing events in the moderation_event table, we assume that the migration has already been run
|
|
369
|
+
// so we just bring over any new reports since last run
|
|
370
|
+
if (existingEventsCount) {
|
|
371
|
+
console.log(
|
|
372
|
+
`Found ${existingEventsCount} existing events. Migrating ${counts.reportsCount} reports only, ignoring actions`,
|
|
373
|
+
)
|
|
374
|
+
const reportMigrationStartedAt = Date.now()
|
|
375
|
+
const latestReportLegacyRefId = await getLatestReportLegacyRefId(primaryDb)
|
|
376
|
+
|
|
377
|
+
if (latestReportLegacyRefId) {
|
|
378
|
+
await createEvents(primaryDb, {
|
|
379
|
+
onlyReportsAboveId: latestReportLegacyRefId,
|
|
380
|
+
})
|
|
381
|
+
const newReportEvents = await getReportEventsAboveLegacyId(
|
|
382
|
+
primaryDb,
|
|
383
|
+
latestReportLegacyRefId,
|
|
384
|
+
)
|
|
385
|
+
await processLegacyReports(
|
|
386
|
+
primaryDb,
|
|
387
|
+
newReportEvents.map((evt) => evt.legacyRefId),
|
|
388
|
+
)
|
|
389
|
+
await setReportedAtTimestamp(primaryDb)
|
|
390
|
+
} else {
|
|
391
|
+
console.log('No reports have been migrated into events yet, bailing.')
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
console.log(
|
|
395
|
+
`Time spent: ${(Date.now() - reportMigrationStartedAt) / 1000} seconds`,
|
|
396
|
+
)
|
|
397
|
+
console.log('Migration complete!')
|
|
398
|
+
return
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const totalEntries = counts.actionsCount + counts.reportsCount
|
|
402
|
+
console.log(`Migrating ${totalEntries} rows of actions and reports`)
|
|
403
|
+
const startedAt = Date.now()
|
|
404
|
+
await createEvents(primaryDb)
|
|
405
|
+
// Important to run this before creation statuses from actions to ensure that we are not attempting to map flag actions
|
|
406
|
+
await remapFlagToAcknlowedge(primaryDb)
|
|
407
|
+
await createStatusFromActions(primaryDb)
|
|
408
|
+
await updateStatusFromUnresolvedReports(primaryDb)
|
|
409
|
+
await setReportedAtTimestamp(primaryDb)
|
|
410
|
+
await syncBlobCids(primaryDb)
|
|
411
|
+
|
|
412
|
+
console.log(`Time spent: ${(Date.now() - startedAt) / 1000 / 60} minutes`)
|
|
413
|
+
console.log('Migration complete!')
|
|
414
|
+
}
|
|
@@ -45,10 +45,7 @@ export class ActorViews {
|
|
|
45
45
|
viewer,
|
|
46
46
|
...opts,
|
|
47
47
|
})
|
|
48
|
-
return this.profilePresentation(dids, hydrated,
|
|
49
|
-
viewer,
|
|
50
|
-
...opts,
|
|
51
|
-
})
|
|
48
|
+
return this.profilePresentation(dids, hydrated, viewer)
|
|
52
49
|
}
|
|
53
50
|
|
|
54
51
|
async profilesBasic(
|
|
@@ -62,10 +59,7 @@ export class ActorViews {
|
|
|
62
59
|
viewer,
|
|
63
60
|
includeSoftDeleted: opts?.includeSoftDeleted,
|
|
64
61
|
})
|
|
65
|
-
return this.profileBasicPresentation(dids, hydrated,
|
|
66
|
-
viewer,
|
|
67
|
-
omitLabels: opts?.omitLabels,
|
|
68
|
-
})
|
|
62
|
+
return this.profileBasicPresentation(dids, hydrated, viewer, opts)
|
|
69
63
|
}
|
|
70
64
|
|
|
71
65
|
async profilesList(
|
|
@@ -293,11 +287,8 @@ export class ActorViews {
|
|
|
293
287
|
labels: Labels
|
|
294
288
|
bam: BlockAndMuteState
|
|
295
289
|
},
|
|
296
|
-
|
|
297
|
-
viewer?: string | null
|
|
298
|
-
},
|
|
290
|
+
viewer: string | null,
|
|
299
291
|
): ProfileViewMap {
|
|
300
|
-
const { viewer } = opts ?? {}
|
|
301
292
|
const { profiles, lists, labels, bam } = state
|
|
302
293
|
return dids.reduce((acc, did) => {
|
|
303
294
|
const prof = profiles[did]
|
|
@@ -357,12 +348,12 @@ export class ActorViews {
|
|
|
357
348
|
profileBasicPresentation(
|
|
358
349
|
dids: string[],
|
|
359
350
|
state: ProfileHydrationState,
|
|
351
|
+
viewer: string | null,
|
|
360
352
|
opts?: {
|
|
361
|
-
viewer?: string | null
|
|
362
353
|
omitLabels?: boolean
|
|
363
354
|
},
|
|
364
355
|
): ProfileViewMap {
|
|
365
|
-
const result = this.profilePresentation(dids, state,
|
|
356
|
+
const result = this.profilePresentation(dids, state, viewer)
|
|
366
357
|
return Object.values(result).reduce((acc, prof) => {
|
|
367
358
|
const profileBasic = {
|
|
368
359
|
did: prof.did,
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
import { FeedViews } from './views'
|
|
45
45
|
import { LabelCache } from '../../label-cache'
|
|
46
46
|
import { threadgateToPostUri, postToThreadgateUri } from './util'
|
|
47
|
+
import { mapDefined } from '@atproto/common'
|
|
47
48
|
|
|
48
49
|
export * from './types'
|
|
49
50
|
|
|
@@ -205,6 +206,11 @@ export class FeedService {
|
|
|
205
206
|
}, {} as Record<string, FeedRow>)
|
|
206
207
|
}
|
|
207
208
|
|
|
209
|
+
async postUrisToFeedItems(uris: string[]): Promise<FeedRow[]> {
|
|
210
|
+
const feedItems = await this.getFeedItems(uris)
|
|
211
|
+
return mapDefined(uris, (uri) => feedItems[uri])
|
|
212
|
+
}
|
|
213
|
+
|
|
208
214
|
feedItemRefs(items: FeedRow[]) {
|
|
209
215
|
const actorDids = new Set<string>()
|
|
210
216
|
const postUris = new Set<string>()
|
|
@@ -399,20 +405,32 @@ export class FeedService {
|
|
|
399
405
|
const actorInfos = this.services.actor.views.profileBasicPresentation(
|
|
400
406
|
[...nestedDids],
|
|
401
407
|
feedState,
|
|
402
|
-
|
|
408
|
+
viewer,
|
|
403
409
|
)
|
|
404
410
|
const recordEmbedViews: RecordEmbedViewRecordMap = {}
|
|
405
411
|
for (const uri of nestedUris) {
|
|
406
412
|
const collection = new AtUri(uri).collection
|
|
407
413
|
if (collection === ids.AppBskyFeedGenerator && feedGenInfos[uri]) {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
414
|
+
const genView = this.views.formatFeedGeneratorView(
|
|
415
|
+
feedGenInfos[uri],
|
|
416
|
+
actorInfos,
|
|
417
|
+
)
|
|
418
|
+
if (genView) {
|
|
419
|
+
recordEmbedViews[uri] = {
|
|
420
|
+
$type: 'app.bsky.feed.defs#generatorView',
|
|
421
|
+
...genView,
|
|
422
|
+
}
|
|
411
423
|
}
|
|
412
424
|
} else if (collection === ids.AppBskyGraphList && listViews[uri]) {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
425
|
+
const listView = this.services.graph.formatListView(
|
|
426
|
+
listViews[uri],
|
|
427
|
+
actorInfos,
|
|
428
|
+
)
|
|
429
|
+
if (listView) {
|
|
430
|
+
recordEmbedViews[uri] = {
|
|
431
|
+
$type: 'app.bsky.graph.defs#listView',
|
|
432
|
+
...listView,
|
|
433
|
+
}
|
|
416
434
|
}
|
|
417
435
|
} else if (collection === ids.AppBskyFeedPost && feedState.posts[uri]) {
|
|
418
436
|
const formatted = this.views.formatPostView(
|
|
@@ -423,6 +441,7 @@ export class FeedService {
|
|
|
423
441
|
feedState.embeds,
|
|
424
442
|
feedState.labels,
|
|
425
443
|
feedState.lists,
|
|
444
|
+
viewer,
|
|
426
445
|
)
|
|
427
446
|
recordEmbedViews[uri] = this.views.getRecordEmbedView(
|
|
428
447
|
uri,
|
|
@@ -36,43 +36,72 @@ export const invalidReplyRoot = (
|
|
|
36
36
|
return parent.record.reply?.root.uri !== replyRoot
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
type ParsedThreadGate = {
|
|
40
|
+
canReply?: boolean
|
|
41
|
+
allowMentions?: boolean
|
|
42
|
+
allowFollowing?: boolean
|
|
43
|
+
allowListUris?: string[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const parseThreadGate = (
|
|
47
|
+
replierDid: string,
|
|
48
|
+
ownerDid: string,
|
|
49
|
+
rootPost: PostRecord | null,
|
|
44
50
|
gate: GateRecord | null,
|
|
45
|
-
) => {
|
|
46
|
-
if (
|
|
47
|
-
|
|
51
|
+
): ParsedThreadGate => {
|
|
52
|
+
if (replierDid === ownerDid) {
|
|
53
|
+
return { canReply: true }
|
|
54
|
+
}
|
|
55
|
+
// if gate.allow is unset then *any* reply is allowed, if it is an empty array then *no* reply is allowed
|
|
56
|
+
if (!gate || !gate.allow) {
|
|
57
|
+
return { canReply: true }
|
|
58
|
+
}
|
|
48
59
|
|
|
49
|
-
const allowMentions = gate.allow.find(isMentionRule)
|
|
50
|
-
const allowFollowing = gate.allow.find(isFollowingRule)
|
|
60
|
+
const allowMentions = !!gate.allow.find(isMentionRule)
|
|
61
|
+
const allowFollowing = !!gate.allow.find(isFollowingRule)
|
|
51
62
|
const allowListUris = gate.allow?.filter(isListRule).map((item) => item.list)
|
|
52
63
|
|
|
53
64
|
// check mentions first since it's quick and synchronous
|
|
54
65
|
if (allowMentions) {
|
|
55
|
-
const isMentioned =
|
|
56
|
-
return facet.features.some(
|
|
66
|
+
const isMentioned = rootPost?.facets?.some((facet) => {
|
|
67
|
+
return facet.features.some(
|
|
68
|
+
(item) => isMention(item) && item.did === replierDid,
|
|
69
|
+
)
|
|
57
70
|
})
|
|
58
71
|
if (isMentioned) {
|
|
59
|
-
return
|
|
72
|
+
return { canReply: true, allowMentions, allowFollowing, allowListUris }
|
|
60
73
|
}
|
|
61
74
|
}
|
|
75
|
+
return { allowMentions, allowFollowing, allowListUris }
|
|
76
|
+
}
|
|
62
77
|
|
|
63
|
-
|
|
64
|
-
|
|
78
|
+
export const violatesThreadGate = async (
|
|
79
|
+
db: DatabaseSchema,
|
|
80
|
+
replierDid: string,
|
|
81
|
+
ownerDid: string,
|
|
82
|
+
rootPost: PostRecord | null,
|
|
83
|
+
gate: GateRecord | null,
|
|
84
|
+
) => {
|
|
85
|
+
const {
|
|
86
|
+
canReply,
|
|
87
|
+
allowFollowing,
|
|
88
|
+
allowListUris = [],
|
|
89
|
+
} = parseThreadGate(replierDid, ownerDid, rootPost, gate)
|
|
90
|
+
if (canReply) {
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
if (!allowFollowing && !allowListUris?.length) {
|
|
65
94
|
return true
|
|
66
95
|
}
|
|
67
96
|
const { ref } = db.dynamic
|
|
68
97
|
const nullResult = sql<null>`${null}`
|
|
69
98
|
const check = await db
|
|
70
|
-
.selectFrom(valuesList([
|
|
99
|
+
.selectFrom(valuesList([replierDid]).as(sql`subject (did)`))
|
|
71
100
|
.select([
|
|
72
101
|
allowFollowing
|
|
73
102
|
? db
|
|
74
103
|
.selectFrom('follow')
|
|
75
|
-
.where('creator', '=',
|
|
104
|
+
.where('creator', '=', ownerDid)
|
|
76
105
|
.whereRef('subjectDid', '=', ref('subject.did'))
|
|
77
106
|
.select('creator')
|
|
78
107
|
.as('isFollowed')
|
|
@@ -91,8 +120,7 @@ export const violatesThreadGate = async (
|
|
|
91
120
|
|
|
92
121
|
if (allowFollowing && check?.isFollowed) {
|
|
93
122
|
return false
|
|
94
|
-
}
|
|
95
|
-
if (allowListUris.length && check?.isInList) {
|
|
123
|
+
} else if (allowListUris.length && check?.isInList) {
|
|
96
124
|
return false
|
|
97
125
|
}
|
|
98
126
|
|