@atproto/ozone 0.1.176 → 0.2.1

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 (180) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/api/chat/getConvo.d.ts +4 -0
  3. package/dist/api/chat/getConvo.d.ts.map +1 -0
  4. package/dist/api/chat/getConvo.js +18 -0
  5. package/dist/api/chat/getConvo.js.map +1 -0
  6. package/dist/api/chat/getConvoMembers.d.ts +4 -0
  7. package/dist/api/chat/getConvoMembers.d.ts.map +1 -0
  8. package/dist/api/chat/getConvoMembers.js +18 -0
  9. package/dist/api/chat/getConvoMembers.js.map +1 -0
  10. package/dist/api/chat/index.d.ts.map +1 -1
  11. package/dist/api/chat/index.js +4 -0
  12. package/dist/api/chat/index.js.map +1 -1
  13. package/dist/daemon/event-reverser.d.ts.map +1 -1
  14. package/dist/daemon/event-reverser.js +1 -0
  15. package/dist/daemon/event-reverser.js.map +1 -1
  16. package/dist/db/migrations/20260513T202941104Z-add-subject-convo-id.d.ts +4 -0
  17. package/dist/db/migrations/20260513T202941104Z-add-subject-convo-id.d.ts.map +1 -0
  18. package/dist/db/migrations/20260513T202941104Z-add-subject-convo-id.js +107 -0
  19. package/dist/db/migrations/20260513T202941104Z-add-subject-convo-id.js.map +1 -0
  20. package/dist/db/migrations/index.d.ts +1 -0
  21. package/dist/db/migrations/index.d.ts.map +1 -1
  22. package/dist/db/migrations/index.js +1 -0
  23. package/dist/db/migrations/index.js.map +1 -1
  24. package/dist/db/schema/expiring_tag.d.ts +1 -0
  25. package/dist/db/schema/expiring_tag.d.ts.map +1 -1
  26. package/dist/db/schema/expiring_tag.js.map +1 -1
  27. package/dist/db/schema/moderation_event.d.ts +2 -1
  28. package/dist/db/schema/moderation_event.d.ts.map +1 -1
  29. package/dist/db/schema/moderation_event.js.map +1 -1
  30. package/dist/db/schema/moderation_subject_status.d.ts +1 -0
  31. package/dist/db/schema/moderation_subject_status.d.ts.map +1 -1
  32. package/dist/db/schema/moderation_subject_status.js.map +1 -1
  33. package/dist/db/schema/report.d.ts +1 -0
  34. package/dist/db/schema/report.d.ts.map +1 -1
  35. package/dist/db/schema/report.js.map +1 -1
  36. package/dist/lexicon/index.d.ts +21 -2
  37. package/dist/lexicon/index.d.ts.map +1 -1
  38. package/dist/lexicon/index.js +36 -2
  39. package/dist/lexicon/index.js.map +1 -1
  40. package/dist/lexicon/lexicons.d.ts +1617 -305
  41. package/dist/lexicon/lexicons.d.ts.map +1 -1
  42. package/dist/lexicon/lexicons.js +762 -44
  43. package/dist/lexicon/lexicons.js.map +1 -1
  44. package/dist/lexicon/types/app/bsky/actor/defs.d.ts +4 -0
  45. package/dist/lexicon/types/app/bsky/actor/defs.d.ts.map +1 -1
  46. package/dist/lexicon/types/app/bsky/actor/defs.js.map +1 -1
  47. package/dist/lexicon/types/app/bsky/embed/external.d.ts +48 -2
  48. package/dist/lexicon/types/app/bsky/embed/external.d.ts.map +1 -1
  49. package/dist/lexicon/types/app/bsky/embed/external.js +21 -0
  50. package/dist/lexicon/types/app/bsky/embed/external.js.map +1 -1
  51. package/dist/lexicon/types/app/bsky/embed/getEmbedExternalView.d.ts +31 -0
  52. package/dist/lexicon/types/app/bsky/embed/getEmbedExternalView.d.ts.map +1 -0
  53. package/dist/lexicon/types/app/bsky/embed/getEmbedExternalView.js +5 -0
  54. package/dist/lexicon/types/app/bsky/embed/getEmbedExternalView.js.map +1 -0
  55. package/dist/lexicon/types/chat/bsky/actor/getStatus.d.ts +24 -0
  56. package/dist/lexicon/types/chat/bsky/actor/getStatus.d.ts.map +1 -0
  57. package/dist/lexicon/types/chat/bsky/{group/getJoinLinkPreview.js → actor/getStatus.js} +2 -2
  58. package/dist/lexicon/types/chat/bsky/actor/getStatus.js.map +1 -0
  59. package/dist/lexicon/types/chat/bsky/convo/defs.d.ts +39 -7
  60. package/dist/lexicon/types/chat/bsky/convo/defs.d.ts.map +1 -1
  61. package/dist/lexicon/types/chat/bsky/convo/defs.js +21 -0
  62. package/dist/lexicon/types/chat/bsky/convo/defs.js.map +1 -1
  63. package/dist/lexicon/types/chat/bsky/convo/getConvoForMembers.d.ts +1 -1
  64. package/dist/lexicon/types/chat/bsky/convo/getConvoForMembers.d.ts.map +1 -1
  65. package/dist/lexicon/types/chat/bsky/convo/getConvoForMembers.js.map +1 -1
  66. package/dist/lexicon/types/chat/bsky/convo/getLog.d.ts +1 -1
  67. package/dist/lexicon/types/chat/bsky/convo/getLog.d.ts.map +1 -1
  68. package/dist/lexicon/types/chat/bsky/convo/getLog.js.map +1 -1
  69. package/dist/lexicon/types/chat/bsky/convo/listConvoRequests.d.ts +1 -1
  70. package/dist/lexicon/types/chat/bsky/convo/listConvoRequests.d.ts.map +1 -1
  71. package/dist/lexicon/types/chat/bsky/convo/listConvoRequests.js.map +1 -1
  72. package/dist/lexicon/types/chat/bsky/embed/joinLink.d.ts +19 -0
  73. package/dist/lexicon/types/chat/bsky/embed/joinLink.d.ts.map +1 -0
  74. package/dist/lexicon/types/chat/bsky/embed/joinLink.js +19 -0
  75. package/dist/lexicon/types/chat/bsky/embed/joinLink.js.map +1 -0
  76. package/dist/lexicon/types/chat/bsky/group/addMembers.d.ts +1 -1
  77. package/dist/lexicon/types/chat/bsky/group/addMembers.d.ts.map +1 -1
  78. package/dist/lexicon/types/chat/bsky/group/addMembers.js.map +1 -1
  79. package/dist/lexicon/types/chat/bsky/group/createGroup.d.ts +1 -1
  80. package/dist/lexicon/types/chat/bsky/group/createGroup.d.ts.map +1 -1
  81. package/dist/lexicon/types/chat/bsky/group/createGroup.js.map +1 -1
  82. package/dist/lexicon/types/chat/bsky/group/defs.d.ts +23 -0
  83. package/dist/lexicon/types/chat/bsky/group/defs.d.ts.map +1 -1
  84. package/dist/lexicon/types/chat/bsky/group/defs.js +14 -0
  85. package/dist/lexicon/types/chat/bsky/group/defs.js.map +1 -1
  86. package/dist/lexicon/types/chat/bsky/group/{getJoinLinkPreview.d.ts → getJoinLinkPreviews.d.ts} +3 -4
  87. package/dist/lexicon/types/chat/bsky/group/getJoinLinkPreviews.d.ts.map +1 -0
  88. package/dist/lexicon/types/chat/bsky/group/getJoinLinkPreviews.js +5 -0
  89. package/dist/lexicon/types/chat/bsky/group/getJoinLinkPreviews.js.map +1 -0
  90. package/dist/lexicon/types/chat/bsky/group/updateJoinRequestsRead.d.ts +24 -0
  91. package/dist/lexicon/types/chat/bsky/group/updateJoinRequestsRead.d.ts.map +1 -0
  92. package/dist/lexicon/types/chat/bsky/group/updateJoinRequestsRead.js +5 -0
  93. package/dist/lexicon/types/chat/bsky/group/updateJoinRequestsRead.js.map +1 -0
  94. package/dist/lexicon/types/chat/bsky/group/withdrawJoinRequest.d.ts +24 -0
  95. package/dist/lexicon/types/chat/bsky/group/withdrawJoinRequest.d.ts.map +1 -0
  96. package/dist/lexicon/types/chat/bsky/group/withdrawJoinRequest.js +5 -0
  97. package/dist/lexicon/types/chat/bsky/group/withdrawJoinRequest.js.map +1 -0
  98. package/dist/lexicon/types/chat/bsky/moderation/defs.d.ts +42 -0
  99. package/dist/lexicon/types/chat/bsky/moderation/defs.d.ts.map +1 -0
  100. package/dist/lexicon/types/chat/bsky/moderation/defs.js +26 -0
  101. package/dist/lexicon/types/chat/bsky/moderation/defs.js.map +1 -0
  102. package/dist/lexicon/types/chat/bsky/moderation/getConvo.d.ts +23 -0
  103. package/dist/lexicon/types/chat/bsky/moderation/getConvo.d.ts.map +1 -0
  104. package/dist/lexicon/types/chat/bsky/moderation/getConvo.js +5 -0
  105. package/dist/lexicon/types/chat/bsky/moderation/getConvo.js.map +1 -0
  106. package/dist/lexicon/types/chat/bsky/moderation/getConvoMembers.d.ts +26 -0
  107. package/dist/lexicon/types/chat/bsky/moderation/getConvoMembers.d.ts.map +1 -0
  108. package/dist/lexicon/types/chat/bsky/moderation/getConvoMembers.js +5 -0
  109. package/dist/lexicon/types/chat/bsky/moderation/getConvoMembers.js.map +1 -0
  110. package/dist/lexicon/types/chat/bsky/moderation/getConvos.d.ts +22 -0
  111. package/dist/lexicon/types/chat/bsky/moderation/getConvos.d.ts.map +1 -0
  112. package/dist/lexicon/types/chat/bsky/moderation/getConvos.js +5 -0
  113. package/dist/lexicon/types/chat/bsky/moderation/getConvos.js.map +1 -0
  114. package/dist/lexicon/types/chat/bsky/moderation/subscribeModEvents.d.ts +13 -1
  115. package/dist/lexicon/types/chat/bsky/moderation/subscribeModEvents.d.ts.map +1 -1
  116. package/dist/lexicon/types/chat/bsky/moderation/subscribeModEvents.js +7 -0
  117. package/dist/lexicon/types/chat/bsky/moderation/subscribeModEvents.js.map +1 -1
  118. package/dist/lexicon/types/com/atproto/server/getServiceAuth.d.ts +1 -1
  119. package/dist/lexicon/types/com/atproto/server/getServiceAuth.d.ts.map +1 -1
  120. package/dist/lexicon/types/com/atproto/server/getServiceAuth.js.map +1 -1
  121. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts +10 -3
  122. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts.map +1 -1
  123. package/dist/lexicon/types/tools/ozone/moderation/defs.js +7 -0
  124. package/dist/lexicon/types/tools/ozone/moderation/defs.js.map +1 -1
  125. package/dist/lexicon/types/tools/ozone/moderation/queryEvents.d.ts +2 -2
  126. package/dist/lexicon/types/tools/ozone/moderation/queryEvents.d.ts.map +1 -1
  127. package/dist/lexicon/types/tools/ozone/moderation/queryEvents.js.map +1 -1
  128. package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.d.ts +2 -2
  129. package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.d.ts.map +1 -1
  130. package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.js.map +1 -1
  131. package/dist/mod-service/expiring-tags.d.ts +3 -0
  132. package/dist/mod-service/expiring-tags.d.ts.map +1 -1
  133. package/dist/mod-service/expiring-tags.js +5 -2
  134. package/dist/mod-service/expiring-tags.js.map +1 -1
  135. package/dist/mod-service/index.d.ts +3 -1
  136. package/dist/mod-service/index.d.ts.map +1 -1
  137. package/dist/mod-service/index.js +48 -10
  138. package/dist/mod-service/index.js.map +1 -1
  139. package/dist/mod-service/status.d.ts +7 -1
  140. package/dist/mod-service/status.d.ts.map +1 -1
  141. package/dist/mod-service/status.js +18 -3
  142. package/dist/mod-service/status.js.map +1 -1
  143. package/dist/mod-service/subject.d.ts +38 -2
  144. package/dist/mod-service/subject.d.ts.map +1 -1
  145. package/dist/mod-service/subject.js +67 -0
  146. package/dist/mod-service/subject.js.map +1 -1
  147. package/dist/mod-service/views.d.ts +2 -1
  148. package/dist/mod-service/views.d.ts.map +1 -1
  149. package/dist/mod-service/views.js +45 -23
  150. package/dist/mod-service/views.js.map +1 -1
  151. package/dist/queue/service.d.ts.map +1 -1
  152. package/dist/queue/service.js +7 -3
  153. package/dist/queue/service.js.map +1 -1
  154. package/dist/safelink/service.d.ts +1 -1
  155. package/package.json +5 -5
  156. package/src/api/chat/getConvo.ts +23 -0
  157. package/src/api/chat/getConvoMembers.ts +23 -0
  158. package/src/api/chat/index.ts +4 -0
  159. package/src/daemon/event-reverser.ts +1 -0
  160. package/src/db/migrations/20260513T202941104Z-add-subject-convo-id.ts +114 -0
  161. package/src/db/migrations/index.ts +1 -0
  162. package/src/db/schema/expiring_tag.ts +1 -0
  163. package/src/db/schema/moderation_event.ts +2 -0
  164. package/src/db/schema/moderation_subject_status.ts +4 -0
  165. package/src/db/schema/report.ts +2 -1
  166. package/src/mod-service/expiring-tags.ts +8 -2
  167. package/src/mod-service/index.ts +52 -16
  168. package/src/mod-service/status.ts +24 -2
  169. package/src/mod-service/subject.ts +79 -1
  170. package/src/mod-service/views.ts +52 -24
  171. package/src/queue/service.ts +8 -4
  172. package/tests/__snapshots__/verification.test.ts.snap +2 -0
  173. package/tests/expiring-tags.test.ts +1 -0
  174. package/tests/moderation-events.test.ts +108 -1
  175. package/tests/moderation-status-tags.test.ts +23 -0
  176. package/tests/moderation-statuses.test.ts +82 -0
  177. package/tests/moderation.test.ts +73 -0
  178. package/tsconfig.build.tsbuildinfo +1 -1
  179. package/dist/lexicon/types/chat/bsky/group/getJoinLinkPreview.d.ts.map +0 -1
  180. package/dist/lexicon/types/chat/bsky/group/getJoinLinkPreview.js.map +0 -1
