@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.
Files changed (170) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/api/com/atproto/moderation/util.d.ts +4 -3
  3. package/dist/context.d.ts +15 -0
  4. package/dist/db/index.js +26 -1
  5. package/dist/db/index.js.map +3 -3
  6. package/dist/db/migrations/20231003T202833377Z-create-moderation-subject-status.d.ts +3 -0
  7. package/dist/db/migrations/index.d.ts +1 -0
  8. package/dist/db/pagination.d.ts +2 -1
  9. package/dist/db/{periodic-moderation-action-reversal.d.ts → periodic-moderation-event-reversal.d.ts} +3 -5
  10. package/dist/db/tables/moderation.d.ts +24 -34
  11. package/dist/feed-gen/types.d.ts +1 -1
  12. package/dist/index.d.ts +2 -1
  13. package/dist/index.js +2750 -2121
  14. package/dist/index.js.map +3 -3
  15. package/dist/lexicon/index.d.ts +11 -18
  16. package/dist/lexicon/lexicons.d.ts +414 -399
  17. package/dist/lexicon/types/app/bsky/feed/defs.d.ts +1 -7
  18. package/dist/lexicon/types/app/bsky/graph/defs.d.ts +1 -0
  19. package/dist/lexicon/types/com/atproto/admin/defs.d.ts +114 -48
  20. package/dist/lexicon/types/com/atproto/admin/{takeModerationAction.d.ts → emitModerationEvent.d.ts} +5 -6
  21. package/dist/lexicon/types/com/atproto/admin/{getModerationAction.d.ts → getModerationEvent.d.ts} +1 -1
  22. package/dist/lexicon/types/com/atproto/admin/{getModerationActions.d.ts → queryModerationEvents.d.ts} +5 -1
  23. package/dist/lexicon/types/com/atproto/admin/{getModerationReports.d.ts → queryModerationStatuses.d.ts} +12 -6
  24. package/dist/lexicon/types/com/atproto/admin/sendEmail.d.ts +1 -0
  25. package/dist/migrate-moderation-data.d.ts +1 -0
  26. package/dist/services/actor/views.d.ts +2 -5
  27. package/dist/services/feed/index.d.ts +1 -0
  28. package/dist/services/feed/util.d.ts +9 -1
  29. package/dist/services/feed/views.d.ts +6 -17
  30. package/dist/services/graph/index.d.ts +5 -29
  31. package/dist/services/graph/types.d.ts +1 -0
  32. package/dist/services/moderation/index.d.ts +135 -72
  33. package/dist/services/moderation/pagination.d.ts +36 -0
  34. package/dist/services/moderation/status.d.ts +13 -0
  35. package/dist/services/moderation/types.d.ts +35 -0
  36. package/dist/services/moderation/views.d.ts +18 -14
  37. package/dist/util/debug.d.ts +1 -1
  38. package/package.json +11 -11
  39. package/src/api/app/bsky/feed/getActorFeeds.ts +2 -1
  40. package/src/api/app/bsky/feed/getActorLikes.ts +1 -3
  41. package/src/api/app/bsky/feed/getAuthorFeed.ts +1 -3
  42. package/src/api/app/bsky/feed/getFeed.ts +9 -9
  43. package/src/api/app/bsky/feed/getFeedGenerator.ts +3 -0
  44. package/src/api/app/bsky/feed/getFeedGenerators.ts +2 -1
  45. package/src/api/app/bsky/feed/getListFeed.ts +1 -3
  46. package/src/api/app/bsky/feed/getPostThread.ts +15 -54
  47. package/src/api/app/bsky/feed/getPosts.ts +21 -18
  48. package/src/api/app/bsky/feed/getSuggestedFeeds.ts +2 -1
  49. package/src/api/app/bsky/feed/getTimeline.ts +1 -3
  50. package/src/api/app/bsky/feed/searchPosts.ts +20 -17
  51. package/src/api/app/bsky/graph/getList.ts +6 -3
  52. package/src/api/app/bsky/graph/getListBlocks.ts +3 -2
  53. package/src/api/app/bsky/graph/getListMutes.ts +2 -1
  54. package/src/api/app/bsky/graph/getLists.ts +2 -1
  55. package/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +3 -1
  56. package/src/api/blob-resolver.ts +6 -11
  57. package/src/api/com/atproto/admin/emitModerationEvent.ts +220 -0
  58. package/src/api/com/atproto/admin/{getModerationActions.ts → getModerationEvent.ts} +5 -11
  59. package/src/api/com/atproto/admin/getRecord.ts +1 -0
  60. package/src/api/com/atproto/admin/{getModerationReports.ts → queryModerationEvents.ts} +13 -16
  61. package/src/api/com/atproto/admin/queryModerationStatuses.ts +55 -0
  62. package/src/api/com/atproto/moderation/createReport.ts +9 -7
  63. package/src/api/com/atproto/moderation/util.ts +38 -20
  64. package/src/api/index.ts +8 -14
  65. package/src/auth.ts +29 -21
  66. package/src/auto-moderator/index.ts +26 -19
  67. package/src/context.ts +4 -0
  68. package/src/db/migrations/20231003T202833377Z-create-moderation-subject-status.ts +123 -0
  69. package/src/db/migrations/index.ts +1 -0
  70. package/src/db/pagination.ts +26 -3
  71. package/src/db/{periodic-moderation-action-reversal.ts → periodic-moderation-event-reversal.ts} +50 -46
  72. package/src/db/tables/moderation.ts +35 -52
  73. package/src/feed-gen/best-of-follows.ts +6 -3
  74. package/src/feed-gen/bsky-team.ts +1 -1
  75. package/src/feed-gen/hot-classic.ts +1 -1
  76. package/src/feed-gen/mutuals.ts +6 -2
  77. package/src/feed-gen/types.ts +1 -1
  78. package/src/feed-gen/whats-hot.ts +1 -1
  79. package/src/feed-gen/with-friends.ts +7 -3
  80. package/src/index.ts +2 -1
  81. package/src/lexicon/index.ts +30 -67
  82. package/src/lexicon/lexicons.ts +526 -491
  83. package/src/lexicon/types/app/bsky/feed/defs.ts +1 -18
  84. package/src/lexicon/types/app/bsky/graph/defs.ts +1 -0
  85. package/src/lexicon/types/com/atproto/admin/defs.ts +276 -84
  86. package/src/lexicon/types/com/atproto/admin/{takeModerationAction.ts → emitModerationEvent.ts} +13 -11
  87. package/src/lexicon/types/com/atproto/admin/{getModerationReport.ts → getModerationEvent.ts} +1 -1
  88. package/src/lexicon/types/com/atproto/admin/{getModerationActions.ts → queryModerationEvents.ts} +8 -1
  89. package/src/lexicon/types/com/atproto/admin/{getModerationReports.ts → queryModerationStatuses.ts} +21 -14
  90. package/src/lexicon/types/com/atproto/admin/sendEmail.ts +1 -0
  91. package/src/migrate-moderation-data.ts +414 -0
  92. package/src/services/actor/views.ts +5 -14
  93. package/src/services/feed/index.ts +26 -7
  94. package/src/services/feed/util.ts +47 -19
  95. package/src/services/feed/views.ts +68 -4
  96. package/src/services/graph/index.ts +21 -3
  97. package/src/services/graph/types.ts +1 -0
  98. package/src/services/indexing/plugins/block.ts +2 -3
  99. package/src/services/indexing/plugins/feed-generator.ts +2 -3
  100. package/src/services/indexing/plugins/follow.ts +2 -3
  101. package/src/services/indexing/plugins/like.ts +2 -3
  102. package/src/services/indexing/plugins/list-block.ts +2 -3
  103. package/src/services/indexing/plugins/list-item.ts +2 -3
  104. package/src/services/indexing/plugins/list.ts +2 -3
  105. package/src/services/indexing/plugins/post.ts +3 -4
  106. package/src/services/indexing/plugins/repost.ts +2 -3
  107. package/src/services/indexing/plugins/thread-gate.ts +2 -3
  108. package/src/services/label/index.ts +2 -3
  109. package/src/services/moderation/index.ts +380 -395
  110. package/src/services/moderation/pagination.ts +96 -0
  111. package/src/services/moderation/status.ts +244 -0
  112. package/src/services/moderation/types.ts +49 -0
  113. package/src/services/moderation/views.ts +278 -329
  114. package/src/util/debug.ts +2 -2
  115. package/tests/__snapshots__/feed-generation.test.ts.snap +322 -6
  116. package/tests/__snapshots__/indexing.test.ts.snap +0 -6
  117. package/tests/admin/__snapshots__/get-record.test.ts.snap +30 -132
  118. package/tests/admin/__snapshots__/get-repo.test.ts.snap +14 -60
  119. package/tests/admin/__snapshots__/moderation-events.test.ts.snap +146 -0
  120. package/tests/admin/__snapshots__/moderation-statuses.test.ts.snap +64 -0
  121. package/tests/admin/__snapshots__/moderation.test.ts.snap +0 -125
  122. package/tests/admin/get-record.test.ts +5 -9
  123. package/tests/admin/get-repo.test.ts +5 -9
  124. package/tests/admin/moderation-events.test.ts +221 -0
  125. package/tests/admin/moderation-statuses.test.ts +145 -0
  126. package/tests/admin/moderation.test.ts +512 -860
  127. package/tests/admin/repo-search.test.ts +2 -3
  128. package/tests/auto-moderator/fuzzy-matcher.test.ts +2 -1
  129. package/tests/auto-moderator/takedowns.test.ts +45 -18
  130. package/tests/feed-generation.test.ts +57 -9
  131. package/tests/views/__snapshots__/block-lists.test.ts.snap +3 -9
  132. package/tests/views/__snapshots__/blocks.test.ts.snap +0 -9
  133. package/tests/views/__snapshots__/mute-lists.test.ts.snap +5 -5
  134. package/tests/views/__snapshots__/mutes.test.ts.snap +0 -3
  135. package/tests/views/__snapshots__/thread.test.ts.snap +0 -30
  136. package/tests/views/actor-search.test.ts +2 -3
  137. package/tests/views/author-feed.test.ts +42 -36
  138. package/tests/views/follows.test.ts +40 -35
  139. package/tests/views/list-feed.test.ts +17 -9
  140. package/tests/views/notifications.test.ts +13 -9
  141. package/tests/views/profile.test.ts +20 -18
  142. package/tests/views/thread.test.ts +54 -26
  143. package/tests/views/threadgating.test.ts +51 -19
  144. package/tests/views/timeline.test.ts +21 -13
  145. package/dist/api/com/atproto/admin/resolveModerationReports.d.ts +0 -3
  146. package/dist/api/com/atproto/admin/reverseModerationAction.d.ts +0 -3
  147. package/dist/api/com/atproto/admin/takeModerationAction.d.ts +0 -3
  148. package/dist/lexicon/types/com/atproto/admin/getModerationReport.d.ts +0 -29
  149. package/dist/lexicon/types/com/atproto/admin/resolveModerationReports.d.ts +0 -36
  150. package/dist/lexicon/types/com/atproto/admin/reverseModerationAction.d.ts +0 -36
  151. package/src/api/com/atproto/admin/getModerationAction.ts +0 -44
  152. package/src/api/com/atproto/admin/getModerationReport.ts +0 -43
  153. package/src/api/com/atproto/admin/resolveModerationReports.ts +0 -24
  154. package/src/api/com/atproto/admin/reverseModerationAction.ts +0 -115
  155. package/src/api/com/atproto/admin/takeModerationAction.ts +0 -156
  156. package/src/lexicon/types/com/atproto/admin/getModerationAction.ts +0 -41
  157. package/src/lexicon/types/com/atproto/admin/resolveModerationReports.ts +0 -49
  158. package/src/lexicon/types/com/atproto/admin/reverseModerationAction.ts +0 -49
  159. package/tests/admin/__snapshots__/get-moderation-action.test.ts.snap +0 -172
  160. package/tests/admin/__snapshots__/get-moderation-actions.test.ts.snap +0 -178
  161. package/tests/admin/__snapshots__/get-moderation-report.test.ts.snap +0 -177
  162. package/tests/admin/__snapshots__/get-moderation-reports.test.ts.snap +0 -307
  163. package/tests/admin/get-moderation-action.test.ts +0 -100
  164. package/tests/admin/get-moderation-actions.test.ts +0 -164
  165. package/tests/admin/get-moderation-report.test.ts +0 -100
  166. package/tests/admin/get-moderation-reports.test.ts +0 -332
  167. /package/dist/api/com/atproto/admin/{getModerationAction.d.ts → emitModerationEvent.d.ts} +0 -0
  168. /package/dist/api/com/atproto/admin/{getModerationActions.d.ts → getModerationEvent.d.ts} +0 -0
  169. /package/dist/api/com/atproto/admin/{getModerationReport.d.ts → queryModerationEvents.d.ts} +0 -0
  170. /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
