@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,16 +1,8 @@
1
1
  import { CID } from 'multiformats/cid'
2
2
  import { InvalidRequestError } from '@atproto/xrpc-server'
3
3
  import { AtUri } from '@atproto/syntax'
4
- import { ModerationAction } from '../../../../db/tables/moderation'
5
- import { ModerationReport } from '../../../../db/tables/moderation'
6
4
  import { InputSchema as ReportInput } from '../../../../lexicon/types/com/atproto/moderation/createReport'
7
- import { InputSchema as ActionInput } from '../../../../lexicon/types/com/atproto/admin/takeModerationAction'
8
- import {
9
- ACKNOWLEDGE,
10
- FLAG,
11
- TAKEDOWN,
12
- ESCALATE,
13
- } from '../../../../lexicon/types/com/atproto/admin/defs'
5
+ import { InputSchema as ActionInput } from '../../../../lexicon/types/com/atproto/admin/emitModerationEvent'
14
6
  import {
15
7
  REASONOTHER,
16
8
  REASONSPAM,
@@ -19,6 +11,13 @@ import {
19
11
  REASONSEXUAL,
20
12
  REASONVIOLATION,
21
13
  } from '../../../../lexicon/types/com/atproto/moderation/defs'
14
+ import {
15
+ REVIEWCLOSED,
16
+ REVIEWESCALATED,
17
+ REVIEWOPEN,
18
+ } from '../../../../lexicon/types/com/atproto/admin/defs'
19
+ import { ModerationEvent } from '../../../../db/tables/moderation'
20
+ import { ModerationSubjectStatusRow } from '../../../../services/moderation/types'
22
21
 
23
22
  type SubjectInput = ReportInput['subject'] | ActionInput['subject']
24
23
 
@@ -34,8 +33,9 @@ export const getSubject = (subject: SubjectInput) => {
34
33
  typeof subject.uri === 'string' &&
35
34
  typeof subject.cid === 'string'
36
35
  ) {
36
+ const uri = new AtUri(subject.uri)
37
37
  return {
38
- uri: new AtUri(subject.uri),
38
+ uri,
39
39
  cid: CID.parse(subject.cid),
40
40
  }
41
41
  }
@@ -44,23 +44,28 @@ export const getSubject = (subject: SubjectInput) => {
44
44
 
45
45
  export const getReasonType = (reasonType: ReportInput['reasonType']) => {
46
46
  if (reasonTypes.has(reasonType)) {
47
- return reasonType as ModerationReport['reasonType']
47
+ return reasonType as NonNullable<ModerationEvent['meta']>['reportType']
48
48
  }
49
49
  throw new InvalidRequestError('Invalid reason type')
50
50
  }
51
51
 
52
- export const getAction = (action: ActionInput['action']) => {
53
- if (
54
- action === TAKEDOWN ||
55
- action === FLAG ||
56
- action === ACKNOWLEDGE ||
57
- action === ESCALATE
58
- ) {
59
- return action as ModerationAction['action']
52
+ export const getEventType = (type: string) => {
53
+ if (eventTypes.has(type)) {
54
+ return type as ModerationEvent['action']
60
55
  }
61
- throw new InvalidRequestError('Invalid action')
56
+ throw new InvalidRequestError('Invalid event type')
62
57
  }
63
58
 
59
+ export const getReviewState = (reviewState?: string) => {
60
+ if (!reviewState) return undefined
61
+ if (reviewStates.has(reviewState)) {
62
+ return reviewState as ModerationSubjectStatusRow['reviewState']
63
+ }
64
+ throw new InvalidRequestError('Invalid review state')
65
+ }
66
+
67
+ const reviewStates = new Set([REVIEWCLOSED, REVIEWESCALATED, REVIEWOPEN])
68
+
64
69
  const reasonTypes = new Set([
65
70
  REASONOTHER,
66
71
  REASONSPAM,
@@ -69,3 +74,16 @@ const reasonTypes = new Set([
69
74
  REASONSEXUAL,
70
75
  REASONVIOLATION,
71
76
  ])
77
+
78
+ const eventTypes = new Set([
79
+ 'com.atproto.admin.defs#modEventTakedown',
80
+ 'com.atproto.admin.defs#modEventAcknowledge',
81
+ 'com.atproto.admin.defs#modEventEscalate',
82
+ 'com.atproto.admin.defs#modEventComment',
83
+ 'com.atproto.admin.defs#modEventLabel',
84
+ 'com.atproto.admin.defs#modEventReport',
85
+ 'com.atproto.admin.defs#modEventMute',
86
+ 'com.atproto.admin.defs#modEventUnmute',
87
+ 'com.atproto.admin.defs#modEventReverseTakedown',
88
+ 'com.atproto.admin.defs#modEventEmail',
89
+ ])
package/src/api/index.ts CHANGED
@@ -41,18 +41,15 @@ import registerPush from './app/bsky/notification/registerPush'
41
41
  import getPopularFeedGenerators from './app/bsky/unspecced/getPopularFeedGenerators'
42
42
  import getTimelineSkeleton from './app/bsky/unspecced/getTimelineSkeleton'
43
43
  import createReport from './com/atproto/moderation/createReport'
44
- import resolveModerationReports from './com/atproto/admin/resolveModerationReports'
45
- import reverseModerationAction from './com/atproto/admin/reverseModerationAction'
46
- import takeModerationAction from './com/atproto/admin/takeModerationAction'
44
+ import emitModerationEvent from './com/atproto/admin/emitModerationEvent'
47
45
  import searchRepos from './com/atproto/admin/searchRepos'
48
46
  import adminGetRecord from './com/atproto/admin/getRecord'
49
47
  import getRepo from './com/atproto/admin/getRepo'
50
- import getModerationAction from './com/atproto/admin/getModerationAction'
51
- import getModerationActions from './com/atproto/admin/getModerationActions'
52
- import getModerationReport from './com/atproto/admin/getModerationReport'
53
- import getModerationReports from './com/atproto/admin/getModerationReports'
48
+ import queryModerationStatuses from './com/atproto/admin/queryModerationStatuses'
54
49
  import resolveHandle from './com/atproto/identity/resolveHandle'
55
50
  import getRecord from './com/atproto/repo/getRecord'
51
+ import queryModerationEvents from './com/atproto/admin/queryModerationEvents'
52
+ import getModerationEvent from './com/atproto/admin/getModerationEvent'
56
53
  import fetchLabels from './com/atproto/temp/fetchLabels'
57
54
 
58
55
  export * as health from './health'
@@ -105,16 +102,13 @@ export default function (server: Server, ctx: AppContext) {
105
102
  getTimelineSkeleton(server, ctx)
106
103
  // com.atproto
107
104
  createReport(server, ctx)
108
- resolveModerationReports(server, ctx)
109
- reverseModerationAction(server, ctx)
110
- takeModerationAction(server, ctx)
105
+ emitModerationEvent(server, ctx)
111
106
  searchRepos(server, ctx)
112
107
  adminGetRecord(server, ctx)
113
108
  getRepo(server, ctx)
114
- getModerationAction(server, ctx)
115
- getModerationActions(server, ctx)
116
- getModerationReport(server, ctx)
117
- getModerationReports(server, ctx)
109
+ getModerationEvent(server, ctx)
110
+ queryModerationEvents(server, ctx)
111
+ queryModerationStatuses(server, ctx)
118
112
  resolveHandle(server, ctx)
119
113
  getRecord(server, ctx)
120
114
  fetchLabels(server, ctx)
package/src/auth.ts CHANGED
@@ -7,35 +7,43 @@ import { ServerConfig } from './config'
7
7
  const BASIC = 'Basic '
8
8
  const BEARER = 'Bearer '
9
9
 
10
- export const authVerifier =
11
- (idResolver: IdResolver, opts: { aud: string | null }) =>
12
- async (reqCtx: { req: express.Request; res: express.Response }) => {
10
+ export const authVerifier = (
11
+ idResolver: IdResolver,
12
+ opts: { aud: string | null },
13
+ ) => {
14
+ const getSigningKey = async (
15
+ did: string,
16
+ forceRefresh: boolean,
17
+ ): Promise<string> => {
18
+ const atprotoData = await idResolver.did.resolveAtprotoData(
19
+ did,
20
+ forceRefresh,
21
+ )
22
+ return atprotoData.signingKey
23
+ }
24
+
25
+ return async (reqCtx: { req: express.Request; res: express.Response }) => {
13
26
  const jwtStr = getJwtStrFromReq(reqCtx.req)
14
27
  if (!jwtStr) {
15
28
  throw new AuthRequiredError('missing jwt', 'MissingJwt')
16
29
  }
17
- const payload = await verifyJwt(
18
- jwtStr,
19
- opts.aud,
20
- async (did, forceRefresh) => {
21
- const atprotoData = await idResolver.did.resolveAtprotoData(
22
- did,
23
- forceRefresh,
24
- )
25
- return atprotoData.signingKey
26
- },
27
- )
30
+ const payload = await verifyJwt(jwtStr, opts.aud, getSigningKey)
28
31
  return { credentials: { did: payload.iss }, artifacts: { aud: opts.aud } }
29
32
  }
33
+ }
30
34
 
31
- export const authOptionalVerifier =
32
- (idResolver: IdResolver, opts: { aud: string | null }) =>
33
- async (reqCtx: { req: express.Request; res: express.Response }) => {
35
+ export const authOptionalVerifier = (
36
+ idResolver: IdResolver,
37
+ opts: { aud: string | null },
38
+ ) => {
39
+ const verifyAccess = authVerifier(idResolver, opts)
40
+ return async (reqCtx: { req: express.Request; res: express.Response }) => {
34
41
  if (!reqCtx.req.headers.authorization) {
35
42
  return { credentials: { did: null } }
36
43
  }
37
- return authVerifier(idResolver, opts)(reqCtx)
44
+ return verifyAccess(reqCtx)
38
45
  }
46
+ }
39
47
 
40
48
  export const authOptionalAccessOrRoleVerifier = (
41
49
  idResolver: IdResolver,
@@ -127,9 +135,9 @@ export const buildBasicAuth = (username: string, password: string): string => {
127
135
  }
128
136
 
129
137
  export const getJwtStrFromReq = (req: express.Request): string | null => {
130
- const { authorization = '' } = req.headers
131
- if (!authorization.startsWith(BEARER)) {
138
+ const { authorization } = req.headers
139
+ if (!authorization?.startsWith(BEARER)) {
132
140
  return null
133
141
  }
134
- return authorization.replace(BEARER, '').trim()
142
+ return authorization.slice(BEARER.length).trim()
135
143
  }
@@ -61,7 +61,6 @@ export class AutoModerator {
61
61
  'moderation service not properly configured',
62
62
  )
63
63
  }
64
-
65
64
  this.imgLabeler = hiveApiKey ? new HiveLabeler(hiveApiKey, ctx) : undefined
66
65
  this.textLabeler = new KeywordLabeler(ctx.cfg.labelerKeywords)
67
66
  if (abyssEndpoint && abyssPassword) {
@@ -157,18 +156,22 @@ export class AutoModerator {
157
156
  if (!this.textFlagger) return
158
157
  const matches = this.textFlagger.getMatches(text)
159
158
  if (matches.length < 1) return
160
- if (!this.services.moderation) {
161
- log.error(
162
- { subject, text, matches },
163
- 'no moderation service setup to flag record text',
164
- )
165
- return
166
- }
167
- await this.services.moderation(this.ctx.db).report({
168
- reasonType: REASONOTHER,
169
- reason: `Automatically flagged for possible slurs: ${matches.join(', ')}`,
170
- subject,
171
- reportedBy: this.ctx.cfg.labelerDid,
159
+ await this.ctx.db.transaction(async (dbTxn) => {
160
+ if (!this.services.moderation) {
161
+ log.error(
162
+ { subject, text, matches },
163
+ 'no moderation service setup to flag record text',
164
+ )
165
+ return
166
+ }
167
+ return this.services.moderation(dbTxn).report({
168
+ reasonType: REASONOTHER,
169
+ reason: `Automatically flagged for possible slurs: ${matches.join(
170
+ ', ',
171
+ )}`,
172
+ subject,
173
+ reportedBy: this.ctx.cfg.labelerDid,
174
+ })
172
175
  })
173
176
  }
174
177
 
@@ -244,15 +247,17 @@ export class AutoModerator {
244
247
  }
245
248
 
246
249
  if (this.pushAgent) {
247
- await this.pushAgent.com.atproto.admin.takeModerationAction({
248
- action: 'com.atproto.admin.defs#takedown',
250
+ await this.pushAgent.com.atproto.admin.emitModerationEvent({
251
+ event: {
252
+ $type: 'com.atproto.admin.defs#modEventTakedown',
253
+ comment: takedownReason,
254
+ },
249
255
  subject: {
250
256
  $type: 'com.atproto.repo.strongRef',
251
257
  uri: uri.toString(),
252
258
  cid: recordCid.toString(),
253
259
  },
254
260
  subjectBlobCids: takedownCids.map((c) => c.toString()),
255
- reason: takedownReason,
256
261
  createdBy: this.ctx.cfg.labelerDid,
257
262
  })
258
263
  } else {
@@ -261,11 +266,13 @@ export class AutoModerator {
261
266
  throw new Error('no mod push agent or uri invalidator setup')
262
267
  }
263
268
  const modSrvc = this.services.moderation(dbTxn)
264
- const action = await modSrvc.logAction({
265
- action: 'com.atproto.admin.defs#takedown',
269
+ const action = await modSrvc.logEvent({
270
+ event: {
271
+ $type: 'com.atproto.admin.defs#modEventTakedown',
272
+ comment: takedownReason,
273
+ },
266
274
  subject: { uri, cid: recordCid },
267
275
  subjectBlobCids: takedownCids,
268
- reason: takedownReason,
269
276
  createdBy: this.ctx.cfg.labelerDid,
270
277
  })
271
278
  await modSrvc.takedownRecord({
@@ -0,0 +1,151 @@
1
+ import { cacheLogger as log } from '../logger'
2
+ import { Redis } from '../redis'
3
+
4
+ export type CacheItem<T> = {
5
+ val: T | null // null here is for negative caching
6
+ updatedAt: number
7
+ }
8
+
9
+ export type CacheOptions<T> = {
10
+ staleTTL: number
11
+ maxTTL: number
12
+ fetchMethod: (key: string) => Promise<T | null>
13
+ fetchManyMethod?: (keys: string[]) => Promise<Record<string, T | null>>
14
+ }
15
+
16
+ export class ReadThroughCache<T> {
17
+ constructor(public redis: Redis, public opts: CacheOptions<T>) {}
18
+
19
+ private async _fetchMany(keys: string[]): Promise<Record<string, T | null>> {
20
+ let result: Record<string, T | null> = {}
21
+ if (this.opts.fetchManyMethod) {
22
+ result = await this.opts.fetchManyMethod(keys)
23
+ } else {
24
+ const got = await Promise.all(keys.map((k) => this.opts.fetchMethod(k)))
25
+ for (let i = 0; i < keys.length; i++) {
26
+ result[keys[i]] = got[i] ?? null
27
+ }
28
+ }
29
+ // ensure caching negatives
30
+ for (const key of keys) {
31
+ result[key] ??= null
32
+ }
33
+ return result
34
+ }
35
+
36
+ private async fetchAndCache(key: string): Promise<T | null> {
37
+ const fetched = await this.opts.fetchMethod(key)
38
+ this.set(key, fetched).catch((err) =>
39
+ log.error({ err, key }, 'failed to set cache value'),
40
+ )
41
+ return fetched
42
+ }
43
+
44
+ private async fetchAndCacheMany(keys: string[]): Promise<Record<string, T>> {
45
+ const fetched = await this._fetchMany(keys)
46
+ this.setMany(fetched).catch((err) =>
47
+ log.error({ err, keys }, 'failed to set cache values'),
48
+ )
49
+ return removeNulls(fetched)
50
+ }
51
+
52
+ async get(key: string, opts?: { revalidate?: boolean }): Promise<T | null> {
53
+ if (opts?.revalidate) {
54
+ return this.fetchAndCache(key)
55
+ }
56
+ let cached: CacheItem<T> | null
57
+ try {
58
+ const got = await this.redis.get(key)
59
+ cached = got ? JSON.parse(got) : null
60
+ } catch (err) {
61
+ cached = null
62
+ log.warn({ key, err }, 'failed to fetch value from cache')
63
+ }
64
+ if (!cached || this.isExpired(cached)) {
65
+ return this.fetchAndCache(key)
66
+ }
67
+ if (this.isStale(cached)) {
68
+ this.fetchAndCache(key).catch((err) =>
69
+ log.warn({ key, err }, 'failed to refresh stale cache value'),
70
+ )
71
+ }
72
+ return cached.val
73
+ }
74
+
75
+ async getMany(
76
+ keys: string[],
77
+ opts?: { revalidate?: boolean },
78
+ ): Promise<Record<string, T>> {
79
+ if (opts?.revalidate) {
80
+ return this.fetchAndCacheMany(keys)
81
+ }
82
+ let cached: Record<string, string>
83
+ try {
84
+ cached = await this.redis.getMulti(keys)
85
+ } catch (err) {
86
+ cached = {}
87
+ log.warn({ keys, err }, 'failed to fetch values from cache')
88
+ }
89
+
90
+ const stale: string[] = []
91
+ const toFetch: string[] = []
92
+ const results: Record<string, T> = {}
93
+ for (const key of keys) {
94
+ const val = cached[key] ? (JSON.parse(cached[key]) as CacheItem<T>) : null
95
+ if (!val || this.isExpired(val)) {
96
+ toFetch.push(key)
97
+ continue
98
+ }
99
+ if (this.isStale(val)) {
100
+ stale.push(key)
101
+ }
102
+ if (val.val) {
103
+ results[key] = val.val
104
+ }
105
+ }
106
+ const fetched = await this.fetchAndCacheMany(toFetch)
107
+ this.fetchAndCacheMany(stale).catch((err) =>
108
+ log.warn({ keys, err }, 'failed to refresh stale cache values'),
109
+ )
110
+ return {
111
+ ...results,
112
+ ...fetched,
113
+ }
114
+ }
115
+
116
+ async set(key: string, val: T | null) {
117
+ await this.setMany({ [key]: val })
118
+ }
119
+
120
+ async setMany(vals: Record<string, T | null>) {
121
+ const items: Record<string, string> = {}
122
+ for (const key of Object.keys(vals)) {
123
+ items[key] = JSON.stringify({
124
+ val: vals[key],
125
+ updatedAt: Date.now(),
126
+ })
127
+ }
128
+ await this.redis.setMulti(items, this.opts.maxTTL)
129
+ }
130
+
131
+ async clearEntry(key: string) {
132
+ await this.redis.del(key)
133
+ }
134
+
135
+ isExpired(result: CacheItem<T>) {
136
+ return Date.now() > result.updatedAt + this.opts.maxTTL
137
+ }
138
+
139
+ isStale(result: CacheItem<T>) {
140
+ return Date.now() > result.updatedAt + this.opts.staleTTL
141
+ }
142
+ }
143
+
144
+ const removeNulls = <T>(obj: Record<string, T | null>): Record<string, T> => {
145
+ return Object.entries(obj).reduce((acc, [key, val]) => {
146
+ if (val !== null) {
147
+ acc[key] = val
148
+ }
149
+ return acc
150
+ }, {} as Record<string, T>)
151
+ }
package/src/config.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  import assert from 'assert'
2
- import { DAY, HOUR, parseIntWithFallback } from '@atproto/common'
2
+ import {
3
+ DAY,
4
+ HOUR,
5
+ MINUTE,
6
+ SECOND,
7
+ parseIntWithFallback,
8
+ } from '@atproto/common'
3
9
 
4
10
  export interface ServerConfigValues {
5
11
  version: string
@@ -12,9 +18,15 @@ export interface ServerConfigValues {
12
18
  dbReplicaPostgresUrls?: string[]
13
19
  dbReplicaTags?: Record<string, number[]> // E.g. { timeline: [0], thread: [1] }
14
20
  dbPostgresSchema?: string
21
+ redisHost?: string // either set redis host, or both sentinel name and hosts
22
+ redisSentinelName?: string
23
+ redisSentinelHosts?: string[]
24
+ redisPassword?: string
15
25
  didPlcUrl: string
16
26
  didCacheStaleTTL: number
17
27
  didCacheMaxTTL: number
28
+ labelCacheStaleTTL: number
29
+ labelCacheMaxTTL: number
18
30
  handleResolveNameservers?: string[]
19
31
  imgUriEndpoint?: string
20
32
  blobCacheLocation?: string
@@ -24,6 +36,9 @@ export interface ServerConfigValues {
24
36
  moderatorPassword?: string
25
37
  triagePassword?: string
26
38
  moderationPushUrl?: string
39
+ rateLimitsEnabled: boolean
40
+ rateLimitBypassKey?: string
41
+ rateLimitBypassIps?: string[]
27
42
  }
28
43
 
29
44
  export class ServerConfig {
@@ -38,6 +53,19 @@ export class ServerConfig {
38
53
  const feedGenDid = process.env.FEED_GEN_DID
39
54
  const envPort = parseInt(process.env.PORT || '', 10)
40
55
  const port = isNaN(envPort) ? 2584 : envPort
56
+ const redisHost =
57
+ overrides?.redisHost || process.env.REDIS_HOST || undefined
58
+ const redisSentinelName =
59
+ overrides?.redisSentinelName ||
60
+ process.env.REDIS_SENTINEL_NAME ||
61
+ undefined
62
+ const redisSentinelHosts =
63
+ overrides?.redisSentinelHosts ||
64
+ (process.env.REDIS_SENTINEL_HOSTS
65
+ ? process.env.REDIS_SENTINEL_HOSTS.split(',')
66
+ : [])
67
+ const redisPassword =
68
+ overrides?.redisPassword || process.env.REDIS_PASSWORD || undefined
41
69
  const didPlcUrl = process.env.DID_PLC_URL || 'http://localhost:2582'
42
70
  const didCacheStaleTTL = parseIntWithFallback(
43
71
  process.env.DID_CACHE_STALE_TTL,
@@ -47,6 +75,14 @@ export class ServerConfig {
47
75
  process.env.DID_CACHE_MAX_TTL,
48
76
  DAY,
49
77
  )
78
+ const labelCacheStaleTTL = parseIntWithFallback(
79
+ process.env.LABEL_CACHE_STALE_TTL,
80
+ 30 * SECOND,
81
+ )
82
+ const labelCacheMaxTTL = parseIntWithFallback(
83
+ process.env.LABEL_CACHE_MAX_TTL,
84
+ MINUTE,
85
+ )
50
86
  const handleResolveNameservers = process.env.HANDLE_RESOLVE_NAMESERVERS
51
87
  ? process.env.HANDLE_RESOLVE_NAMESERVERS.split(',')
52
88
  : []
@@ -82,6 +118,14 @@ export class ServerConfig {
82
118
  overrides?.moderationPushUrl ||
83
119
  process.env.MODERATION_PUSH_URL ||
84
120
  undefined
121
+ const rateLimitsEnabled = process.env.RATE_LIMITS_ENABLED === 'true'
122
+ const rateLimitBypassKey = process.env.RATE_LIMIT_BYPASS_KEY
123
+ const rateLimitBypassIps = process.env.RATE_LIMIT_BYPASS_IPS
124
+ ? process.env.RATE_LIMIT_BYPASS_IPS.split(',').map((ipOrCidr) =>
125
+ ipOrCidr.split('/')[0]?.trim(),
126
+ )
127
+ : undefined
128
+
85
129
  return new ServerConfig({
86
130
  version,
87
131
  debugMode,
@@ -93,9 +137,15 @@ export class ServerConfig {
93
137
  dbReplicaPostgresUrls,
94
138
  dbReplicaTags,
95
139
  dbPostgresSchema,
140
+ redisHost,
141
+ redisSentinelName,
142
+ redisSentinelHosts,
143
+ redisPassword,
96
144
  didPlcUrl,
97
145
  didCacheStaleTTL,
98
146
  didCacheMaxTTL,
147
+ labelCacheStaleTTL,
148
+ labelCacheMaxTTL,
99
149
  handleResolveNameservers,
100
150
  imgUriEndpoint,
101
151
  blobCacheLocation,
@@ -105,6 +155,9 @@ export class ServerConfig {
105
155
  moderatorPassword,
106
156
  triagePassword,
107
157
  moderationPushUrl,
158
+ rateLimitsEnabled,
159
+ rateLimitBypassKey,
160
+ rateLimitBypassIps,
108
161
  ...stripUndefineds(overrides ?? {}),
109
162
  })
110
163
  }
@@ -162,6 +215,22 @@ export class ServerConfig {
162
215
  return this.cfg.dbPostgresSchema
163
216
  }
164
217
 
218
+ get redisHost() {
219
+ return this.cfg.redisHost
220
+ }
221
+
222
+ get redisSentinelName() {
223
+ return this.cfg.redisSentinelName
224
+ }
225
+
226
+ get redisSentinelHosts() {
227
+ return this.cfg.redisSentinelHosts
228
+ }
229
+
230
+ get redisPassword() {
231
+ return this.cfg.redisPassword
232
+ }
233
+
165
234
  get didCacheStaleTTL() {
166
235
  return this.cfg.didCacheStaleTTL
167
236
  }
@@ -170,6 +239,14 @@ export class ServerConfig {
170
239
  return this.cfg.didCacheMaxTTL
171
240
  }
172
241
 
242
+ get labelCacheStaleTTL() {
243
+ return this.cfg.labelCacheStaleTTL
244
+ }
245
+
246
+ get labelCacheMaxTTL() {
247
+ return this.cfg.labelCacheMaxTTL
248
+ }
249
+
173
250
  get handleResolveNameservers() {
174
251
  return this.cfg.handleResolveNameservers
175
252
  }
@@ -209,6 +286,18 @@ export class ServerConfig {
209
286
  get moderationPushUrl() {
210
287
  return this.cfg.moderationPushUrl
211
288
  }
289
+
290
+ get rateLimitsEnabled() {
291
+ return this.cfg.rateLimitsEnabled
292
+ }
293
+
294
+ get rateLimitBypassKey() {
295
+ return this.cfg.rateLimitBypassKey
296
+ }
297
+
298
+ get rateLimitBypassIps() {
299
+ return this.cfg.rateLimitBypassIps
300
+ }
212
301
  }
213
302
 
214
303
  function getTagIdxs(str?: string): number[] {
package/src/context.ts CHANGED
@@ -8,11 +8,11 @@ import { ServerConfig } from './config'
8
8
  import { ImageUriBuilder } from './image/uri'
9
9
  import { Services } from './services'
10
10
  import * as auth from './auth'
11
- import DidSqlCache from './did-cache'
11
+ import DidRedisCache from './did-cache'
12
12
  import { BackgroundQueue } from './background'
13
13
  import { MountedAlgos } from './feed-gen/types'
14
- import { LabelCache } from './label-cache'
15
14
  import { NotificationServer } from './notifications'
15
+ import { Redis } from './redis'
16
16
 
17
17
  export class AppContext {
18
18
  public moderationPushAgent: AtpAgent | undefined
@@ -24,8 +24,8 @@ export class AppContext {
24
24
  services: Services
25
25
  signingKey: Keypair
26
26
  idResolver: IdResolver
27
- didCache: DidSqlCache
28
- labelCache: LabelCache
27
+ didCache: DidRedisCache
28
+ redis: Redis
29
29
  backgroundQueue: BackgroundQueue
30
30
  searchAgent?: AtpAgent
31
31
  algos: MountedAlgos
@@ -70,12 +70,12 @@ export class AppContext {
70
70
  return this.opts.idResolver
71
71
  }
72
72
 
73
- get didCache(): DidSqlCache {
73
+ get didCache(): DidRedisCache {
74
74
  return this.opts.didCache
75
75
  }
76
76
 
77
- get labelCache(): LabelCache {
78
- return this.opts.labelCache
77
+ get redis(): Redis {
78
+ return this.opts.redis
79
79
  }
80
80
 
81
81
  get notifServer(): NotificationServer {
@@ -94,6 +94,10 @@ export class AppContext {
94
94
  return auth.authVerifier(this.idResolver, { aud: null })
95
95
  }
96
96
 
97
+ get authOptionalVerifierAnyAudience() {
98
+ return auth.authOptionalVerifier(this.idResolver, { aud: null })
99
+ }
100
+
97
101
  get authOptionalVerifier() {
98
102
  return auth.authOptionalVerifier(this.idResolver, {
99
103
  aud: this.cfg.serverDid,