@atproto/api 0.10.2 → 0.10.3

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.
package/dist/util.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function sanitizeMutedWordValue(value: string): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/api",
3
- "version": "0.10.2",
3
+ "version": "0.10.3",
4
4
  "license": "MIT",
5
5
  "description": "Client library for atproto and Bluesky",
6
6
  "keywords": [
@@ -28,7 +28,7 @@
28
28
  "devDependencies": {
29
29
  "common-tags": "^1.8.2",
30
30
  "@atproto/lex-cli": "^0.3.1",
31
- "@atproto/dev-env": "^0.2.34"
31
+ "@atproto/dev-env": "^0.2.35"
32
32
  },
33
33
  "scripts": {
34
34
  "codegen": "pnpm docgen && node ./scripts/generate-code.mjs && lex gen-api ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*",
package/src/bsky-agent.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  BskyThreadViewPreference,
14
14
  BskyInterestsPreference,
15
15
  } from './types'
16
+ import { sanitizeMutedWordValue } from './util'
16
17
 
17
18
  const FEED_VIEW_PREF_DEFAULTS = {
18
19
  hideReplies: false,
@@ -565,16 +566,108 @@ export class BskyAgent extends AtpAgent {
565
566
  })
566
567
  }
567
568
 
568
- async upsertMutedWords(mutedWords: AppBskyActorDefs.MutedWord[]) {
569
- await updateMutedWords(this, mutedWords, 'upsert')
569
+ async upsertMutedWords(newMutedWords: AppBskyActorDefs.MutedWord[]) {
570
+ await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => {
571
+ let mutedWordsPref = prefs.findLast(
572
+ (pref) =>
573
+ AppBskyActorDefs.isMutedWordsPref(pref) &&
574
+ AppBskyActorDefs.validateMutedWordsPref(pref).success,
575
+ )
576
+
577
+ if (mutedWordsPref && AppBskyActorDefs.isMutedWordsPref(mutedWordsPref)) {
578
+ for (const updatedWord of newMutedWords) {
579
+ let foundMatch = false
580
+ const sanitizedUpdatedValue = sanitizeMutedWordValue(
581
+ updatedWord.value,
582
+ )
583
+
584
+ // was trimmed down to an empty string e.g. single `#`
585
+ if (!sanitizedUpdatedValue) continue
586
+
587
+ for (const existingItem of mutedWordsPref.items) {
588
+ if (existingItem.value === sanitizedUpdatedValue) {
589
+ existingItem.targets = Array.from(
590
+ new Set([...existingItem.targets, ...updatedWord.targets]),
591
+ )
592
+ foundMatch = true
593
+ break
594
+ }
595
+ }
596
+
597
+ if (!foundMatch) {
598
+ mutedWordsPref.items.push({
599
+ ...updatedWord,
600
+ value: sanitizedUpdatedValue,
601
+ })
602
+ }
603
+ }
604
+ } else {
605
+ // if the pref doesn't exist, create it
606
+ mutedWordsPref = {
607
+ items: newMutedWords.map((w) => ({
608
+ ...w,
609
+ value: sanitizeMutedWordValue(w.value),
610
+ })),
611
+ }
612
+ }
613
+
614
+ return prefs
615
+ .filter((p) => !AppBskyActorDefs.isMutedWordsPref(p))
616
+ .concat([
617
+ { ...mutedWordsPref, $type: 'app.bsky.actor.defs#mutedWordsPref' },
618
+ ])
619
+ })
570
620
  }
571
621
 
572
622
  async updateMutedWord(mutedWord: AppBskyActorDefs.MutedWord) {
573
- await updateMutedWords(this, [mutedWord], 'update')
623
+ await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => {
624
+ let mutedWordsPref = prefs.findLast(
625
+ (pref) =>
626
+ AppBskyActorDefs.isMutedWordsPref(pref) &&
627
+ AppBskyActorDefs.validateMutedWordsPref(pref).success,
628
+ )
629
+
630
+ if (mutedWordsPref && AppBskyActorDefs.isMutedWordsPref(mutedWordsPref)) {
631
+ for (const existingItem of mutedWordsPref.items) {
632
+ if (existingItem.value === mutedWord.value) {
633
+ existingItem.targets = mutedWord.targets
634
+ break
635
+ }
636
+ }
637
+ }
638
+
639
+ return prefs
640
+ .filter((p) => !AppBskyActorDefs.isMutedWordsPref(p))
641
+ .concat([
642
+ { ...mutedWordsPref, $type: 'app.bsky.actor.defs#mutedWordsPref' },
643
+ ])
644
+ })
574
645
  }
