@atproto/ozone 0.1.149 → 0.1.151

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 (205) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/api/moderation/queryEvents.d.ts.map +1 -1
  3. package/dist/api/moderation/queryEvents.js +2 -1
  4. package/dist/api/moderation/queryEvents.js.map +1 -1
  5. package/dist/context.d.ts +3 -0
  6. package/dist/context.d.ts.map +1 -1
  7. package/dist/context.js +7 -1
  8. package/dist/context.js.map +1 -1
  9. package/dist/daemon/context.d.ts +3 -0
  10. package/dist/daemon/context.d.ts.map +1 -1
  11. package/dist/daemon/context.js +11 -1
  12. package/dist/daemon/context.js.map +1 -1
  13. package/dist/daemon/index.d.ts +1 -0
  14. package/dist/daemon/index.d.ts.map +1 -1
  15. package/dist/daemon/index.js +3 -1
  16. package/dist/daemon/index.js.map +1 -1
  17. package/dist/daemon/strike-expiry-processor.d.ts +18 -0
  18. package/dist/daemon/strike-expiry-processor.d.ts.map +1 -0
  19. package/dist/daemon/strike-expiry-processor.js +111 -0
  20. package/dist/daemon/strike-expiry-processor.js.map +1 -0
  21. package/dist/db/migrations/20251008T120000000Z-add-strike-system.d.ts +4 -0
  22. package/dist/db/migrations/20251008T120000000Z-add-strike-system.d.ts.map +1 -0
  23. package/dist/db/migrations/20251008T120000000Z-add-strike-system.js +75 -0
  24. package/dist/db/migrations/20251008T120000000Z-add-strike-system.js.map +1 -0
  25. package/dist/db/migrations/index.d.ts +1 -0
  26. package/dist/db/migrations/index.d.ts.map +1 -1
  27. package/dist/db/migrations/index.js +2 -1
  28. package/dist/db/migrations/index.js.map +1 -1
  29. package/dist/db/schema/account_strike.d.ts +12 -0
  30. package/dist/db/schema/account_strike.d.ts.map +1 -0
  31. package/dist/db/schema/account_strike.js +5 -0
  32. package/dist/db/schema/account_strike.js.map +1 -0
  33. package/dist/db/schema/index.d.ts +3 -1
  34. package/dist/db/schema/index.d.ts.map +1 -1
  35. package/dist/db/schema/index.js.map +1 -1
  36. package/dist/db/schema/job_cursor.d.ts +11 -0
  37. package/dist/db/schema/job_cursor.d.ts.map +1 -0
  38. package/dist/db/schema/job_cursor.js +5 -0
  39. package/dist/db/schema/job_cursor.js.map +1 -0
  40. package/dist/db/schema/moderation_event.d.ts +3 -0
  41. package/dist/db/schema/moderation_event.d.ts.map +1 -1
  42. package/dist/db/schema/moderation_event.js.map +1 -1
  43. package/dist/lexicon/lexicons.d.ts +176 -0
  44. package/dist/lexicon/lexicons.d.ts.map +1 -1
  45. package/dist/lexicon/lexicons.js +88 -0
  46. package/dist/lexicon/lexicons.js.map +1 -1
  47. package/dist/lexicon/types/app/bsky/actor/profile.d.ts +4 -3
  48. package/dist/lexicon/types/app/bsky/actor/profile.d.ts.map +1 -1
  49. package/dist/lexicon/types/app/bsky/actor/profile.js +9 -7
  50. package/dist/lexicon/types/app/bsky/actor/profile.js.map +1 -1
  51. package/dist/lexicon/types/app/bsky/actor/status.d.ts +4 -3
  52. package/dist/lexicon/types/app/bsky/actor/status.d.ts.map +1 -1
  53. package/dist/lexicon/types/app/bsky/actor/status.js +9 -7
  54. package/dist/lexicon/types/app/bsky/actor/status.js.map +1 -1
  55. package/dist/lexicon/types/app/bsky/feed/generator.d.ts +4 -3
  56. package/dist/lexicon/types/app/bsky/feed/generator.d.ts.map +1 -1
  57. package/dist/lexicon/types/app/bsky/feed/generator.js +9 -7
  58. package/dist/lexicon/types/app/bsky/feed/generator.js.map +1 -1
  59. package/dist/lexicon/types/app/bsky/feed/like.d.ts +4 -3
  60. package/dist/lexicon/types/app/bsky/feed/like.d.ts.map +1 -1
  61. package/dist/lexicon/types/app/bsky/feed/like.js +9 -7
  62. package/dist/lexicon/types/app/bsky/feed/like.js.map +1 -1
  63. package/dist/lexicon/types/app/bsky/feed/post.d.ts +4 -3
  64. package/dist/lexicon/types/app/bsky/feed/post.d.ts.map +1 -1
  65. package/dist/lexicon/types/app/bsky/feed/post.js +9 -7
  66. package/dist/lexicon/types/app/bsky/feed/post.js.map +1 -1
  67. package/dist/lexicon/types/app/bsky/feed/postgate.d.ts +4 -3
  68. package/dist/lexicon/types/app/bsky/feed/postgate.d.ts.map +1 -1
  69. package/dist/lexicon/types/app/bsky/feed/postgate.js +9 -7
  70. package/dist/lexicon/types/app/bsky/feed/postgate.js.map +1 -1
  71. package/dist/lexicon/types/app/bsky/feed/repost.d.ts +4 -3
  72. package/dist/lexicon/types/app/bsky/feed/repost.d.ts.map +1 -1
  73. package/dist/lexicon/types/app/bsky/feed/repost.js +9 -7
  74. package/dist/lexicon/types/app/bsky/feed/repost.js.map +1 -1
  75. package/dist/lexicon/types/app/bsky/feed/threadgate.d.ts +4 -3
  76. package/dist/lexicon/types/app/bsky/feed/threadgate.d.ts.map +1 -1
  77. package/dist/lexicon/types/app/bsky/feed/threadgate.js +9 -7
  78. package/dist/lexicon/types/app/bsky/feed/threadgate.js.map +1 -1
  79. package/dist/lexicon/types/app/bsky/graph/block.d.ts +4 -3
  80. package/dist/lexicon/types/app/bsky/graph/block.d.ts.map +1 -1
  81. package/dist/lexicon/types/app/bsky/graph/block.js +9 -7
  82. package/dist/lexicon/types/app/bsky/graph/block.js.map +1 -1
  83. package/dist/lexicon/types/app/bsky/graph/follow.d.ts +4 -3
  84. package/dist/lexicon/types/app/bsky/graph/follow.d.ts.map +1 -1
  85. package/dist/lexicon/types/app/bsky/graph/follow.js +9 -7
  86. package/dist/lexicon/types/app/bsky/graph/follow.js.map +1 -1
  87. package/dist/lexicon/types/app/bsky/graph/list.d.ts +4 -3
  88. package/dist/lexicon/types/app/bsky/graph/list.d.ts.map +1 -1
  89. package/dist/lexicon/types/app/bsky/graph/list.js +9 -7
  90. package/dist/lexicon/types/app/bsky/graph/list.js.map +1 -1
  91. package/dist/lexicon/types/app/bsky/graph/listblock.d.ts +4 -3
  92. package/dist/lexicon/types/app/bsky/graph/listblock.d.ts.map +1 -1
  93. package/dist/lexicon/types/app/bsky/graph/listblock.js +9 -7
  94. package/dist/lexicon/types/app/bsky/graph/listblock.js.map +1 -1
  95. package/dist/lexicon/types/app/bsky/graph/listitem.d.ts +4 -3
  96. package/dist/lexicon/types/app/bsky/graph/listitem.d.ts.map +1 -1
  97. package/dist/lexicon/types/app/bsky/graph/listitem.js +9 -7
  98. package/dist/lexicon/types/app/bsky/graph/listitem.js.map +1 -1
  99. package/dist/lexicon/types/app/bsky/graph/starterpack.d.ts +4 -3
  100. package/dist/lexicon/types/app/bsky/graph/starterpack.d.ts.map +1 -1
  101. package/dist/lexicon/types/app/bsky/graph/starterpack.js +9 -7
  102. package/dist/lexicon/types/app/bsky/graph/starterpack.js.map +1 -1
  103. package/dist/lexicon/types/app/bsky/graph/verification.d.ts +4 -3
  104. package/dist/lexicon/types/app/bsky/graph/verification.d.ts.map +1 -1
  105. package/dist/lexicon/types/app/bsky/graph/verification.js +9 -7
  106. package/dist/lexicon/types/app/bsky/graph/verification.js.map +1 -1
  107. package/dist/lexicon/types/app/bsky/labeler/service.d.ts +4 -3
  108. package/dist/lexicon/types/app/bsky/labeler/service.d.ts.map +1 -1
  109. package/dist/lexicon/types/app/bsky/labeler/service.js +9 -7
  110. package/dist/lexicon/types/app/bsky/labeler/service.js.map +1 -1
  111. package/dist/lexicon/types/app/bsky/notification/declaration.d.ts +4 -3
  112. package/dist/lexicon/types/app/bsky/notification/declaration.d.ts.map +1 -1
  113. package/dist/lexicon/types/app/bsky/notification/declaration.js +9 -7
  114. package/dist/lexicon/types/app/bsky/notification/declaration.js.map +1 -1
  115. package/dist/lexicon/types/chat/bsky/actor/declaration.d.ts +4 -3
  116. package/dist/lexicon/types/chat/bsky/actor/declaration.d.ts.map +1 -1
  117. package/dist/lexicon/types/chat/bsky/actor/declaration.js +9 -7
  118. package/dist/lexicon/types/chat/bsky/actor/declaration.js.map +1 -1
  119. package/dist/lexicon/types/com/atproto/lexicon/schema.d.ts +4 -3
  120. package/dist/lexicon/types/com/atproto/lexicon/schema.d.ts.map +1 -1
  121. package/dist/lexicon/types/com/atproto/lexicon/schema.js +9 -7
  122. package/dist/lexicon/types/com/atproto/lexicon/schema.js.map +1 -1
  123. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts +35 -0
  124. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts.map +1 -1
  125. package/dist/lexicon/types/tools/ozone/moderation/defs.js +9 -0
  126. package/dist/lexicon/types/tools/ozone/moderation/defs.js.map +1 -1
  127. package/dist/lexicon/types/tools/ozone/moderation/queryEvents.d.ts +2 -0
  128. package/dist/lexicon/types/tools/ozone/moderation/queryEvents.d.ts.map +1 -1
  129. package/dist/lexicon/types/tools/ozone/moderation/queryEvents.js.map +1 -1
  130. package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.d.ts +2 -0
  131. package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.d.ts.map +1 -1
  132. package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.js.map +1 -1
  133. package/dist/mod-service/index.d.ts +9 -3
  134. package/dist/mod-service/index.d.ts.map +1 -1
  135. package/dist/mod-service/index.js +58 -5
  136. package/dist/mod-service/index.js.map +1 -1
  137. package/dist/mod-service/status.d.ts +44 -2
  138. package/dist/mod-service/status.d.ts.map +1 -1
  139. package/dist/mod-service/status.js +7 -0
  140. package/dist/mod-service/status.js.map +1 -1
  141. package/dist/mod-service/strike.d.ts +19 -0
  142. package/dist/mod-service/strike.d.ts.map +1 -0
  143. package/dist/mod-service/strike.js +86 -0
  144. package/dist/mod-service/strike.js.map +1 -0
  145. package/dist/mod-service/types.d.ts +4 -0
  146. package/dist/mod-service/types.d.ts.map +1 -1
  147. package/dist/mod-service/types.js.map +1 -1
  148. package/dist/mod-service/views.d.ts.map +1 -1
  149. package/dist/mod-service/views.js +20 -4
  150. package/dist/mod-service/views.js.map +1 -1
  151. package/dist/setting/constants.d.ts +1 -0
  152. package/dist/setting/constants.d.ts.map +1 -1
  153. package/dist/setting/constants.js +2 -1
  154. package/dist/setting/constants.js.map +1 -1
  155. package/dist/setting/validators.d.ts.map +1 -1
  156. package/dist/setting/validators.js +179 -0
  157. package/dist/setting/validators.js.map +1 -1
  158. package/package.json +4 -4
  159. package/src/api/moderation/queryEvents.ts +2 -0
  160. package/src/context.ts +20 -11
  161. package/src/daemon/context.ts +15 -1
  162. package/src/daemon/index.ts +1 -0
  163. package/src/daemon/strike-expiry-processor.ts +111 -0
  164. package/src/db/migrations/20251008T120000000Z-add-strike-system.ts +87 -0
  165. package/src/db/migrations/index.ts +1 -0
  166. package/src/db/schema/account_strike.ts +13 -0
  167. package/src/db/schema/index.ts +4 -0
  168. package/src/db/schema/job_cursor.ts +13 -0
  169. package/src/db/schema/moderation_event.ts +3 -0
  170. package/src/lexicon/lexicons.ts +103 -0
  171. package/src/lexicon/types/app/bsky/actor/profile.ts +12 -6
  172. package/src/lexicon/types/app/bsky/actor/status.ts +12 -6
  173. package/src/lexicon/types/app/bsky/feed/generator.ts +12 -6
  174. package/src/lexicon/types/app/bsky/feed/like.ts +12 -6
  175. package/src/lexicon/types/app/bsky/feed/post.ts +12 -6
  176. package/src/lexicon/types/app/bsky/feed/postgate.ts +12 -6
  177. package/src/lexicon/types/app/bsky/feed/repost.ts +12 -6
  178. package/src/lexicon/types/app/bsky/feed/threadgate.ts +12 -6
  179. package/src/lexicon/types/app/bsky/graph/block.ts +12 -6
  180. package/src/lexicon/types/app/bsky/graph/follow.ts +12 -6
  181. package/src/lexicon/types/app/bsky/graph/list.ts +12 -6
  182. package/src/lexicon/types/app/bsky/graph/listblock.ts +12 -6
  183. package/src/lexicon/types/app/bsky/graph/listitem.ts +12 -6
  184. package/src/lexicon/types/app/bsky/graph/starterpack.ts +12 -6
  185. package/src/lexicon/types/app/bsky/graph/verification.ts +12 -6
  186. package/src/lexicon/types/app/bsky/labeler/service.ts +12 -6
  187. package/src/lexicon/types/app/bsky/notification/declaration.ts +12 -6
  188. package/src/lexicon/types/chat/bsky/actor/declaration.ts +12 -6
  189. package/src/lexicon/types/com/atproto/lexicon/schema.ts +12 -6
  190. package/src/lexicon/types/tools/ozone/moderation/defs.ts +44 -0
  191. package/src/lexicon/types/tools/ozone/moderation/queryEvents.ts +2 -0
  192. package/src/lexicon/types/tools/ozone/moderation/queryStatuses.ts +2 -0
  193. package/src/mod-service/index.ts +69 -2
  194. package/src/mod-service/status.ts +9 -0
  195. package/src/mod-service/strike.ts +96 -0
  196. package/src/mod-service/types.ts +6 -0
  197. package/src/mod-service/views.ts +25 -4
  198. package/src/setting/constants.ts +1 -0
  199. package/src/setting/validators.ts +231 -1
  200. package/tests/__snapshots__/account-strikes.test.ts.snap +159 -0
  201. package/tests/account-strikes.test.ts +184 -0
  202. package/tests/query-labels.test.ts +1 -0
  203. package/tests/strike-expiry-processor.test.ts +299 -0
  204. package/tsconfig.build.tsbuildinfo +1 -1
  205. package/tsconfig.tests.tsbuildinfo +1 -1
