@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
@@ -2,11 +2,14 @@ import AppContext from '../../../../context'
2
2
  import { Server } from '../../../../lexicon'
3
3
  import { InvalidRequestError } from '@atproto/xrpc-server'
4
4
  import AtpAgent from '@atproto/api'
5
- import { AtUri } from '@atproto/syntax'
6
5
  import { mapDefined } from '@atproto/common'
7
6
  import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/searchPosts'
8
7
  import { Database } from '../../../../db'
9
- import { FeedHydrationState, FeedService } from '../../../../services/feed'
8
+ import {
9
+ FeedHydrationState,
10
+ FeedRow,
11
+ FeedService,
12
+ } from '../../../../services/feed'
10
13
  import { ActorService } from '../../../../services/actor'
11
14
  import { createPipeline } from '../../../../pipeline'
12
15
 
@@ -51,9 +54,11 @@ const skeleton = async (
51
54
  cursor: params.cursor,
52
55
  limit: params.limit,
53
56
  })
57
+ const postUris = res.data.posts.map((a) => a.uri)
58
+ const feedItems = await ctx.feedService.postUrisToFeedItems(postUris)
54
59
  return {
55
60
  params,
56
- postUris: res.data.posts.map((a) => a.uri),
61
+ feedItems,
57
62
  cursor: res.data.cursor,
58
63
  hitsTotal: res.data.hitsTotal,
59
64
  }
