@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
package/src/agent.ts DELETED
@@ -1,1645 +0,0 @@
1
- import AwaitLock from 'await-lock'
2
- import { TID, retry } from '@atproto/common-web'
3
- import { AtUri, DidString, ensureValidDidRegex } from '@atproto/syntax'
4
- import {
5
- FetchHandler,
6
- FetchHandlerOptions,
7
- XrpcClient,
8
- buildFetchHandler,
9
- } from '@atproto/xrpc'
10
- import {
11
- AppBskyActorDefs,
12
- AppBskyActorProfile,
13
- AppBskyFeedPost,
14
- AppBskyLabelerDefs,
15
- AppNS,
16
- ChatNS,
17
- ComAtprotoRepoPutRecord,
18
- ComNS,
19
- ToolsNS,
20
- } from './client/index.js'
21
- import { schemas } from './client/lexicons.js'
22
- import { MutedWord, Nux } from './client/types/app/bsky/actor/defs.js'
23
- import { $Typed, Un$Typed } from './client/util.js'
24
- import { BSKY_LABELER_DID } from './const.js'
25
- import { DEFAULT_LABEL_SETTINGS } from './moderation/const/labels.js'
26
- import { interpretLabelValueDefinitions } from './moderation/index.js'
27
- import {
28
- InterpretedLabelValueDefinition,
29
- LabelPreference,
30
- ModerationPrefs,
31
- } from './moderation/types.js'
32
- import * as predicate from './predicate.js'
33
- import { SessionManager } from './session-manager.js'
34
- import {
35
- AtpAgentGlobalOpts,
36
- AtprotoProxy,
37
- AtprotoServiceType,
38
- BskyFeedViewPreference,
39
- BskyInterestsPreference,
40
- BskyPreferences,
41
- BskyThreadViewPreference,
42
- asAtprotoProxy,
43
- asDid,
44
- isDid,
45
- } from './types.js'
46
- import {
47
- getSavedFeedType,
48
- sanitizeMutedWordValue,
49
- savedFeedsToUriArrays,
50
- validateNux,
51
- validateSavedFeed,
52
- } from './util.js'
53
-
54
- const FEED_VIEW_PREF_DEFAULTS = {
55
- hideReplies: false,
56
- hideRepliesByUnfollowed: true,
57
- hideRepliesByLikeCount: 0,
58
- hideReposts: false,
59
- hideQuotePosts: false,
60
- }
61
-
62
- const THREAD_VIEW_PREF_DEFAULTS = {
63
- sort: 'hotness',
64
- }
65
-
66
- export type { DidString, FetchHandler }
67
-
68
- /**
69
- * An {@link Agent} is an {@link AtpBaseClient} with the following
70
- * additional features:
71
- * - AT Protocol labelers configuration utilities
72
- * - AT Protocol proxy configuration utilities
73
- * - Cloning utilities
74
- * - `app.bsky` syntactic sugar
75
- * - `com.atproto` syntactic sugar
76
- */
77
- export class Agent extends XrpcClient {
78
- //#region Static configuration
79
-
80
- /**
81
- * The labelers to be used across all requests with the takedown capability
82
- */
83
- static appLabelers: readonly string[] = [BSKY_LABELER_DID]
84
-
85
- /**
86
- * Configures the Agent (or its sub classes) globally.
87
- */
88
- static configure(opts: AtpAgentGlobalOpts) {
89
- if (opts.appLabelers) {
90
- this.appLabelers = opts.appLabelers.map(asDid) // Validate & copy
91
- }
92
- }
93
-
94
- //#endregion
95
-
96
- com = new ComNS(this)
97
- app = new AppNS(this)
98
- chat = new ChatNS(this)
99
- tools = new ToolsNS(this)
100
-
101
- /** @deprecated use `this` instead */
102
- get xrpc(): XrpcClient {
103
- return this
104
- }
105
-
106
- readonly sessionManager: SessionManager
107
-
108
- constructor(options: SessionManager | FetchHandler | FetchHandlerOptions) {
109
- const sessionManager: SessionManager =
110
- typeof options === 'object' && 'fetchHandler' in options
111
- ? options
112
- : {
113
- did: undefined,
114
- fetchHandler: buildFetchHandler(options),
115
- }
116
-
117
- super((url, init) => {
118
- const headers = new Headers(init?.headers)
119
-
120
- if (this.proxy && !headers.has('atproto-proxy')) {
121
- headers.set('atproto-proxy', this.proxy)
122
- }
123
-
124
- // Merge the labelers header of this particular request with the app &
125
- // instance labelers.
126
- headers.set(
127
- 'atproto-accept-labelers',
128
- [
129
- ...this.appLabelers.map((l) => `${l};redact`),
130
- ...this.labelers,
131
- headers.get('atproto-accept-labelers')?.trim(),
132
- ]
133
- .filter(Boolean)
134
- .join(', '),
135
- )
136
-
137
- return this.sessionManager.fetchHandler(url, { ...init, headers })
138
- }, schemas)
139
-
140
- this.sessionManager = sessionManager
141
- }
142
-
143
- //#region Cloning utilities
144
-
145
- clone(): Agent {
146
- return this.copyInto(new Agent(this.sessionManager))
147
- }
148
-
149
- copyInto<T extends Agent>(inst: T): T {
150
- inst.configureLabelers(this.labelers)
151
- inst.configureProxy(this.proxy ?? null)
152
- inst.clearHeaders()
153
- for (const [key, value] of this.headers) inst.setHeader(key, value)
154
- return inst
155
- }
156
-
157
- withProxy(serviceType: AtprotoServiceType, did: string) {
158
- const inst = this.clone()
159
- inst.configureProxy(`${asDid(did)}#${serviceType}`)
160
- return inst as ReturnType<this['clone']>
161
- }
162
-
163
- //#endregion
164
-
165
- //#region ATPROTO labelers configuration utilities
166
-
167
- /**
168
- * The labelers statically configured on the class of the current instance.
169
- */
170
- get appLabelers() {
171
- return (this.constructor as typeof Agent).appLabelers
172
- }
173
-
174
- labelers: readonly string[] = []
175
-
176
- configureLabelers(labelerDids: readonly string[]) {
177
- this.labelers = labelerDids.map(asDid) // Validate & copy
178
- }
179
-
180
- /** @deprecated use {@link configureLabelers} instead */
181
- configureLabelersHeader(labelerDids: readonly string[]) {
182
- // Filtering non-did values for backwards compatibility
183
- this.configureLabelers(labelerDids.filter(isDid))
184
- }
185
-
186
- //#endregion
187
-
188
- //#region ATPROTO proxy configuration utilities
189
-
190
- proxy?: AtprotoProxy
191
-
192
- configureProxy(value: AtprotoProxy | null) {
193
- if (value === null) this.proxy = undefined
194
- else this.proxy = asAtprotoProxy(value)
195
- }
196
-
197
- /** @deprecated use {@link configureProxy} instead */
198
- configureProxyHeader(serviceType: AtprotoServiceType, did: string) {
199
- // Ignoring non-did values for backwards compatibility
200
- if (isDid(did)) this.configureProxy(`${did}#${serviceType}`)
201
- }
202
-
203
- //#endregion
204
-
205
- //#region Session management
206
-
207
- /**
208
- * Get the authenticated user's DID, if any.
209
- */
210
- get did(): DidString | undefined {
211
- const { did } = this.sessionManager
212
- if (!did) return undefined
213
- ensureValidDidRegex(did)
214
- return did
215
- }
216
-
217
- /** @deprecated Use {@link Agent.assertDid} instead */
218
- get accountDid() {
219
- return this.assertDid
220
- }
221
-
222
- /**
223
- * Get the authenticated user's DID, or throw an error if not authenticated.
224
- */
225
- get assertDid(): DidString {
226
- this.assertAuthenticated()
227
- return this.did
228
- }
229
-
230
- /**
231
- * Assert that the user is authenticated.
232
- */
233
- public assertAuthenticated(): asserts this is { did: DidString } {
234
- if (!this.did) throw new Error('Not logged in')
235
- }
236
-
237
- //#endregion
238
-
239
- /** @deprecated use "this" instead */
240
- get api() {
241
- return this
242
- }
243
-
244
- //#region "com.atproto" lexicon short hand methods
245
-
246
- /**
247
- * Upload a binary blob to the server
248
- */
249
- uploadBlob: typeof this.com.atproto.repo.uploadBlob = (data, opts) =>
250
- this.com.atproto.repo.uploadBlob(data, opts)
251
-
252
- /**
253
- * Resolve a handle to a DID
254
- */
255
- resolveHandle: typeof this.com.atproto.identity.resolveHandle = (
256
- params,
257
- opts,
258
- ) => this.com.atproto.identity.resolveHandle(params, opts)
259
-
260
- /**
261
- * Change the user's handle
262
- */
263
- updateHandle: typeof this.com.atproto.identity.updateHandle = (data, opts) =>
264
- this.com.atproto.identity.updateHandle(data, opts)
265
-
266
- /**
267
- * Create a moderation report
268
- */
269
- createModerationReport: typeof this.com.atproto.moderation.createReport = (
270
- data,
271
- opts,
272
- ) => this.com.atproto.moderation.createReport(data, opts)
273
-
274
- //#endregion
275
-
276
- //#region "app.bsky" lexicon short hand methods
277
-
278
- getTimeline: typeof this.app.bsky.feed.getTimeline = (params, opts) =>
279
- this.app.bsky.feed.getTimeline(params, opts)
280
-
281
- getAuthorFeed: typeof this.app.bsky.feed.getAuthorFeed = (params, opts) =>
282
- this.app.bsky.feed.getAuthorFeed(params, opts)
283
-
284
- getActorLikes: typeof this.app.bsky.feed.getActorLikes = (params, opts) =>
285
- this.app.bsky.feed.getActorLikes(params, opts)
286
-
287
- getPostThread: typeof this.app.bsky.feed.getPostThread = (params, opts) =>
288
- this.app.bsky.feed.getPostThread(params, opts)
289
-
290
- getPost: typeof this.app.bsky.feed.post.get = (params) =>
291
- this.app.bsky.feed.post.get(params)
292
-
293
- getPosts: typeof this.app.bsky.feed.getPosts = (params, opts) =>
294
- this.app.bsky.feed.getPosts(params, opts)
295
-
296
- getLikes: typeof this.app.bsky.feed.getLikes = (params, opts) =>
297
- this.app.bsky.feed.getLikes(params, opts)
298
-
299
- getRepostedBy: typeof this.app.bsky.feed.getRepostedBy = (params, opts) =>
300
- this.app.bsky.feed.getRepostedBy(params, opts)
301
-
302
- getFollows: typeof this.app.bsky.graph.getFollows = (params, opts) =>
303
- this.app.bsky.graph.getFollows(params, opts)
304
-
305
- getFollowers: typeof this.app.bsky.graph.getFollowers = (params, opts) =>
306
- this.app.bsky.graph.getFollowers(params, opts)
307
-
308
- getProfile: typeof this.app.bsky.actor.getProfile = (params, opts) =>
309
- this.app.bsky.actor.getProfile(params, opts)
310
-
311
- getProfiles: typeof this.app.bsky.actor.getProfiles = (params, opts) =>
312
- this.app.bsky.actor.getProfiles(params, opts)
313
-
314
- getSuggestions: typeof this.app.bsky.actor.getSuggestions = (params, opts) =>
315
- this.app.bsky.actor.getSuggestions(params, opts)
316
-
317
- searchActors: typeof this.app.bsky.actor.searchActors = (params, opts) =>
318
- this.app.bsky.actor.searchActors(params, opts)
319
-
320
- searchActorsTypeahead: typeof this.app.bsky.actor.searchActorsTypeahead = (
321
- params,
322
- opts,
323
- ) => this.app.bsky.actor.searchActorsTypeahead(params, opts)
324
-
325
- listNotifications: typeof this.app.bsky.notification.listNotifications = (
326
- params,
327
- opts,
328
- ) => this.app.bsky.notification.listNotifications(params, opts)
329
-
330
- countUnreadNotifications: typeof this.app.bsky.notification.getUnreadCount = (
331
- params,
332
- opts,
333
- ) => this.app.bsky.notification.getUnreadCount(params, opts)
334
-
335
- getLabelers: typeof this.app.bsky.labeler.getServices = (params, opts) =>
336
- this.app.bsky.labeler.getServices(params, opts)
337
-
338
- async getLabelDefinitions(
339
- prefs: BskyPreferences | ModerationPrefs | string[],
340
- ): Promise<Record<string, InterpretedLabelValueDefinition[]>> {
341
- // collect the labeler dids
342
- const dids: string[] = [...this.appLabelers]
343
- if (isBskyPrefs(prefs)) {
344
- dids.push(...prefs.moderationPrefs.labelers.map((l) => l.did))
345
- } else if (isModPrefs(prefs)) {
346
- dids.push(...prefs.labelers.map((l) => l.did))
347
- } else {
348
- dids.push(...prefs)
349
- }
350
-
351
- // fetch their definitions
352
- const labelers = await this.getLabelers({
353
- dids,
354
- detailed: true,
355
- })
356
-
357
- // assemble a map of labeler dids to the interpreted label value definitions
358
- const labelDefs = {}
359
- if (labelers.data) {
360
- for (const labeler of labelers.data
361
- .views as AppBskyLabelerDefs.LabelerViewDetailed[]) {
362
- labelDefs[labeler.creator.did] = interpretLabelValueDefinitions(labeler)
363
- }
364
- }
365
-
366
- return labelDefs
367
- }
368
-
369
- async post(
370
- record: Partial<AppBskyFeedPost.Record> &
371
- Omit<AppBskyFeedPost.Record, 'createdAt'>,
372
- ) {
373
- record.createdAt ||= new Date().toISOString()
374
- return this.app.bsky.feed.post.create(
375
- { repo: this.accountDid },
376
- record as AppBskyFeedPost.Record,
377
- )
378
- }
379
-
380
- async deletePost(postUri: string) {
381
- this.assertAuthenticated()
382
-
383
- const postUrip = new AtUri(postUri)
384
- return this.app.bsky.feed.post.delete({
385
- repo: postUrip.hostname,
386
- rkey: postUrip.rkey,
387
- })
388
- }
389
-
390
- async like(uri: string, cid: string, via?: { uri: string; cid: string }) {
391
- return this.app.bsky.feed.like.create(
392
- { repo: this.accountDid },
393
- {
394
- subject: { uri, cid },
395
- createdAt: new Date().toISOString(),
396
- via,
397
- },
398
- )
399
- }
400
-
401
- async deleteLike(likeUri: string) {
402
- this.assertAuthenticated()
403
-
404
- const likeUrip = new AtUri(likeUri)
405
- return this.app.bsky.feed.like.delete({
406
- repo: likeUrip.hostname,
407
- rkey: likeUrip.rkey,
408
- })
409
- }
410
-
411
- async repost(uri: string, cid: string, via?: { uri: string; cid: string }) {
412
- return this.app.bsky.feed.repost.create(
413
- { repo: this.accountDid },
414
- {
415
- subject: { uri, cid },
416
- createdAt: new Date().toISOString(),
417
- via,
418
- },
419
- )
420
- }
421
-
422
- async deleteRepost(repostUri: string) {
423
- this.assertAuthenticated()
424
-
425
- const repostUrip = new AtUri(repostUri)
426
- return this.app.bsky.feed.repost.delete({
427
- repo: repostUrip.hostname,
428
- rkey: repostUrip.rkey,
429
- })
430
- }
431
-
432
- async follow(subjectDid: string, via?: { uri: string; cid: string }) {
433
- return this.app.bsky.graph.follow.create(
434
- { repo: this.accountDid },
435
- {
436
- subject: subjectDid,
437
- createdAt: new Date().toISOString(),
438
- via,
439
- },
440
- )
441
- }
442
-
443
- async deleteFollow(followUri: string) {
444
- this.assertAuthenticated()
445
-
446
- const followUrip = new AtUri(followUri)
447
- return this.app.bsky.graph.follow.delete({
448
- repo: followUrip.hostname,
449
- rkey: followUrip.rkey,
450
- })
451
- }
452
-
453
- /**
454
- * @note: Using this method will reset the whole profile record if it
455
- * previously contained invalid values (wrt to the profile lexicon).
456
- */
457
- async upsertProfile(
458
- updateFn: (
459
- existing: AppBskyActorProfile.Record | undefined,
460
- ) =>
461
- | Un$Typed<AppBskyActorProfile.Record>
462
- | Promise<Un$Typed<AppBskyActorProfile.Record>>,
463
- ): Promise<void> {
464
- const upsert = async () => {
465
- const repo = this.assertDid
466
- const collection = 'app.bsky.actor.profile'
467
-
468
- const existing = await this.com.atproto.repo
469
- .getRecord({ repo, collection, rkey: 'self' })
470
- .catch((_) => undefined)
471
-
472
- const existingRecord: AppBskyActorProfile.Record | undefined =
473
- existing && predicate.isValidProfile(existing.data.value)
474
- ? existing.data.value
475
- : undefined
476
-
477
- // run the update
478
- const updated = await updateFn(existingRecord)
479
-
480
- // validate the value returned by the update function
481
- const validation = AppBskyActorProfile.validateRecord({
482
- $type: collection,
483
- ...updated,
484
- })
485
-
486
- if (!validation.success) {
487
- throw validation.error
488
- }
489
-
490
- await this.com.atproto.repo.putRecord({
491
- repo,
492
- collection,
493
- rkey: 'self',
494
- record: validation.value,
495
- swapRecord: existing?.data.cid || null,
496
- })
497
- }
498
-
499
- return retry(upsert, {
500
- maxRetries: 5,
501
- retryable: (e) => e instanceof ComAtprotoRepoPutRecord.InvalidSwapError,
502
- })
503
- }
504
-
505
- async mute(actor: string) {
506
- return this.app.bsky.graph.muteActor({ actor })
507
- }
508
-
509
- async unmute(actor: string) {
510
- return this.app.bsky.graph.unmuteActor({ actor })
511
- }
512
-
513
- async muteModList(uri: string) {
514
- return this.app.bsky.graph.muteActorList({ list: uri })
515
- }
516
-
517
- async unmuteModList(uri: string) {
518
- return this.app.bsky.graph.unmuteActorList({ list: uri })
519
- }
520
-
521
- async blockModList(uri: string) {
522
- return this.app.bsky.graph.listblock.create(
523
- { repo: this.accountDid },
524
- {
525
- subject: uri,
526
- createdAt: new Date().toISOString(),
527
- },
528
- )
529
- }
530
-
531
- async unblockModList(uri: string) {
532
- const repo = this.accountDid
533
-
534
- const listInfo = await this.app.bsky.graph.getList({
535
- list: uri,
536
- limit: 1,
537
- })
538
-
539
- const blocked = listInfo.data.list.viewer?.blocked
540
- if (blocked) {
541
- const { rkey } = new AtUri(blocked)
542
- return this.app.bsky.graph.listblock.delete({
543
- repo,
544
- rkey,
545
- })
546
- }
547
- }
548
-
549
- async updateSeenNotifications(seenAt = new Date().toISOString()) {
550
- return this.app.bsky.notification.updateSeen({ seenAt })
551
- }
552
-
553
- async getPreferences(): Promise<BskyPreferences> {
554
- const prefs: BskyPreferences = {
555
- feeds: {
556
- saved: undefined,
557
- pinned: undefined,
558
- },
559
- // @ts-ignore populating below
560
- savedFeeds: undefined,
561
- feedViewPrefs: {
562
- home: {
563
- ...FEED_VIEW_PREF_DEFAULTS,
564
- },
565
- },
566
- threadViewPrefs: { ...THREAD_VIEW_PREF_DEFAULTS },
567
- moderationPrefs: {
568
- adultContentEnabled: false,
569
- labels: { ...DEFAULT_LABEL_SETTINGS },
570
- labelers: this.appLabelers.map((did) => ({
571
- did,
572
- labels: {},
573
- })),
574
- mutedWords: [],
575
- hiddenPosts: [],
576
- },
577
- birthDate: undefined,
578
- interests: {
579
- tags: [],
580
- },
581
- bskyAppState: {
582
- queuedNudges: [],
583
- activeProgressGuide: undefined,
584
- nuxs: [],
585
- },
586
- postInteractionSettings: {
587
- threadgateAllowRules: undefined,
588
- postgateEmbeddingRules: undefined,
589
- },
590
- verificationPrefs: {
591
- hideBadges: false,
592
- },
593
- liveEventPreferences: {
594
- hiddenFeedIds: [],
595
- hideAllFeeds: false,
596
- },
597
- }
598
- const res = await this.app.bsky.actor.getPreferences({})
599
- const labelPrefs: AppBskyActorDefs.ContentLabelPref[] = []
600
- for (const pref of res.data.preferences) {
601
- if (predicate.isValidAdultContentPref(pref)) {
602
- // adult content preferences
603
- prefs.moderationPrefs.adultContentEnabled = pref.enabled
604
- } else if (predicate.isValidContentLabelPref(pref)) {
605
- // content label preference
606
- const adjustedPref = adjustLegacyContentLabelPref(pref)
607
- labelPrefs.push(adjustedPref)
608
- } else if (predicate.isValidLabelersPref(pref)) {
609
- // labelers preferences
610
- prefs.moderationPrefs.labelers = this.appLabelers
611
- .map((did: string) => ({ did, labels: {} }))
612
- .concat(
613
- pref.labelers.map((labeler) => ({
614
- ...labeler,
615
- labels: {},
616
- })),
617
- )
618
- } else if (predicate.isValidSavedFeedsPrefV2(pref)) {
619
- prefs.savedFeeds = pref.items
620
- } else if (predicate.isValidSavedFeedsPref(pref)) {
621
- // saved and pinned feeds
622
- prefs.feeds.saved = pref.saved
623
- prefs.feeds.pinned = pref.pinned
624
- } else if (predicate.isValidPersonalDetailsPref(pref)) {
625
- // birth date (irl)
626
- if (pref.birthDate) {
627
- prefs.birthDate = new Date(pref.birthDate)
628
- }
629
- } else if (predicate.isValidDeclaredAgePref(pref)) {
630
- const { $type: _, ...declaredAgePref } = pref
631
- prefs.declaredAge = declaredAgePref
632
- } else if (predicate.isValidFeedViewPref(pref)) {
633
- // feed view preferences
634
- const { $type: _, feed, ...v } = pref
635
- prefs.feedViewPrefs[feed] = { ...FEED_VIEW_PREF_DEFAULTS, ...v }
636
- } else if (predicate.isValidThreadViewPref(pref)) {
637
- // thread view preferences
638
- const { $type: _, ...v } = pref
639
- prefs.threadViewPrefs = { ...prefs.threadViewPrefs, ...v }
640
- } else if (predicate.isValidInterestsPref(pref)) {
641
- const { $type: _, ...v } = pref
642
- prefs.interests = { ...prefs.interests, ...v }
643
- } else if (predicate.isValidMutedWordsPref(pref)) {
644
- prefs.moderationPrefs.mutedWords = pref.items
645
-
646
- if (prefs.moderationPrefs.mutedWords.length) {
647
- prefs.moderationPrefs.mutedWords =
648
- prefs.moderationPrefs.mutedWords.map((word) => {
649
- word.actorTarget = word.actorTarget || 'all'
650
- return word
651
- })
652
- }
653
- } else if (predicate.isValidHiddenPostsPref(pref)) {
654
- prefs.moderationPrefs.hiddenPosts = pref.items
655
- } else if (predicate.isValidBskyAppStatePref(pref)) {
656
- prefs.bskyAppState.queuedNudges = pref.queuedNudges || []
657
- prefs.bskyAppState.activeProgressGuide = pref.activeProgressGuide
658
- prefs.bskyAppState.nuxs = pref.nuxs || []
659
- } else if (predicate.isValidPostInteractionSettingsPref(pref)) {
660
- prefs.postInteractionSettings.threadgateAllowRules =
661
- pref.threadgateAllowRules
662
- prefs.postInteractionSettings.postgateEmbeddingRules =
663
- pref.postgateEmbeddingRules
664
- } else if (predicate.isValidVerificationPrefs(pref)) {
665
- prefs.verificationPrefs = {
666
- hideBadges: pref.hideBadges,
667
- }
668
- } else if (predicate.isValidLiveEventPreferences(pref)) {
669
- prefs.liveEventPreferences = {
670
- hiddenFeedIds: pref.hiddenFeedIds || [],
671
- hideAllFeeds: pref.hideAllFeeds ?? false,
672
- }
673
- }
674
- }
675
-
676
- /*
677
- * If `prefs.savedFeeds` is undefined, no `savedFeedsPrefV2` exists, which
678
- * means we want to try to migrate if needed.
679
- *
680
- * If v1 prefs exist, they will be migrated to v2.
681
- *
682
- * If no v1 prefs exist, the user is either new, or could be old and has
683
- * never edited their feeds.
684
- */
685
- if (prefs.savedFeeds == null) {
686
- const { saved, pinned } = prefs.feeds
687
-
688
- if (saved && pinned) {
689
- const uniqueMigratedSavedFeeds: Map<
690
- string,
691
- AppBskyActorDefs.SavedFeed
692
- > = new Map()
693
-
694
- // insert Following feed first
695
- uniqueMigratedSavedFeeds.set('timeline', {
696
- id: TID.nextStr(),
697
- type: 'timeline',
698
- value: 'following',
699
- pinned: true,
700
- })
701
-
702
- // use pinned as source of truth for feed order
703
- for (const uri of pinned) {
704
- const type = getSavedFeedType(uri)
705
- // only want supported types
706
- if (type === 'unknown') continue
707
- uniqueMigratedSavedFeeds.set(uri, {
708
- id: TID.nextStr(),
709
- type,
710
- value: uri,
711
- pinned: true,
712
- })
713
- }
714
-
715
- for (const uri of saved) {
716
- if (!uniqueMigratedSavedFeeds.has(uri)) {
717
- const type = getSavedFeedType(uri)
718
- // only want supported types
719
- if (type === 'unknown') continue
720
- uniqueMigratedSavedFeeds.set(uri, {
721
- id: TID.nextStr(),
722
- type,
723
- value: uri,
724
- pinned: false,
725
- })
726
- }
727
- }
728
-
729
- prefs.savedFeeds = Array.from(uniqueMigratedSavedFeeds.values())
730
- } else {
731
- prefs.savedFeeds = [
732
- {
733
- id: TID.nextStr(),
734
- type: 'timeline',
735
- value: 'following',
736
- pinned: true,
737
- },
738
- ]
739
- }
740
-
741
- // save to user preferences so this migration doesn't re-occur
742
- await this.overwriteSavedFeeds(prefs.savedFeeds)
743
- }
744
-
745
- // apply the label prefs
746
- for (const pref of labelPrefs) {
747
- if (pref.labelerDid) {
748
- const labeler = prefs.moderationPrefs.labelers.find(
749
- (labeler) => labeler.did === pref.labelerDid,
750
- )
751
- if (!labeler) continue
752
- labeler.labels[pref.label] = pref.visibility as LabelPreference
753
- } else {
754
- prefs.moderationPrefs.labels[pref.label] =
755
- pref.visibility as LabelPreference
756
- }
757
- }
758
-
759
- prefs.moderationPrefs.labels = remapLegacyLabels(
760
- prefs.moderationPrefs.labels,
761
- )
762
-
763
- // automatically configure the client
764
- this.configureLabelers(prefsArrayToLabelerDids(res.data.preferences))
765
-
766
- return prefs
767
- }
768
-
769
- async overwriteSavedFeeds(savedFeeds: AppBskyActorDefs.SavedFeed[]) {
770
- savedFeeds.forEach(validateSavedFeed)
771
- const uniqueSavedFeeds = new Map<string, AppBskyActorDefs.SavedFeed>()
772
- savedFeeds.forEach((feed) => {
773
- // remove and re-insert to preserve order
774
- if (uniqueSavedFeeds.has(feed.id)) {
775
- uniqueSavedFeeds.delete(feed.id)
776
- }
777
- uniqueSavedFeeds.set(feed.id, feed)
778
- })
779
- return this.updateSavedFeedsV2Preferences(() =>
780
- Array.from(uniqueSavedFeeds.values()),
781
- )
782
- }
783
-
784
- async updateSavedFeeds(savedFeedsToUpdate: AppBskyActorDefs.SavedFeed[]) {
785
- savedFeedsToUpdate.map(validateSavedFeed)
786
- return this.updateSavedFeedsV2Preferences((savedFeeds) => {
787
- return savedFeeds.map((savedFeed) => {
788
- const updatedVersion = savedFeedsToUpdate.find(
789
- (updated) => savedFeed.id === updated.id,
790
- )
791
- if (updatedVersion) {
792
- return {
793
- ...savedFeed,
794
- // only update pinned
795
- pinned: updatedVersion.pinned,
796
- }
797
- }
798
- return savedFeed
799
- })
800
- })
801
- }
802
-
803
- async addSavedFeeds(
804
- savedFeeds: Pick<AppBskyActorDefs.SavedFeed, 'type' | 'value' | 'pinned'>[],
805
- ) {
806
- const toSave: AppBskyActorDefs.SavedFeed[] = savedFeeds.map((f) => ({
807
- ...f,
808
- id: TID.nextStr(),
809
- }))
810
- toSave.forEach(validateSavedFeed)
811
- return this.updateSavedFeedsV2Preferences((savedFeeds) => [
812
- ...savedFeeds,
813
- ...toSave,
814
- ])
815
- }
816
-
817
- async removeSavedFeeds(ids: string[]) {
818
- return this.updateSavedFeedsV2Preferences((savedFeeds) => [
819
- ...savedFeeds.filter((feed) => !ids.find((id) => feed.id === id)),
820
- ])
821
- }
822
-
823
- /**
824
- * @deprecated use `overwriteSavedFeeds`
825
- */
826
- async setSavedFeeds(saved: string[], pinned: string[]) {
827
- return this.updateFeedPreferences(() => ({
828
- saved,
829
- pinned,
830
- }))
831
- }
832
-
833
- /**
834
- * @deprecated use `addSavedFeeds`
835
- */
836
- async addSavedFeed(v: string) {
837
- return this.updateFeedPreferences((saved: string[], pinned: string[]) => ({
838
- saved: [...saved.filter((uri) => uri !== v), v],
839
- pinned,
840
- }))
841
- }
842
-
843
- /**
844
- * @deprecated use `removeSavedFeeds`
845
- */
846
- async removeSavedFeed(v: string) {
847
- return this.updateFeedPreferences((saved: string[], pinned: string[]) => ({
848
- saved: saved.filter((uri) => uri !== v),
849
- pinned: pinned.filter((uri) => uri !== v),
850
- }))
851
- }
852
-
853
- /**
854
- * @deprecated use `addSavedFeeds` or `updateSavedFeeds`
855
- */
856
- async addPinnedFeed(v: string) {
857
- return this.updateFeedPreferences((saved: string[], pinned: string[]) => ({
858
- saved: [...saved.filter((uri) => uri !== v), v],
859
- pinned: [...pinned.filter((uri) => uri !== v), v],
860
- }))
861
- }
862
-
863
- /**
864
- * @deprecated use `updateSavedFeeds` or `removeSavedFeeds`
865
- */
866
- async removePinnedFeed(v: string) {
867
- return this.updateFeedPreferences((saved: string[], pinned: string[]) => ({
868
- saved,
869
- pinned: pinned.filter((uri) => uri !== v),
870
- }))
871
- }
872
-
873
- async setAdultContentEnabled(v: boolean) {
874
- await this.updatePreferences((prefs) => {
875
- const adultContentPref = prefs.findLast(
876
- predicate.isValidAdultContentPref,
877
- ) || {
878
- $type: 'app.bsky.actor.defs#adultContentPref',
879
- enabled: v,
880
- }
881
-
882
- adultContentPref.enabled = v
883
-
884
- return prefs
885
- .filter((pref) => !AppBskyActorDefs.isAdultContentPref(pref))
886
- .concat(adultContentPref)
887
- })
888
- }
889
-
890
- async setContentLabelPref(
891
- key: string,
892
- value: LabelPreference,
893
- labelerDid?: string,
894
- ) {
895
- if (labelerDid) {
896
- ensureValidDidRegex(labelerDid)
897
- }
898
- await this.updatePreferences((prefs) => {
899
- const labelPref = prefs
900
- .filter(predicate.isValidContentLabelPref)
901
- .findLast(
902
- (pref) => pref.label === key && pref.labelerDid === labelerDid,
903
- ) || {
904
- $type: 'app.bsky.actor.defs#contentLabelPref',
905
- label: key,
906
- labelerDid,
907
- visibility: value,
908
- }
909
-
910
- labelPref.visibility = value
911
-
912
- let legacyLabelPref: $Typed<AppBskyActorDefs.ContentLabelPref> | undefined
913
- if (AppBskyActorDefs.isContentLabelPref(labelPref)) {
914
- // is global
915
- if (!labelPref.labelerDid) {
916
- const legacyLabelValue = {
917
- 'graphic-media': 'gore',
918
- porn: 'nsfw',
919
- sexual: 'suggestive',
920
- // Protect against using toString, hasOwnProperty, etc. as a label:
921
- __proto__: null,
922
- }[labelPref.label]
923
-
924
- // if it's a legacy label, double-write the legacy label
925
- if (legacyLabelValue) {
926
- legacyLabelPref = prefs
927
- .filter(predicate.isValidContentLabelPref)
928
- .findLast(
929
- (pref) =>
930
- pref.label === legacyLabelValue &&
931
- pref.labelerDid === undefined,
932
- ) || {
933
- $type: 'app.bsky.actor.defs#contentLabelPref',
934
- label: legacyLabelValue,
935
- labelerDid: undefined,
936
- visibility: value,
937
- }
938
-
939
- legacyLabelPref!.visibility = value
940
- }
941
- }
942
- }
943
-
944
- return prefs
945
- .filter(
946
- (pref) =>
947
- !AppBskyActorDefs.isContentLabelPref(pref) ||
948
- !(pref.label === key && pref.labelerDid === labelerDid),
949
- )
950
- .concat(labelPref)
951
- .filter((pref) => {
952
- if (!legacyLabelPref) return true
953
- return (
954
- !AppBskyActorDefs.isContentLabelPref(pref) ||
955
- !(
956
- pref.label === legacyLabelPref.label &&
957
- pref.labelerDid === undefined
958
- )
959
- )
960
- })
961
- .concat(legacyLabelPref ? [legacyLabelPref] : [])
962
- })
963
- }
964
-
965
- async addLabeler(did: string) {
966
- const prefs = await this.updatePreferences((prefs) => {
967
- const labelersPref = prefs.findLast(predicate.isValidLabelersPref) || {
968
- $type: 'app.bsky.actor.defs#labelersPref',
969
- labelers: [],
970
- }
971
-
972
- if (!labelersPref.labelers.some((labeler) => labeler.did === did)) {
973
- labelersPref.labelers.push({ did })
974
- }
975
-
976
- return prefs
977
- .filter((pref) => !AppBskyActorDefs.isLabelersPref(pref))
978
- .concat(labelersPref)
979
- })
980
- // automatically configure the client
981
- this.configureLabelers(prefsArrayToLabelerDids(prefs))
982
- }
983
-
984
- async removeLabeler(did: string) {
985
- const prefs = await this.updatePreferences((prefs) => {
986
- const labelersPref = prefs.findLast(predicate.isValidLabelersPref) || {
987
- $type: 'app.bsky.actor.defs#labelersPref',
988
- labelers: [],
989
- }
990
-
991
- labelersPref.labelers = labelersPref.labelers.filter((l) => l.did !== did)
992
-
993
- return prefs
994
- .filter((pref) => !AppBskyActorDefs.isLabelersPref(pref))
995
- .concat(labelersPref)
996
- })
997
- // automatically configure the client
998
- this.configureLabelers(prefsArrayToLabelerDids(prefs))
999
- }
1000
-
1001
- async setPersonalDetails({
1002
- birthDate,
1003
- }: {
1004
- birthDate: string | Date | undefined
1005
- }) {
1006
- await this.updatePreferences((prefs) => {
1007
- const personalDetailsPref = prefs.findLast(
1008
- predicate.isValidPersonalDetailsPref,
1009
- ) || {
1010
- $type: 'app.bsky.actor.defs#personalDetailsPref',
1011
- }
1012
-
1013
- personalDetailsPref.birthDate =
1014
- birthDate instanceof Date ? birthDate.toISOString() : birthDate
1015
-
1016
- return prefs
1017
- .filter((pref) => !AppBskyActorDefs.isPersonalDetailsPref(pref))
1018
- .concat(personalDetailsPref)
1019
- })
1020
- }
1021
-
1022
- async setFeedViewPrefs(feed: string, pref: Partial<BskyFeedViewPreference>) {
1023
- await this.updatePreferences((prefs) => {
1024
- const existing = prefs
1025
- .filter(predicate.isValidFeedViewPref)
1026
- .findLast((pref) => pref.feed === feed)
1027
-
1028
- return prefs
1029
- .filter((p) => !AppBskyActorDefs.isFeedViewPref(p) || p.feed !== feed)
1030
- .concat({
1031
- ...existing,
1032
- ...pref,
1033
- $type: 'app.bsky.actor.defs#feedViewPref',
1034
- feed,
1035
- })
1036
- })
1037
- }
1038
-
1039
- async setThreadViewPrefs(pref: Partial<BskyThreadViewPreference>) {
1040
- await this.updatePreferences((prefs) => {
1041
- const existing = prefs.findLast(predicate.isValidThreadViewPref)
1042
-
1043
- return prefs
1044
- .filter((p) => !AppBskyActorDefs.isThreadViewPref(p))
1045
- .concat({
1046
- ...existing,
1047
- ...pref,
1048
- $type: 'app.bsky.actor.defs#threadViewPref',
1049
- })
1050
- })
1051
- }
1052
-
1053
- async setInterestsPref(pref: Partial<BskyInterestsPreference>) {
1054
- await this.updatePreferences((prefs) => {
1055
- const existing = prefs.findLast(predicate.isValidInterestsPref)
1056
-
1057
- return prefs
1058
- .filter((p) => !AppBskyActorDefs.isInterestsPref(p))
1059
- .concat({
1060
- ...existing,
1061
- ...pref,
1062
- $type: 'app.bsky.actor.defs#interestsPref',
1063
- })
1064
- })
1065
- }
1066
-
1067
- /**
1068
- * Add a muted word to user preferences.
1069
- */
1070
- async addMutedWord(
1071
- mutedWord: Pick<
1072
- MutedWord,
1073
- 'value' | 'targets' | 'actorTarget' | 'expiresAt'
1074
- >,
1075
- ) {
1076
- const sanitizedValue = sanitizeMutedWordValue(mutedWord.value)
1077
-
1078
- if (!sanitizedValue) return
1079
-
1080
- await this.updatePreferences((prefs) => {
1081
- let mutedWordsPref = prefs.findLast(predicate.isValidMutedWordsPref)
1082
-
1083
- const newMutedWord: AppBskyActorDefs.MutedWord = {
1084
- id: TID.nextStr(),
1085
- value: sanitizedValue,
1086
- targets: mutedWord.targets || [],
1087
- actorTarget: mutedWord.actorTarget || 'all',
1088
- expiresAt: mutedWord.expiresAt || undefined,
1089
- }
1090
-
1091
- if (mutedWordsPref) {
1092
- mutedWordsPref.items.push(newMutedWord)
1093
-
1094
- /**
1095
- * Migrate any old muted words that don't have an id
1096
- */
1097
- mutedWordsPref.items = migrateLegacyMutedWordsItems(
1098
- mutedWordsPref.items,
1099
- )
1100
- } else {
1101
- // if the pref doesn't exist, create it
1102
- mutedWordsPref = {
1103
- $type: 'app.bsky.actor.defs#mutedWordsPref',
1104
- items: [newMutedWord],
1105
- }
1106
- }
1107
-
1108
- return prefs
1109
- .filter((p) => !AppBskyActorDefs.isMutedWordsPref(p))
1110
- .concat(mutedWordsPref)
1111
- })
1112
- }
1113
-
1114
- /**
1115
- * Convenience method to add muted words to user preferences
1116
- */
1117
- async addMutedWords(newMutedWords: AppBskyActorDefs.MutedWord[]) {
1118
- await Promise.all(newMutedWords.map((word) => this.addMutedWord(word)))
1119
- }
1120
-
1121
- /**
1122
- * @deprecated use `addMutedWords` or `addMutedWord` instead
1123
- */
1124
- async upsertMutedWords(
1125
- mutedWords: Pick<
1126
- MutedWord,
1127
- 'value' | 'targets' | 'actorTarget' | 'expiresAt'
1128
- >[],
1129
- ) {
1130
- await this.addMutedWords(mutedWords)
1131
- }
1132
-
1133
- /**
1134
- * Update a muted word in user preferences.
1135
- */
1136
- async updateMutedWord(mutedWord: AppBskyActorDefs.MutedWord) {
1137
- await this.updatePreferences((prefs) => {
1138
- const mutedWordsPref = prefs.findLast(predicate.isValidMutedWordsPref)
1139
-
1140
- if (mutedWordsPref) {
1141
- mutedWordsPref.items = mutedWordsPref.items.map((existingItem) => {
1142
- const match = matchMutedWord(existingItem, mutedWord)
1143
-
1144
- if (match) {
1145
- const updated = {
1146
- ...existingItem,
1147
- ...mutedWord,
1148
- }
1149
- return {
1150
- id: existingItem.id || TID.nextStr(),
1151
- value:
1152
- sanitizeMutedWordValue(updated.value) || existingItem.value,
1153
- targets: updated.targets || [],
1154
- actorTarget: updated.actorTarget || 'all',
1155
- expiresAt: updated.expiresAt || undefined,
1156
- }
1157
- } else {
1158
- return existingItem
1159
- }
1160
- })
1161
-
1162
- /**
1163
- * Migrate any old muted words that don't have an id
1164
- */
1165
- mutedWordsPref.items = migrateLegacyMutedWordsItems(
1166
- mutedWordsPref.items,
1167
- )
1168
-
1169
- return prefs
1170
- .filter((p) => !AppBskyActorDefs.isMutedWordsPref(p))
1171
- .concat(mutedWordsPref)
1172
- }
1173
-
1174
- return prefs
1175
- })
1176
- }
1177
-
1178
- /**
1179
- * Remove a muted word from user preferences.
1180
- */
1181
- async removeMutedWord(mutedWord: AppBskyActorDefs.MutedWord) {
1182
- await this.updatePreferences((prefs) => {
1183
- const mutedWordsPref = prefs.findLast(predicate.isValidMutedWordsPref)
1184
-
1185
- if (mutedWordsPref) {
1186
- for (let i = 0; i < mutedWordsPref.items.length; i++) {
1187
- const match = matchMutedWord(mutedWordsPref.items[i], mutedWord)
1188
-
1189
- if (match) {
1190
- mutedWordsPref.items.splice(i, 1)
1191
- break
1192
- }
1193
- }
1194
-
1195
- /**
1196
- * Migrate any old muted words that don't have an id
1197
- */
1198
- mutedWordsPref.items = migrateLegacyMutedWordsItems(
1199
- mutedWordsPref.items,
1200
- )
1201
-
1202
- return prefs
1203
- .filter((p) => !AppBskyActorDefs.isMutedWordsPref(p))
1204
- .concat(mutedWordsPref)
1205
- }
1206
-
1207
- return prefs
1208
- })
1209
- }
1210
-
1211
- /**
1212
- * Convenience method to remove muted words from user preferences
1213
- */
1214
- async removeMutedWords(mutedWords: AppBskyActorDefs.MutedWord[]) {
1215
- await Promise.all(mutedWords.map((word) => this.removeMutedWord(word)))
1216
- }
1217
-
1218
- async hidePost(postUri: string) {
1219
- await this.updateHiddenPost(postUri, 'hide')
1220
- }
1221
-
1222
- async unhidePost(postUri: string) {
1223
- await this.updateHiddenPost(postUri, 'unhide')
1224
- }
1225
-
1226
- async bskyAppQueueNudges(nudges: string | string[]) {
1227
- await this.updatePreferences((prefs) => {
1228
- const pref = prefs.findLast(predicate.isValidBskyAppStatePref) || {
1229
- $type: 'app.bsky.actor.defs#bskyAppStatePref',
1230
- }
1231
-
1232
- pref.queuedNudges = (pref.queuedNudges || []).concat(nudges)
1233
-
1234
- return prefs
1235
- .filter((p) => !AppBskyActorDefs.isBskyAppStatePref(p))
1236
- .concat(pref)
1237
- })
1238
- }
1239
-
1240
- async bskyAppDismissNudges(nudges: string | string[]) {
1241
- await this.updatePreferences((prefs) => {
1242
- const pref = prefs.findLast(predicate.isValidBskyAppStatePref) || {
1243
- $type: 'app.bsky.actor.defs#bskyAppStatePref',
1244
- }
1245
-
1246
- nudges = Array.isArray(nudges) ? nudges : [nudges]
1247
- pref.queuedNudges = (pref.queuedNudges || []).filter(
1248
- (nudge) => !nudges.includes(nudge),
1249
- )
1250
-
1251
- return prefs
1252
- .filter((p) => !AppBskyActorDefs.isBskyAppStatePref(p))
1253
- .concat(pref)
1254
- })
1255
- }
1256
-
1257
- async bskyAppSetActiveProgressGuide(
1258
- guide: AppBskyActorDefs.BskyAppProgressGuide | undefined,
1259
- ) {
1260
- if (guide) {
1261
- const result = AppBskyActorDefs.validateBskyAppProgressGuide(guide)
1262
- if (!result.success) throw result.error
1263
- }
1264
-
1265
- await this.updatePreferences((prefs) => {
1266
- const pref = prefs.findLast(predicate.isValidBskyAppStatePref) || {
1267
- $type: 'app.bsky.actor.defs#bskyAppStatePref',
1268
- }
1269
-
1270
- pref.activeProgressGuide = guide
1271
-
1272
- return prefs
1273
- .filter((p) => !AppBskyActorDefs.isBskyAppStatePref(p))
1274
- .concat(pref)
1275
- })
1276
- }
1277
-
1278
- /**
1279
- * Insert or update a NUX in user prefs
1280
- */
1281
- async bskyAppUpsertNux(nux: Nux) {
1282
- validateNux(nux)
1283
-
1284
- await this.updatePreferences((prefs) => {
1285
- const pref = prefs.findLast(predicate.isValidBskyAppStatePref) || {
1286
- $type: 'app.bsky.actor.defs#bskyAppStatePref',
1287
- }
1288
-
1289
- pref.nuxs = pref.nuxs || []
1290
-
1291
- const existing = pref.nuxs?.find((n) => {
1292
- return n.id === nux.id
1293
- })
1294
-
1295
- let next: AppBskyActorDefs.Nux
1296
-
1297
- if (existing) {
1298
- next = {
1299
- id: existing.id,
1300
- completed: nux.completed,
1301
- data: nux.data,
1302
- expiresAt: nux.expiresAt,
1303
- }
1304
- } else {
1305
- next = nux
1306
- }
1307
-
1308
- // remove duplicates and append
1309
- pref.nuxs = pref.nuxs.filter((n) => n.id !== nux.id).concat(next)
1310
-
1311
- return prefs
1312
- .filter((p) => !AppBskyActorDefs.isBskyAppStatePref(p))
1313
- .concat(pref)
1314
- })
1315
- }
1316
-
1317
- /**
1318
- * Removes NUXs from user preferences.
1319
- */
1320
- async bskyAppRemoveNuxs(ids: string[]) {
1321
- await this.updatePreferences((prefs) => {
1322
- const pref = prefs.findLast(predicate.isValidBskyAppStatePref) || {
1323
- $type: 'app.bsky.actor.defs#bskyAppStatePref',
1324
- }
1325
-
1326
- pref.nuxs = (pref.nuxs || []).filter((nux) => !ids.includes(nux.id))
1327
-
1328
- return prefs
1329
- .filter((p) => !AppBskyActorDefs.isBskyAppStatePref(p))
1330
- .concat(pref)
1331
- })
1332
- }
1333
-
1334
- async setPostInteractionSettings(
1335
- settings: AppBskyActorDefs.PostInteractionSettingsPref,
1336
- ) {
1337
- const result =
1338
- AppBskyActorDefs.validatePostInteractionSettingsPref(settings)
1339
- // Fool-proofing (should not be needed because of type safety)
1340
- if (!result.success) throw result.error
1341
-
1342
- await this.updatePreferences((prefs) => {
1343
- const pref = prefs.findLast(
1344
- predicate.isValidPostInteractionSettingsPref,
1345
- ) || {
1346
- $type: 'app.bsky.actor.defs#postInteractionSettingsPref',
1347
- }
1348
-
1349
- /**
1350
- * Matches handling of `threadgate.allow` where `undefined` means "everyone"
1351
- */
1352
- pref.threadgateAllowRules = settings.threadgateAllowRules
1353
- pref.postgateEmbeddingRules = settings.postgateEmbeddingRules
1354
-
1355
- return prefs
1356
- .filter((p) => !AppBskyActorDefs.isPostInteractionSettingsPref(p))
1357
- .concat(pref)
1358
- })
1359
- }
1360
-
1361
- async setVerificationPrefs(settings: AppBskyActorDefs.VerificationPrefs) {
1362
- const result = AppBskyActorDefs.validateVerificationPrefs(settings)
1363
- // Fool-proofing (should not be needed because of type safety)
1364
- if (!result.success) throw result.error
1365
-
1366
- await this.updatePreferences((prefs) => {
1367
- const pref = prefs.findLast(predicate.isValidVerificationPrefs) || {
1368
- $type: 'app.bsky.actor.defs#verificationPrefs',
1369
- hideBadges: false,
1370
- }
1371
-
1372
- pref.hideBadges = settings.hideBadges
1373
-
1374
- return prefs
1375
- .filter((p) => !AppBskyActorDefs.isVerificationPrefs(p))
1376
- .concat(pref)
1377
- })
1378
- }
1379
-
1380
- async updateLiveEventPreferences(
1381
- action:
1382
- | { type: 'hideFeed'; id: string }
1383
- | { type: 'unhideFeed'; id: string }
1384
- | { type: 'toggleHideAllFeeds' },
1385
- ) {
1386
- return this.updatePreferences((prefs) => {
1387
- const pref = prefs.findLast(predicate.isValidLiveEventPreferences) || {
1388
- $type: 'app.bsky.actor.defs#liveEventPreferences',
1389
- hiddenFeedIds: [],
1390
- hideAllFeeds: false,
1391
- }
1392
-
1393
- const hiddenFeedIds = new Set(pref.hiddenFeedIds || [])
1394
-
1395
- switch (action.type) {
1396
- case 'hideFeed':
1397
- hiddenFeedIds.add(action.id)
1398
- break
1399
- case 'unhideFeed':
1400
- hiddenFeedIds.delete(action.id)
1401
- break
1402
- case 'toggleHideAllFeeds':
1403
- pref.hideAllFeeds = !pref.hideAllFeeds
1404
- break
1405
- }
1406
-
1407
- pref.hiddenFeedIds = [...hiddenFeedIds]
1408
-
1409
- return prefs
1410
- .filter((p) => !AppBskyActorDefs.isLiveEventPreferences(p))
1411
- .concat(pref)
1412
- })
1413
- }
1414
-
1415
- //- Private methods
1416
-
1417
- #prefsLock = new AwaitLock()
1418
-
1419
- /**
1420
- * This function updates the preferences of a user and allows for a callback function to be executed
1421
- * before the update.
1422
- * @param cb - cb is a callback function that takes in a single parameter of type
1423
- * AppBskyActorDefs.Preferences and returns either a boolean or void. This callback function is used to
1424
- * update the preferences of the user. The function is called with the current preferences as an
1425
- * argument and if the callback returns false, the preferences are not updated.
1426
- */
1427
- private async updatePreferences(
1428
- cb: (
1429
- prefs: AppBskyActorDefs.Preferences,
1430
- ) => AppBskyActorDefs.Preferences | false,
1431
- ) {
1432
- try {
1433
- await this.#prefsLock.acquireAsync()
1434
- const res = await this.app.bsky.actor.getPreferences({})
1435
- const newPrefs = cb(res.data.preferences)
1436
- if (newPrefs === false) {
1437
- return res.data.preferences
1438
- }
1439
- await this.app.bsky.actor.putPreferences({
1440
- preferences: newPrefs,
1441
- })
1442
- return newPrefs
1443
- } finally {
1444
- this.#prefsLock.release()
1445
- }
1446
- }
1447
-
1448
- private async updateHiddenPost(postUri: string, action: 'hide' | 'unhide') {
1449
- await this.updatePreferences((prefs) => {
1450
- const pref = prefs.findLast(predicate.isValidHiddenPostsPref) || {
1451
- $type: 'app.bsky.actor.defs#hiddenPostsPref',
1452
- items: [],
1453
- }
1454
-
1455
- const hiddenItems = new Set(pref.items)
1456
-
1457
- if (action === 'hide') hiddenItems.add(postUri)
1458
- else hiddenItems.delete(postUri)
1459
-
1460
- pref.items = [...hiddenItems]
1461
-
1462
- return prefs
1463
- .filter((p) => !AppBskyActorDefs.isHiddenPostsPref(p))
1464
- .concat(pref)
1465
- })
1466
- }
1467
-
1468
- /**
1469
- * A helper specifically for updating feed preferences
1470
- */
1471
- private async updateFeedPreferences(
1472
- cb: (
1473
- saved: string[],
1474
- pinned: string[],
1475
- ) => { saved: string[]; pinned: string[] },
1476
- ): Promise<{ saved: string[]; pinned: string[] }> {
1477
- let res
1478
- await this.updatePreferences((prefs) => {
1479
- const feedsPref = prefs.findLast(predicate.isValidSavedFeedsPref) || {
1480
- $type: 'app.bsky.actor.defs#savedFeedsPref',
1481
- saved: [],
1482
- pinned: [],
1483
- }
1484
-
1485
- res = cb(feedsPref.saved, feedsPref.pinned)
1486
- feedsPref.saved = res.saved
1487
- feedsPref.pinned = res.pinned
1488
-
1489
- return prefs
1490
- .filter((pref) => !AppBskyActorDefs.isSavedFeedsPref(pref))
1491
- .concat(feedsPref)
1492
- })
1493
- return res
1494
- }
1495
-
1496
- private async updateSavedFeedsV2Preferences(
1497
- cb: (
1498
- savedFeedsPref: AppBskyActorDefs.SavedFeed[],
1499
- ) => AppBskyActorDefs.SavedFeed[],
1500
- ): Promise<AppBskyActorDefs.SavedFeed[]> {
1501
- let maybeMutatedSavedFeeds: AppBskyActorDefs.SavedFeed[] = []
1502
-
1503
- await this.updatePreferences((prefs) => {
1504
- const existingV2Pref = prefs.findLast(
1505
- predicate.isValidSavedFeedsPrefV2,
1506
- ) || {
1507
- $type: 'app.bsky.actor.defs#savedFeedsPrefV2',
1508
- items: [],
1509
- }
1510
-
1511
- const newSavedFeeds = cb(existingV2Pref.items)
1512
-
1513
- // enforce ordering: pinned first, then saved
1514
- existingV2Pref.items = [...newSavedFeeds].sort((a, b) =>
1515
- // @NOTE: preserve order of items with the same pinned status
1516
- a.pinned === b.pinned ? 0 : a.pinned ? -1 : 1,
1517
- )
1518
-
1519
- // Store the return value
1520
- maybeMutatedSavedFeeds = newSavedFeeds
1521
-
1522
- let updatedPrefs = prefs
1523
- .filter((pref) => !AppBskyActorDefs.isSavedFeedsPrefV2(pref))
1524
- .concat(existingV2Pref)
1525
-
1526
- /*
1527
- * If there's a v2 pref present, it means this account was migrated from v1
1528
- * to v2. During the transition period, we double write v2 prefs back to
1529
- * v1, but NOT the other way around.
1530
- */
1531
- let existingV1Pref = prefs.findLast(predicate.isValidSavedFeedsPref)
1532
- if (existingV1Pref) {
1533
- const { saved, pinned } = existingV1Pref
1534
- const v2Compat = savedFeedsToUriArrays(
1535
- // v1 only supports feeds and lists
1536
- existingV2Pref.items.filter((i) => ['feed', 'list'].includes(i.type)),
1537
- )
1538
- existingV1Pref = {
1539
- ...existingV1Pref,
1540
- saved: Array.from(new Set([...saved, ...v2Compat.saved])),
1541
- pinned: Array.from(new Set([...pinned, ...v2Compat.pinned])),
1542
- }
1543
- updatedPrefs = updatedPrefs
1544
- .filter((pref) => !AppBskyActorDefs.isSavedFeedsPref(pref))
1545
- .concat(existingV1Pref)
1546
- }
1547
-
1548
- return updatedPrefs
1549
- })
1550
-
1551
- return maybeMutatedSavedFeeds
1552
- }
1553
-
1554
- //#endregion
1555
- }
1556
-
1557
- /**
1558
- * Helper to transform the legacy content preferences.
1559
- */
1560
- function adjustLegacyContentLabelPref(
1561
- pref: AppBskyActorDefs.ContentLabelPref,
1562
- ): AppBskyActorDefs.ContentLabelPref {
1563
- let visibility = pref.visibility
1564
-
1565
- // adjust legacy values
1566
- if (visibility === 'show') {
1567
- visibility = 'ignore'
1568
- }
1569
-
1570
- return { ...pref, visibility }
1571
- }
1572
-
1573
- /**
1574
- * Re-maps legacy labels to new labels on READ. Does not save these changes to
1575
- * the user's preferences.
1576
- */
1577
- function remapLegacyLabels(
1578
- labels: BskyPreferences['moderationPrefs']['labels'],
1579
- ) {
1580
- const _labels = { ...labels }
1581
- const legacyToNewMap: Record<string, string | undefined> = {
1582
- gore: 'graphic-media',
1583
- nsfw: 'porn',
1584
- suggestive: 'sexual',
1585
- }
1586
-
1587
- for (const labelName in _labels) {
1588
- const newLabelName = legacyToNewMap[labelName]!
1589
- if (newLabelName) {
1590
- _labels[newLabelName] = _labels[labelName]
1591
- }
1592
- }
1593
-
1594
- return _labels
1595
- }
1596
-
1597
- /**
1598
- * A helper to get the currently enabled labelers from the full preferences array
1599
- */
1600
- function prefsArrayToLabelerDids(
1601
- prefs: AppBskyActorDefs.Preferences,
1602
- ): string[] {
1603
- const labelersPref = prefs.findLast(predicate.isValidLabelersPref)
1604
- let dids: string[] = []
1605
- if (labelersPref) {
1606
- dids = (labelersPref as AppBskyActorDefs.LabelersPref).labelers.map(
1607
- (labeler) => labeler.did,
1608
- )
1609
- }
1610
- return dids
1611
- }
1612
-
1613
- function isBskyPrefs(v: any): v is BskyPreferences {
1614
- return (
1615
- v &&
1616
- typeof v === 'object' &&
1617
- 'moderationPrefs' in v &&
1618
- isModPrefs(v.moderationPrefs)
1619
- )
1620
- }
1621
-
1622
- function isModPrefs(v: any): v is ModerationPrefs {
1623
- return v && typeof v === 'object' && 'labelers' in v
1624
- }
1625
-
1626
- function migrateLegacyMutedWordsItems(items: AppBskyActorDefs.MutedWord[]) {
1627
- return items.map((item) => ({
1628
- ...item,
1629
- id: item.id || TID.nextStr(),
1630
- }))
1631
- }
1632
-
1633
- function matchMutedWord(
1634
- existingWord: AppBskyActorDefs.MutedWord,
1635
- newWord: AppBskyActorDefs.MutedWord,
1636
- ): boolean {
1637
- // id is undefined in legacy implementation
1638
- const existingId = existingWord.id
1639
- // prefer matching based on id
1640
- const matchById = existingId && existingId === newWord.id
1641
- // handle legacy case where id is not set
1642
- const legacyMatchByValue = !existingId && existingWord.value === newWord.value
1643
-
1644
- return matchById || legacyMatchByValue
1645
- }