@atproto/ozone 0.1.14 → 0.1.16

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 (206) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/api/chat/getActorMetadata.d.ts +4 -0
  3. package/dist/api/chat/getActorMetadata.d.ts.map +1 -0
  4. package/dist/api/chat/getActorMetadata.js +20 -0
  5. package/dist/api/chat/getActorMetadata.js.map +1 -0
  6. package/dist/api/chat/getMessageContext.d.ts +4 -0
  7. package/dist/api/chat/getMessageContext.d.ts.map +1 -0
  8. package/dist/api/chat/getMessageContext.js +34 -0
  9. package/dist/api/chat/getMessageContext.js.map +1 -0
  10. package/dist/api/chat/index.d.ts +4 -0
  11. package/dist/api/chat/index.d.ts.map +1 -0
  12. package/dist/api/chat/index.js +14 -0
  13. package/dist/api/chat/index.js.map +1 -0
  14. package/dist/api/index.d.ts.map +1 -1
  15. package/dist/api/index.js +2 -0
  16. package/dist/api/index.js.map +1 -1
  17. package/dist/api/moderation/emitEvent.d.ts.map +1 -1
  18. package/dist/api/moderation/emitEvent.js +22 -0
  19. package/dist/api/moderation/emitEvent.js.map +1 -1
  20. package/dist/api/moderation/getRecord.d.ts.map +1 -1
  21. package/dist/api/moderation/getRecord.js +3 -2
  22. package/dist/api/moderation/getRecord.js.map +1 -1
  23. package/dist/api/moderation/getRepo.d.ts.map +1 -1
  24. package/dist/api/moderation/getRepo.js +3 -2
  25. package/dist/api/moderation/getRepo.js.map +1 -1
  26. package/dist/api/proxied.d.ts.map +1 -1
  27. package/dist/api/proxied.js +10 -0
  28. package/dist/api/proxied.js.map +1 -1
  29. package/dist/api/util.js +1 -0
  30. package/dist/api/util.js.map +1 -1
  31. package/dist/config/config.d.ts +5 -0
  32. package/dist/config/config.d.ts.map +1 -1
  33. package/dist/config/config.js +9 -0
  34. package/dist/config/config.js.map +1 -1
  35. package/dist/config/env.d.ts +2 -0
  36. package/dist/config/env.d.ts.map +1 -1
  37. package/dist/config/env.js +2 -0
  38. package/dist/config/env.js.map +1 -1
  39. package/dist/context.d.ts +10 -0
  40. package/dist/context.d.ts.map +1 -1
  41. package/dist/context.js +26 -0
  42. package/dist/context.js.map +1 -1
  43. package/dist/db/migrations/20240506T225055595Z-message-subject.d.ts +4 -0
  44. package/dist/db/migrations/20240506T225055595Z-message-subject.d.ts.map +1 -0
  45. package/dist/db/migrations/20240506T225055595Z-message-subject.js +24 -0
  46. package/dist/db/migrations/20240506T225055595Z-message-subject.js.map +1 -0
  47. package/dist/db/migrations/index.d.ts +1 -0
  48. package/dist/db/migrations/index.d.ts.map +1 -1
  49. package/dist/db/migrations/index.js +2 -1
  50. package/dist/db/migrations/index.js.map +1 -1
  51. package/dist/db/schema/moderation_event.d.ts +2 -1
  52. package/dist/db/schema/moderation_event.d.ts.map +1 -1
  53. package/dist/lexicon/index.d.ts +59 -0
  54. package/dist/lexicon/index.d.ts.map +1 -1
  55. package/dist/lexicon/index.js +164 -1
  56. package/dist/lexicon/index.js.map +1 -1
  57. package/dist/lexicon/lexicons.d.ts +898 -0
  58. package/dist/lexicon/lexicons.d.ts.map +1 -1
  59. package/dist/lexicon/lexicons.js +924 -0
  60. package/dist/lexicon/lexicons.js.map +1 -1
  61. package/dist/lexicon/types/chat/bsky/actor/declaration.d.ts +11 -0
  62. package/dist/lexicon/types/chat/bsky/actor/declaration.d.ts.map +1 -0
  63. package/dist/lexicon/types/chat/bsky/actor/declaration.js +17 -0
  64. package/dist/lexicon/types/chat/bsky/actor/declaration.js.map +1 -0
  65. package/dist/lexicon/types/chat/bsky/actor/defs.d.ts +21 -0
  66. package/dist/lexicon/types/chat/bsky/actor/defs.d.ts.map +1 -0
  67. package/dist/lexicon/types/chat/bsky/actor/defs.js +16 -0
  68. package/dist/lexicon/types/chat/bsky/actor/defs.js.map +1 -0
  69. package/dist/lexicon/types/chat/bsky/actor/deleteAccount.d.ts +33 -0
  70. package/dist/lexicon/types/chat/bsky/actor/deleteAccount.d.ts.map +1 -0
  71. package/dist/lexicon/types/chat/bsky/actor/deleteAccount.js +3 -0
  72. package/dist/lexicon/types/chat/bsky/actor/deleteAccount.js.map +1 -0
  73. package/dist/lexicon/types/chat/bsky/actor/exportAccountData.d.ts +32 -0
  74. package/dist/lexicon/types/chat/bsky/actor/exportAccountData.d.ts.map +1 -0
  75. package/dist/lexicon/types/chat/bsky/actor/exportAccountData.js +3 -0
  76. package/dist/lexicon/types/chat/bsky/actor/exportAccountData.js.map +1 -0
  77. package/dist/lexicon/types/chat/bsky/convo/defs.d.ts +109 -0
  78. package/dist/lexicon/types/chat/bsky/convo/defs.d.ts.map +1 -0
  79. package/dist/lexicon/types/chat/bsky/convo/defs.js +106 -0
  80. package/dist/lexicon/types/chat/bsky/convo/defs.js.map +1 -0
  81. package/dist/lexicon/types/chat/bsky/convo/deleteMessageForSelf.d.ts +39 -0
  82. package/dist/lexicon/types/chat/bsky/convo/deleteMessageForSelf.d.ts.map +1 -0
  83. package/dist/lexicon/types/chat/bsky/convo/deleteMessageForSelf.js +3 -0
  84. package/dist/lexicon/types/chat/bsky/convo/deleteMessageForSelf.js.map +1 -0
  85. package/dist/lexicon/types/chat/bsky/convo/getConvo.d.ts +36 -0
  86. package/dist/lexicon/types/chat/bsky/convo/getConvo.d.ts.map +1 -0
  87. package/dist/lexicon/types/chat/bsky/convo/getConvo.js +3 -0
  88. package/dist/lexicon/types/chat/bsky/convo/getConvo.js.map +1 -0
  89. package/dist/lexicon/types/chat/bsky/convo/getConvoForMembers.d.ts +36 -0
  90. package/dist/lexicon/types/chat/bsky/convo/getConvoForMembers.d.ts.map +1 -0
  91. package/dist/lexicon/types/chat/bsky/convo/getConvoForMembers.js +3 -0
  92. package/dist/lexicon/types/chat/bsky/convo/getConvoForMembers.js.map +1 -0
  93. package/dist/lexicon/types/chat/bsky/convo/getLog.d.ts +40 -0
  94. package/dist/lexicon/types/chat/bsky/convo/getLog.d.ts.map +1 -0
  95. package/dist/lexicon/types/chat/bsky/convo/getLog.js +3 -0
  96. package/dist/lexicon/types/chat/bsky/convo/getLog.js.map +1 -0
  97. package/dist/lexicon/types/chat/bsky/convo/getMessages.d.ts +42 -0
  98. package/dist/lexicon/types/chat/bsky/convo/getMessages.d.ts.map +1 -0
  99. package/dist/lexicon/types/chat/bsky/convo/getMessages.js +3 -0
  100. package/dist/lexicon/types/chat/bsky/convo/getMessages.js.map +1 -0
  101. package/dist/lexicon/types/chat/bsky/convo/leaveConvo.d.ts +41 -0
  102. package/dist/lexicon/types/chat/bsky/convo/leaveConvo.d.ts.map +1 -0
  103. package/dist/lexicon/types/chat/bsky/convo/leaveConvo.js +3 -0
  104. package/dist/lexicon/types/chat/bsky/convo/leaveConvo.js.map +1 -0
  105. package/dist/lexicon/types/chat/bsky/convo/listConvos.d.ts +38 -0
  106. package/dist/lexicon/types/chat/bsky/convo/listConvos.d.ts.map +1 -0
  107. package/dist/lexicon/types/chat/bsky/convo/listConvos.js +3 -0
  108. package/dist/lexicon/types/chat/bsky/convo/listConvos.js.map +1 -0
  109. package/dist/lexicon/types/chat/bsky/convo/muteConvo.d.ts +41 -0
  110. package/dist/lexicon/types/chat/bsky/convo/muteConvo.d.ts.map +1 -0
  111. package/dist/lexicon/types/chat/bsky/convo/muteConvo.js +3 -0
  112. package/dist/lexicon/types/chat/bsky/convo/muteConvo.js.map +1 -0
  113. package/dist/lexicon/types/chat/bsky/convo/sendMessage.d.ts +39 -0
  114. package/dist/lexicon/types/chat/bsky/convo/sendMessage.d.ts.map +1 -0
  115. package/dist/lexicon/types/chat/bsky/convo/sendMessage.js +3 -0
  116. package/dist/lexicon/types/chat/bsky/convo/sendMessage.js.map +1 -0
  117. package/dist/lexicon/types/chat/bsky/convo/sendMessageBatch.d.ts +49 -0
  118. package/dist/lexicon/types/chat/bsky/convo/sendMessageBatch.d.ts.map +1 -0
  119. package/dist/lexicon/types/chat/bsky/convo/sendMessageBatch.js +16 -0
  120. package/dist/lexicon/types/chat/bsky/convo/sendMessageBatch.js.map +1 -0
  121. package/dist/lexicon/types/chat/bsky/convo/unmuteConvo.d.ts +41 -0
  122. package/dist/lexicon/types/chat/bsky/convo/unmuteConvo.d.ts.map +1 -0
  123. package/dist/lexicon/types/chat/bsky/convo/unmuteConvo.js +3 -0
  124. package/dist/lexicon/types/chat/bsky/convo/unmuteConvo.js.map +1 -0
  125. package/dist/lexicon/types/chat/bsky/convo/updateRead.d.ts +42 -0
  126. package/dist/lexicon/types/chat/bsky/convo/updateRead.d.ts.map +1 -0
  127. package/dist/lexicon/types/chat/bsky/convo/updateRead.js +3 -0
  128. package/dist/lexicon/types/chat/bsky/convo/updateRead.js.map +1 -0
  129. package/dist/lexicon/types/chat/bsky/moderation/getActorMetadata.d.ts +47 -0
  130. package/dist/lexicon/types/chat/bsky/moderation/getActorMetadata.d.ts.map +1 -0
  131. package/dist/lexicon/types/chat/bsky/moderation/getActorMetadata.js +16 -0
  132. package/dist/lexicon/types/chat/bsky/moderation/getActorMetadata.js.map +1 -0
  133. package/dist/lexicon/types/chat/bsky/moderation/getMessageContext.d.ts +43 -0
  134. package/dist/lexicon/types/chat/bsky/moderation/getMessageContext.d.ts.map +1 -0
  135. package/dist/lexicon/types/chat/bsky/moderation/getMessageContext.js +3 -0
  136. package/dist/lexicon/types/chat/bsky/moderation/getMessageContext.js.map +1 -0
  137. package/dist/lexicon/types/chat/bsky/moderation/updateActorAccess.d.ts +31 -0
  138. package/dist/lexicon/types/chat/bsky/moderation/updateActorAccess.d.ts.map +1 -0
  139. package/dist/lexicon/types/chat/bsky/moderation/updateActorAccess.js +3 -0
  140. package/dist/lexicon/types/chat/bsky/moderation/updateActorAccess.js.map +1 -0
  141. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts +2 -1
  142. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts.map +1 -1
  143. package/dist/lexicon/types/tools/ozone/moderation/defs.js.map +1 -1
  144. package/dist/mod-service/index.d.ts +2 -1
  145. package/dist/mod-service/index.d.ts.map +1 -1
  146. package/dist/mod-service/index.js +10 -2
  147. package/dist/mod-service/index.js.map +1 -1
  148. package/dist/mod-service/subject.d.ts +35 -2
  149. package/dist/mod-service/subject.d.ts.map +1 -1
  150. package/dist/mod-service/subject.js +85 -1
  151. package/dist/mod-service/subject.js.map +1 -1
  152. package/dist/mod-service/views.d.ts +5 -2
  153. package/dist/mod-service/views.d.ts.map +1 -1
  154. package/dist/mod-service/views.js +17 -6
  155. package/dist/mod-service/views.js.map +1 -1
  156. package/dist/util.d.ts +8 -0
  157. package/dist/util.d.ts.map +1 -1
  158. package/dist/util.js +41 -1
  159. package/dist/util.js.map +1 -1
  160. package/package.json +5 -4
  161. package/src/api/chat/getActorMetadata.ts +22 -0
  162. package/src/api/chat/getMessageContext.ts +39 -0
  163. package/src/api/chat/index.ts +10 -0
  164. package/src/api/index.ts +2 -0
  165. package/src/api/moderation/emitEvent.ts +31 -0
  166. package/src/api/moderation/getRecord.ts +3 -2
  167. package/src/api/moderation/getRepo.ts +3 -2
  168. package/src/api/proxied.ts +14 -0
  169. package/src/api/util.ts +1 -0
  170. package/src/config/config.ts +16 -0
  171. package/src/config/env.ts +4 -0
  172. package/src/context.ts +36 -1
  173. package/src/db/migrations/20240506T225055595Z-message-subject.ts +21 -0
  174. package/src/db/migrations/index.ts +1 -0
  175. package/src/db/schema/moderation_event.ts +5 -1
  176. package/src/lexicon/index.ts +254 -0
  177. package/src/lexicon/lexicons.ts +926 -0
  178. package/src/lexicon/types/chat/bsky/actor/declaration.ts +25 -0
  179. package/src/lexicon/types/chat/bsky/actor/defs.ts +34 -0
  180. package/src/lexicon/types/chat/bsky/actor/deleteAccount.ts +42 -0
  181. package/src/lexicon/types/chat/bsky/actor/exportAccountData.ts +38 -0
  182. package/src/lexicon/types/chat/bsky/convo/defs.ts +215 -0
  183. package/src/lexicon/types/chat/bsky/convo/deleteMessageForSelf.ts +48 -0
  184. package/src/lexicon/types/chat/bsky/convo/getConvo.ts +46 -0
  185. package/src/lexicon/types/chat/bsky/convo/getConvoForMembers.ts +46 -0
  186. package/src/lexicon/types/chat/bsky/convo/getLog.ts +53 -0
  187. package/src/lexicon/types/chat/bsky/convo/getMessages.ts +53 -0
  188. package/src/lexicon/types/chat/bsky/convo/leaveConvo.ts +50 -0
  189. package/src/lexicon/types/chat/bsky/convo/listConvos.ts +48 -0
  190. package/src/lexicon/types/chat/bsky/convo/muteConvo.ts +50 -0
  191. package/src/lexicon/types/chat/bsky/convo/sendMessage.ts +48 -0
  192. package/src/lexicon/types/chat/bsky/convo/sendMessageBatch.ts +68 -0
  193. package/src/lexicon/types/chat/bsky/convo/unmuteConvo.ts +50 -0
  194. package/src/lexicon/types/chat/bsky/convo/updateRead.ts +51 -0
  195. package/src/lexicon/types/chat/bsky/moderation/getActorMetadata.ts +67 -0
  196. package/src/lexicon/types/chat/bsky/moderation/getMessageContext.ts +54 -0
  197. package/src/lexicon/types/chat/bsky/moderation/updateActorAccess.ts +40 -0
  198. package/src/lexicon/types/tools/ozone/moderation/defs.ts +2 -0
  199. package/src/mod-service/index.ts +10 -3
  200. package/src/mod-service/subject.ts +73 -2
  201. package/src/mod-service/views.ts +30 -5
  202. package/src/util.ts +50 -0
  203. package/tests/3p-labeler.test.ts +271 -0
  204. package/tests/__snapshots__/moderation.test.ts.snap +30 -0
  205. package/tests/get-lists.test.ts +109 -0
  206. package/tests/moderation.test.ts +39 -0