@@ -64,12 +69,10 @@ const hydration = async (
64
69
  ctx: Context,
65
70
  ): Promise<HydrationState> => {
66
71
  const { feedService } = ctx
67
- const { params, postUris } = state
68
- const uris = new Set<string>(postUris)
69
- const dids = new Set<string>(postUris.map((uri) => new AtUri(uri).hostname))
72
+ const { params, feedItems } = state
73
+ const refs = feedService.feedItemRefs(feedItems)
70
74
  const hydrated = await feedService.feedHydration({
71
- uris,
72
- dids,
75
+ ...refs,
73
76
  viewer: params.viewer,
74
77
  })
75
78
  return { ...state, ...hydrated }
@@ -77,32 +80,32 @@ const hydration = async (
77
80
 
78
81
  const noBlocks = (state: HydrationState): HydrationState => {
79
82
  const { viewer } = state.params
80
- state.postUris = state.postUris.filter((uri) => {
81
- const post = state.posts[uri]
82
- if (!viewer || !post) return true
83
- return !state.bam.block([viewer, post.creator])
83
+ state.feedItems = state.feedItems.filter((item) => {
84
+ if (!viewer) return true
85
+ return !state.bam.block([viewer, item.postAuthorDid])
84
86
  })
85
87
  return state
86
88
  }
87
89
 
88
90
  const presentation = (state: HydrationState, ctx: Context) => {
89
91
  const { feedService, actorService } = ctx
90
- const { postUris, profiles, params } = state
92
+ const { feedItems, profiles, params } = state
91
93
  const actors = actorService.views.profileBasicPresentation(
92
94
  Object.keys(profiles),
93
95
  state,
94
- { viewer: params.viewer },
96
+ params.viewer,
95
97
  )
96
98
 
97
- const postViews = mapDefined(postUris, (uri) =>
99
+ const postViews = mapDefined(feedItems, (item) =>
98
100
  feedService.views.formatPostView(
99
- uri,
101
+ item.postUri,
100
102
  actors,
101
103
  state.posts,
102
104
  state.threadgates,
103
105
  state.embeds,
104
106
  state.labels,
105
107
  state.lists,
108
+ params.viewer,
106
109
  ),
107
110
  )
108
111
  return { posts: postViews, cursor: state.cursor, hitsTotal: state.hitsTotal }
@@ -119,7 +122,7 @@ type Params = QueryParams & { viewer: string | null }
119
122
 
120
123
  type SkeletonState = {
121
124
  params: Params
122
- postUris: string[]
125
+ feedItems: FeedRow[]
123
126
  hitsTotal?: number
124
127
  cursor?: string
125
128
  }
@@ -91,17 +91,20 @@ const presentation = (state: HydrationState, ctx: Context) => {
91
91
  const actors = actorService.views.profilePresentation(
92
92
  Object.keys(profileState.profiles),
93
93
  profileState,
94
- { viewer: params.viewer },
94
+ params.viewer,
95
95
  )
96
96
  const creator = actors[list.creator]
97
97
  if (!creator) {
98
98
  throw new InvalidRequestError(`Actor not found: ${list.handle}`)
99
99
  }
100
100
  const listView = graphService.formatListView(list, actors)
101
+ if (!listView) {
102
+ throw new InvalidRequestError('List not found')
103
+ }
101
104
  const items = mapDefined(listItems, (item) => {
102
105
  const subject = actors[item.did]
103
106
  if (!subject) return
104
- return { subject }
107
+ return { uri: item.uri, subject }
105
108
  })
106
109
  return { list: listView, items, cursor }
107
110
  }
@@ -119,7 +122,7 @@ type Params = QueryParams & {
119
122
  type SkeletonState = {
120
123
  params: Params
121
124
  list: Actor & ListInfo
122
- listItems: (Actor & { cid: string; sortAt: string })[]
125
+ listItems: (Actor & { uri: string; cid: string; sortAt: string })[]
123
126
  cursor?: string
124
127
  }
125
128
 
@@ -1,3 +1,4 @@
1
+ import { mapDefined } from '@atproto/common'
1
2
  import { Server } from '../../../../lexicon'
2
3
  import { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getListBlocks'
3
4
  import { paginate, TimeCidKeyset } from '../../../../db/pagination'
@@ -87,9 +88,9 @@ const presentation = (state: HydrationState, ctx: Context) => {
87
88
  const actors = actorService.views.profilePresentation(
88
89
  Object.keys(profileState.profiles),
89
90
  profileState,
90
- { viewer: params.viewer },
91
+ params.viewer,
91
92
  )
92
- const lists = listInfos.map((list) =>
93
+ const lists = mapDefined(listInfos, (list) =>
93
94
  graphService.formatListView(list, actors),
94
95
  )
95
96
  return { lists, cursor }
@@ -1,3 +1,4 @@
1
+ import { mapDefined } from '@atproto/common'
1
2
  import { Server } from '../../../../lexicon'
2
3
  import { paginate, TimeCidKeyset } from '../../../../db/pagination'
3
4
  import AppContext from '../../../../context'
@@ -34,7 +35,7 @@ export default function (server: Server, ctx: AppContext) {
34
35
  const actorService = ctx.services.actor(db)
35
36
  const profiles = await actorService.views.profiles(listsRes, requester)
36
37
 
37
- const lists = listsRes.map((row) =>
38
+ const lists = mapDefined(listsRes, (row) =>
38
39
  graphService.formatListView(row, profiles),
39
40
  )
40
41
 
@@ -1,3 +1,4 @@
1
+ import { mapDefined } from '@atproto/common'
1
2
  import { InvalidRequestError } from '@atproto/xrpc-server'
2
3
  import { Server } from '../../../../lexicon'
3
4
  import { paginate, TimeCidKeyset } from '../../../../db/pagination'
@@ -39,7 +40,7 @@ export default function (server: Server, ctx: AppContext) {
39
40
  throw new InvalidRequestError(`Actor not found: ${actor}`)
40
41
  }
41
42
 
42
- const lists = listsRes.map((row) =>
43
+ const lists = mapDefined(listsRes, (row) =>
43
44
  graphService.formatListView(row, profiles),
44
45
  )
45
46
 
@@ -57,7 +57,9 @@ export default function (server: Server, ctx: AppContext) {
57
57
  const gen = genInfos[row.uri]
58
58
  if (!gen) continue
59
59
  const view = feedService.views.formatFeedGeneratorView(gen, profiles)
60
- genViews.push(view)
60
+ if (view) {
61
+ genViews.push(view)
62
+ }
61
63
  }
62
64
 
63
65
  return {
@@ -6,11 +6,11 @@ import { CID } from 'multiformats/cid'
6
6
  import { ensureValidDid } from '@atproto/syntax'
7
7
  import { forwardStreamErrors, VerifyCidTransform } from '@atproto/common'
8
8
  import { IdResolver, DidNotFoundError } from '@atproto/identity'
9
- import { TAKEDOWN } from '../lexicon/types/com/atproto/admin/defs'
10
9
  import AppContext from '../context'
11
10
  import { httpLogger as log } from '../logger'
12
11
  import { retryHttp } from '../util/retry'
13
12
  import { Database } from '../db'
13
+ import { sql } from 'kysely'
14
14
 
15
15
  // Resolve and verify blob from its origin host
16
16
 
@@ -84,19 +84,14 @@ export async function resolveBlob(
84
84
  idResolver: IdResolver,
85
85
  ) {
86
86
  const cidStr = cid.toString()
87
+
87
88
  const [{ pds }, takedown] = await Promise.all([
88
89
  idResolver.did.resolveAtprotoData(did), // @TODO cache did info
89
90
  db.db
90
- .selectFrom('moderation_action_subject_blob')
91
- .select('actionId')
92
- .innerJoin(
93
- 'moderation_action',
94
- 'moderation_action.id',
95
- 'moderation_action_subject_blob.actionId',
96
- )
97
- .where('cid', '=', cidStr)
98
- .where('action', '=', TAKEDOWN)
99
- .where('reversedAt', 'is', null)
91
+ .selectFrom('moderation_subject_status')
92
+ .select('id')
93
+ .where('blobCids', '@>', sql`CAST(${JSON.stringify([cidStr])} AS JSONB)`)
94
+ .where('takendown', 'is', true)
100
95
  .executeTakeFirst(),
101
96
  ])
102
97
  if (takedown) {
@@ -0,0 +1,220 @@
1
+ import { CID } from 'multiformats/cid'
2
+ import { AtUri } from '@atproto/syntax'
3
+ import {
4
+ AuthRequiredError,
5
+ InvalidRequestError,
6
+ UpstreamFailureError,
7
+ } from '@atproto/xrpc-server'
8
+ import { Server } from '../../../../lexicon'
9
+ import AppContext from '../../../../context'
10
+ import { getSubject } from '../moderation/util'
11
+ import {
12
+ isModEventLabel,
13
+ isModEventReverseTakedown,
14
+ isModEventTakedown,
15
+ } from '../../../../lexicon/types/com/atproto/admin/defs'
16
+ import { TakedownSubjects } from '../../../../services/moderation'
17
+ import { retryHttp } from '../../../../util/retry'
18
+
19
+ export default function (server: Server, ctx: AppContext) {
20
+ server.com.atproto.admin.emitModerationEvent({
21
+ auth: ctx.roleVerifier,
22
+ handler: async ({ input, auth }) => {
23
+ const access = auth.credentials
24
+ const db = ctx.db.getPrimary()
25
+ const moderationService = ctx.services.moderation(db)
26
+ const { subject, createdBy, subjectBlobCids, event } = input.body
27
+ const isTakedownEvent = isModEventTakedown(event)
28
+ const isReverseTakedownEvent = isModEventReverseTakedown(event)
29
+ const isLabelEvent = isModEventLabel(event)
30
+
31
+ // apply access rules
32
+
33
+ // if less than moderator access then can not takedown an account
34
+ if (!access.moderator && isTakedownEvent && 'did' in subject) {
35
+ throw new AuthRequiredError(
36
+ 'Must be a full moderator to perform an account takedown',
37
+ )
38
+ }
39
+ // if less than moderator access then can only take ack and escalation actions
40
+ if (!access.moderator && (isTakedownEvent || isReverseTakedownEvent)) {
41
+ throw new AuthRequiredError(
42
+ 'Must be a full moderator to take this type of action',
43
+ )
44
+ }
45
+ // if less than moderator access then can not apply labels
46
+ if (!access.moderator && isLabelEvent) {
47
+ throw new AuthRequiredError('Must be a full moderator to label content')
48
+ }
49
+
50
+ if (isLabelEvent) {
51
+ validateLabels([
52
+ ...(event.createLabelVals ?? []),
53
+ ...(event.negateLabelVals ?? []),
54
+ ])
55
+ }
56
+
57
+ const subjectInfo = getSubject(subject)
58
+
59
+ if (isTakedownEvent || isReverseTakedownEvent) {
60
+ const isSubjectTakendown = await moderationService.isSubjectTakendown(
61
+ subjectInfo,
62
+ )
63
+
64
+ if (isSubjectTakendown && isTakedownEvent) {
65
+ throw new InvalidRequestError(`Subject is already taken down`)
66
+ }
67
+
68
+ if (!isSubjectTakendown && isReverseTakedownEvent) {
69
+ throw new InvalidRequestError(`Subject is not taken down`)
70
+ }
71
+ }
72
+
73
+ const { result: moderationEvent, takenDown } = await db.transaction(
74
+ async (dbTxn) => {
75
+ const moderationTxn = ctx.services.moderation(dbTxn)
76
+ const labelTxn = ctx.services.label(dbTxn)
77
+
78
+ const result = await moderationTxn.logEvent({
79
+ event,
80
+ subject: subjectInfo,
81
+ subjectBlobCids:
82
+ subjectBlobCids?.map((cid) => CID.parse(cid)) ?? [],
83
+ createdBy,
84
+ })
85
+
86
+ let takenDown: TakedownSubjects | undefined
87
+
88
+ if (
89
+ result.subjectType === 'com.atproto.admin.defs#repoRef' &&
90
+ result.subjectDid
91
+ ) {
92
+ // No credentials to revoke on appview
93
+ if (isTakedownEvent) {
94
+ takenDown = await moderationTxn.takedownRepo({
95
+ takedownId: result.id,
96
+ did: result.subjectDid,
97
+ })
98
+ }
99
+
100
+ if (isReverseTakedownEvent) {
101
+ await moderationTxn.reverseTakedownRepo({
102
+ did: result.subjectDid,
103
+ })
104
+ takenDown = {
105
+ subjects: [
106
+ {
107
+ $type: 'com.atproto.admin.defs#repoRef',
108
+ did: result.subjectDid,
109
+ },
110
+ ],
111
+ did: result.subjectDid,
112
+ }
113
+ }
114
+ }
115
+
116
+ if (
117
+ result.subjectType === 'com.atproto.repo.strongRef' &&
118
+ result.subjectUri
119
+ ) {
120
+ const blobCids = subjectBlobCids?.map((cid) => CID.parse(cid)) ?? []
121
+ if (isTakedownEvent) {
122
+ takenDown = await moderationTxn.takedownRecord({
123
+ takedownId: result.id,
124
+ uri: new AtUri(result.subjectUri),
125
+ // TODO: I think this will always be available for strongRefs?
126
+ cid: CID.parse(result.subjectCid as string),
127
+ blobCids,
128
+ })
129
+ }
130
+
131
+ if (isReverseTakedownEvent) {
132
+ await moderationTxn.reverseTakedownRecord({
133
+ uri: new AtUri(result.subjectUri),
134
+ })
135
+ takenDown = {
136
+ did: result.subjectDid,
137
+ subjects: [
138
+ {
139
+ $type: 'com.atproto.repo.strongRef',
140
+ uri: result.subjectUri,
141
+ cid: result.subjectCid ?? '',
142
+ },
143
+ ...blobCids.map((cid) => ({
144
+ $type: 'com.atproto.admin.defs#repoBlobRef',
145
+ did: result.subjectDid,
146
+ cid: cid.toString(),
147
+ recordUri: result.subjectUri,
148
+ })),
149
+ ],
150
+ }
151
+ }
152
+ }
153
+
154
+ if (isLabelEvent) {
155
+ await labelTxn.formatAndCreate(
156
+ ctx.cfg.labelerDid,
157
+ result.subjectUri ?? result.subjectDid,
158
+ result.subjectCid,
159
+ {
160
+ create: result.createLabelVals?.length
161
+ ? result.createLabelVals.split(' ')
162
+ : undefined,
163
+ negate: result.negateLabelVals?.length
164
+ ? result.negateLabelVals.split(' ')
165
+ : undefined,
166
+ },
167
+ )
168
+ }
169
+
170
+ return { result, takenDown }
171
+ },
172
+ )
173
+
174
+ if (takenDown && ctx.moderationPushAgent) {
175
+ const { did, subjects } = takenDown
176
+ if (did && subjects.length > 0) {
177
+ const agent = ctx.moderationPushAgent
178
+ const results = await Promise.allSettled(
179
+ subjects.map((subject) =>
180
+ retryHttp(() =>
181
+ agent.api.com.atproto.admin.updateSubjectStatus({
182
+ subject,
183
+ takedown: isTakedownEvent
184
+ ? {
185
+ applied: true,
186
+ ref: moderationEvent.id.toString(),
187
+ }
188
+ : {
189
+ applied: false,
190
+ },
191
+ }),
192
+ ),
193
+ ),
194
+ )
195
+ const hadFailure = results.some((r) => r.status === 'rejected')
196
+ if (hadFailure) {
197
+ throw new UpstreamFailureError('failed to apply action on PDS')
198
+ }
199
+ }
200
+ }
201
+
202
+ return {
203
+ encoding: 'application/json',
204
+ body: await moderationService.views.event(moderationEvent),
205
+ }
206
+ },
207
+ })
208
+ }
209
+
210
+ const validateLabels = (labels: string[]) => {
211
+ for (const label of labels) {
212
+ for (const char of badChars) {
213
+ if (label.includes(char)) {
214
+ throw new InvalidRequestError(`Invalid label: ${label}`)
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ const badChars = [' ', ',', ';', `'`, `"`]
@@ -2,23 +2,17 @@ import { Server } from '../../../../lexicon'
2
2
  import AppContext from '../../../../context'
3
3
 
4
4
  export default function (server: Server, ctx: AppContext) {
5
- server.com.atproto.admin.getModerationActions({
5
+ server.com.atproto.admin.getModerationEvent({
6
6
  auth: ctx.roleVerifier,
7
7
  handler: async ({ params }) => {
8
- const { subject, limit = 50, cursor } = params
8
+ const { id } = params
9
9
  const db = ctx.db.getPrimary()
10
10
  const moderationService = ctx.services.moderation(db)
11
- const results = await moderationService.getActions({
12
- subject,
13
- limit,
14
- cursor,
15
- })
11
+ const event = await moderationService.getEventOrThrow(id)
12
+ const eventDetail = await moderationService.views.eventDetail(event)
16
13
  return {
17
14
  encoding: 'application/json',
18
- body: {
19
- cursor: results.at(-1)?.id.toString() ?? undefined,
20
- actions: await moderationService.views.action(results),
21
- },
15
+ body: eventDetail,
22
16
  }
23
17
  },
24
18
  })
@@ -18,6 +18,7 @@ export default function (server: Server, ctx: AppContext) {
18
18
  if (!result) {
19
19
  throw new InvalidRequestError('Record not found', 'RecordNotFound')
20
20
  }
21
+
21
22
  const [record, accountInfo] = await Promise.all([
22
23
  ctx.services.moderation(db).views.recordDetail(result),
23
24
  getPdsAccountInfo(ctx, result.did),
@@ -1,39 +1,36 @@
1
1
  import { Server } from '../../../../lexicon'
2
2
  import AppContext from '../../../../context'
3
+ import { getEventType } from '../moderation/util'
3
4
 
4
5
  export default function (server: Server, ctx: AppContext) {
5
- server.com.atproto.admin.getModerationReports({
6
+ server.com.atproto.admin.queryModerationEvents({
6
7
  auth: ctx.roleVerifier,
7
8
  handler: async ({ params }) => {
8
9
  const {
9
10
  subject,
10
- resolved,
11
- actionType,
12
11
  limit = 50,
13
12
  cursor,
14
- ignoreSubjects,
15
- reverse = false,
16
- reporters = [],
17
- actionedBy,
13
+ sortDirection = 'desc',
14
+ types,
15
+ includeAllUserRecords = false,
16
+ createdBy,
18
17
  } = params
19
18
  const db = ctx.db.getPrimary()
20
19
  const moderationService = ctx.services.moderation(db)
21
- const results = await moderationService.getReports({
20
+ const results = await moderationService.getEvents({
21
+ types: types?.length ? types.map(getEventType) : [],
22
22
  subject,
23
- resolved,
24
- actionType,
23
+ createdBy,
25
24
  limit,
26
25
  cursor,
27
- ignoreSubjects,
28
- reverse,
29
- reporters,
30
- actionedBy,
26
+ sortDirection,
27
+ includeAllUserRecords,
31
28
  })
32
29
  return {
33
30
  encoding: 'application/json',
34
31
  body: {
35
- cursor: results.at(-1)?.id.toString() ?? undefined,
36
- reports: await moderationService.views.report(results),
32
+ cursor: results.cursor,
33
+ events: await moderationService.views.event(results.events),
37
34
  },
38
35
  }
39
36
  },
@@ -0,0 +1,55 @@
1
+ import { Server } from '../../../../lexicon'
2
+ import AppContext from '../../../../context'
3
+ import { getReviewState } from '../moderation/util'
4
+
5
+ export default function (server: Server, ctx: AppContext) {
6
+ server.com.atproto.admin.queryModerationStatuses({
7
+ auth: ctx.roleVerifier,
8
+ handler: async ({ params }) => {
9
+ const {
10
+ subject,
11
+ takendown,
12
+ reviewState,
13
+ reviewedAfter,
14
+ reviewedBefore,
15
+ reportedAfter,
16
+ reportedBefore,
17
+ ignoreSubjects,
18
+ lastReviewedBy,
19
+ sortDirection = 'desc',
20
+ sortField = 'lastReportedAt',
21
+ includeMuted = false,
22
+ limit = 50,
23
+ cursor,
24
+ } = params
25
+ const db = ctx.db.getPrimary()
26
+ const moderationService = ctx.services.moderation(db)
27
+ const results = await moderationService.getSubjectStatuses({
28
+ reviewState: getReviewState(reviewState),
29
+ subject,
30
+ takendown,
31
+ reviewedAfter,
32
+ reviewedBefore,
33
+ reportedAfter,
34
+ reportedBefore,
35
+ includeMuted,
36
+ ignoreSubjects,
37
+ sortDirection,
38
+ lastReviewedBy,
39
+ sortField,
40
+ limit,
41
+ cursor,
42
+ })
43
+ const subjectStatuses = moderationService.views.subjectStatus(
44
+ results.statuses,
45
+ )
46
+ return {
47
+ encoding: 'application/json',
48
+ body: {
49
+ cursor: results.cursor,
50
+ subjectStatuses,
51
+ },
52
+ }
53
+ },
54
+ })
55
+ }
@@ -22,15 +22,17 @@ export default function (server: Server, ctx: AppContext) {
22
22
  }
23
23
  }
24
24
 
25
- const moderationService = ctx.services.moderation(db)
26
-
27
- const report = await moderationService.report({
28
- reasonType: getReasonType(reasonType),
29
- reason,
30
- subject: getSubject(subject),
31
- reportedBy: requester || ctx.cfg.serverDid,
25
+ const report = await db.transaction(async (dbTxn) => {
26
+ const moderationTxn = ctx.services.moderation(dbTxn)
27
+ return moderationTxn.report({
28
+ reasonType: getReasonType(reasonType),
29
+ reason,
30
+ subject: getSubject(subject),
31
+ reportedBy: requester || ctx.cfg.serverDid,
32
+ })
32
33
  })
33
34
 
35
+ const moderationService = ctx.services.moderation(db)
34
36
  return {
35
37
  encoding: 'application/json',
36
38
  body: moderationService.views.reportPublic(report),