- opts?: {
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, opts)
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
- { viewer },
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
- recordEmbedViews[uri] = {
409
- $type: 'app.bsky.feed.defs#generatorView',
410
- ...this.views.formatFeedGeneratorView(feedGenInfos[uri], actorInfos),
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
- recordEmbedViews[uri] = {
414
- $type: 'app.bsky.graph.defs#listView',
415
- ...this.services.graph.formatListView(listViews[uri], actorInfos),
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
- export const violatesThreadGate = async (
40
- db: DatabaseSchema,
41
- did: string,
42
- owner: string,
43
- root: PostRecord | null,
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 (did === owner) return false
47
- if (!gate?.allow) return false
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 = root?.facets?.some((facet) => {
56
- return facet.features.some((item) => isMention(item) && item.did === did)
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 false
72
+ return { canReply: true, allowMentions, allowFollowing, allowListUris }
60
73
  }
61
74
  }
75
+ return { allowMentions, allowFollowing, allowListUris }
76
+ }
62
77
 
63
- // check follows and list containment
64
- if (!allowFollowing && !allowListUris.length) {
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([did]).as(sql`subject (did)`))
99
+ .selectFrom(valuesList([replierDid]).as(sql`subject (did)`))
71
100
  .select([
72
101
  allowFollowing
73
102
  ? db
74
103
  .selectFrom('follow')
75
- .where('creator', '=', owner)
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