@@ -28,10 +28,12 @@ import { formatLabel, signLabel } from './util'
28
28
  import { LabelRow } from '../db/schema/label'
29
29
  import { dbLogger } from '../logger'
30
30
  import { httpLogger } from '../logger'
31
+ import { ParsedLabelers } from '../util'
31
32
 
32
33
  export type AuthHeaders = {
33
34
  headers: {
34
35
  authorization: string
36
+ 'atproto-accept-labelers'?: string
35
37
  }
36
38
  }
37
39
 
@@ -210,10 +212,14 @@ export class ModerationViews {
210
212
  }
211
213
  }
212
214
 
213
- async repoDetail(did: string): Promise<RepoViewDetail | undefined> {
214
- const [repos, labels] = await Promise.all([
215
+ async repoDetail(
216
+ did: string,
217
+ labelers?: ParsedLabelers,
218
+ ): Promise<RepoViewDetail | undefined> {
219
+ const [repos, localLabels, externalLabels] = await Promise.all([
215
220
  this.repos([did]),
216
221
  this.labels(did),
222
+ this.getExternalLabels([did], labelers),
217
223
  ])
218
224
  const repo = repos.get(did)
219
225
  if (!repo) return
@@ -223,7 +229,7 @@ export class ModerationViews {
223
229
  moderation: {
224
230
  ...repo.moderation,
225
231
  },
226
- labels,
232
+ labels: [...localLabels, ...externalLabels],
227
233
  }
228
234
  }
229
235
 
@@ -293,6 +299,7 @@ export class ModerationViews {
293
299
 
294
300
  async recordDetail(
295
301
  subject: RecordSubject,
302
+ labelers?: ParsedLabelers,
296
303
  ): Promise<RecordViewDetail | undefined> {
297
304
  const [records, subjectStatusesResult] = await Promise.all([
298
305
  this.records([subject]),
@@ -303,9 +310,10 @@ export class ModerationViews {
303
310
 
304
311
  const status = subjectStatusesResult.get(subject.uri)
305
312
 
306
- const [blobs, labels, subjectStatus] = await Promise.all([
313
+ const [blobs, labels, externalLabels, subjectStatus] = await Promise.all([
307
314
  this.blob(findBlobRefs(record.value)),
308
315
  this.labels(record.uri),
316
+ this.getExternalLabels([record.uri], labelers),
309
317
  status ? this.formatSubjectStatus(status) : Promise.resolve(undefined),
310
318
  ])
311
319
  const selfLabels = getSelfLabels({
@@ -313,6 +321,7 @@ export class ModerationViews {
313
321
  cid: record.cid,
314
322
  record: record.value,
315
323
  })
324
+
316
325
  return {
317
326
  ...record,
318
327
  blobs,
@@ -320,10 +329,26 @@ export class ModerationViews {
320
329
  ...record.moderation,
321
330
  subjectStatus,
322
331
  },
323
- labels: [...labels, ...selfLabels],
332
+ labels: [...labels, ...selfLabels, ...externalLabels],
324
333
  }
325
334
  }
326
335
 
336
+ async getExternalLabels(
337
+ subjects: string[],
338
+ labelers?: ParsedLabelers,
339
+ ): Promise<Label[]> {
340
+ if (!labelers?.dids.length && !labelers?.redact.size) return []
341
+
342
+ const {
343
+ data: { labels },
344
+ } = await this.appviewAgent.api.com.atproto.label.queryLabels({
345
+ uriPatterns: subjects,
346
+ sources: labelers.dids,
347
+ })
348
+
349
+ return labels
350
+ }
351
+
327
352
  formatReport(report: ModerationEventRowWithHandle): ReportOutput {
328
353
  return {
329
354
  id: report.id,
package/src/util.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { AxiosError } from 'axios'
2
+ import { parseList } from 'structured-headers'
2
3
  import { XRPCError, ResponseType } from '@atproto/xrpc'
3
4
  import { RetryOptions, retry } from '@atproto/common'
4
5
  import Database from './db'
@@ -45,3 +46,52 @@ export function retryableHttp(err: unknown) {
45
46
  const retryableHttpStatusCodes = new Set([
46
47
  408, 425, 429, 500, 502, 503, 504, 522, 524,
47
48
  ])
49
+
50
+ export type ParsedLabelers = {
51
+ dids: string[]
52
+ redact: Set<string>
53
+ }
54
+
55
+ export const LABELER_HEADER_NAME = 'atproto-accept-labelers'
56
+
57
+ export const parseLabelerHeader = (
58
+ header: string | undefined,
59
+ ignoreDid?: string,
60
+ ): ParsedLabelers | null => {
61
+ if (!header) return null
62
+ const labelerDids = new Set<string>()
63
+ const redactDids = new Set<string>()
64
+ const parsed = parseList(header)
65
+ for (const item of parsed) {
66
+ const did = item[0].toString()
67
+ if (!did) {
68
+ return null
69
+ }
70
+ if (did === ignoreDid) {
71
+ continue
72
+ }
73
+ labelerDids.add(did)
74
+ const redact = item[1].get('redact')?.valueOf()
75
+ if (redact === true) {
76
+ redactDids.add(did)
77
+ }
78
+ }
79
+ return {
80
+ dids: [...labelerDids],
81
+ redact: redactDids,
82
+ }
83
+ }
84
+
85
+ export const defaultLabelerHeader = (dids: string[]): ParsedLabelers => {
86
+ return {
87
+ dids,
88
+ redact: new Set(dids),
89
+ }
90
+ }
91
+
92
+ export const formatLabelerHeader = (parsed: ParsedLabelers): string => {
93
+ const parts = parsed.dids.map((did) =>
94
+ parsed.redact.has(did) ? `${did};redact` : did,
95
+ )
96
+ return parts.join(',')
97
+ }
@@ -0,0 +1,271 @@
1
+ import {
2
+ SeedClient,
3
+ TestNetwork,
4
+ basicSeed,
5
+ TestOzone,
6
+ ModeratorClient,
7
+ createOzoneDid,
8
+ } from '@atproto/dev-env'
9
+ import AtpAgent from '@atproto/api'
10
+ import { Secp256k1Keypair } from '@atproto/crypto'
11
+ import { LABELER_HEADER_NAME } from '../src/util'
12
+
13
+ describe('labels from 3p labelers', () => {
14
+ let network: TestNetwork
15
+ let ozone: TestOzone
16
+ let thirdPartyLabeler: TestOzone
17
+ let agent: AtpAgent
18
+ let thirdPartyAgent: AtpAgent
19
+ let sc: SeedClient
20
+ let modClient: ModeratorClient
21
+ let thirdPartyModClient: ModeratorClient
22
+
23
+ beforeAll(async () => {
24
+ network = await TestNetwork.create({
25
+ dbPostgresSchema: 'ozone_admin_third_party_labeler',
26
+ })
27
+ ozone = network.ozone
28
+
29
+ const ozoneKey = await Secp256k1Keypair.create({ exportable: true })
30
+ const ozoneDid = await createOzoneDid(network.plc.url, ozoneKey)
31
+ thirdPartyLabeler = await TestOzone.create({
32
+ port: ozone.port + 10,
33
+ plcUrl: network.plc.url,
34
+ signingKey: ozoneKey,
35
+ serverDid: ozoneDid,
36
+ dbPostgresSchema: `ozone_admin_third_party_labeler`,
37
+ dbPostgresUrl: ozone.ctx.cfg.db.postgresUrl,
38
+ appviewUrl: network.bsky.url,
39
+ appviewDid: network.bsky.ctx.cfg.serverDid,
40
+ appviewPushEvents: true,
41
+ pdsUrl: network.pds.url,
42
+ pdsDid: network.pds.ctx.cfg.service.did,
43
+ })
44
+
45
+ thirdPartyAgent = thirdPartyLabeler.getClient()
46
+ agent = ozone.getClient()
47
+ sc = network.getSeedClient()
48
+ modClient = ozone.getModClient()
49
+ thirdPartyModClient = thirdPartyLabeler.getModClient()
50
+ await basicSeed(sc)
51
+ await network.processAll()
52
+ })
53
+
54
+ afterAll(async () => {
55
+ await network.close()
56
+ await thirdPartyLabeler.close()
57
+ })
58
+
59
+ const getPostSubject = () => ({
60
+ $type: 'com.atproto.repo.strongRef',
61
+ uri: sc.posts[sc.dids.alice][0].ref.uriStr,
62
+ cid: sc.posts[sc.dids.alice][0].ref.cidStr,
63
+ })
64
+
65
+ const adjustLabels = async ({
66
+ uri,
67
+ cid,
68
+ src,
69
+ createLabelVals = [],
70
+ negateLabelVals = [],
71
+ }: {
72
+ uri: string
73
+ src: string
74
+ cid?: string
75
+ createLabelVals?: string[]
76
+ negateLabelVals?: string[]
77
+ }) => {
78
+ const labelEntries = createLabelVals.map((val) => ({
79
+ uri,
80
+ cid: cid || '',
81
+ val,
82
+ cts: new Date().toISOString(),
83
+ neg: false,
84
+ src,
85
+ }))
86
+
87
+ negateLabelVals.forEach((val) => {
88
+ labelEntries.push({
89
+ uri,
90
+ cid: cid || '',
91
+ val,
92
+ cts: new Date().toISOString(),
93
+ neg: true,
94
+ src,
95
+ })
96
+ })
97
+ await network.bsky.db.db.insertInto('label').values(labelEntries).execute()
98
+ }
99
+
100
+ const labelAccount = async (
101
+ client: ModeratorClient,
102
+ {
103
+ createLabelVals = [],
104
+ negateLabelVals = [],
105
+ }: { createLabelVals?: string[]; negateLabelVals?: string[] },
106
+ ) => {
107
+ const subject = {
108
+ $type: 'com.atproto.admin.defs#repoRef',
109
+ did: sc.dids.alice,
110
+ }
111
+ await client.emitEvent(
112
+ {
113
+ subject,
114
+ event: {
115
+ $type: 'tools.ozone.moderation.defs#modEventLabel',
116
+ createLabelVals,
117
+ negateLabelVals,
118
+ },
119
+ createdBy: sc.dids.carol,
120
+ },
121
+ 'moderator',
122
+ )
123
+ await adjustLabels({
124
+ createLabelVals,
125
+ negateLabelVals,
126
+ uri: sc.dids.alice,
127
+ src: client.ozone.ctx.cfg.service.did,
128
+ })
129
+ }
130
+
131
+ const labelPost = async (
132
+ client: ModeratorClient,
133
+ {
134
+ createLabelVals = [],
135
+ negateLabelVals = [],
136
+ }: { createLabelVals?: string[]; negateLabelVals?: string[] },
137
+ ) => {
138
+ const subject = getPostSubject()
139
+ await client.emitEvent(
140
+ {
141
+ subject,
142
+ event: {
143
+ $type: 'tools.ozone.moderation.defs#modEventLabel',
144
+ createLabelVals,
145
+ negateLabelVals,
146
+ },
147
+ createdBy: sc.dids.carol,
148
+ },
149
+ 'moderator',
150
+ )
151
+ await adjustLabels({
152
+ createLabelVals,
153
+ negateLabelVals,
154
+ uri: subject.uri,
155
+ cid: subject.cid,
156
+ src: client.ozone.ctx.cfg.service.did,
157
+ })
158
+ }
159
+
160
+ describe('record labels', () => {
161
+ it('includes only labels from current authority by default', async () => {
162
+ await Promise.all([
163
+ labelPost(modClient, { createLabelVals: ['spam'] }),
164
+ labelPost(thirdPartyModClient, { createLabelVals: ['weird'] }),
165
+ ])
166
+
167
+ const [
168
+ { data: recordFromCurrentAuthority },
169
+ { data: recordFromThirdParty },
170
+ ] = await Promise.all([
171
+ agent.api.tools.ozone.moderation.getRecord(
172
+ { uri: sc.posts[sc.dids.alice][0].ref.uriStr },
173
+ { headers: await ozone.modHeaders() },
174
+ ),
175
+ thirdPartyAgent.api.tools.ozone.moderation.getRecord(
176
+ { uri: sc.posts[sc.dids.alice][0].ref.uriStr },
177
+ { headers: await thirdPartyLabeler.modHeaders() },
178
+ ),
179
+ ])
180
+
181
+ const currentAuthorityLabels = recordFromCurrentAuthority.labels?.map(
182
+ (l) => l.val,
183
+ )
184
+ const thirdPartyLabels = recordFromThirdParty.labels?.map((l) => l.val)
185
+ expect(currentAuthorityLabels).toContain('spam')
186
+ expect(currentAuthorityLabels).not.toContain('weird')
187
+ expect(thirdPartyLabels).toContain('weird')
188
+ expect(thirdPartyLabels).not.toContain('spam')
189
+ })
190
+
191
+ it('includes labels from all authorities requested via header', async () => {
192
+ const authHeaders = await ozone.modHeaders()
193
+ const { data: recordIncludingExternalLabels } =
194
+ await agent.api.tools.ozone.moderation.getRecord(
195
+ { uri: sc.posts[sc.dids.alice][0].ref.uriStr },
196
+ {
197
+ headers: {
198
+ ...authHeaders,
199
+ [LABELER_HEADER_NAME]: [
200
+ thirdPartyLabeler.ctx.cfg.service.did,
201
+ ozone.ctx.cfg.service.did,
202
+ ].join(','),
203
+ },
204
+ },
205
+ )
206
+ const labelVals = recordIncludingExternalLabels.labels?.map((l) => l.val)
207
+ const labelSources = recordIncludingExternalLabels.labels?.map(
208
+ (l) => l.src,
209
+ )
210
+ expect(labelVals).toContain('weird')
211
+ expect(labelVals).toContain('spam')
212
+ expect(labelSources).toContain(thirdPartyLabeler.ctx.cfg.service.did)
213
+ expect(labelSources).toContain(ozone.ctx.cfg.service.did)
214
+ })
215
+ })
216
+
217
+ describe('repo labels', () => {
218
+ it('includes only labels from current authority by default', async () => {
219
+ await Promise.all([
220
+ labelAccount(modClient, { createLabelVals: ['spam'] }),
221
+ labelAccount(thirdPartyModClient, { createLabelVals: ['weird'] }),
222
+ ])
223
+
224
+ const [{ data: repoFromCurrentAuthority }, { data: repoFromThirdParty }] =
225
+ await Promise.all([
226
+ agent.api.tools.ozone.moderation.getRepo(
227
+ { did: sc.dids.alice },
228
+ { headers: await ozone.modHeaders() },
229
+ ),
230
+ thirdPartyAgent.api.tools.ozone.moderation.getRepo(
231
+ { did: sc.dids.alice },
232
+ { headers: await thirdPartyLabeler.modHeaders() },
233
+ ),
234
+ ])
235
+
236
+ const currentAuthorityLabels = repoFromCurrentAuthority.labels?.map(
237
+ (l) => l.val,
238
+ )
239
+ const thirdPartyLabels = repoFromThirdParty.labels?.map((l) => l.val)
240
+ expect(currentAuthorityLabels).toContain('spam')
241
+ expect(currentAuthorityLabels).not.toContain('weird')
242
+ expect(thirdPartyLabels).toContain('weird')
243
+ expect(thirdPartyLabels).not.toContain('spam')
244
+ })
245
+
246
+ it('includes labels from all authorities requested via header', async () => {
247
+ const authHeaders = await ozone.modHeaders()
248
+ const { data: recordIncludingExternalLabels } =
249
+ await agent.api.tools.ozone.moderation.getRepo(
250
+ { did: sc.dids.alice },
251
+ {
252
+ headers: {
253
+ ...authHeaders,
254
+ [LABELER_HEADER_NAME]: [
255
+ thirdPartyLabeler.ctx.cfg.service.did,
256
+ ozone.ctx.cfg.service.did,
257
+ ].join(','),
258
+ },
259
+ },
260
+ )
261
+ const labelVals = recordIncludingExternalLabels.labels?.map((l) => l.val)
262
+ const labelSources = recordIncludingExternalLabels.labels?.map(
263
+ (l) => l.src,
264
+ )
265
+ expect(labelVals).toContain('weird')
266
+ expect(labelVals).toContain('spam')
267
+ expect(labelSources).toContain(thirdPartyLabeler.ctx.cfg.service.did)
268
+ expect(labelSources).toContain(ozone.ctx.cfg.service.did)
269
+ })
270
+ })
271
+ })
@@ -1,5 +1,35 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
+ exports[`moderation reporting creates reports of a DM chat. 1`] = `
4
+ Array [
5
+ Object {
6
+ "createdAt": "1970-01-01T00:00:00.000Z",
7
+ "id": 12,
8
+ "reasonType": "com.atproto.moderation.defs#reasonSpam",
9
+ "reportedBy": "user(0)",
10
+ "subject": Object {
11
+ "$type": "chat.bsky.convo.defs#messageRef",
12
+ "convoId": "testconvoid1",
13
+ "did": "user(1)",
14
+ "messageId": "testmessageid1",
15
+ },
16
+ },
17
+ Object {
18
+ "createdAt": "1970-01-01T00:00:00.000Z",
19
+ "id": 14,
20
+ "reason": "defamation",
21
+ "reasonType": "com.atproto.moderation.defs#reasonOther",
22
+ "reportedBy": "user(1)",
23
+ "subject": Object {
24
+ "$type": "chat.bsky.convo.defs#messageRef",
25
+ "convoId": "testconvoid2",
26
+ "did": "user(1)",
27
+ "messageId": "testmessageid2",
28
+ },
29
+ },
30
+ ]
31
+ `;
32
+
3
33
  exports[`moderation reporting creates reports of a record. 1`] = `
4
34
  Array [
5
35
  Object {
@@ -0,0 +1,109 @@
1
+ import {
2
+ SeedClient,
3
+ TestNetwork,
4
+ TestOzone,
5
+ basicSeed,
6
+ ModeratorClient,
7
+ RecordRef,
8
+ } from '@atproto/dev-env'
9
+ import AtpAgent, { BSKY_LABELER_DID } from '@atproto/api'
10
+ import { TAKEDOWN_LABEL } from '../src/mod-service'
11
+
12
+ describe('admin get lists', () => {
13
+ let network: TestNetwork
14
+ let ozone: TestOzone
15
+ let agent: AtpAgent
16
+ let appviewAgent: AtpAgent
17
+ let sc: SeedClient
18
+ let modClient: ModeratorClient
19
+ let alicesList: RecordRef
20
+
21
+ beforeAll(async () => {
22
+ network = await TestNetwork.create({
23
+ dbPostgresSchema: 'ozone_admin_get_lists',
24
+ })
25
+ ozone = network.ozone
26
+ agent = ozone.getClient()
27
+ appviewAgent = network.bsky.getClient()
28
+ sc = network.getSeedClient()
29
+ modClient = ozone.getModClient()
30
+ await basicSeed(sc)
31
+ alicesList = await sc.createList(sc.dids.alice, "Alice's List", 'mod')
32
+ AtpAgent.configure({ appLabelers: [ozone.ctx.cfg.service.did] })
33
+ await network.processAll()
34
+ })
35
+
36
+ afterAll(async () => {
37
+ AtpAgent.configure({ appLabelers: [BSKY_LABELER_DID] })
38
+ await network.close()
39
+ })
40
+
41
+ const getAlicesList = async () => {
42
+ const [{ data: fromOzone }, { data: fromAppview }] = await Promise.all([
43
+ agent.api.app.bsky.graph.getLists(
44
+ { actor: sc.dids.alice },
45
+ { headers: await ozone.modHeaders() },
46
+ ),
47
+ appviewAgent.api.app.bsky.graph.getLists({ actor: sc.dids.alice }),
48
+ ])
49
+
50
+ return { fromOzone, fromAppview }
51
+ }
52
+
53
+ it('returns lists from takendown account', async () => {
54
+ const beforeTakedown = await getAlicesList()
55
+ expect(beforeTakedown.fromOzone.lists[0].uri).toEqual(alicesList.uriStr)
56
+ expect(beforeTakedown.fromAppview.lists[0].uri).toEqual(alicesList.uriStr)
57
+
58
+ // Takedown alice's account
59
+ await modClient.emitEvent({
60
+ event: { $type: 'tools.ozone.moderation.defs#modEventTakedown' },
61
+ subject: {
62
+ $type: 'com.atproto.admin.defs#repoRef',
63
+ did: sc.dids.alice,
64
+ },
65
+ })
66
+ await network.processAll()
67
+
68
+ const afterTakedown = await getAlicesList()
69
+
70
+ // Verify that takendown list is shown when queried through ozone but not through appview
71
+ expect(afterTakedown.fromAppview.lists.length).toBe(0)
72
+ expect(afterTakedown.fromOzone.lists[0].uri).toEqual(alicesList.uriStr)
73
+
74
+ // Reverse alice's account takedown
75
+ await modClient.emitEvent({
76
+ event: { $type: 'tools.ozone.moderation.defs#modEventReverseTakedown' },
77
+ subject: {
78
+ $type: 'com.atproto.admin.defs#repoRef',
79
+ did: sc.dids.alice,
80
+ },
81
+ })
82
+ await network.processAll()
83
+ })
84
+
85
+ it('returns takendown lists', async () => {
86
+ const beforeTakedown = await getAlicesList()
87
+ expect(beforeTakedown.fromOzone.lists[0].uri).toEqual(alicesList.uriStr)
88
+ expect(beforeTakedown.fromAppview.lists[0].uri).toEqual(alicesList.uriStr)
89
+
90
+ // Takedown alice's list using a !takedown label
91
+ await network.bsky.db.db
92
+ .insertInto('label')
93
+ .values({
94
+ src: ozone.ctx.cfg.service.did,
95
+ uri: alicesList.uriStr,
96
+ cid: alicesList.cidStr,
97
+ val: TAKEDOWN_LABEL,
98
+ neg: false,
99
+ cts: new Date().toISOString(),
100
+ })
101
+ .execute()
102
+
103
+ const afterTakedown = await getAlicesList()
104
+
105
+ // Verify that takendown list is shown when queried through ozone but not through appview
106
+ expect(afterTakedown.fromAppview.lists.length).toBe(0)
107
+ expect(afterTakedown.fromOzone.lists[0].uri).toEqual(alicesList.uriStr)
108
+ })
109
+ })
@@ -149,6 +149,45 @@ describe('moderation', () => {
149
149
  })
150
150
  await expect(promiseB).resolves.toBeDefined()
151
151
  })
152
+
153
+ it('creates reports of a DM chat.', async () => {
154
+ const messageId1 = 'testmessageid1'
155
+ const messageId2 = 'testmessageid2'
156
+ const reportA = await sc.createReport({
157
+ reportedBy: sc.dids.alice,
158
+ reasonType: REASONSPAM,
159
+ subject: {
160
+ $type: 'chat.bsky.convo.defs#messageRef',
161
+ did: sc.dids.carol,
162
+ messageId: messageId1,
163
+ convoId: 'testconvoid1',
164
+ },
165
+ })
166
+ const reportB = await sc.createReport({
167
+ reportedBy: sc.dids.carol,
168
+ reasonType: REASONOTHER,
169
+ reason: 'defamation',
170
+ subject: {
171
+ $type: 'chat.bsky.convo.defs#messageRef',
172
+ did: sc.dids.carol,
173
+ messageId: messageId2,
174
+ convoId: 'testconvoid2',
175
+ },
176
+ })
177
+ expect(forSnapshot([reportA, reportB])).toMatchSnapshot()
178
+ const events = await ozone.ctx.db.db
179
+ .selectFrom('moderation_event')
180
+ .selectAll()
181
+ .where('subjectMessageId', 'in', [messageId1, messageId2])
182
+ .where('action', '=', 'tools.ozone.moderation.defs#modEventReport')
183
+ .execute()
184
+ expect(events.length).toBe(2)
185
+ expect(
186
+ events.every(
187
+ (row) => row.subjectType === 'chat.bsky.convo.defs#messageRef',
188
+ ),
189
+ ).toBe(true)
190
+ })
152
191
  })
153
192
 
154
193
  describe('actioning', () => {