@atproto/api 0.20.22 → 0.20.25

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 (64) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/client/lexicons.d.ts +28 -12
  3. package/dist/client/lexicons.d.ts.map +1 -1
  4. package/dist/client/lexicons.js +14 -6
  5. package/dist/client/lexicons.js.map +1 -1
  6. package/dist/client/types/app/bsky/ageassurance/defs.d.ts +2 -0
  7. package/dist/client/types/app/bsky/ageassurance/defs.d.ts.map +1 -1
  8. package/dist/client/types/app/bsky/ageassurance/defs.js.map +1 -1
  9. package/package.json +22 -16
  10. package/definitions/labels.json +0 -170
  11. package/docs/moderation.md +0 -260
  12. package/jest.config.cjs +0 -23
  13. package/jest.d.ts +0 -20
  14. package/jest.setup.ts +0 -97
  15. package/scripts/code/labels.mjs +0 -77
  16. package/scripts/generate-code.mjs +0 -4
  17. package/src/age-assurance.test.ts +0 -218
  18. package/src/age-assurance.ts +0 -137
  19. package/src/agent.ts +0 -1645
  20. package/src/atp-agent.ts +0 -570
  21. package/src/bsky-agent.ts +0 -14
  22. package/src/const.ts +0 -1
  23. package/src/index.ts +0 -44
  24. package/src/mocker.ts +0 -220
  25. package/src/moderation/decision.ts +0 -389
  26. package/src/moderation/index.ts +0 -69
  27. package/src/moderation/mutewords.ts +0 -170
  28. package/src/moderation/subjects/account.ts +0 -44
  29. package/src/moderation/subjects/feed-generator.ts +0 -24
  30. package/src/moderation/subjects/notification.ts +0 -25
  31. package/src/moderation/subjects/post.ts +0 -433
  32. package/src/moderation/subjects/profile.ts +0 -26
  33. package/src/moderation/subjects/status.ts +0 -27
  34. package/src/moderation/subjects/user-list.ts +0 -48
  35. package/src/moderation/types.ts +0 -191
  36. package/src/moderation/ui.ts +0 -21
  37. package/src/moderation/util.ts +0 -111
  38. package/src/predicate.ts +0 -52
  39. package/src/rich-text/detection.ts +0 -146
  40. package/src/rich-text/rich-text.ts +0 -418
  41. package/src/rich-text/sanitization.ts +0 -42
  42. package/src/rich-text/unicode.ts +0 -47
  43. package/src/rich-text/util.ts +0 -15
  44. package/src/session-manager.ts +0 -5
  45. package/src/types.ts +0 -165
  46. package/src/util.ts +0 -96
  47. package/tests/atp-agent.test.ts +0 -3863
  48. package/tests/dispatcher.test.ts +0 -527
  49. package/tests/errors.test.ts +0 -29
  50. package/tests/moderation-behaviors.test.ts +0 -951
  51. package/tests/moderation-custom-labels.test.ts +0 -334
  52. package/tests/moderation-mutewords.test.ts +0 -1299
  53. package/tests/moderation-prefs.test.ts +0 -402
  54. package/tests/moderation-quoteposts.test.ts +0 -277
  55. package/tests/moderation.test.ts +0 -739
  56. package/tests/rich-text-detection.test.ts +0 -456
  57. package/tests/rich-text-sanitization.test.ts +0 -216
  58. package/tests/rich-text.test.ts +0 -705
  59. package/tests/util/echo-server.ts +0 -37
  60. package/tests/util/moderation-behavior.ts +0 -268
  61. package/tsconfig.build.json +0 -9
  62. package/tsconfig.build.tsbuildinfo +0 -1
  63. package/tsconfig.json +0 -7
  64. package/tsconfig.tests.json +0 -10
