@atproto/ozone 0.1.57 → 0.1.58

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 (30) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/api/moderation/emitEvent.d.ts.map +1 -1
  3. package/dist/api/moderation/emitEvent.js +4 -2
  4. package/dist/api/moderation/emitEvent.js.map +1 -1
  5. package/dist/lexicon/lexicons.d.ts +22234 -9820
  6. package/dist/lexicon/lexicons.d.ts.map +1 -1
  7. package/dist/lexicon/lexicons.js +5 -1
  8. package/dist/lexicon/lexicons.js.map +1 -1
  9. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts +2 -0
  10. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts.map +1 -1
  11. package/dist/lexicon/types/tools/ozone/moderation/defs.js.map +1 -1
  12. package/dist/lexicon/types/tools/ozone/moderation/queryEvents.d.ts +1 -1
  13. package/dist/lexicon/types/tools/ozone/moderation/queryEvents.d.ts.map +1 -1
  14. package/dist/mod-service/index.d.ts +14 -14
  15. package/dist/mod-service/index.d.ts.map +1 -1
  16. package/dist/mod-service/index.js +22 -5
  17. package/dist/mod-service/index.js.map +1 -1
  18. package/dist/mod-service/views.d.ts.map +1 -1
  19. package/dist/mod-service/views.js +3 -2
  20. package/dist/mod-service/views.js.map +1 -1
  21. package/package.json +3 -3
  22. package/src/api/moderation/emitEvent.ts +11 -2
  23. package/src/lexicon/lexicons.ts +9 -3
  24. package/src/lexicon/types/tools/ozone/moderation/defs.ts +2 -0
  25. package/src/lexicon/types/tools/ozone/moderation/queryEvents.ts +1 -1
  26. package/src/mod-service/index.ts +30 -7
  27. package/src/mod-service/views.ts +3 -2
  28. package/tests/__snapshots__/moderation-events.test.ts.snap +123 -0
  29. package/tests/ack-all-subjects-of-account.test.ts +76 -30
  30. package/tests/moderation-events.test.ts +40 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/ozone",
3
- "version": "0.1.57",
3
+ "version": "0.1.58",
4
4
  "license": "MIT",
5
5
  "description": "Backend service for moderating the Bluesky network.",
6
6
  "keywords": [
@@ -32,7 +32,7 @@
32
32
  "structured-headers": "^1.0.1",
33
33
  "typed-emitter": "^2.1.0",
34
34
  "uint8arrays": "3.0.0",
35
- "@atproto/api": "^0.13.18",
35
+ "@atproto/api": "^0.13.19",
36
36
  "@atproto/common": "^0.4.4",
37
37
  "@atproto/crypto": "^0.4.2",
38
38
  "@atproto/identity": "^0.4.3",
@@ -53,7 +53,7 @@
53
53
  "ts-node": "^10.8.2",
54
54
  "typescript": "^5.6.3",
55
55
  "@atproto/lex-cli": "^0.5.2",
56
- "@atproto/pds": "^0.4.73"
56
+ "@atproto/pds": "^0.4.75"
57
57
  },
