@atproto/api 0.20.23 → 0.20.26

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 (74) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/agent.d.ts +4 -0
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js +17 -0
  5. package/dist/agent.js.map +1 -1
  6. package/dist/client/lexicons.d.ts +8 -0
  7. package/dist/client/lexicons.d.ts.map +1 -1
  8. package/dist/client/lexicons.js +4 -0
  9. package/dist/client/lexicons.js.map +1 -1
  10. package/dist/client/types/app/bsky/actor/defs.d.ts +2 -0
  11. package/dist/client/types/app/bsky/actor/defs.d.ts.map +1 -1
  12. package/dist/client/types/app/bsky/actor/defs.js.map +1 -1
  13. package/dist/rich-text/detection.d.ts.map +1 -1
  14. package/dist/rich-text/detection.js +4 -1
  15. package/dist/rich-text/detection.js.map +1 -1
  16. package/dist/types.d.ts +1 -0
  17. package/dist/types.d.ts.map +1 -1
  18. package/dist/types.js.map +1 -1
  19. package/package.json +22 -16
  20. package/definitions/labels.json +0 -170
  21. package/docs/moderation.md +0 -260
  22. package/jest.config.cjs +0 -23
  23. package/jest.d.ts +0 -20
  24. package/jest.setup.ts +0 -97
  25. package/scripts/code/labels.mjs +0 -77
  26. package/scripts/generate-code.mjs +0 -4
  27. package/src/age-assurance.test.ts +0 -218
  28. package/src/age-assurance.ts +0 -137
  29. package/src/agent.ts +0 -1645
  30. package/src/atp-agent.ts +0 -570
  31. package/src/bsky-agent.ts +0 -14
  32. package/src/const.ts +0 -1
  33. package/src/index.ts +0 -44
  34. package/src/mocker.ts +0 -220
  35. package/src/moderation/decision.ts +0 -389
  36. package/src/moderation/index.ts +0 -69
  37. package/src/moderation/mutewords.ts +0 -170
  38. package/src/moderation/subjects/account.ts +0 -44
  39. package/src/moderation/subjects/feed-generator.ts +0 -24
  40. package/src/moderation/subjects/notification.ts +0 -25
  41. package/src/moderation/subjects/post.ts +0 -433
  42. package/src/moderation/subjects/profile.ts +0 -26
  43. package/src/moderation/subjects/status.ts +0 -27
  44. package/src/moderation/subjects/user-list.ts +0 -48
  45. package/src/moderation/types.ts +0 -191
  46. package/src/moderation/ui.ts +0 -21
  47. package/src/moderation/util.ts +0 -111
  48. package/src/predicate.ts +0 -52
  49. package/src/rich-text/detection.ts +0 -146
  50. package/src/rich-text/rich-text.ts +0 -418
  51. package/src/rich-text/sanitization.ts +0 -42
  52. package/src/rich-text/unicode.ts +0 -47
  53. package/src/rich-text/util.ts +0 -15
  54. package/src/session-manager.ts +0 -5
  55. package/src/types.ts +0 -165
  56. package/src/util.ts +0 -96
  57. package/tests/atp-agent.test.ts +0 -3863
  58. package/tests/dispatcher.test.ts +0 -527
  59. package/tests/errors.test.ts +0 -29
  60. package/tests/moderation-behaviors.test.ts +0 -951
  61. package/tests/moderation-custom-labels.test.ts +0 -334
  62. package/tests/moderation-mutewords.test.ts +0 -1299
  63. package/tests/moderation-prefs.test.ts +0 -402
  64. package/tests/moderation-quoteposts.test.ts +0 -277
  65. package/tests/moderation.test.ts +0 -739
  66. package/tests/rich-text-detection.test.ts +0 -456
  67. package/tests/rich-text-sanitization.test.ts +0 -216
  68. package/tests/rich-text.test.ts +0 -705
  69. package/tests/util/echo-server.ts +0 -37
  70. package/tests/util/moderation-behavior.ts +0 -268
  71. package/tsconfig.build.json +0 -9
  72. package/tsconfig.build.tsbuildinfo +0 -1
  73. package/tsconfig.json +0 -7
  74. 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
- })