@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/CHANGELOG.md +12 -0
- package/dist/bsky-agent.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +70 -49
- package/dist/index.js.map +3 -3
- package/dist/util.d.ts +1 -0
- package/package.json +2 -2
- package/src/bsky-agent.ts +97 -74
- package/src/index.ts +1 -0
- package/src/util.ts +6 -0
- package/tests/bsky-agent.test.ts +131 -7
- package/tests/rich-text-detection.test.ts +10 -0
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.
|
|
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.
|
|
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(
|
|
569
|
-
await
|
|
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
|
|
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
|
|
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
package/src/util.ts
ADDED
package/tests/bsky-agent.test.ts
CHANGED
|
@@ -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
|
|
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: '
|
|
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 === '
|
|
1247
|
-
value: '
|
|
1248
|
-
targets: ['tag'
|
|
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
|
|
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
|
-
|
|
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
|
|