@atproto/bsky 0.0.15 → 0.0.17

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