575
646
 
576
647
  async removeMutedWord(mutedWord: AppBskyActorDefs.MutedWord) {
577
- await updateMutedWords(this, [mutedWord], 'remove')
648
+ await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => {
649
+ let mutedWordsPref = prefs.findLast(
650
+ (pref) =>
651
+ AppBskyActorDefs.isMutedWordsPref(pref) &&
652
+ AppBskyActorDefs.validateMutedWordsPref(pref).success,
653
+ )
654
+
655
+ if (mutedWordsPref && AppBskyActorDefs.isMutedWordsPref(mutedWordsPref)) {
656
+ for (let i = 0; i < mutedWordsPref.items.length; i++) {
657
+ const existing = mutedWordsPref.items[i]
658
+ if (existing.value === mutedWord.value) {
659
+ mutedWordsPref.items.splice(i, 1)
660
+ break
661
+ }
662
+ }
663
+ }
664
+
665
+ return prefs
666
+ .filter((p) => !AppBskyActorDefs.isMutedWordsPref(p))
667
+ .concat([
668
+ { ...mutedWordsPref, $type: 'app.bsky.actor.defs#mutedWordsPref' },
669
+ ])
670
+ })
578
671
  }
579
672
 
580
673
  async hidePost(postUri: string) {
@@ -646,76 +739,6 @@ async function updateFeedPreferences(
646
739
  return res
647
740
  }
648
741
 
649
- /**
650
- * A helper specifically for updating muted words preferences
651
- */
652
- async function updateMutedWords(
653
- agent: BskyAgent,
654
- mutedWords: AppBskyActorDefs.MutedWord[],
655
- action: 'upsert' | 'update' | 'remove',
656
- ) {
657
- const sanitizeMutedWord = (word: AppBskyActorDefs.MutedWord) => ({
658
- value: word.value.replace(/^#/, ''),
659
- targets: word.targets,
660
- })
661
-
662
- await updatePreferences(agent, (prefs: AppBskyActorDefs.Preferences) => {
663
- let mutedWordsPref = prefs.findLast(
664
- (pref) =>
665
- AppBskyActorDefs.isMutedWordsPref(pref) &&
666
- AppBskyActorDefs.validateMutedWordsPref(pref).success,
667
- )
668
-
669
- if (mutedWordsPref && AppBskyActorDefs.isMutedWordsPref(mutedWordsPref)) {
670
- if (action === 'upsert' || action === 'update') {
671
- for (const word of mutedWords) {
672
- let foundMatch = false
673
-
674
- for (const existingItem of mutedWordsPref.items) {
675
- if (existingItem.value === sanitizeMutedWord(word).value) {
676
- existingItem.targets =
677
- action === 'upsert'
678
- ? Array.from(
679
- new Set([...existingItem.targets, ...word.targets]),
680
- )
681
- : word.targets
682
- foundMatch = true
683
- break
684
- }
685
- }
686
-
687
- if (action === 'upsert' && !foundMatch) {
688
- mutedWordsPref.items.push(sanitizeMutedWord(word))
689
- }
690
- }
691
- } else if (action === 'remove') {
692
- for (const word of mutedWords) {
693
- for (let i = 0; i < mutedWordsPref.items.length; i++) {
694
- const existing = mutedWordsPref.items[i]
695
- if (existing.value === sanitizeMutedWord(word).value) {
696
- mutedWordsPref.items.splice(i, 1)
697
- break
698
- }
699
- }
700
- }
701
- }
702
- } else {
703
- // if the pref doesn't exist, create it
704
- if (action === 'upsert') {
705
- mutedWordsPref = {
706
- items: mutedWords.map(sanitizeMutedWord),
707
- }
708
- }
709
- }
710
-
711
- return prefs
712
- .filter((p) => !AppBskyActorDefs.isMutedWordsPref(p))
713
- .concat([
714
- { ...mutedWordsPref, $type: 'app.bsky.actor.defs#mutedWordsPref' },
715
- ])
716
- })
717
- }
718
-
719
742
  async function updateHiddenPost(
720
743
  agent: BskyAgent,
721
744
  postUri: string,
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ export {
8
8
  } from '@atproto/lexicon'
9
9
  export { parseLanguage } from '@atproto/common-web'
10
10
  export * from './types'
11
+ export * from './util'
11
12
  export * from './client'
12
13
  export * from './agent'
13
14
  export * from './rich-text/rich-text'
package/src/util.ts ADDED
@@ -0,0 +1,6 @@
1
+ export function sanitizeMutedWordValue(value: string) {
2
+ return value
3
+ .trim()
4
+ .replace(/^#(?!\ufe0f)/, '')
5
+ .replace(/[\r\n\u00AD\u2060\u200D\u200C\u200B]+/, '')
6
+ }
@@ -1202,13 +1202,18 @@ describe('agent', () => {
1202
1202
  await agent.upsertMutedWords([
1203
1203
  { value: 'hashtag', targets: ['content'] },
1204
1204
  ])
1205
+ // is sanitized to `hashtag`
1205
1206
  await agent.upsertMutedWords([{ value: '#hashtag', targets: ['tag'] }])
1207
+
1206
1208
  const { mutedWords } = await agent.getPreferences()
1209
+
1207
1210
  expect(mutedWords.find((m) => m.value === '#hashtag')).toBeFalsy()
1211
+ // merged with existing
1208
1212
  expect(mutedWords.find((m) => m.value === 'hashtag')).toStrictEqual({
1209
1213
  value: 'hashtag',
1210
1214
  targets: ['content', 'tag'],
1211
1215
  })
1216
+ // only one added
1212
1217
  expect(mutedWords.filter((m) => m.value === 'hashtag').length).toBe(1)
1213
1218
  })
1214
1219
 
@@ -1237,15 +1242,21 @@ describe('agent', () => {
1237
1242
  expect(mutedWords.find((m) => m.value === 'no_exist')).toBeFalsy()
1238
1243
  })
1239
1244
 
1240
- it('updateMutedWord with #', async () => {
1245
+ it('updateMutedWord with #, does not update', async () => {
1246
+ await agent.upsertMutedWords([
1247
+ {
1248
+ value: '#just_a_tag',
1249
+ targets: ['tag'],
1250
+ },
1251
+ ])
1241
1252
  await agent.updateMutedWord({
1242
- value: 'hashtag',
1253
+ value: '#just_a_tag',
1243
1254
  targets: ['tag', 'content'],
1244
1255
  })
1245
1256
  const { mutedWords } = await agent.getPreferences()
1246
- expect(mutedWords.find((m) => m.value === 'hashtag')).toStrictEqual({
1247
- value: 'hashtag',
1248
- targets: ['tag', 'content'],
1257
+ expect(mutedWords.find((m) => m.value === 'just_a_tag')).toStrictEqual({
1258
+ value: 'just_a_tag',
1259
+ targets: ['tag'],
1249
1260
  })
1250
1261
  })
1251
1262
 
@@ -1262,11 +1273,124 @@ describe('agent', () => {
1262
1273
  expect(mutedWords.find((m) => m.value === 'tag_then_none')).toBeFalsy()
1263
1274
  })
1264
1275
 
1265
- it('removeMutedWord with #', async () => {
1276
+ it('removeMutedWord with #, no match, no removal', async () => {
1266
1277
  await agent.removeMutedWord({ value: '#hashtag', targets: [] })
1267
1278
  const { mutedWords } = await agent.getPreferences()
1268
1279
 
1269
- expect(mutedWords.find((m) => m.value === 'hashtag')).toBeFalsy()
1280
+ // was inserted with #hashtag, but we don't sanitize on remove
1281
+ expect(mutedWords.find((m) => m.value === 'hashtag')).toBeTruthy()
1282
+ })
1283
+
1284
+ it('single-hash #', async () => {
1285
+ const prev = await agent.getPreferences()
1286
+ const length = prev.mutedWords.length
1287
+ await agent.upsertMutedWords([{ value: '#', targets: [] }])
1288
+ const end = await agent.getPreferences()
1289
+
1290
+ // sanitized to empty string, not inserted
1291
+ expect(end.mutedWords.length).toEqual(length)
1292
+ })
1293
+
1294
+ it('multi-hash ##', async () => {
1295
+ await agent.upsertMutedWords([{ value: '##', targets: [] }])
1296
+ const { mutedWords } = await agent.getPreferences()
1297
+
1298
+ expect(mutedWords.find((m) => m.value === '#')).toBeTruthy()
1299
+ })
1300
+
1301
+ it('multi-hash ##hashtag', async () => {
1302
+ await agent.upsertMutedWords([{ value: '##hashtag', targets: [] }])
1303
+ const a = await agent.getPreferences()
1304
+
1305
+ expect(a.mutedWords.find((w) => w.value === '#hashtag')).toBeTruthy()
1306
+
1307
+ await agent.removeMutedWord({ value: '#hashtag', targets: [] })
1308
+ const b = await agent.getPreferences()
1309
+
1310
+ expect(b.mutedWords.find((w) => w.value === '#hashtag')).toBeFalsy()
1311
+ })
1312
+
1313
+ it('hash emoji #️⃣', async () => {
1314
+ await agent.upsertMutedWords([{ value: '#️⃣', targets: [] }])
1315
+ const { mutedWords } = await agent.getPreferences()
1316
+
1317
+ expect(mutedWords.find((m) => m.value === '#️⃣')).toBeTruthy()
1318
+
1319
+ await agent.removeMutedWord({ value: '#️⃣', targets: [] })
1320
+ const end = await agent.getPreferences()
1321
+
1322
+ expect(end.mutedWords.find((m) => m.value === '#️⃣')).toBeFalsy()
1323
+ })
1324
+
1325
+ it('hash emoji ##️⃣', async () => {
1326
+ await agent.upsertMutedWords([{ value: '##️⃣', targets: [] }])
1327
+ const { mutedWords } = await agent.getPreferences()
1328
+
1329
+ expect(mutedWords.find((m) => m.value === '#️⃣')).toBeTruthy()
1330
+
1331
+ await agent.removeMutedWord({ value: '#️⃣', targets: [] })
1332
+ const end = await agent.getPreferences()
1333
+
1334
+ expect(end.mutedWords.find((m) => m.value === '#️⃣')).toBeFalsy()
1335
+ })
1336
+
1337
+ it('hash emoji ###️⃣', async () => {
1338
+ await agent.upsertMutedWords([{ value: '###️⃣', targets: [] }])
1339
+ const { mutedWords } = await agent.getPreferences()
1340
+
1341
+ expect(mutedWords.find((m) => m.value === '##️⃣')).toBeTruthy()
1342
+
1343
+ await agent.removeMutedWord({ value: '##️⃣', targets: [] })
1344
+ const end = await agent.getPreferences()
1345
+
1346
+ expect(end.mutedWords.find((m) => m.value === '##️⃣')).toBeFalsy()
1347
+ })
1348
+
1349
+ describe(`invalid characters`, () => {
1350
+ it('zero width space', async () => {
1351
+ const prev = await agent.getPreferences()
1352
+ const length = prev.mutedWords.length
1353
+ await agent.upsertMutedWords([{ value: '#​', targets: [] }])
1354
+ const { mutedWords } = await agent.getPreferences()
1355
+
1356
+ expect(mutedWords.length).toEqual(length)
1357
+ })
1358
+
1359
+ it('newline', async () => {
1360
+ await agent.upsertMutedWords([
1361
+ { value: 'test value\n with newline', targets: [] },
1362
+ ])
1363
+ const { mutedWords } = await agent.getPreferences()
1364
+
1365
+ expect(
1366
+ mutedWords.find((m) => m.value === 'test value with newline'),
1367
+ ).toBeTruthy()
1368
+ })
1369
+
1370
+ it('newline(s)', async () => {
1371
+ await agent.upsertMutedWords([
1372
+ { value: 'test value\n\r with newline', targets: [] },
1373
+ ])
1374
+ const { mutedWords } = await agent.getPreferences()
1375
+
1376
+ expect(
1377
+ mutedWords.find((m) => m.value === 'test value with newline'),
1378
+ ).toBeTruthy()
1379
+ })
1380
+
1381
+ it('empty space', async () => {
1382
+ await agent.upsertMutedWords([{ value: ' ', targets: [] }])
1383
+ const { mutedWords } = await agent.getPreferences()
1384
+
1385
+ expect(mutedWords.find((m) => m.value === ' ')).toBeFalsy()
1386
+ })
1387
+
1388
+ it('leading/trailing space', async () => {
1389
+ await agent.upsertMutedWords([{ value: ' trim ', targets: [] }])
1390
+ const { mutedWords } = await agent.getPreferences()
1391
+
1392
+ expect(mutedWords.find((m) => m.value === 'trim')).toBeTruthy()
1393
+ })
1270
1394
  })
1271
1395
  })
1272
1396
 
@@ -309,6 +309,16 @@ describe('detectFacets', () => {
309
309
  },
310
310
  ],
311
311
  ],
312
+ [
313
+ 'this #t\nag should be a tag',
314
+ ['t'],
315
+ [
316
+ {
317
+ byteStart: 5,
318
+ byteEnd: 7,
319
+ },
320
+ ],
321
+ ],
312
322
  ]
313
323
 
314
324
  for (const [input, tags, indices] of inputs) {