@@ -1,1299 +0,0 @@
1
- import { $Typed } from '../src/client/util.js'
2
- import {
3
- AppBskyEmbedGallery,
4
- AppBskyEmbedRecord,
5
- AppBskyEmbedRecordWithMedia,
6
- AppBskyFeedPost,
7
- BlobRef,
8
- RichText,
9
- mock,
10
- moderatePost,
11
- } from '../src/index.js'
12
- import { matchMuteWords } from '../src/moderation/mutewords.js'
13
- import { ModerationOpts } from '../src/moderation/types.js'
14
-
15
- const FAKE_CID = 'bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq'
16
-
17
- const fakeBlob = (): BlobRef =>
18
- new BlobRef({ '/': FAKE_CID } as any, 'image/jpeg', 1234)
19
-
20
- const galleryRecord = (alts: string[]): $Typed<AppBskyEmbedGallery.Main> => ({
21
- $type: 'app.bsky.embed.gallery',
22
- items: alts.map(
23
- (alt): $Typed<AppBskyEmbedGallery.Image> => ({
24
- $type: 'app.bsky.embed.gallery#image',
25
- image: fakeBlob(),
26
- alt,
27
- aspectRatio: { width: 4, height: 3 },
28
- }),
29
- ),
30
- })
31
-
32
- const galleryView = (alts: string[]): $Typed<AppBskyEmbedGallery.View> => ({
33
- $type: 'app.bsky.embed.gallery#view',
34
- items: alts.map(
35
- (alt): $Typed<AppBskyEmbedGallery.ViewImage> => ({
36
- $type: 'app.bsky.embed.gallery#viewImage',
37
- thumbnail: 'https://example.test/thumb.jpg',
38
- fullsize: 'https://example.test/full.jpg',
39
- alt,
40
- aspectRatio: { width: 4, height: 3 },
41
- }),
42
- ),
43
- })
44
-
45
- const galleryMuteOpts = (): ModerationOpts => ({
46
- userDid: 'did:web:alice.test',
47
- prefs: {
48
- adultContentEnabled: false,
49
- labels: {},
50
- labelers: [],
51
- mutedWords: [
52
- { value: 'badword', targets: ['content'], actorTarget: 'all' },
53
- ],
54
- hiddenPosts: [],
55
- },
56
- labelDefs: {},
57
- })
58
-
59
- describe(`matchMuteWords`, () => {
60
- describe(`tags`, () => {
61
- it(`match: outline tag`, () => {
62
- const rt = new RichText({
63
- text: `This is a post #inlineTag`,
64
- })
65
- rt.detectFacetsWithoutResolution()
66
-
67
- const muteWord = {
68
- value: 'outlineTag',
69
- targets: ['tag'],
70
- actorTarget: 'all',
71
- }
72
- const match = matchMuteWords({
73
- mutedWords: [muteWord],
74
- text: rt.text,
75
- facets: rt.facets,
76
- outlineTags: ['outlineTag'],
77
- })
78
-
79
- expect(match).toEqual([{ word: muteWord, predicate: muteWord.value }])
80
- })
81
-
82
- it(`match: inline tag`, () => {
83
- const rt = new RichText({
84
- text: `This is a post #inlineTag`,
85
- })
86
- rt.detectFacetsWithoutResolution()
87
-
88
- const muteWord = {
89
- value: 'inlineTag',
90
- targets: ['tag'],
91
- actorTarget: 'all',
92
- }
93
- const match = matchMuteWords({
94
- mutedWords: [muteWord],
95
- text: rt.text,
96
- facets: rt.facets,
97
- outlineTags: ['outlineTag'],
98
- })
99
-
100
- expect(match).toEqual([{ word: muteWord, predicate: muteWord.value }])
101
- })
102
-
103
- it(`match: content target matches inline tag`, () => {
104
- const rt = new RichText({
105
- text: `This is a post #inlineTag`,
106
- })
107
- rt.detectFacetsWithoutResolution()
108
-
109
- const match = matchMuteWords({
110
- mutedWords: [
111
- { value: 'inlineTag', targets: ['content'], actorTarget: 'all' },
112
- ],
113
- text: rt.text,
114
- facets: rt.facets,
115
- outlineTags: ['outlineTag'],
116
- })
117
-
118
- expect(match).toBeTruthy()
119
- })
120
-
121
- it(`no match: only tag targets`, () => {
122
- const rt = new RichText({
123
- text: `This is a post`,
124
- })
125
- rt.detectFacetsWithoutResolution()
126
-
127
- const match = matchMuteWords({
128
- mutedWords: [
129
- { value: 'inlineTag', targets: ['tag'], actorTarget: 'all' },
130
- ],
131
- text: rt.text,
132
- facets: rt.facets,
133
- outlineTags: [],
134
- })
135
-
136
- expect(match).toBeUndefined()
137
- })
138
- })
139
-
140
- describe(`early exits`, () => {
141
- it(`match: single character 希`, () => {
142
- /**
143
- * @see https://bsky.app/profile/mukuuji.bsky.social/post/3klji4fvsdk2c
144
- */
145
- const rt = new RichText({
146
- text: `改善希望です`,
147
- })
148
- rt.detectFacetsWithoutResolution()
149
-
150
- const muteWord = { value: '希', targets: ['content'], actorTarget: 'all' }
151
- const match = matchMuteWords({
152
- mutedWords: [muteWord],
153
- text: rt.text,
154
- facets: rt.facets,
155
- outlineTags: [],
156
- })
157
-
158
- expect(match).toEqual([{ word: muteWord, predicate: muteWord.value }])
159
- })
160
-
161
- it(`match: single char with length > 1 ☠︎`, () => {
162
- const rt = new RichText({
163
- text: `Idk why ☠︎ but maybe`,
164
- })
165
- rt.detectFacetsWithoutResolution()
166
-
167
- const match = matchMuteWords({
168
- mutedWords: [
169
- { value: '☠︎', targets: ['content'], actorTarget: 'all' },
170
- ],
171
- text: rt.text,
172
- facets: rt.facets,
173
- outlineTags: [],
174
- })
175
-
176
- expect(match).toBeTruthy()
177
- })
178
-
179
- it(`no match: long muted word, short post`, () => {
180
- const rt = new RichText({
181
- text: `hey`,
182
- })
183
- rt.detectFacetsWithoutResolution()
184
-
185
- const match = matchMuteWords({
186
- mutedWords: [
187
- { value: 'politics', targets: ['content'], actorTarget: 'all' },
188
- ],
189
- text: rt.text,
190
- facets: rt.facets,
191
- outlineTags: [],
192
- })
193
-
194
- expect(match).toBeUndefined()
195
- })
196
-
197
- it(`match: exact text`, () => {
198
- const rt = new RichText({
199
- text: `javascript`,
200
- })
201
- rt.detectFacetsWithoutResolution()
202
-
203
- const match = matchMuteWords({
204
- mutedWords: [
205
- { value: 'javascript', targets: ['content'], actorTarget: 'all' },
206
- ],
207
- text: rt.text,
208
- facets: rt.facets,
209
- outlineTags: [],
210
- })
211
-
212
- expect(match).toBeTruthy()
213
- })
214
- })
215
-
216
- describe(`general content`, () => {
217
- it(`match: word within post`, () => {
218
- const rt = new RichText({
219
- text: `This is a post about javascript`,
220
- })
221
- rt.detectFacetsWithoutResolution()
222
-
223
- const muteWord = {
224
- value: 'javascript',
225
- targets: ['content'],
226
- actorTarget: 'all',
227
- }
228
- const match = matchMuteWords({
229
- mutedWords: [muteWord],
230
- text: rt.text,
231
- facets: rt.facets,
232
- outlineTags: [],
233
- })
234
-
235
- expect(match).toEqual([{ word: muteWord, predicate: muteWord.value }])
236
- })
237
-
238
- it(`no match: partial word`, () => {
239
- const rt = new RichText({
240
- text: `Use your brain, Eric`,
241
- })
242
- rt.detectFacetsWithoutResolution()
243
-
244
- const match = matchMuteWords({
245
- mutedWords: [{ value: 'ai', targets: ['content'], actorTarget: 'all' }],
246
- text: rt.text,
247
- facets: rt.facets,
248
- outlineTags: [],
249
- })
250
-
251
- expect(match).toBeUndefined()
252
- })
253
-
254
- it(`match: multiline`, () => {
255
- const rt = new RichText({
256
- text: `Use your\n\tbrain, Eric`,
257
- })
258
- rt.detectFacetsWithoutResolution()
259
-
260
- const match = matchMuteWords({
261
- mutedWords: [
262
- { value: 'brain', targets: ['content'], actorTarget: 'all' },
263
- ],
264
- text: rt.text,
265
- facets: rt.facets,
266
- outlineTags: [],
267
- })
268
-
269
- expect(match).toBeTruthy()
270
- })
271
-
272
- it(`match: :)`, () => {
273
- const rt = new RichText({
274
- text: `So happy :)`,
275
- })
276
- rt.detectFacetsWithoutResolution()
277
-
278
- const match = matchMuteWords({
279
- mutedWords: [{ value: `:)`, targets: ['content'], actorTarget: 'all' }],
280
- text: rt.text,
281
- facets: rt.facets,
282
- outlineTags: [],
283
- })
284
-
285
- expect(match).toBeTruthy()
286
- })
287
- })
288
-
289
- describe(`punctuation semi-fuzzy`, () => {
290
- describe(`yay!`, () => {
291
- const rt = new RichText({
292
- text: `We're federating, yay!`,
293
- })
294
- rt.detectFacetsWithoutResolution()
295
-
296
- it(`match: yay!`, () => {
297
- const match = matchMuteWords({
298
- mutedWords: [
299
- { value: 'yay!', targets: ['content'], actorTarget: 'all' },
300
- ],
301
- text: rt.text,
302
- facets: rt.facets,
303
- outlineTags: [],
304
- })
305
-
306
- expect(match).toBeTruthy()
307
- })
308
-
309
- it(`match: yay`, () => {
310
- const match = matchMuteWords({
311
- mutedWords: [
312
- { value: 'yay', targets: ['content'], actorTarget: 'all' },
313
- ],
314
- text: rt.text,
315
- facets: rt.facets,
316
- outlineTags: [],
317
- })
318
-
319
- expect(match).toBeTruthy()
320
- })
321
- })
322
-
323
- describe(`y!ppee!!`, () => {
324
- const rt = new RichText({
325
- text: `We're federating, y!ppee!!`,
326
- })
327
- rt.detectFacetsWithoutResolution()
328
-
329
- it(`match: y!ppee`, () => {
330
- const match = matchMuteWords({
331
- mutedWords: [
332
- { value: 'y!ppee', targets: ['content'], actorTarget: 'all' },
333
- ],
334
- text: rt.text,
335
- facets: rt.facets,
336
- outlineTags: [],
337
- })
338
-
339
- expect(match).toBeTruthy()
340
- })
341
-
342
- // single exclamation point, source has double
343
- it(`no match: y!ppee!`, () => {
344
- const match = matchMuteWords({
345
- mutedWords: [
346
- { value: 'y!ppee!', targets: ['content'], actorTarget: 'all' },
347
- ],
348
- text: rt.text,
349
- facets: rt.facets,
350
- outlineTags: [],
351
- })
352
-
353
- expect(match).toBeTruthy()
354
- })
355
- })
356
-
357
- describe(`apostrophes: Bluesky's`, () => {
358
- const rt = new RichText({
359
- text: `Yay, Bluesky's mutewords work`,
360
- })
361
- rt.detectFacetsWithoutResolution()
362
-
363
- it(`match: Bluesky's`, () => {
364
- const match = matchMuteWords({
365
- mutedWords: [
366
- { value: `Bluesky's`, targets: ['content'], actorTarget: 'all' },
367
- ],
368
- text: rt.text,
369
- facets: rt.facets,
370
- outlineTags: [],
371
- })
372
-
373
- expect(match).toBeTruthy()
374
- })
375
-
376
- it(`match: Bluesky`, () => {
377
- const match = matchMuteWords({
378
- mutedWords: [
379
- { value: 'Bluesky', targets: ['content'], actorTarget: 'all' },
380
- ],
381
- text: rt.text,
382
- facets: rt.facets,
383
- outlineTags: [],
384
- })
385
-
386
- expect(match).toBeTruthy()
387
- })
388
-
389
- it(`match: bluesky`, () => {
390
- const match = matchMuteWords({
391
- mutedWords: [
392
- { value: 'bluesky', targets: ['content'], actorTarget: 'all' },
393
- ],
394
- text: rt.text,
395
- facets: rt.facets,
396
- outlineTags: [],
397
- })
398
-
399
- expect(match).toBeTruthy()
400
- })
401
-
402
- it(`match: blueskys`, () => {
403
- const match = matchMuteWords({
404
- mutedWords: [
405
- { value: 'blueskys', targets: ['content'], actorTarget: 'all' },
406
- ],
407
- text: rt.text,
408
- facets: rt.facets,
409
- outlineTags: [],
410
- })
411
-
412
- expect(match).toBeTruthy()
413
- })
414
- })
415
-
416
- describe(`Why so S@assy?`, () => {
417
- const rt = new RichText({
418
- text: `Why so S@assy?`,
419
- })
420
- rt.detectFacetsWithoutResolution()
421
-
422
- it(`match: S@assy`, () => {
423
- const match = matchMuteWords({
424
- mutedWords: [
425
- { value: 'S@assy', targets: ['content'], actorTarget: 'all' },
426
- ],
427
- text: rt.text,
428
- facets: rt.facets,
429
- outlineTags: [],
430
- })
431
-
432
- expect(match).toBeTruthy()
433
- })
434
-
435
- it(`match: s@assy`, () => {
436
- const match = matchMuteWords({
437
- mutedWords: [
438
- { value: 's@assy', targets: ['content'], actorTarget: 'all' },
439
- ],
440
- text: rt.text,
441
- facets: rt.facets,
442
- outlineTags: [],
443
- })
444
-
445
- expect(match).toBeTruthy()
446
- })
447
- })
448
-
449
- describe(`New York Times`, () => {
450
- const rt = new RichText({
451
- text: `New York Times`,
452
- })
453
- rt.detectFacetsWithoutResolution()
454
-
455
- // case insensitive
456
- it(`match: new york times`, () => {
457
- const match = matchMuteWords({
458
- mutedWords: [
459
- {
460
- value: 'new york times',
461
- targets: ['content'],
462
- actorTarget: 'all',
463
- },
464
- ],
465
- text: rt.text,
466
- facets: rt.facets,
467
- outlineTags: [],
468
- })
469
-
470
- expect(match).toBeTruthy()
471
- })
472
- })
473
-
474
- describe(`!command`, () => {
475
- const rt = new RichText({
476
- text: `Idk maybe a bot !command`,
477
- })
478
- rt.detectFacetsWithoutResolution()
479
-
480
- it(`match: !command`, () => {
481
- const match = matchMuteWords({
482
- mutedWords: [
483
- { value: `!command`, targets: ['content'], actorTarget: 'all' },
484
- ],
485
- text: rt.text,
486
- facets: rt.facets,
487
- outlineTags: [],
488
- })
489
-
490
- expect(match).toBeTruthy()
491
- })
492
-
493
- it(`match: command`, () => {
494
- const match = matchMuteWords({
495
- mutedWords: [
496
- { value: `command`, targets: ['content'], actorTarget: 'all' },
497
- ],
498
- text: rt.text,
499
- facets: rt.facets,
500
- outlineTags: [],
501
- })
502
-
503
- expect(match).toBeTruthy()
504
- })
505
-
506
- it(`no match: !command`, () => {
507
- const rt = new RichText({
508
- text: `Idk maybe a bot command`,
509
- })
510
- rt.detectFacetsWithoutResolution()
511
-
512
- const match = matchMuteWords({
513
- mutedWords: [
514
- { value: `!command`, targets: ['content'], actorTarget: 'all' },
515
- ],
516
- text: rt.text,
517
- facets: rt.facets,
518
- outlineTags: [],
519
- })
520
-
521
- expect(match).toBeUndefined()
522
- })
523
- })
524
-
525
- describe(`and/or`, () => {
526
- const rt = new RichText({
527
- text: `Tomatoes are fruits and/or vegetables`,
528
- })
529
- rt.detectFacetsWithoutResolution()
530
-
531
- it(`match: and/or`, () => {
532
- const match = matchMuteWords({
533
- mutedWords: [
534
- { value: `and/or`, targets: ['content'], actorTarget: 'all' },
535
- ],
536
- text: rt.text,
537
- facets: rt.facets,
538
- outlineTags: [],
539
- })
540
-
541
- expect(match).toBeTruthy()
542
- })
543
-
544
- it(`no match: Andor`, () => {
545
- const match = matchMuteWords({
546
- mutedWords: [
547
- { value: `Andor`, targets: ['content'], actorTarget: 'all' },
548
- ],
549
- text: rt.text,
550
- facets: rt.facets,
551
- outlineTags: [],
552
- })
553
-
554
- expect(match).toBeUndefined()
555
- })
556
- })
557
-
558
- describe(`super-bad`, () => {
559
- const rt = new RichText({
560
- text: `I'm super-bad`,
561
- })
562
- rt.detectFacetsWithoutResolution()
563
-
564
- it(`match: super-bad`, () => {
565
- const match = matchMuteWords({
566
- mutedWords: [
567
- { value: `super-bad`, targets: ['content'], actorTarget: 'all' },
568
- ],
569
- text: rt.text,
570
- facets: rt.facets,
571
- outlineTags: [],
572
- })
573
-
574
- expect(match).toBeTruthy()
575
- })
576
-
577
- it(`match: super`, () => {
578
- const match = matchMuteWords({
579
- mutedWords: [
580
- { value: `super`, targets: ['content'], actorTarget: 'all' },
581
- ],
582
- text: rt.text,
583
- facets: rt.facets,
584
- outlineTags: [],
585
- })
586
-
587
- expect(match).toBeTruthy()
588
- })
589
-
590
- it(`match: bad`, () => {
591
- const match = matchMuteWords({
592
- mutedWords: [
593
- { value: `bad`, targets: ['content'], actorTarget: 'all' },
594
- ],
595
- text: rt.text,
596
- facets: rt.facets,
597
- outlineTags: [],
598
- })
599
-
600
- expect(match).toBeTruthy()
601
- })
602
-
603
- it(`match: super bad`, () => {
604
- const match = matchMuteWords({
605
- mutedWords: [
606
- { value: `super bad`, targets: ['content'], actorTarget: 'all' },
607
- ],
608
- text: rt.text,
609
- facets: rt.facets,
610
- outlineTags: [],
611
- })
612
-
613
- expect(match).toBeTruthy()
614
- })
615
-
616
- it(`match: superbad`, () => {
617
- const match = matchMuteWords({
618
- mutedWords: [
619
- { value: `superbad`, targets: ['content'], actorTarget: 'all' },
620
- ],
621
- text: rt.text,
622
- facets: rt.facets,
623
- outlineTags: [],
624
- })
625
-
626
- expect(match).toBeTruthy()
627
- })
628
- })
629
-
630
- describe(`idk_what_this_would_be`, () => {
631
- const rt = new RichText({
632
- text: `Weird post with idk_what_this_would_be`,
633
- })
634
- rt.detectFacetsWithoutResolution()
635
-
636
- it(`match: idk what this would be`, () => {
637
- const match = matchMuteWords({
638
- mutedWords: [
639
- {
640
- value: `idk what this would be`,
641
- targets: ['content'],
642
- actorTarget: 'all',
643
- },
644
- ],
645
- text: rt.text,
646
- facets: rt.facets,
647
- outlineTags: [],
648
- })
649
-
650
- expect(match).toBeTruthy()
651
- })
652
-
653
- it(`no match: idk what this would be for`, () => {
654
- // extra word
655
- const match = matchMuteWords({
656
- mutedWords: [
657
- {
658
- value: `idk what this would be for`,
659
- targets: ['content'],
660
- actorTarget: 'all',
661
- },
662
- ],
663
- text: rt.text,
664
- facets: rt.facets,
665
- outlineTags: [],
666
- })
667
-
668
- expect(match).toBeUndefined()
669
- })
670
-
671
- it(`match: idk`, () => {
672
- // extra word
673
- const match = matchMuteWords({
674
- mutedWords: [
675
- { value: `idk`, targets: ['content'], actorTarget: 'all' },
676
- ],
677
- text: rt.text,
678
- facets: rt.facets,
679
- outlineTags: [],
680
- })
681
-
682
- expect(match).toBeTruthy()
683
- })
684
-
685
- it(`match: idkwhatthiswouldbe`, () => {
686
- const match = matchMuteWords({
687
- mutedWords: [
688
- {
689
- value: `idkwhatthiswouldbe`,
690
- targets: ['content'],
691
- actorTarget: 'all',
692
- },
693
- ],
694
- text: rt.text,
695
- facets: rt.facets,
696
- outlineTags: [],
697
- })
698
-
699
- expect(match).toBeTruthy()
700
- })
701
- })
702
-
703
- describe(`parentheses`, () => {
704
- const rt = new RichText({
705
- text: `Post with context(iykyk)`,
706
- })
707
- rt.detectFacetsWithoutResolution()
708
-
709
- it(`match: context(iykyk)`, () => {
710
- const match = matchMuteWords({
711
- mutedWords: [
712
- {
713
- value: `context(iykyk)`,
714
- targets: ['content'],
715
- actorTarget: 'all',
716
- },
717
- ],
718
- text: rt.text,
719
- facets: rt.facets,
720
- outlineTags: [],
721
- })
722
-
723
- expect(match).toBeTruthy()
724
- })
725
-
726
- it(`match: context`, () => {
727
- const match = matchMuteWords({
728
- mutedWords: [
729
- { value: `context`, targets: ['content'], actorTarget: 'all' },
730
- ],
731
- text: rt.text,
732
- facets: rt.facets,
733
- outlineTags: [],
734
- })
735
-
736
- expect(match).toBeTruthy()
737
- })
738
-
739
- it(`match: iykyk`, () => {
740
- const match = matchMuteWords({
741
- mutedWords: [
742
- { value: `iykyk`, targets: ['content'], actorTarget: 'all' },
743
- ],
744
- text: rt.text,
745
- facets: rt.facets,
746
- outlineTags: [],
747
- })
748
-
749
- expect(match).toBeTruthy()
750
- })
751
-
752
- it(`match: (iykyk)`, () => {
753
- const match = matchMuteWords({
754
- mutedWords: [
755
- { value: `(iykyk)`, targets: ['content'], actorTarget: 'all' },
756
- ],
757
- text: rt.text,
758
- facets: rt.facets,
759
- outlineTags: [],
760
- })
761
-
762
- expect(match).toBeTruthy()
763
- })
764
- })
765
-
766
- describe(`🦋`, () => {
767
- const rt = new RichText({
768
- text: `Post with 🦋`,
769
- })
770
- rt.detectFacetsWithoutResolution()
771
-
772
- it(`match: 🦋`, () => {
773
- const match = matchMuteWords({
774
- mutedWords: [
775
- { value: `🦋`, targets: ['content'], actorTarget: 'all' },
776
- ],
777
- text: rt.text,
778
- facets: rt.facets,
779
- outlineTags: [],
780
- })
781
-
782
- expect(match).toBeTruthy()
783
- })
784
- })
785
- })
786
-
787
- describe(`phrases`, () => {
788
- describe(`I like turtles, or how I learned to stop worrying and love the internet.`, () => {
789
- const rt = new RichText({
790
- text: `I like turtles, or how I learned to stop worrying and love the internet.`,
791
- })
792
- rt.detectFacetsWithoutResolution()
793
-
794
- it(`match: stop worrying`, () => {
795
- const match = matchMuteWords({
796
- mutedWords: [
797
- {
798
- value: 'stop worrying',
799
- targets: ['content'],
800
- actorTarget: 'all',
801
- },
802
- ],
803
- text: rt.text,
804
- facets: rt.facets,
805
- outlineTags: [],
806
- })
807
-
808
- expect(match).toBeTruthy()
809
- })
810
-
811
- it(`match: turtles, or how`, () => {
812
- const match = matchMuteWords({
813
- mutedWords: [
814
- {
815
- value: 'turtles, or how',
816
- targets: ['content'],
817
- actorTarget: 'all',
818
- },
819
- ],
820
- text: rt.text,
821
- facets: rt.facets,
822
- outlineTags: [],
823
- })
824
-
825
- expect(match).toBeTruthy()
826
- })
827
- })
828
- })
829
-
830
- describe(`languages without spaces`, () => {
831
- // I love turtles, or how I learned to stop worrying and love the internet
832
- describe(`私はカメが好きです、またはどのようにして心配するのをやめてインターネットを愛するようになったのか`, () => {
833
- const rt = new RichText({
834
- text: `私はカメが好きです、またはどのようにして心配するのをやめてインターネットを愛するようになったのか`,
835
- })
836
- rt.detectFacetsWithoutResolution()
837
-
838
- // internet
839
- it(`match: インターネット`, () => {
840
- const match = matchMuteWords({
841
- mutedWords: [
842
- {
843
- value: 'インターネット',
844
- targets: ['content'],
845
- actorTarget: 'all',
846
- },
847
- ],
848
- text: rt.text,
849
- facets: rt.facets,
850
- outlineTags: [],
851
- languages: ['ja'],
852
- })
853
-
854
- expect(match).toBeTruthy()
855
- })
856
- })
857
- })
858
-
859
- describe(`facet with multiple features`, () => {
860
- it(`multiple tags`, () => {
861
- const match = matchMuteWords({
862
- mutedWords: [
863
- { value: 'bad', targets: ['content'], actorTarget: 'all' },
864
- ],
865
- text: 'tags',
866
- facets: [
867
- {
868
- features: [
869
- {
870
- $type: 'app.bsky.richtext.facet#tag',
871
- tag: 'good',
872
- },
873
- {
874
- $type: 'app.bsky.richtext.facet#tag',
875
- tag: 'bad',
876
- },
877
- ],
878
- index: {
879
- byteEnd: 4,
880
- byteStart: 0,
881
- },
882
- },
883
- ],
884
- })
885
- expect(match).toBeTruthy()
886
- })
887
-
888
- it(`other features`, () => {
889
- const match = matchMuteWords({
890
- mutedWords: [
891
- { value: 'bad', targets: ['content'], actorTarget: 'all' },
892
- ],
893
- text: 'test',
894
- facets: [
895
- {
896
- features: [
897
- {
898
- $type: 'com.example.richtext.facet#other',
899
- // @ts-expect-error
900
- foo: 'bar',
901
- },
902
- {
903
- $type: 'app.bsky.richtext.facet#tag',
904
- tag: 'bad',
905
- },
906
- ],
907
- index: {
908
- byteEnd: 4,
909
- byteStart: 0,
910
- },
911
- },
912
- ],
913
- })
914
- expect(match).toBeTruthy()
915
- })
916
- })
917
-
918
- describe(`doesn't mute own post`, () => {
919
- it(`does mute if it isn't own post`, () => {
920
- const res = moderatePost(
921
- mock.postView({
922
- record: mock.post({
923
- text: 'Mute words!',
924
- }),
925
- author: mock.profileViewBasic({
926
- handle: 'bob.test',
927
- displayName: 'Bob',
928
- }),
929
- labels: [],
930
- }),
931
- {
932
- userDid: 'did:web:alice.test',
933
- prefs: {
934
- adultContentEnabled: false,
935
- labels: {},
936
- labelers: [],
937
- mutedWords: [
938
- { value: 'words', targets: ['content'], actorTarget: 'all' },
939
- ],
940
- hiddenPosts: [],
941
- },
942
- labelDefs: {},
943
- },
944
- )
945
- expect(res.causes[0].type).toBe('mute-word')
946
- })
947
-
948
- it(`doesn't mute own post when muted word is in text`, () => {
949
- const res = moderatePost(
950
- mock.postView({
951
- record: mock.post({
952
- text: 'Mute words!',
953
- }),
954
- author: mock.profileViewBasic({
955
- handle: 'bob.test',
956
- displayName: 'Bob',
957
- }),
958
- labels: [],
959
- }),
960
- {
961
- userDid: 'did:web:bob.test',
962
- prefs: {
963
- adultContentEnabled: false,
964
- labels: {},
965
- labelers: [],
966
- mutedWords: [
967
- { value: 'words', targets: ['content'], actorTarget: 'all' },
968
- ],
969
- hiddenPosts: [],
970
- },
971
- labelDefs: {},
972
- },
973
- )
974
- expect(res.causes.length).toBe(0)
975
- })
976
-
977
- it(`doesn't mute own post when muted word is in tags`, () => {
978
- const rt = new RichText({
979
- text: `Mute #words!`,
980
- })
981
- const res = moderatePost(
982
- mock.postView({
983
- record: mock.post({
984
- text: rt.text,
985
- facets: rt.facets,
986
- }),
987
- author: mock.profileViewBasic({
988
- handle: 'bob.test',
989
- displayName: 'Bob',
990
- }),
991
- labels: [],
992
- }),
993
- {
994
- userDid: 'did:web:bob.test',
995
- prefs: {
996
- adultContentEnabled: false,
997
- labels: {},
998
- labelers: [],
999
- mutedWords: [
1000
- { value: 'words', targets: ['tags'], actorTarget: 'all' },
1001
- ],
1002
- hiddenPosts: [],
1003
- },
1004
- labelDefs: {},
1005
- },
1006
- )
1007
- expect(res.causes.length).toBe(0)
1008
- })
1009
- })
1010
-
1011
- describe(`timed mute words`, () => {
1012
- it(`non-expired word`, () => {
1013
- const now = Date.now()
1014
-
1015
- const res = moderatePost(
1016
- mock.postView({
1017
- record: mock.post({
1018
- text: 'Mute words!',
1019
- }),
1020
- author: mock.profileViewBasic({
1021
- handle: 'bob.test',
1022
- displayName: 'Bob',
1023
- }),
1024
- labels: [],
1025
- }),
1026
- {
1027
- userDid: 'did:web:alice.test',
1028
- prefs: {
1029
- adultContentEnabled: false,
1030
- labels: {},
1031
- labelers: [],
1032
- mutedWords: [
1033
- {
1034
- value: 'words',
1035
- targets: ['content'],
1036
- expiresAt: new Date(now + 1e3).toISOString(),
1037
- actorTarget: 'all',
1038
- },
1039
- ],
1040
- hiddenPosts: [],
1041
- },
1042
- labelDefs: {},
1043
- },
1044
- )
1045
-
1046
- expect(res.causes[0].type).toBe('mute-word')
1047
- })
1048
-
1049
- it(`expired word`, () => {
1050
- const now = Date.now()
1051
-
1052
- const res = moderatePost(
1053
- mock.postView({
1054
- record: mock.post({
1055
- text: 'Mute words!',
1056
- }),
1057
- author: mock.profileViewBasic({
1058
- handle: 'bob.test',
1059
- displayName: 'Bob',
1060
- }),
1061
- labels: [],
1062
- }),
1063
- {
1064
- userDid: 'did:web:alice.test',
1065
- prefs: {
1066
- adultContentEnabled: false,
1067
- labels: {},
1068
- labelers: [],
1069
- mutedWords: [
1070
- {
1071
- value: 'words',
1072
- targets: ['content'],
1073
- expiresAt: new Date(now - 1e3).toISOString(),
1074
- actorTarget: 'all',
1075
- },
1076
- ],
1077
- hiddenPosts: [],
1078
- },
1079
- labelDefs: {},
1080
- },
1081
- )
1082
-
1083
- expect(res.causes.length).toBe(0)
1084
- })
1085
- })
1086
-
1087
- describe(`actor-based mute words`, () => {
1088
- const viewer = {
1089
- userDid: 'did:web:alice.test',
1090
- prefs: {
1091
- adultContentEnabled: false,
1092
- labels: {},
1093
- labelers: [],
1094
- mutedWords: [
1095
- {
1096
- value: 'words',
1097
- targets: ['content'],
1098
- actorTarget: 'exclude-following',
1099
- },
1100
- ],
1101
- hiddenPosts: [],
1102
- },
1103
- labelDefs: {},
1104
- }
1105
-
1106
- it(`followed actor`, () => {
1107
- const res = moderatePost(
1108
- mock.postView({
1109
- record: mock.post({
1110
- text: 'Mute words!',
1111
- }),
1112
- author: mock.profileViewBasic({
1113
- handle: 'bob.test',
1114
- displayName: 'Bob',
1115
- viewer: {
1116
- following: 'true',
1117
- },
1118
- }),
1119
- labels: [],
1120
- }),
1121
- viewer,
1122
- )
1123
- expect(res.causes.length).toBe(0)
1124
- })
1125
-
1126
- it(`non-followed actor`, () => {
1127
- const res = moderatePost(
1128
- mock.postView({
1129
- record: mock.post({
1130
- text: 'Mute words!',
1131
- }),
1132
- author: mock.profileViewBasic({
1133
- handle: 'carla.test',
1134
- displayName: 'Carla',
1135
- viewer: {
1136
- following: undefined,
1137
- },
1138
- }),
1139
- labels: [],
1140
- }),
1141
- viewer,
1142
- )
1143
- expect(res.causes[0].type).toBe('mute-word')
1144
- })
1145
- })
1146
-
1147
- describe(`returning MuteWordMatch`, () => {
1148
- it(`matches all`, () => {
1149
- const rt = new RichText({
1150
- text: `This is a post about javascript`,
1151
- })
1152
- rt.detectFacetsWithoutResolution()
1153
-
1154
- const muteWord1 = {
1155
- value: 'post',
1156
- targets: ['content'],
1157
- actorTarget: 'all',
1158
- }
1159
- const muteWord2 = {
1160
- value: 'javascript',
1161
- targets: ['content'],
1162
- actorTarget: 'all',
1163
- }
1164
- const match = matchMuteWords({
1165
- mutedWords: [muteWord1, muteWord2],
1166
- text: rt.text,
1167
- facets: rt.facets,
1168
- outlineTags: [],
1169
- })
1170
-
1171
- expect(match).toEqual([
1172
- { word: muteWord1, predicate: muteWord1.value },
1173
- { word: muteWord2, predicate: muteWord2.value },
1174
- ])
1175
- })
1176
- })
1177
-
1178
- describe(`gallery embed alt text`, () => {
1179
- const bob = mock.profileViewBasic({
1180
- handle: 'bob.test',
1181
- displayName: 'Bob',
1182
- })
1183
- const carol = mock.profileViewBasic({
1184
- handle: 'carol.test',
1185
- displayName: 'Carol',
1186
- })
1187
-
1188
- it(`matches alt text on a directly-attached gallery embed`, () => {
1189
- const res = moderatePost(
1190
- mock.postView({
1191
- record: mock.post({
1192
- text: 'innocent text',
1193
- embed: galleryRecord(['fine', 'this contains badword here']),
1194
- }),
1195
- author: bob,
1196
- labels: [],
1197
- }),
1198
- galleryMuteOpts(),
1199
- )
1200
- expect(res.causes.find((c) => c.type === 'mute-word')).toBeDefined()
1201
- })
1202
-
1203
- it(`does not match when no item alt contains a muted word`, () => {
1204
- const res = moderatePost(
1205
- mock.postView({
1206
- record: mock.post({
1207
- text: 'innocent text',
1208
- embed: galleryRecord(['nothing here', 'still nothing']),
1209
- }),
1210
- author: bob,
1211
- labels: [],
1212
- }),
1213
- galleryMuteOpts(),
1214
- )
1215
- expect(res.causes.find((c) => c.type === 'mute-word')).toBeUndefined()
1216
- })
1217
-
1218
- it(`matches alt text inside a quoted record's gallery`, () => {
1219
- const quoted: AppBskyFeedPost.Record = mock.post({
1220
- text: 'inside quoted',
1221
- embed: galleryRecord(['contains badword inside']),
1222
- })
1223
- const res = moderatePost(
1224
- mock.postView({
1225
- record: mock.post({ text: 'outer post' }),
1226
- embed: mock.embedRecordView({
1227
- record: quoted,
1228
- author: carol,
1229
- labels: [],
1230
- }),
1231
- author: bob,
1232
- labels: [],
1233
- }),
1234
- galleryMuteOpts(),
1235
- )
1236
- expect(res.causes.find((c) => c.type === 'mute-word')).toBeDefined()
1237
- })
1238
-
1239
- it(`matches alt text on a quoted record's recordWithMedia gallery`, () => {
1240
- const quotedRecord: AppBskyFeedPost.Record = mock.post({
1241
- text: 'inside quoted record-with-media',
1242
- embed: {
1243
- $type: 'app.bsky.embed.recordWithMedia',
1244
- record: {
1245
- $type: 'app.bsky.embed.record',
1246
- record: {
1247
- uri: 'at://did:web:dave.test/app.bsky.feed.post/inner',
1248
- cid: FAKE_CID,
1249
- },
1250
- } satisfies $Typed<AppBskyEmbedRecord.Main>,
1251
- media: galleryRecord(['contains badword in nested gallery']),
1252
- } satisfies $Typed<AppBskyEmbedRecordWithMedia.Main>,
1253
- })
1254
- const res = moderatePost(
1255
- mock.postView({
1256
- record: mock.post({ text: 'outer post' }),
1257
- embed: mock.embedRecordView({
1258
- record: quotedRecord,
1259
- author: carol,
1260
- labels: [],
1261
- }),
1262
- author: bob,
1263
- labels: [],
1264
- }),
1265
- galleryMuteOpts(),
1266
- )
1267
- expect(res.causes.find((c) => c.type === 'mute-word')).toBeDefined()
1268
- })
1269
-
1270
- it(`matches alt text on the view-side recordWithMedia gallery`, () => {
1271
- const recordWithMediaView: $Typed<AppBskyEmbedRecordWithMedia.View> = {
1272
- $type: 'app.bsky.embed.recordWithMedia#view',
1273
- record: {
1274
- $type: 'app.bsky.embed.record#view',
1275
- record: {
1276
- $type: 'app.bsky.embed.record#viewRecord',
1277
- uri: 'at://did:web:dave.test/app.bsky.feed.post/inner',
1278
- cid: FAKE_CID,
1279
- author: carol,
1280
- value: mock.post({ text: 'inner content' }),
1281
- labels: [],
1282
- indexedAt: new Date().toISOString(),
1283
- },
1284
- },
1285
- media: galleryView(['contains badword on view side']),
1286
- }
1287
- const res = moderatePost(
1288
- mock.postView({
1289
- record: mock.post({ text: 'outer post' }),
1290
- embed: recordWithMediaView,
1291
- author: bob,
1292
- labels: [],
1293
- }),
1294
- galleryMuteOpts(),
1295
- )
1296
- expect(res.causes.find((c) => c.type === 'mute-word')).toBeDefined()
1297
- })
1298
- })
1299
- })