58
58
  "scripts": {
59
59
  "codegen": "lex gen-server --yes ./src/lexicon ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/* ../../lexicons/chat/bsky/*/* ../../lexicons/tools/ozone/*/*",
@@ -2,6 +2,7 @@ import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'
2
2
  import { Server } from '../../lexicon'
3
3
  import AppContext from '../../context'
4
4
  import {
5
+ isModEventAcknowledge,
5
6
  isModEventDivert,
6
7
  isModEventEmail,
7
8
  isModEventLabel,
@@ -39,6 +40,7 @@ const handleModerationEvent = async ({
39
40
  const moderationService = ctx.modService(db)
40
41
  const settingService = ctx.settingService(db)
41
42
  const { event } = input.body
43
+ const isAcknowledgeEvent = isModEventAcknowledge(event)
42
44
  const isTakedownEvent = isModEventTakedown(event)
43
45
  const isReverseTakedownEvent = isModEventReverseTakedown(event)
44
46
  const isLabelEvent = isModEventLabel(event)
@@ -219,8 +221,15 @@ const handleModerationEvent = async ({
219
221
  }
220
222
  }
221
223
 
222
- if (isTakedownEvent && result.event.meta?.acknowledgeAccountSubjects) {
223
- await moderationTxn.resolveSubjectsForAccount(subject.did, createdBy)
224
+ if (
225
+ (isTakedownEvent || isAcknowledgeEvent) &&
226
+ result.event.meta?.acknowledgeAccountSubjects
227
+ ) {
228
+ await moderationTxn.resolveSubjectsForAccount(
229
+ subject.did,
230
+ createdBy,
231
+ result.event,
232
+ )
224
233
  }
225
234
 
226
235
  if (isLabelEvent) {
@@ -11310,6 +11310,11 @@ export const schemaDict = {
11310
11310
  comment: {
11311
11311
  type: 'string',
11312
11312
  },
11313
+ acknowledgeAccountSubjects: {
11314
+ type: 'boolean',
11315
+ description:
11316
+ 'If true, all other reports on content authored by this account will be resolved (acknowledged).',
11317
+ },
11313
11318
  },
11314
11319
  },
11315
11320
  modEventEscalate: {
@@ -12218,7 +12223,7 @@ export const schemaDict = {
12218
12223
  comment: {
12219
12224
  type: 'string',
12220
12225
  description:
12221
- 'If specified, only events with comments containing the keyword are returned',
12226
+ 'If specified, only events with comments containing the keyword are returned. Apply || separator to use multiple keywords and match using OR condition.',
12222
12227
  },
12223
12228
  addedLabels: {
12224
12229
  type: 'array',
@@ -13489,8 +13494,9 @@ export const schemaDict = {
13489
13494
  },
13490
13495
  },
13491
13496
  },
13492
- }
13493
- export const schemas: LexiconDoc[] = Object.values(schemaDict) as LexiconDoc[]
13497
+ } as const satisfies Record<string, LexiconDoc>
13498
+
13499
+ export const schemas = Object.values(schemaDict)
13494
13500
  export const lexicons: Lexicons = new Lexicons(schemas)
13495
13501
  export const ids = {
13496
13502
  ComAtprotoAdminDefs: 'com.atproto.admin.defs',
@@ -300,6 +300,8 @@ export function validateModEventLabel(v: unknown): ValidationResult {
300
300
 
301
301
  export interface ModEventAcknowledge {
302
302
  comment?: string
303
+ /** If true, all other reports on content authored by this account will be resolved (acknowledged). */
304
+ acknowledgeAccountSubjects?: boolean
303
305
  [k: string]: unknown
304
306
  }
305
307
 
@@ -29,7 +29,7 @@ export interface QueryParams {
29
29
  limit: number
30
30
  /** If true, only events with comments are returned */
31
31
  hasComment?: boolean
32
- /** If specified, only events with comments containing the keyword are returned */
32
+ /** If specified, only events with comments containing the keyword are returned. Apply || separator to use multiple keywords and match using OR condition. */
33
33
  comment?: string
34
34
  /** If specified, only events where all of these labels were added are returned */
35
35
  addedLabels?: string[]
@@ -23,6 +23,7 @@ import {
23
23
  isRecordEvent,
24
24
  REVIEWESCALATED,
25
25
  REVIEWOPEN,
26
+ isModEventAcknowledge,
26
27
  } from '../lexicon/types/tools/ozone/moderation/defs'
27
28
  import { RepoRef, RepoBlobRef } from '../lexicon/types/com/atproto/admin/defs'
28
29
  import {
@@ -222,8 +223,20 @@ export class ModerationService {
222
223
  if (createdBefore) {
223
224
  builder = builder.where('createdAt', '<=', createdBefore)
224
225
  }
226
+
225
227
  if (comment) {
226
- builder = builder.where('comment', 'ilike', `%${comment}%`)
228
+ // the input may end in || in which case, there may be item in the array which is just '' and we want to ignore those
229
+ const keywords = comment.split('||').filter((keyword) => !!keyword.trim())
230
+ if (keywords.length > 1) {
231
+ builder = builder.where((qb) => {
232
+ keywords.forEach((keyword) => {
233
+ qb = qb.orWhere('comment', 'ilike', `%${keyword}%`)
234
+ })
235
+ return qb
236
+ })
237
+ } else if (keywords.length === 1) {
238
+ builder = builder.where('comment', 'ilike', `%${keywords[0]}%`)
239
+ }
227
240
  }
228
241
  if (hasComment) {
229
242
  builder = builder.where('comment', 'is not', null)
@@ -302,7 +315,11 @@ export class ModerationService {
302
315
  return await builder.execute()
303
316
  }
304
317
 
305
- async resolveSubjectsForAccount(did: string, createdBy: string) {
318
+ async resolveSubjectsForAccount(
319
+ did: string,
320
+ createdBy: string,
321
+ accountEvent: ModerationEventRow,
322
+ ) {
306
323
  const subjectsToBeResolved = await this.db.db
307
324
  .selectFrom('moderation_subject_status')
308
325
  .where('did', '=', did)
@@ -315,6 +332,10 @@ export class ModerationService {
315
332
  return
316
333
  }
317
334
 
335
+ let accountEventInfo = `Account Event ID: ${accountEvent.id}`
336
+ if (accountEvent.comment) {
337
+ accountEventInfo += ` | Account Event Comment: ${accountEvent.comment}`
338
+ }
318
339
  // Process subjects in chunks of 100 since each of these will trigger multiple db queries
319
340
  for (const subjects of chunkArray(subjectsToBeResolved, 100)) {
320
341
  await Promise.all(
@@ -323,13 +344,13 @@ export class ModerationService {
323
344
  createdBy,
324
345
  subject: subjectFromStatusRow(subject),
325
346
  }
347
+
326
348
  // For consistency's sake, when acknowledging appealed subjects, we should first resolve the appeal
327
349
  if (subject.appealed) {
328
350
  await this.logEvent({
329
351
  event: {
330
352
  $type: 'tools.ozone.moderation.defs#modEventResolveAppeal',
331
- comment:
332
- '[AUTO_RESOLVE_FOR_TAKENDOWN_ACCOUNT]: Automatically resolving all appealed content for a takendown account',
353
+ comment: `[AUTO_RESOLVE_ON_ACCOUNT_ACTION]: Automatically resolving all appealed content due to account level action | ${accountEventInfo}`,
333
354
  },
334
355
  ...eventData,
335
356
  })
@@ -338,8 +359,7 @@ export class ModerationService {
338
359
  await this.logEvent({
339
360
  event: {
340
361
  $type: 'tools.ozone.moderation.defs#modEventAcknowledge',
341
- comment:
342
- '[AUTO_RESOLVE_FOR_TAKENDOWN_ACCOUNT]: Automatically resolving all reported content for a takendown account',
362
+ comment: `[AUTO_RESOLVE_ON_ACCOUNT_ACTION]: Automatically resolving all reported content due to account level action | ${accountEventInfo}`,
343
363
  },
344
364
  ...eventData,
345
365
  })
@@ -408,7 +428,10 @@ export class ModerationService {
408
428
  if (event.cid) meta.cid = event.cid
409
429
  }
410
430
 
411
- if (isModEventTakedown(event) && event.acknowledgeAccountSubjects) {
431
+ if (
432
+ (isModEventTakedown(event) || isModEventAcknowledge(event)) &&
433
+ event.acknowledgeAccountSubjects
434
+ ) {
412
435
  meta.acknowledgeAccountSubjects = true
413
436
  }
414
437
 
@@ -127,12 +127,13 @@ export class ModerationViews {
127
127
  }
128
128
 
129
129
  if (
130
- event.action === 'tools.ozone.moderation.defs#modEventTakedown' &&
130
+ (event.action === 'tools.ozone.moderation.defs#modEventTakedown' ||
131
+ event.action === 'tools.ozone.moderation.defs#modEventAcknowledge') &&
131
132
  event.meta?.acknowledgeAccountSubjects
132
133
  ) {
133
134
  eventView.event = {
134
135
  ...eventView.event,
135
- acknowledgeAccountSubjects: event.meta?.acknowledgeAccountSubjects,
136
+ acknowledgeAccountSubjects: event.meta.acknowledgeAccountSubjects,
136
137
  }
137
138
  }
138
139
 
@@ -197,3 +197,126 @@ Array [
197
197
  },
198
198
  ]
199
199
  `;
200
+
201
+ exports[`moderation-events query events returns events matching multiple keywords in comment 1`] = `
202
+ Array [
203
+ Object {
204
+ "createdAt": "1970-01-01T00:00:00.000Z",
205
+ "createdBy": "user(1)",
206
+ "creatorHandle": "bob.test",
207
+ "event": Object {
208
+ "$type": "tools.ozone.moderation.defs#modEventReport",
209
+ "comment": "rainy days feel lazy",
210
+ "isReporterMuted": false,
211
+ "reportType": "com.atproto.moderation.defs#reasonSpam",
212
+ },
213
+ "id": 16,
214
+ "subject": Object {
215
+ "$type": "com.atproto.admin.defs#repoRef",
216
+ "did": "user(0)",
217
+ },
218
+ "subjectBlobCids": Array [],
219
+ "subjectHandle": "alice.test",
220
+ },
221
+ Object {
222
+ "createdAt": "1970-01-01T00:00:00.000Z",
223
+ "createdBy": "user(1)",
224
+ "creatorHandle": "bob.test",
225
+ "event": Object {
226
+ "$type": "tools.ozone.moderation.defs#modEventReport",
227
+ "comment": "november rain",
228
+ "isReporterMuted": false,
229
+ "reportType": "com.atproto.moderation.defs#reasonSpam",
230
+ },
231
+ "id": 15,
232
+ "subject": Object {
233
+ "$type": "com.atproto.admin.defs#repoRef",
234
+ "did": "user(0)",
235
+ },
236
+ "subjectBlobCids": Array [],
237
+ "subjectHandle": "alice.test",
238
+ },
239
+ ]
240
+ `;
241
+
242
+ exports[`moderation-events query events returns events matching multiple keywords in comment 2`] = `
243
+ Array [
244
+ Object {
245
+ "createdAt": "1970-01-01T00:00:00.000Z",
246
+ "createdBy": "user(1)",
247
+ "creatorHandle": "bob.test",
248
+ "event": Object {
249
+ "$type": "tools.ozone.moderation.defs#modEventReport",
250
+ "comment": "rainy days feel lazy",
251
+ "isReporterMuted": false,
252
+ "reportType": "com.atproto.moderation.defs#reasonSpam",
253
+ },
254
+ "id": 16,
255
+ "subject": Object {
256
+ "$type": "com.atproto.admin.defs#repoRef",
257
+ "did": "user(0)",
258
+ },
259
+ "subjectBlobCids": Array [],
260
+ "subjectHandle": "alice.test",
261
+ },
262
+ Object {
263
+ "createdAt": "1970-01-01T00:00:00.000Z",
264
+ "createdBy": "user(1)",
265
+ "creatorHandle": "bob.test",
266
+ "event": Object {
267
+ "$type": "tools.ozone.moderation.defs#modEventReport",
268
+ "comment": "november rain",
269
+ "isReporterMuted": false,
270
+ "reportType": "com.atproto.moderation.defs#reasonSpam",
271
+ },
272
+ "id": 15,
273
+ "subject": Object {
274
+ "$type": "com.atproto.admin.defs#repoRef",
275
+ "did": "user(0)",
276
+ },
277
+ "subjectBlobCids": Array [],
278
+ "subjectHandle": "alice.test",
279
+ },
280
+ ]
281
+ `;
282
+
283
+ exports[`moderation-events query events returns events matching multiple keywords in comment 3`] = `
284
+ Array [
285
+ Object {
286
+ "createdAt": "1970-01-01T00:00:00.000Z",
287
+ "createdBy": "user(1)",
288
+ "creatorHandle": "bob.test",
289
+ "event": Object {
290
+ "$type": "tools.ozone.moderation.defs#modEventReport",
291
+ "comment": "rainy days feel lazy",
292
+ "isReporterMuted": false,
293
+ "reportType": "com.atproto.moderation.defs#reasonSpam",
294
+ },
295
+ "id": 16,
296
+ "subject": Object {
297
+ "$type": "com.atproto.admin.defs#repoRef",
298
+ "did": "user(0)",
299
+ },
300
+ "subjectBlobCids": Array [],
301
+ "subjectHandle": "alice.test",
302
+ },
303
+ Object {
304
+ "createdAt": "1970-01-01T00:00:00.000Z",
305
+ "createdBy": "user(1)",
306
+ "creatorHandle": "bob.test",
307
+ "event": Object {
308
+ "$type": "tools.ozone.moderation.defs#modEventReport",
309
+ "comment": "november rain",
310
+ "isReporterMuted": false,
311
+ "reportType": "com.atproto.moderation.defs#reasonSpam",
312
+ },
313
+ "id": 15,
314
+ "subject": Object {
315
+ "$type": "com.atproto.admin.defs#repoRef",
316
+ "did": "user(0)",
317
+ },
318
+ "subjectBlobCids": Array [],
319
+ "subjectHandle": "alice.test",
320
+ },
321
+ ]
322
+ `;
@@ -35,28 +35,28 @@ describe('moderation', () => {
35
35
  cid: ref.cidStr,
36
36
  })
37
37
 
38
- beforeAll(async () => {
39
- network = await TestNetwork.create({
40
- dbPostgresSchema: 'ozone_ack_all_subjects_of_account',
38
+ const getReviewStateBySubject = (subjects: SubjectStatusView[]) => {
39
+ const states = new Map<string, SubjectStatusView>()
40
+
41
+ subjects.forEach((item) => {
42
+ if (ComAtprotoRepoStrongRef.isMain(item.subject)) {
43
+ states.set(item.subject.uri, item)
44
+ } else if (isRepoRef(item.subject)) {
45
+ states.set(item.subject.did, item)
46
+ }
41
47
  })
42
- sc = network.getSeedClient()
43
- modClient = network.ozone.getModClient()
44
- await basicSeed(sc)
45
- await network.processAll()
46
- })
47
48
 
48
- afterAll(async () => {
49
- await network.close()
50
- })
49
+ return states
50
+ }
51
51
 
52
- it('acknowledges all open/escalated review subjects.', async () => {
53
- const postOne = sc.posts[sc.dids.bob][0].ref
54
- const postTwo = sc.posts[sc.dids.bob][1].ref
52
+ const reportUserAndPost = async (did: string) => {
53
+ const postOne = sc.posts[did][0].ref
54
+ const postTwo = sc.posts[did][1].ref
55
55
  await Promise.all([
56
56
  sc.createReport({
57
57
  reasonType: REASONSPAM,
58
- subject: repoSubject(sc.dids.bob),
59
- reportedBy: sc.dids.alice,
58
+ subject: repoSubject(did),
59
+ reportedBy: sc.dids.carol,
60
60
  }),
61
61
  sc.createReport({
62
62
  reasonType: REASONOTHER,
@@ -71,7 +71,6 @@ describe('moderation', () => {
71
71
  reportedBy: sc.dids.carol,
72
72
  }),
73
73
  ])
74
-
75
74
  await modClient.emitEvent({
76
75
  event: {
77
76
  $type: 'tools.ozone.moderation.defs#modEventReport',
@@ -80,6 +79,26 @@ describe('moderation', () => {
80
79
  subject: recordSubject(postTwo),
81
80
  })
82
81
 
82
+ return { postOne, postTwo }
83
+ }
84
+
85
+ beforeAll(async () => {
86
+ network = await TestNetwork.create({
87
+ dbPostgresSchema: 'ozone_ack_all_subjects_of_account',
88
+ })
89
+ sc = network.getSeedClient()
90
+ modClient = network.ozone.getModClient()
91
+ await basicSeed(sc)
92
+ await network.processAll()
93
+ })
94
+
95
+ afterAll(async () => {
96
+ await network.close()
97
+ })
98
+
99
+ it('acknowledges all open/escalated review subjects with takedown.', async () => {
100
+ const { postOne, postTwo } = await reportUserAndPost(sc.dids.bob)
101
+
83
102
  const { subjectStatuses: statusesBefore } = await modClient.queryStatuses({
84
103
  subject: sc.dids.bob,
85
104
  includeAllUserRecords: true,
@@ -95,19 +114,46 @@ describe('moderation', () => {
95
114
  includeAllUserRecords: true,
96
115
  })
97
116
 
98
- const getReviewStateBySubject = (subjects: SubjectStatusView[]) => {
99
- const states = new Map<string, SubjectStatusView>()
117
+ const reviewStatesBefore = getReviewStateBySubject(statusesBefore)
118
+ const reviewStatesAfter = getReviewStateBySubject(statusesAfter)
119
+
120
+ // Check that review states before were different for different subjects
121
+ expect(reviewStatesBefore.get(postOne.uriStr)?.reviewState).toBe(REVIEWOPEN)
122
+ expect(reviewStatesBefore.get(postTwo.uriStr)?.reviewState).toBe(
123
+ REVIEWESCALATED,
124
+ )
125
+ expect(reviewStatesBefore.get(sc.dids.bob)?.reviewState).toBe(REVIEWOPEN)
100
126
 
101
- subjects.forEach((item) => {
102
- if (ComAtprotoRepoStrongRef.isMain(item.subject)) {
103
- states.set(item.subject.uri, item)
104
- } else if (isRepoRef(item.subject)) {
105
- states.set(item.subject.did, item)
106
- }
107
- })
127
+ // Check that review states after are all closed
128
+ expect(reviewStatesAfter.get(postOne.uriStr)?.reviewState).toBe(
129
+ REVIEWCLOSED,
130
+ )
131
+ expect(reviewStatesAfter.get(postTwo.uriStr)?.reviewState).toBe(
132
+ REVIEWCLOSED,
133
+ )
134
+ expect(reviewStatesAfter.get(sc.dids.bob)?.reviewState).toBe(REVIEWCLOSED)
135
+ })
108
136
 
109
- return states
110
- }
137
+ it('acknowledges all open/escalated review subjects with acknowledge.', async () => {
138
+ const { postOne, postTwo } = await reportUserAndPost(sc.dids.alice)
139
+
140
+ const { subjectStatuses: statusesBefore } = await modClient.queryStatuses({
141
+ subject: sc.dids.alice,
142
+ includeAllUserRecords: true,
143
+ })
144
+
145
+ await modClient.emitEvent({
146
+ subject: repoSubject(sc.dids.alice),
147
+ event: {
148
+ $type: 'tools.ozone.moderation.defs#modEventAcknowledge',
149
+ acknowledgeAccountSubjects: true,
150
+ },
151
+ })
152
+
153
+ const { subjectStatuses: statusesAfter } = await modClient.queryStatuses({
154
+ subject: sc.dids.alice,
155
+ includeAllUserRecords: true,
156
+ })
111
157
 
112
158
  const reviewStatesBefore = getReviewStateBySubject(statusesBefore)
113
159
  const reviewStatesAfter = getReviewStateBySubject(statusesAfter)
@@ -117,7 +163,7 @@ describe('moderation', () => {
117
163
  expect(reviewStatesBefore.get(postTwo.uriStr)?.reviewState).toBe(
118
164
  REVIEWESCALATED,
119
165
  )
120
- expect(reviewStatesBefore.get(sc.dids.bob)?.reviewState).toBe(REVIEWOPEN)
166
+ expect(reviewStatesBefore.get(sc.dids.alice)?.reviewState).toBe(REVIEWOPEN)
121
167
 
122
168
  // Check that review states after are all closed
123
169
  expect(reviewStatesAfter.get(postOne.uriStr)?.reviewState).toBe(
@@ -126,6 +172,6 @@ describe('moderation', () => {
126
172
  expect(reviewStatesAfter.get(postTwo.uriStr)?.reviewState).toBe(
127
173
  REVIEWCLOSED,
128
174
  )
129
- expect(reviewStatesAfter.get(sc.dids.bob)?.reviewState).toBe(REVIEWCLOSED)
175
+ expect(reviewStatesAfter.get(sc.dids.alice)?.reviewState).toBe(REVIEWCLOSED)
130
176
  })
131
177
  })
@@ -224,6 +224,46 @@ describe('moderation-events', () => {
224
224
  expect(eventsWithComment.events.length).toEqual(10)
225
225
  })
226
226
 
227
+ it('returns events matching multiple keywords in comment', async () => {
228
+ await sc.createReport({
229
+ reasonType: REASONSPAM,
230
+ reason: 'november rain',
231
+ subject: {
232
+ $type: 'com.atproto.admin.defs#repoRef',
233
+ did: sc.dids.alice,
234
+ },
235
+ reportedBy: sc.dids.bob,
236
+ })
237
+ await sc.createReport({
238
+ reasonType: REASONSPAM,
239
+ reason: 'rainy days feel lazy',
240
+ subject: {
241
+ $type: 'com.atproto.admin.defs#repoRef',
242
+ did: sc.dids.alice,
243
+ },
244
+ reportedBy: sc.dids.bob,
245
+ })
246
+ const [eventsMatchingBothKeywords, unusedTrailingSeparator, extraSpaces] =
247
+ await Promise.all([
248
+ modClient.queryEvents({
249
+ hasComment: true,
250
+ comment: 'november||lazy',
251
+ }),
252
+ modClient.queryEvents({
253
+ hasComment: true,
254
+ comment: 'november||lazy||',
255
+ }),
256
+ modClient.queryEvents({
257
+ hasComment: true,
258
+ comment: '||november||lazy|| ',
259
+ }),
260
+ ])
261
+
262
+ expect(forSnapshot(eventsMatchingBothKeywords.events)).toMatchSnapshot()
263
+ expect(forSnapshot(unusedTrailingSeparator.events)).toMatchSnapshot()
264
+ expect(forSnapshot(extraSpaces.events)).toMatchSnapshot()
265
+ })
266
+
227
267
  it('returns events matching filter params for labels', async () => {
228
268
  const [negatedLabelEvent, createdLabelEvent] = await Promise.all([
229
269
  modClient.emitEvent({