package/src/context.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  ModerationServiceProfile,
22
22
  ModerationServiceProfileCreator,
23
23
  } from './mod-service/profile'
24
+ import { StrikeService, StrikeServiceCreator } from './mod-service/strike'
24
25
  import {
25
26
  SafelinkRuleService,
26
27
  SafelinkRuleServiceCreator,
@@ -59,6 +60,7 @@ export type AppContextOptions = {
59
60
  scheduledActionService: ScheduledActionServiceCreator
60
61
  setService: SetServiceCreator
61
62
  settingService: SettingServiceCreator
63
+ strikeService: StrikeServiceCreator
62
64
  teamService: TeamServiceCreator
63
65
  appviewAgent: AtpAgent
64
66
  pdsAgent: AtpAgent | undefined
@@ -132,17 +134,6 @@ export class AppContext {
132
134
  appview: cfg.appview.pushEvents ? cfg.appview : undefined,
133
135
  pds: cfg.pds ?? undefined,
134
136
  })
135
- const modService = ModerationService.creator(
136
- signingKey,
137
- signingKeyId,
138
- cfg,
139
- backgroundQueue,
140
- idResolver,
141
- eventPusher,
142
- appviewAgent,
143
- createAuthHeaders,
144
- overrides?.imgInvalidator,
145
- )
146
137
 
147
138
  const communicationTemplateService = CommunicationTemplateService.creator()
148
139
  const safelinkRuleService = SafelinkRuleService.creator()
@@ -154,12 +145,25 @@ export class AppContext {
154
145
  )
155
146
  const setService = SetService.creator()
156
147
  const settingService = SettingService.creator()
148
+ const strikeService = StrikeService.creator()
157
149
  const verificationService = VerificationService.creator()
158
150
  const verificationIssuer = VerificationIssuer.creator()
159
151
  const moderationServiceProfile = ModerationServiceProfile.creator(
160
152
  cfg,
161
153
  appviewAgent,
162
154
  )
155
+ const modService = ModerationService.creator(
156
+ signingKey,
157
+ signingKeyId,
158
+ cfg,
159
+ backgroundQueue,
160
+ idResolver,
161
+ eventPusher,
162
+ appviewAgent,
163
+ createAuthHeaders,
164
+ strikeService,
165
+ overrides?.imgInvalidator,
166
+ )
163
167
 
164
168
  const sequencer = new Sequencer(modService(db))
165
169
 
@@ -181,6 +185,7 @@ export class AppContext {
181
185
  teamService,
182
186
  setService,
183
187
  settingService,
188
+ strikeService,
184
189
  appviewAgent,
185
190
  pdsAgent,
186
191
  chatAgent,
@@ -248,6 +253,10 @@ export class AppContext {
248
253
  return this.opts.settingService
249
254
  }
250
255
 
256
+ get strikeService(): StrikeServiceCreator {
257
+ return this.opts.strikeService
258
+ }
259
+
251
260
  get verificationService(): VerificationServiceCreator {
252
261
  return this.opts.verificationService
253
262
  }
@@ -7,6 +7,7 @@ import { BackgroundQueue } from '../background'
7
7
  import { OzoneConfig, OzoneSecrets } from '../config'
8
8
  import { Database } from '../db'
9
9
  import { ModerationService } from '../mod-service'
10
+ import { StrikeService } from '../mod-service/strike'
10
11
  import { ScheduledActionService } from '../scheduled-action/service'
11
12
  import { SettingService } from '../setting/service'
12
13
  import { TeamService } from '../team'
@@ -15,6 +16,7 @@ import { EventPusher } from './event-pusher'
15
16
  import { EventReverser } from './event-reverser'
16
17
  import { MaterializedViewRefresher } from './materialized-view-refresher'
17
18
  import { ScheduledActionProcessor } from './scheduled-action-processor'
19
+ import { StrikeExpiryProcessor } from './strike-expiry-processor'
18
20
  import { TeamProfileSynchronizer } from './team-profile-synchronizer'
19
21
  import { VerificationListener } from './verification-listener'
20
22
 
@@ -28,6 +30,7 @@ export type DaemonContextOptions = {
28
30
  materializedViewRefresher: MaterializedViewRefresher
29
31
  teamProfileSynchronizer: TeamProfileSynchronizer
30
32
  scheduledActionProcessor: ScheduledActionProcessor
33
+ strikeExpiryProcessor: StrikeExpiryProcessor
31
34
  verificationListener?: VerificationListener
32
35
  }
33
36
 
@@ -66,6 +69,8 @@ export class DaemonContext {
66
69
 
67
70
  const backgroundQueue = new BackgroundQueue(db)
68
71
 
72
+ const settingService = SettingService.creator()
73
+ const strikeService = StrikeService.creator()
69
74
  const modService = ModerationService.creator(
70
75
  signingKey,
71
76
  signingKeyId,
@@ -75,8 +80,8 @@ export class DaemonContext {
75
80
  eventPusher,
76
81
  appviewAgent,
77
82
  createAuthHeaders,
83
+ strikeService,
78
84
  )
79
- const settingService = SettingService.creator()
80
85
  const scheduledActionService = ScheduledActionService.creator()
81
86
  const teamService = TeamService.creator(
82
87
  appviewAgent,
@@ -104,6 +109,8 @@ export class DaemonContext {
104
109
  scheduledActionService,
105
110
  )
106
111
 
112
+ const strikeExpiryProcessor = new StrikeExpiryProcessor(db, strikeService)
113
+
107
114
  // Only spawn the listener if verifier config exists and a jetstream URL is provided
108
115
  const verificationListener =
109
116
  cfg.verifier && cfg.jetstreamUrl
@@ -124,6 +131,7 @@ export class DaemonContext {
124
131
  materializedViewRefresher,
125
132
  teamProfileSynchronizer,
126
133
  scheduledActionProcessor,
134
+ strikeExpiryProcessor,
127
135
  verificationListener,
128
136
  ...(overrides ?? {}),
129
137
  })
@@ -161,6 +169,10 @@ export class DaemonContext {
161
169
  return this.opts.scheduledActionProcessor
162
170
  }
163
171
 
172
+ get strikeExpiryProcessor(): StrikeExpiryProcessor {
173
+ return this.opts.strikeExpiryProcessor
174
+ }
175
+
164
176
  get verificationListener(): VerificationListener | undefined {
165
177
  return this.opts.verificationListener
166
178
  }
@@ -171,6 +183,7 @@ export class DaemonContext {
171
183
  this.materializedViewRefresher.start()
172
184
  this.teamProfileSynchronizer.start()
173
185
  this.scheduledActionProcessor.start()
186
+ this.strikeExpiryProcessor.start()
174
187
  this.verificationListener?.start()
175
188
  }
176
189
 
@@ -189,6 +202,7 @@ export class DaemonContext {
189
202
  this.materializedViewRefresher.destroy(),
190
203
  this.teamProfileSynchronizer.destroy(),
191
204
  this.scheduledActionProcessor.destroy(),
205
+ this.strikeExpiryProcessor.destroy(),
192
206
  this.verificationListener?.stop(),
193
207
  ])
194
208
  } finally {
@@ -6,6 +6,7 @@ export { EventPusher } from './event-pusher'
6
6
  export { BlobDiverter } from './blob-diverter'
7
7
  export { EventReverser } from './event-reverser'
8
8
  export { ScheduledActionProcessor } from './scheduled-action-processor'
9
+ export { StrikeExpiryProcessor } from './strike-expiry-processor'
9
10
 
10
11
  export class OzoneDaemon {
11
12
  constructor(public ctx: DaemonContext) {}
@@ -0,0 +1,111 @@
1
+ import { HOUR } from '@atproto/common'
2
+ import { Database } from '../db'
3
+ import { dbLogger } from '../logger'
4
+ import { StrikeServiceCreator } from '../mod-service/strike'
5
+
6
+ const JOB_NAME = 'strike_expiry'
7
+
8
+ export class StrikeExpiryProcessor {
9
+ destroyed = false
10
+ processingPromise: Promise<void> = Promise.resolve()
11
+ timer?: NodeJS.Timeout
12
+
13
+ constructor(
14
+ private db: Database,
15
+ private strikeServiceCreator: StrikeServiceCreator,
16
+ ) {}
17
+
18
+ start() {
19
+ this.initializeCursor().then(() => this.poll())
20
+ }
21
+
22
+ poll() {
23
+ if (this.destroyed) return
24
+ this.processingPromise = this.processExpiredStrikes()
25
+ .catch((err) =>
26
+ dbLogger.error({ err }, 'strike expiry processing errored'),
27
+ )
28
+ .finally(() => {
29
+ this.timer = setTimeout(() => this.poll(), getInterval())
30
+ })
31
+ }
32
+
33
+ async destroy() {
34
+ this.destroyed = true
35
+ if (this.timer) {
36
+ clearTimeout(this.timer)
37
+ this.timer = undefined
38
+ }
39
+ await this.processingPromise
40
+ }
41
+
42
+ async initializeCursor() {
43
+ await this.db.db
44
+ .insertInto('job_cursor')
45
+ .values({
46
+ job: JOB_NAME,
47
+ cursor: null,
48
+ })
49
+ .onConflict((oc) => oc.doNothing())
50
+ .execute()
51
+ }
52
+
53
+ async getCursor(): Promise<string | null> {
54
+ const entry = await this.db.db
55
+ .selectFrom('job_cursor')
56
+ .select('cursor')
57
+ .where('job', '=', JOB_NAME)
58
+ .executeTakeFirst()
59
+
60
+ return entry?.cursor || null
61
+ }
62
+
63
+ async updateCursor(cursor: string): Promise<void> {
64
+ await this.db.db
65
+ .updateTable('job_cursor')
66
+ .set({ cursor })
67
+ .where('job', '=', JOB_NAME)
68
+ .execute()
69
+ }
70
+
71
+ async processExpiredStrikes() {
72
+ const now = new Date()
73
+ const strikeService = this.strikeServiceCreator(this.db)
74
+ const lastProcessedAt = await this.getCursor()
75
+ const affectedSubjects = await strikeService.getExpiredStrikeSubjects(
76
+ lastProcessedAt || undefined,
77
+ )
78
+
79
+ if (!affectedSubjects.length) {
80
+ dbLogger.info('no expired strikes to process')
81
+ await this.updateCursor(now.toISOString())
82
+ return
83
+ }
84
+
85
+ dbLogger.info(
86
+ { count: affectedSubjects.length },
87
+ 'processing subjects with expired strikes',
88
+ )
89
+
90
+ await Promise.all(
91
+ affectedSubjects.map(({ subjectDid }) => {
92
+ return strikeService.updateSubjectStrikeCount(subjectDid)
93
+ }),
94
+ )
95
+
96
+ await this.updateCursor(now.toISOString())
97
+
98
+ dbLogger.info(
99
+ { processed: affectedSubjects.length },
100
+ 'strike expiry processing completed',
101
+ )
102
+ }
103
+ }
104
+
105
+ const getInterval = (): number => {
106
+ // Run every hour, synchronized to the hour boundary
107
+ const now = Date.now()
108
+ const intervalMs = HOUR
109
+ const nextIteration = Math.ceil(now / intervalMs)
110
+ return nextIteration * intervalMs - now
111
+ }
@@ -0,0 +1,87 @@
1
+ import { Kysely, sql } from 'kysely'
2
+
3
+ export async function up(db: Kysely<unknown>): Promise<void> {
4
+ await db.schema
5
+ .alterTable('moderation_event')
6
+ .addColumn('severityLevel', 'varchar')
7
+ .execute()
8
+ await db.schema
9
+ .alterTable('moderation_event')
10
+ .addColumn('strikeCount', 'integer')
11
+ .execute()
12
+ await db.schema
13
+ .alterTable('moderation_event')
14
+ .addColumn('strikeExpiresAt', 'varchar')
15
+ .execute()
16
+
17
+ await db.schema
18
+ .createTable('account_strike')
19
+ .addColumn('did', 'text', (col) => col.primaryKey())
20
+ .addColumn('firstStrikeAt', 'varchar')
21
+ .addColumn('lastStrikeAt', 'varchar')
22
+ .addColumn('activeStrikeCount', 'integer', (col) =>
23
+ col.notNull().defaultTo(0),
24
+ )
25
+ .addColumn('totalStrikeCount', 'integer', (col) =>
26
+ col.notNull().defaultTo(0),
27
+ )
28
+ .execute()
29
+
30
+ await db.schema
31
+ .createTable('job_cursor')
32
+ .addColumn('job', 'text', (col) => col.primaryKey())
33
+ .addColumn('cursor', 'text')
34
+ .addColumn('updatedAt', 'text', (col) =>
35
+ col.defaultTo(sql`now()`).notNull(),
36
+ )
37
+ .execute()
38
+
39
+ // This supports fast look up for background job that aggregates strike data per subjectDid
40
+ await db.schema
41
+ .createIndex('moderation_event_subject_did_strike_count_idx')
42
+ .on('moderation_event')
43
+ .columns(['subjectDid', 'strikeCount'])
44
+ .execute()
45
+
46
+ // This supports fast lookup in the background job that needs to find strikes that have expired
47
+ await sql`
48
+ CREATE INDEX moderation_event_strike_expires_at_strike_count_idx
49
+ ON moderation_event ("strikeExpiresAt", "strikeCount")
50
+ WHERE "strikeExpiresAt" IS NOT NULL AND "strikeCount" IS NOT NULL
51
+ `.execute(db)
52
+
53
+ // for sorting and filtering by active strike count
54
+ await db.schema
55
+ .createIndex('account_strike_active_count_idx')
56
+ .on('account_strike')
57
+ .column('activeStrikeCount')
58
+ .execute()
59
+ }
60
+
61
+ export async function down(db: Kysely<unknown>): Promise<void> {
62
+ await db.schema
63
+ .dropIndex('moderation_event_subject_did_strike_count_idx')
64
+ .execute()
65
+ await db.schema
66
+ .dropIndex('moderation_event_strike_expires_at_strike_count_idx')
67
+ .execute()
68
+ await db.schema.dropIndex('account_strike_active_count_idx').execute()
69
+
70
+ await db.schema.dropTable('account_strike').execute()
71
+ await db.schema.dropTable('job_cursor').execute()
72
+
73
+ await db.schema
74
+ .alterTable('moderation_event')
75
+ .dropColumn('severityLevel')
76
+ .execute()
77
+
78
+ await db.schema
79
+ .alterTable('moderation_event')
80
+ .dropColumn('strikeCount')
81
+ .execute()
82
+
83
+ await db.schema
84
+ .alterTable('moderation_event')
85
+ .dropColumn('strikeExpiresAt')
86
+ .execute()
87
+ }
@@ -32,3 +32,4 @@ export * as _20250715T000000000Z from './20250715T000000000Z-add-mod-event-exter
32
32
  export * as _20250718T150931000Z from './20250718T150931000Z-update-appeal-reason-stats'
33
33
  export * as _20250813T000000000Z from './20250813T000000000Z-mod-tool-batch-id-index'
34
34
  export * as _20250923T000000000Z from './20250923T000000000Z-scheduled-actions'
35
+ export * as _20251008T120000000Z from './20251008T120000000Z-add-strike-system'
@@ -0,0 +1,13 @@
1
+ export const accountStrikeTableName = 'account_strike'
2
+
3
+ export interface AccountStrike {
4
+ did: string // Primary key
5
+ firstStrikeAt: string | null
6
+ lastStrikeAt: string | null
7
+ activeStrikeCount: number
8
+ totalStrikeCount: number
9
+ }
10
+
11
+ export type PartialDB = {
12
+ [accountStrikeTableName]: AccountStrike
13
+ }
@@ -2,9 +2,11 @@ import { Kysely } from 'kysely'
2
2
  import * as accountEventsStats from './account_events_stats'
3
3
  import * as accountRecordEventsStats from './account_record_events_stats'
4
4
  import * as accountRecordStatusStats from './account_record_status_stats'
5
+ import * as accountStrike from './account_strike'
5
6
  import * as blobPushEvent from './blob_push_event'
6
7
  import * as communicationTemplate from './communication_template'
7
8
  import * as firehoseCursor from './firehose_cursor'
9
+ import * as jobCursor from './job_cursor'
8
10
  import * as label from './label'
9
11
  import * as member from './member'
10
12
  import * as modEvent from './moderation_event'
@@ -34,8 +36,10 @@ export type DatabaseSchemaType = modEvent.PartialDB &
34
36
  recordEventsStats.PartialDB &
35
37
  accountRecordEventsStats.PartialDB &
36
38
  accountRecordStatusStats.PartialDB &
39
+ accountStrike.PartialDB &
37
40
  verification.PartialDB &
38
41
  firehoseCursor.PartialDB &
42
+ jobCursor.PartialDB &
39
43
  safelink.PartialDB &
40
44
  scheduledAction.PartialDB
41
45
 
@@ -0,0 +1,13 @@
1
+ import { Generated } from 'kysely'
2
+
3
+ export const jobCursorTableName = 'job_cursor'
4
+
5
+ export interface JobCursor {
6
+ job: string
7
+ cursor: string | null
8
+ updatedAt: Generated<string>
9
+ }
10
+
11
+ export type PartialDB = {
12
+ [jobCursorTableName]: JobCursor
13
+ }
@@ -48,6 +48,9 @@ export interface ModerationEvent {
48
48
  legacyRefId: number | null
49
49
  modTool: { name: string; meta?: { [_ in string]: unknown } } | null
50
50
  externalId: string | null
51
+ severityLevel: string | null
52
+ strikeCount: number | null
53
+ strikeExpiresAt: string | null
51
54
  }
52
55
 
53
56
  export type PartialDB = {
@@ -14630,6 +14630,12 @@ export const schemaDict = {
14630
14630
  type: 'ref',
14631
14631
  ref: 'lex:tools.ozone.moderation.defs#recordsStats',
14632
14632
  },
14633
+ accountStrike: {
14634
+ description:
14635
+ 'Strike information for the account (account-level only)',
14636
+ type: 'ref',
14637
+ ref: 'lex:tools.ozone.moderation.defs#accountStrike',
14638
+ },
14633
14639
  ageAssuranceState: {
14634
14640
  type: 'string',
14635
14641
  description: 'Current age assurance state of the subject.',
@@ -14742,6 +14748,32 @@ export const schemaDict = {
14742
14748
  },
14743
14749
  },
14744
14750
  },
14751
+ accountStrike: {
14752
+ description: 'Strike information for an account',
14753
+ type: 'object',
14754
+ properties: {
14755
+ activeStrikeCount: {
14756
+ description:
14757
+ 'Current number of active strikes (excluding expired strikes)',
14758
+ type: 'integer',
14759
+ },
14760
+ totalStrikeCount: {
14761
+ description:
14762
+ 'Total number of strikes ever received (including expired strikes)',
14763
+ type: 'integer',
14764
+ },
14765
+ firstStrikeAt: {
14766
+ description: 'Timestamp of the first strike received',
14767
+ type: 'string',
14768
+ format: 'datetime',
14769
+ },
14770
+ lastStrikeAt: {
14771
+ description: 'Timestamp of the most recent strike received',
14772
+ type: 'string',
14773
+ format: 'datetime',
14774
+ },
14775
+ },
14776
+ },
14745
14777
  subjectReviewState: {
14746
14778
  type: 'string',
14747
14779
  knownValues: [
@@ -14797,6 +14829,22 @@ export const schemaDict = {
14797
14829
  description:
14798
14830
  'Names/Keywords of the policies that drove the decision.',
14799
14831
  },
14832
+ severityLevel: {
14833
+ type: 'string',
14834
+ description:
14835
+ "Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.).",
14836
+ },
14837
+ strikeCount: {
14838
+ type: 'integer',
14839
+ description:
14840
+ 'Number of strikes to assign to the user for this violation.',
14841
+ },
14842
+ strikeExpiresAt: {
14843
+ type: 'string',
14844
+ format: 'datetime',
14845
+ description:
14846
+ 'When the strike should expire. If not provided, the strike never expires.',
14847
+ },
14800
14848
  },
14801
14849
  },
14802
14850
  modEventReverseTakedown: {
@@ -14807,6 +14855,25 @@ export const schemaDict = {
14807
14855
  type: 'string',
14808
14856
  description: 'Describe reasoning behind the reversal.',
14809
14857
  },
14858
+ policies: {
14859
+ type: 'array',
14860
+ maxLength: 5,
14861
+ items: {
14862
+ type: 'string',
14863
+ },
14864
+ description:
14865
+ 'Names/Keywords of the policy infraction for which takedown is being reversed.',
14866
+ },
14867
+ severityLevel: {
14868
+ type: 'string',
14869
+ description:
14870
+ "Severity level of the violation. Usually set from the last policy infraction's severity.",
14871
+ },
14872
+ strikeCount: {
14873
+ type: 'integer',
14874
+ description:
14875
+ "Number of strikes to subtract from the user's strike count. Usually set from the last policy infraction's severity.",
14876
+ },
14810
14877
  },
14811
14878
  },
14812
14879
  modEventResolveAppeal: {
@@ -15050,6 +15117,31 @@ export const schemaDict = {
15050
15117
  type: 'string',
15051
15118
  description: 'Additional comment about the outgoing comm.',
15052
15119
  },
15120
+ policies: {
15121
+ type: 'array',
15122
+ maxLength: 5,
15123
+ items: {
15124
+ type: 'string',
15125
+ },
15126
+ description:
15127
+ 'Names/Keywords of the policies that necessitated the email.',
15128
+ },
15129
+ severityLevel: {
15130
+ type: 'string',
15131
+ description:
15132
+ "Severity level of the violation. Normally 'sev-1' that adds strike on repeat offense",
15133
+ },
15134
+ strikeCount: {
15135
+ type: 'integer',
15136
+ description:
15137
+ 'Number of strikes to assign to the user for this violation. Normally 0 as an indicator of a warning and only added as a strike on a repeat offense.',
15138
+ },
15139
+ strikeExpiresAt: {
15140
+ type: 'string',
15141
+ format: 'datetime',
15142
+ description:
15143
+ 'When the strike should expire. If not provided, the strike never expires.',
15144
+ },
15053
15145
  },
15054
15146
  },
15055
15147
  modEventDivert: {
@@ -16448,6 +16540,11 @@ export const schemaDict = {
16448
16540
  'blocked',
16449
16541
  ],
16450
16542
  },
16543
+ withStrike: {
16544
+ type: 'boolean',
16545
+ description:
16546
+ 'If specified, only events where strikeCount value is set are returned.',
16547
+ },
16451
16548
  cursor: {
16452
16549
  type: 'string',
16453
16550
  },
@@ -16678,6 +16775,12 @@ export const schemaDict = {
16678
16775
  description:
16679
16776
  'If specified, only subjects that have priority score value above the given value will be returned.',
16680
16777
  },
16778
+ minStrikeCount: {
16779
+ type: 'integer',
16780
+ minimum: 1,
16781
+ description:
16782
+ 'If specified, only subjects that belong to an account that has at least this many active strikes will be returned.',
16783
+ },
16681
16784
  ageAssuranceState: {
16682
16785
  type: 'string',
16683
16786
  description:
@@ -16,7 +16,7 @@ const is$typed = _is$typed,
16
16
  validate = _validate
17
17
  const id = 'app.bsky.actor.profile'
18
18
 
19
- export interface Record {
19
+ export interface Main {
20
20
  $type: 'app.bsky.actor.profile'
21
21
  displayName?: string
22
22
  /** Free-form profile description text. */
@@ -35,12 +35,18 @@ export interface Record {
35
35
  [k: string]: unknown
36
36
  }
37
37
 
38
- const hashRecord = 'main'
38
+ const hashMain = 'main'
39
39
 
40
- export function isRecord<V>(v: V) {
41
- return is$typed(v, id, hashRecord)
40
+ export function isMain<V>(v: V) {
41
+ return is$typed(v, id, hashMain)
42
42
  }
43
43
 
44
- export function validateRecord<V>(v: V) {
45
- return validate<Record & V>(v, id, hashRecord, true)
44
+ export function validateMain<V>(v: V) {
45
+ return validate<Main & V>(v, id, hashMain, true)
46
+ }
47
+
48
+ export {
49
+ type Main as Record,
50
+ isMain as isRecord,
51
+ validateMain as validateRecord,
46
52
  }
@@ -15,7 +15,7 @@ const is$typed = _is$typed,
15
15
  validate = _validate
16
16
  const id = 'app.bsky.actor.status'
17
17
 
18
- export interface Record {
18
+ export interface Main {
19
19
  $type: 'app.bsky.actor.status'
20
20
  /** The status for the account. */
21
21
  status: 'app.bsky.actor.status#live' | (string & {})
@@ -26,14 +26,20 @@ export interface Record {
26
26
  [k: string]: unknown
27
27
  }
28
28
 
29
- const hashRecord = 'main'
29
+ const hashMain = 'main'
30
30
 
31
- export function isRecord<V>(v: V) {
32
- return is$typed(v, id, hashRecord)
31
+ export function isMain<V>(v: V) {
32
+ return is$typed(v, id, hashMain)
33
33
  }
34
34
 
35
- export function validateRecord<V>(v: V) {
36
- return validate<Record & V>(v, id, hashRecord, true)
35
+ export function validateMain<V>(v: V) {
36
+ return validate<Main & V>(v, id, hashMain, true)
37
+ }
38
+
39
+ export {
40
+ type Main as Record,
41
+ isMain as isRecord,
42
+ validateMain as validateRecord,
37
43
  }
38
44
 
39
45
  /** Advertises an account as currently offering live content. */
@@ -16,7 +16,7 @@ const is$typed = _is$typed,
16
16
  validate = _validate
17
17
  const id = 'app.bsky.feed.generator'
18
18
 
19
- export interface Record {
19
+ export interface Main {
20
20
  $type: 'app.bsky.feed.generator'
21
21
  did: string
22
22
  displayName: string
@@ -34,12 +34,18 @@ export interface Record {
34
34
  [k: string]: unknown
35
35
  }
36
36
 
37
- const hashRecord = 'main'
37
+ const hashMain = 'main'
38
38
 
39
- export function isRecord<V>(v: V) {
40
- return is$typed(v, id, hashRecord)
39
+ export function isMain<V>(v: V) {
40
+ return is$typed(v, id, hashMain)
41
41
  }
42
42
 
43
- export function validateRecord<V>(v: V) {
44
- return validate<Record & V>(v, id, hashRecord, true)
43
+ export function validateMain<V>(v: V) {
44
+ return validate<Main & V>(v, id, hashMain, true)
45
+ }
46
+
47
+ export {
48
+ type Main as Record,
49
+ isMain as isRecord,
50
+ validateMain as validateRecord,
45
51
  }