@@ -16,6 +16,9 @@ const isStrongRef = asPredicate(ComAtprotoRepoStrongRef.validateMain)
16
16
  type MessageRef = ChatBskyConvoDefs.MessageRef
17
17
  const isValidMessageRef = asPredicate(ChatBskyConvoDefs.validateMessageRef)
18
18
 
19
+ type ConvoRef = ChatBskyConvoDefs.ConvoRef
20
+ const isValidConvoRef = asPredicate(ChatBskyConvoDefs.validateConvoRef)
21
+
19
22
  const isMessageRefWithoutConvoId = (
20
23
  subject: unknown,
21
24
  ): subject is $Typed<Omit<MessageRef, 'convoId'> & { convoId?: string }> =>
@@ -41,6 +44,11 @@ export const subjectFromInput = (
41
44
  if (isValidMessageRef(subject)) {
42
45
  return new MessageSubject(subject.did, subject.convoId, subject.messageId)
43
46
  }
47
+ // @NOTE #convoRef is a new type for reporting entire conversations.
48
+ // Similar to messageRef, we take advantage of the open union to support this.
49
+ if (isValidConvoRef(subject)) {
50
+ return new ConvoSubject(subject.did, subject.convoId)
51
+ }
44
52
  // @TODO we should start to require subject.convoId is a string in order to properly validate
45
53
  // the #messageRef. temporarily allowing it to be optional as a stopgap for rollout.
46
54
  // The next "if" can be removed once convoId is consistently provided.
@@ -73,6 +81,11 @@ export const subjectFromEventRow = (row: ModerationEventRow): ModSubject => {
73
81
  const convoId =
74
82
  typeof row.meta?.['convoId'] === 'string' ? row.meta['convoId'] : ''
75
83
  return new MessageSubject(row.subjectDid, convoId, row.subjectMessageId)
84
+ } else if (
85
+ row.subjectType === 'chat.bsky.convo.defs#convoRef' &&
86
+ row.subjectConvoId
87
+ ) {
88
+ return new ConvoSubject(row.subjectDid, row.subjectConvoId)
76
89
  } else {
77
90
  return new RepoSubject(row.subjectDid)
78
91
  }
@@ -86,6 +99,9 @@ export const subjectFromStatusRow = (
86
99
  // which is what the last 2 params of .make() arguments are
87
100
  const uri = AtUri.make(row.did, ...row.recordPath.split('/')).toString()
88
101
  return new RecordSubject(uri.toString(), row.recordCid, row.blobCids ?? [])
102
+ } else if (row.convoId) {
103
+ // Conversation subject - restore from subject status row
104
+ return new ConvoSubject(row.did, row.convoId)
89
105
  } else {
90
106
  return new RepoSubject(row.did)
91
107
  }
@@ -96,28 +112,37 @@ type SubjectInfo = {
96
112
  | 'com.atproto.admin.defs#repoRef'
97
113
  | 'com.atproto.repo.strongRef'
98
114
  | 'chat.bsky.convo.defs#messageRef'
115
+ | 'chat.bsky.convo.defs#convoRef'
99
116
  subjectDid: string
100
117
  subjectUri: string | null
101
118
  subjectCid: string | null
102
119
  subjectBlobCids: string[] | null
103
120
  subjectMessageId: string | null
121
+ subjectConvoId: string | null
104
122
  meta: Record<string, string | undefined> | null
105
123
  }
106
124
 
107
125
  export interface ModSubject {
108
126
  did: string
109
127
  recordPath: string | undefined
128
+ convoId: string | undefined
110
129
  blobCids?: string[]
111
130
  isRepo(): this is RepoSubject
112
131
  isRecord(): this is RecordSubject
113
132
  isMessage(): this is MessageSubject
133
+ isConvo(): this is ConvoSubject
114
134
  info(): SubjectInfo
115
- lex(): $Typed<RepoRef> | $Typed<StrongRef> | $Typed<MessageRef>
135
+ lex():
136
+ | $Typed<RepoRef>
137
+ | $Typed<StrongRef>
138
+ | $Typed<MessageRef>
139
+ | $Typed<ConvoRef>
116
140
  }
117
141
 
118
142
  export class RepoSubject implements ModSubject {
119
143
  blobCids = undefined
120
144
  recordPath = undefined
145
+ convoId = undefined
121
146
  constructor(public did: string) {}
122
147
  isRepo(): this is RepoSubject {
123
148
  return true
@@ -128,6 +153,9 @@ export class RepoSubject implements ModSubject {
128
153
  isMessage(): this is MessageSubject {
129
154
  return false
130
155
  }
156
+ isConvo(): this is ConvoSubject {
157
+ return false
158
+ }
131
159
  info() {
132
160
  return {
133
161
  subjectType: 'com.atproto.admin.defs#repoRef' as const,
@@ -136,6 +164,7 @@ export class RepoSubject implements ModSubject {
136
164
  subjectCid: null,
137
165
  subjectBlobCids: null,
138
166
  subjectMessageId: null,
167
+ subjectConvoId: null,
139
168
  meta: null,
140
169
  }
141
170
  }
@@ -151,6 +180,7 @@ export class RecordSubject implements ModSubject {
151
180
  parsedUri: AtUri
152
181
  did: string
153
182
  recordPath: string
183
+ convoId = undefined
154
184
  constructor(
155
185
  public uri: string,
156
186
  public cid: string,
@@ -169,6 +199,9 @@ export class RecordSubject implements ModSubject {
169
199
  isMessage(): this is MessageSubject {
170
200
  return false
171
201
  }
202
+ isConvo(): this is ConvoSubject {
203
+ return false
204
+ }
172
205
  info() {
173
206
  return {
174
207
  subjectType: 'com.atproto.repo.strongRef' as const,
@@ -177,6 +210,7 @@ export class RecordSubject implements ModSubject {
177
210
  subjectCid: this.cid,
178
211
  subjectBlobCids: this.blobCids ?? [],
179
212
  subjectMessageId: null,
213
+ subjectConvoId: null,
180
214
  meta: null,
181
215
  }
182
216
  }
@@ -206,6 +240,9 @@ export class MessageSubject implements ModSubject {
206
240
  isMessage(): this is MessageSubject {
207
241
  return true
208
242
  }
243
+ isConvo(): this is ConvoSubject {
244
+ return false
245
+ }
209
246
  info() {
210
247
  return {
211
248
  subjectType: 'chat.bsky.convo.defs#messageRef' as const,
@@ -214,6 +251,7 @@ export class MessageSubject implements ModSubject {
214
251
  subjectCid: null,
215
252
  subjectBlobCids: null,
216
253
  subjectMessageId: this.messageId,
254
+ subjectConvoId: this.convoId,
217
255
  meta: { convoId: this.convoId || undefined },
218
256
  }
219
257
  }
@@ -226,3 +264,43 @@ export class MessageSubject implements ModSubject {
226
264
  }
227
265
  }
228
266
  }
267
+
268
+ export class ConvoSubject implements ModSubject {
269
+ blobCids = undefined
270
+ recordPath = undefined
271
+ constructor(
272
+ public did: string,
273
+ public convoId: string,
274
+ ) {}
275
+ isRepo(): this is RepoSubject {
276
+ return false
277
+ }
278
+ isRecord(): this is RecordSubject {
279
+ return false
280
+ }
281
+ isMessage(): this is MessageSubject {
282
+ return false
283
+ }
284
+ isConvo(): this is ConvoSubject {
285
+ return true
286
+ }
287
+ info() {
288
+ return {
289
+ subjectType: 'chat.bsky.convo.defs#convoRef' as const,
290
+ subjectDid: this.did,
291
+ subjectUri: null,
292
+ subjectCid: null,
293
+ subjectBlobCids: null,
294
+ subjectMessageId: null,
295
+ subjectConvoId: this.convoId,
296
+ meta: { convoId: this.convoId || undefined },
297
+ }
298
+ }
299
+ lex(): $Typed<ConvoRef> {
300
+ return {
301
+ $type: 'chat.bsky.convo.defs#convoRef',
302
+ did: this.did,
303
+ convoId: this.convoId,
304
+ }
305
+ }
306
+ }
@@ -51,7 +51,11 @@ import { Un$Typed, asPredicate } from '../lexicon/util.js'
51
51
  import { dbLogger, httpLogger } from '../logger.js'
52
52
  import { ParsedLabelers } from '../util.js'
53
53
  import { moderationSubjectStatusQueryBuilder } from './status.js'
54
- import { subjectFromEventRow, subjectFromStatusRow } from './subject.js'
54
+ import {
55
+ ModSubject,
56
+ subjectFromEventRow,
57
+ subjectFromStatusRow,
58
+ } from './subject.js'
55
59
  import {
56
60
  ModerationEventRowWithHandle,
57
61
  ModerationSubjectStatusRowWithHandle,
@@ -148,7 +152,9 @@ export class ModerationViews {
148
152
  modTool: row.modTool
149
153
  ? {
150
154
  name: row.modTool.name,
151
- meta: row.modTool.meta,
155
+ meta: sanitizeUnsafeIntegers(row.modTool.meta) as
156
+ | Record<string, unknown>
157
+ | undefined,
152
158
  }
153
159
  : undefined,
154
160
  }
@@ -297,14 +303,8 @@ export class ModerationViews {
297
303
  async eventDetail(
298
304
  result: ModerationEventRowWithHandle,
299
305
  ): Promise<ModEventViewDetail> {
300
- const subjectId =
301
- result.subjectType === 'com.atproto.admin.defs#repoRef'
302
- ? result.subjectDid
303
- : result.subjectUri
304
- if (!subjectId) {
305
- throw new Error(`Bad subject: ${result.id}`)
306
- }
307
- const subject = await this.subject(subjectId)
306
+ const modSubject = subjectFromEventRow(result)
307
+ const subject = await this.subject(modSubject)
308
308
  const eventView = this.formatEvent(result)
309
309
  const allBlobs = 'value' in subject ? findBlobRefs(subject.value) : []
310
310
  const subjectBlobs = await this.blob(
@@ -555,10 +555,16 @@ export class ModerationViews {
555
555
  }
556
556
  // Partial view for subjects
557
557
 
558
- async subject(subject: string): Promise<SubjectView> {
559
- if (subject.startsWith('did:')) {
560
- const repos = await this.repos([subject])
561
- const repo = repos.get(subject)
558
+ async subject(subject: ModSubject): Promise<SubjectView> {
559
+ if (subject.isConvo()) {
560
+ return {
561
+ $type: 'tools.ozone.moderation.defs#convoView',
562
+ did: subject.did,
563
+ convoId: subject.convoId,
564
+ }
565
+ } else if (subject.isRepo()) {
566
+ const repos = await this.repos([subject.did])
567
+ const repo = repos.get(subject.did)
562
568
  if (repo) {
563
569
  return {
564
570
  ...repo,
@@ -567,24 +573,24 @@ export class ModerationViews {
567
573
  } else {
568
574
  return {
569
575
  $type: 'tools.ozone.moderation.defs#repoViewNotFound',
570
- did: subject,
576
+ did: subject.did,
571
577
  }
572
578
  }
573
- } else {
574
- const records = await this.records([{ uri: subject }])
575
- const record = records.get(subject)
579
+ } else if (subject.isRecord()) {
580
+ const uri = subject.uri
581
+ const records = await this.records([{ uri }])
582
+ const record = records.get(uri)
576
583
  if (record) {
577
584
  return {
578
585
  ...record,
579
586
  $type: 'tools.ozone.moderation.defs#recordView',
580
587
  }
581
- } else {
582
- return {
583
- $type: 'tools.ozone.moderation.defs#recordViewNotFound',
584
- uri: subject,
585
- }
586
588
  }
587
589
  }
590
+ return {
591
+ $type: 'tools.ozone.moderation.defs#repoViewNotFound',
592
+ did: subject.did,
593
+ }
588
594
  }
589
595
 
590
596
  // Partial view for blobs
@@ -691,7 +697,8 @@ export class ModerationViews {
691
697
  'moderation_subject_status.recordPath',
692
698
  '=',
693
699
  sub.recordPath ?? '',
694
- ),
700
+ )
701
+ .where('moderation_subject_status.convoId', '=', ''),
695
702
  )
696
703
  }
697
704
  return qb
@@ -882,3 +889,24 @@ export function getSelfLabels(details: {
882
889
  return { src, uri, cid, val, cts }
883
890
  })
884
891
  }
892
+
893
+ // The atproto data model requires all integers to fit within 53 bits
894
+ // (Number.MAX_SAFE_INTEGER). External tools may write larger numbers into
895
+ // "unknown"-typed fields like modTool.meta, which causes lex-json parse
896
+ // failures on the client side. This helper converts unsafe integers to strings.
897
+ function sanitizeUnsafeIntegers(value: unknown): unknown {
898
+ if (typeof value === 'number') {
899
+ return Number.isSafeInteger(value) ? value : String(value)
900
+ }
901
+ if (Array.isArray(value)) {
902
+ return value.map(sanitizeUnsafeIntegers)
903
+ }
904
+ if (value !== null && typeof value === 'object') {
905
+ const out: Record<string, unknown> = {}
906
+ for (const [k, v] of Object.entries(value)) {
907
+ out[k] = sanitizeUnsafeIntegers(v)
908
+ }
909
+ return out
910
+ }
911
+ return value
912
+ }
@@ -13,7 +13,7 @@ import { viewQueueStats } from '../report/views.js'
13
13
  const MOD_EVENT_REPORT_ACTION = 'tools.ozone.moderation.defs#modEventReport'
14
14
  const REASON_OTHER = 'com.atproto.moderation.defs#reasonOther'
15
15
 
16
- type SubjectType = 'account' | 'record' | 'message'
16
+ type SubjectType = 'account' | 'record' | 'message' | 'conversation'
17
17
 
18
18
  type ResolvedAssignment = {
19
19
  queueId: number
@@ -478,6 +478,7 @@ export class QueueService {
478
478
  'subjectDid',
479
479
  'subjectUri',
480
480
  'subjectMessageId',
481
+ 'subjectConvoId',
481
482
  'meta',
482
483
  'createdAt',
483
484
  ])
@@ -503,9 +504,11 @@ export class QueueService {
503
504
  const rows = events.map((event) => {
504
505
  const subjectType: SubjectType = event.subjectMessageId
505
506
  ? 'message'
506
- : event.subjectUri
507
- ? 'record'
508
- : 'account'
507
+ : event.subjectConvoId
508
+ ? 'conversation'
509
+ : event.subjectUri
510
+ ? 'record'
511
+ : 'account'
509
512
 
510
513
  let collection: string | null = null
511
514
  let recordPath = ''
@@ -545,6 +548,7 @@ export class QueueService {
545
548
  did: event.subjectDid,
546
549
  recordPath,
547
550
  subjectMessageId: event.subjectMessageId,
551
+ subjectConvoId: event.subjectConvoId,
548
552
  createdAt: now,
549
553
  updatedAt: now,
550
554
  }
@@ -122,6 +122,8 @@ exports[`verification list returns paginated list of verifications 1`] = `
122
122
  "createdAt": "1970-01-01T00:00:00.000Z",
123
123
  "isValid": true,
124
124
  "issuer": "user(0)",
125
+ "issuerDisplayName": "ali",
126
+ "issuerHandle": "alice.test",
125
127
  "uri": "record(0)",
126
128
  },
127
129
  ],
@@ -192,6 +192,7 @@ describe('expiring tags', () => {
192
192
  eventId: 0,
193
193
  did: sc.dids.carol,
194
194
  recordPath: '',
195
+ convoId: '',
195
196
  tag: 'skip-tag',
196
197
  expiresAt: new Date(Date.now() - 1000).toISOString(),
197
198
  createdBy: sc.dids.alice,
@@ -207,8 +207,25 @@ describe('moderation-events', () => {
207
207
  }),
208
208
  ])
209
209
 
210
- expect(misleadingEvents.events.length).toEqual(2)
210
+ // Verify all spam events have the correct report type
211
211
  expect(spamEvents.events.length).toEqual(6)
212
+ spamEvents.events.forEach((event) => {
213
+ expect(event.event.$type).toEqual(
214
+ 'tools.ozone.moderation.defs#modEventReport',
215
+ )
216
+ expect((event.event as any).reportType).toEqual(REASONSPAM)
217
+ })
218
+
219
+ // Verify all misleading events have one of the correct report types
220
+ expect(misleadingEvents.events.length).toEqual(2)
221
+ misleadingEvents.events.forEach((event) => {
222
+ expect(event.event.$type).toEqual(
223
+ 'tools.ozone.moderation.defs#modEventReport',
224
+ )
225
+ expect([REASONMISLEADING, REASONAPPEAL]).toContain(
226
+ (event.event as any).reportType,
227
+ )
228
+ })
212
229
  })
213
230
 
214
231
  it('returns events matching keyword in comment', async () => {
@@ -501,6 +518,96 @@ describe('moderation-events', () => {
501
518
  expect(event.createdBy).toEqual(network.ozone.moderatorAccnt.did)
502
519
  })
503
520
  })
521
+
522
+ it('queries events by conversation', async () => {
523
+ const convoId1 = 'conversation-123'
524
+ const convoId2 = 'conversation-456'
525
+
526
+ // create reports
527
+ await sc.createReport({
528
+ reasonType: REASONSPAM,
529
+ reason: 'spam in convo 1',
530
+ subject: {
531
+ $type: 'chat.bsky.convo.defs#convoRef',
532
+ did: sc.dids.carol,
533
+ convoId: convoId1,
534
+ },
535
+ reportedBy: sc.dids.alice,
536
+ })
537
+ await sc.createReport({
538
+ reasonType: REASONMISLEADING,
539
+ reason: 'misleading in convo 1',
540
+ subject: {
541
+ $type: 'chat.bsky.convo.defs#convoRef',
542
+ did: sc.dids.carol,
543
+ convoId: convoId1,
544
+ },
545
+ reportedBy: sc.dids.bob,
546
+ })
547
+ await sc.createReport({
548
+ reasonType: REASONSPAM,
549
+ reason: 'spam in convo 2',
550
+ subject: {
551
+ $type: 'chat.bsky.convo.defs#convoRef',
552
+ did: sc.dids.carol,
553
+ convoId: convoId2,
554
+ },
555
+ reportedBy: sc.dids.alice,
556
+ })
557
+
558
+ // Query events (filter by report type since auto-tagging creates extra events)
559
+ const convo1Events = await modClient.queryEvents({
560
+ subject: `at://${sc.dids.carol}/chat.bsky.convo/${convoId1}`,
561
+ includeAllUserRecords: false,
562
+ types: ['tools.ozone.moderation.defs#modEventReport'],
563
+ })
564
+ const convo2Events = await modClient.queryEvents({
565
+ subject: `at://${sc.dids.carol}/chat.bsky.convo/${convoId2}`,
566
+ includeAllUserRecords: false,
567
+ types: ['tools.ozone.moderation.defs#modEventReport'],
568
+ })
569
+
570
+ // Verify conversation 1 events are correct
571
+ expect(convo1Events.events.length).toBeGreaterThan(0)
572
+ convo1Events.events.forEach((e) => {
573
+ // All events should be conversation refs
574
+ expect(e.subject.$type).toEqual('chat.bsky.convo.defs#convoRef')
575
+ // All events should be for conversation 1
576
+ const subject = e.subject as any
577
+ expect(subject.convoId).toEqual(convoId1)
578
+ expect(subject.did).toEqual(sc.dids.carol)
579
+ // All events should be reports
580
+ expect(e.event.$type).toEqual(
581
+ 'tools.ozone.moderation.defs#modEventReport',
582
+ )
583
+ })
584
+ // Verify we got both reports we created
585
+ const convo1Comments = convo1Events.events.map(
586
+ (e) => (e.event as any).comment,
587
+ )
588
+ expect(convo1Comments).toContain('spam in convo 1')
589
+ expect(convo1Comments).toContain('misleading in convo 1')
590
+
591
+ // Verify conversation 2 events are correct
592
+ expect(convo2Events.events.length).toBeGreaterThan(0)
593
+ convo2Events.events.forEach((e) => {
594
+ // All events should be conversation refs
595
+ expect(e.subject.$type).toEqual('chat.bsky.convo.defs#convoRef')
596
+ // All events should be for conversation 2
597
+ const subject = e.subject as any
598
+ expect(subject.convoId).toEqual(convoId2)
599
+ expect(subject.did).toEqual(sc.dids.carol)
600
+ // All events should be reports
601
+ expect(e.event.$type).toEqual(
602
+ 'tools.ozone.moderation.defs#modEventReport',
603
+ )
604
+ })
605
+ // Verify we got the report we created
606
+ const convo2Comments = convo2Events.events.map(
607
+ (e) => (e.event as any).comment,
608
+ )
609
+ expect(convo2Comments).toContain('spam in convo 2')
610
+ })
504
611
  })
505
612
 
506
613
  describe('get event', () => {
@@ -113,5 +113,28 @@ describe('moderation-status-tags', () => {
113
113
  expect(englishOrJapaneseDids).toContain(sc.dids.alice)
114
114
  expect(englishOrJapaneseDids).toContain(sc.dids.bob)
115
115
  })
116
+
117
+ it('adds tag to conversation', async () => {
118
+ const subject = {
119
+ $type: 'chat.bsky.convo.defs#convoRef',
120
+ did: sc.dids.alice,
121
+ convoId: '123',
122
+ }
123
+ await modClient.emitEvent({
124
+ subject: subject,
125
+ event: {
126
+ $type: 'tools.ozone.moderation.defs#modEventTag',
127
+ add: ['interaction-churn'],
128
+ remove: [],
129
+ },
130
+ })
131
+ const status = await network.ozone.ctx.db.db
132
+ .selectFrom('moderation_subject_status')
133
+ .selectAll()
134
+ .where('did', '=', subject.did)
135
+ .where('convoId', '=', subject.convoId)
136
+ .executeTakeFirstOrThrow()
137
+ expect(status.tags).toContain('interaction-churn')
138
+ })
116
139
  })
117
140
  })
@@ -276,6 +276,88 @@ describe('moderation-statuses', () => {
276
276
  "only bob's account statuses are returned, no events have a URI even though the subjectType is record",
277
277
  )
278
278
  })
279
+
280
+ it('returns statuses for conversations', async () => {
281
+ const convoId1 = 'test-convo-123'
282
+ const convoId2 = 'test-convo-456'
283
+
284
+ // Create reports for conversation 1
285
+ await sc.createReport({
286
+ reasonType: REASONSPAM,
287
+ reason: 'spam in convo 1',
288
+ subject: {
289
+ $type: 'chat.bsky.convo.defs#convoRef',
290
+ did: sc.dids.carol,
291
+ convoId: convoId1,
292
+ },
293
+ reportedBy: sc.dids.alice,
294
+ })
295
+
296
+ // Create another report for conversation 1
297
+ await sc.createReport({
298
+ reasonType: REASONMISLEADING,
299
+ reason: 'misleading in convo 1',
300
+ subject: {
301
+ $type: 'chat.bsky.convo.defs#convoRef',
302
+ did: sc.dids.carol,
303
+ convoId: convoId1,
304
+ },
305
+ reportedBy: sc.dids.bob,
306
+ })
307
+
308
+ // Create report for conversation 2
309
+ await sc.createReport({
310
+ reasonType: REASONSPAM,
311
+ reason: 'spam in convo 2',
312
+ subject: {
313
+ $type: 'chat.bsky.convo.defs#convoRef',
314
+ did: sc.dids.carol,
315
+ convoId: convoId2,
316
+ },
317
+ reportedBy: sc.dids.alice,
318
+ })
319
+
320
+ // Query statuses for conversation 1 using AT URI format
321
+ const convo1Statuses = await modClient.queryStatuses({
322
+ subject: `at://${sc.dids.carol}/chat.bsky.convo/${convoId1}`,
323
+ })
324
+
325
+ // Query statuses for conversation 2
326
+ const convo2Statuses = await modClient.queryStatuses({
327
+ subject: `at://${sc.dids.carol}/chat.bsky.convo/${convoId2}`,
328
+ })
329
+
330
+ // Query all conversation statuses for carol
331
+ const allCarolConvoStatuses = await modClient.queryStatuses({
332
+ subject: sc.dids.carol,
333
+ includeAllUserRecords: true,
334
+ })
335
+
336
+ // Verify conversation 1 has exactly 1 status (multiple reports create one status)
337
+ expect(convo1Statuses.subjectStatuses.length).toEqual(1)
338
+ expect(convo1Statuses.subjectStatuses[0].subject.$type).toEqual(
339
+ 'chat.bsky.convo.defs#convoRef',
340
+ )
341
+ expect(convo1Statuses.subjectStatuses[0].reviewState).toEqual(REVIEWOPEN)
342
+
343
+ // Verify conversation 2 has exactly 1 status
344
+ expect(convo2Statuses.subjectStatuses.length).toEqual(1)
345
+ expect(convo2Statuses.subjectStatuses[0].subject.$type).toEqual(
346
+ 'chat.bsky.convo.defs#convoRef',
347
+ )
348
+
349
+ // Verify statuses are properly isolated by conversation
350
+ const convo1Subject = convo1Statuses.subjectStatuses[0].subject as any
351
+ const convo2Subject = convo2Statuses.subjectStatuses[0].subject as any
352
+ expect(convo1Subject.convoId).toEqual(convoId1)
353
+ expect(convo2Subject.convoId).toEqual(convoId2)
354
+
355
+ // Verify includeAllUserRecords includes conversations
356
+ const convoStatuses = allCarolConvoStatuses.subjectStatuses.filter(
357
+ (s) => s.subject.$type === 'chat.bsky.convo.defs#convoRef',
358
+ )
359
+ expect(convoStatuses.length).toBeGreaterThanOrEqual(2)
360
+ })
279
361
  })
280
362
 
281
363
  describe('reviewState changes', () => {