diff --git a/packages/api/src/bsky-agent.ts b/packages/api/src/bsky-agent.ts index 29fedfa2122..b7dd1bc1931 100644 --- a/packages/api/src/bsky-agent.ts +++ b/packages/api/src/bsky-agent.ts @@ -8,6 +8,15 @@ import { } from './client' import { BskyPreferences, BskyLabelPreference } from './types' +declare global { + interface Array { + findLast( + predicate: (value: T, index: number, obj: T[]) => unknown, + thisArg?: any, + ): T + } +} + export class BskyAgent extends AtpAgent { get app() { return this.api.app @@ -247,6 +256,7 @@ export class BskyAgent extends AtpAgent { }, adultContentEnabled: false, contentLabels: {}, + birthDate: undefined, } const res = await this.app.bsky.actor.getPreferences({}) for (const pref of res.data.preferences) { @@ -272,6 +282,13 @@ export class BskyAgent extends AtpAgent { ) { prefs.feeds.saved = pref.saved prefs.feeds.pinned = pref.pinned + } else if ( + AppBskyActorDefs.isPersonalDetailsPref(pref) && + AppBskyActorDefs.validatePersonalDetailsPref(pref).success + ) { + if (pref.birthDate) { + prefs.birthDate = new Date(pref.birthDate) + } } } return prefs @@ -314,20 +331,22 @@ export class BskyAgent extends AtpAgent { async setAdultContentEnabled(v: boolean) { await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => { - const existing = prefs.find( + let adultContentPref = prefs.findLast( (pref) => AppBskyActorDefs.isAdultContentPref(pref) && AppBskyActorDefs.validateAdultContentPref(pref).success, ) - if (existing) { - existing.enabled = v + if (adultContentPref) { + adultContentPref.enabled = v } else { - prefs.push({ + adultContentPref = { $type: 'app.bsky.actor.defs#adultContentPref', enabled: v, - }) + } } return prefs + .filter((pref) => !AppBskyActorDefs.isAdultContentPref(pref)) + .concat([adultContentPref]) }) } @@ -338,22 +357,53 @@ export class BskyAgent extends AtpAgent { } await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => { - const existing = prefs.find( + let labelPref = prefs.findLast( (pref) => AppBskyActorDefs.isContentLabelPref(pref) && AppBskyActorDefs.validateAdultContentPref(pref).success && pref.label === key, ) - if (existing) { - existing.visibility = value + if (labelPref) { + labelPref.visibility = value } else { - prefs.push({ + labelPref = { $type: 'app.bsky.actor.defs#contentLabelPref', label: key, visibility: value, - }) + } + } + return prefs + .filter( + (pref) => + !AppBskyActorDefs.isContentLabelPref(pref) || pref.label !== key, + ) + .concat([labelPref]) + }) + } + + async setPersonalDetails({ + birthDate, + }: { + birthDate: string | Date | undefined + }) { + birthDate = birthDate instanceof Date ? birthDate.toISOString() : birthDate + await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => { + let personalDetailsPref = prefs.findLast( + (pref) => + AppBskyActorDefs.isPersonalDetailsPref(pref) && + AppBskyActorDefs.validatePersonalDetailsPref(pref).success, + ) + if (personalDetailsPref) { + personalDetailsPref.birthDate = birthDate + } else { + personalDetailsPref = { + $type: 'app.bsky.actor.defs#personalDetailsPref', + birthDate, + } } return prefs + .filter((pref) => !AppBskyActorDefs.isPersonalDetailsPref(pref)) + .concat([personalDetailsPref]) }) } } @@ -394,7 +444,7 @@ async function updateFeedPreferences( ): Promise<{ saved: string[]; pinned: string[] }> { let res await updatePreferences(agent, (prefs: AppBskyActorDefs.Preferences) => { - let feedsPref = prefs.find( + let feedsPref = prefs.findLast( (pref) => AppBskyActorDefs.isSavedFeedsPref(pref) && AppBskyActorDefs.validateSavedFeedsPref(pref).success, diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index e8597795979..0310d6743b8 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -89,4 +89,5 @@ export interface BskyPreferences { } adultContentEnabled: boolean contentLabels: Record + birthDate: Date | undefined } diff --git a/packages/api/tests/bsky-agent.test.ts b/packages/api/tests/bsky-agent.test.ts index 981b192c1d4..24b40153458 100644 --- a/packages/api/tests/bsky-agent.test.ts +++ b/packages/api/tests/bsky-agent.test.ts @@ -215,6 +215,7 @@ describe('agent', () => { feeds: { pinned: undefined, saved: undefined }, adultContentEnabled: false, contentLabels: {}, + birthDate: undefined, }) await agent.setAdultContentEnabled(true) @@ -222,6 +223,7 @@ describe('agent', () => { feeds: { pinned: undefined, saved: undefined }, adultContentEnabled: true, contentLabels: {}, + birthDate: undefined, }) await agent.setAdultContentEnabled(false) @@ -229,6 +231,7 @@ describe('agent', () => { feeds: { pinned: undefined, saved: undefined }, adultContentEnabled: false, contentLabels: {}, + birthDate: undefined, }) await agent.setContentLabelPref('impersonation', 'warn') @@ -238,6 +241,7 @@ describe('agent', () => { contentLabels: { impersonation: 'warn', }, + birthDate: undefined, }) await agent.setContentLabelPref('spam', 'show') // will convert to 'ignore' @@ -249,6 +253,7 @@ describe('agent', () => { impersonation: 'hide', spam: 'ignore', }, + birthDate: undefined, }) await agent.addSavedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -262,6 +267,7 @@ describe('agent', () => { impersonation: 'hide', spam: 'ignore', }, + birthDate: undefined, }) await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -275,6 +281,7 @@ describe('agent', () => { impersonation: 'hide', spam: 'ignore', }, + birthDate: undefined, }) await agent.removePinnedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -288,6 +295,7 @@ describe('agent', () => { impersonation: 'hide', spam: 'ignore', }, + birthDate: undefined, }) await agent.removeSavedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -301,6 +309,7 @@ describe('agent', () => { impersonation: 'hide', spam: 'ignore', }, + birthDate: undefined, }) await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -314,6 +323,7 @@ describe('agent', () => { impersonation: 'hide', spam: 'ignore', }, + birthDate: undefined, }) await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake2') @@ -333,6 +343,7 @@ describe('agent', () => { impersonation: 'hide', spam: 'ignore', }, + birthDate: undefined, }) await agent.removeSavedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -346,7 +357,178 @@ describe('agent', () => { impersonation: 'hide', spam: 'ignore', }, + birthDate: undefined, }) + + await agent.setPersonalDetails({ birthDate: '2023-09-11T18:05:42.556Z' }) + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { + pinned: ['at://bob.com/app.bsky.feed.generator/fake2'], + saved: ['at://bob.com/app.bsky.feed.generator/fake2'], + }, + adultContentEnabled: false, + contentLabels: { + impersonation: 'hide', + spam: 'ignore', + }, + birthDate: new Date('2023-09-11T18:05:42.556Z'), + }) + }) + + it('resolves duplicates correctly', async () => { + const agent = new BskyAgent({ service: server.url }) + + await agent.createAccount({ + handle: 'user6.test', + email: 'user6@test.com', + password: 'password', + }) + + await agent.app.bsky.actor.putPreferences({ + preferences: [ + { + $type: 'app.bsky.actor.defs#contentLabelPref', + label: 'nsfw', + visibility: 'show', + }, + { + $type: 'app.bsky.actor.defs#contentLabelPref', + label: 'nsfw', + visibility: 'hide', + }, + { + $type: 'app.bsky.actor.defs#contentLabelPref', + label: 'nsfw', + visibility: 'show', + }, + { + $type: 'app.bsky.actor.defs#contentLabelPref', + label: 'nsfw', + visibility: 'warn', + }, + { + $type: 'app.bsky.actor.defs#adultContentPref', + enabled: true, + }, + { + $type: 'app.bsky.actor.defs#adultContentPref', + enabled: false, + }, + { + $type: 'app.bsky.actor.defs#adultContentPref', + enabled: true, + }, + { + $type: 'app.bsky.actor.defs#savedFeedsPref', + pinned: [ + 'at://bob.com/app.bsky.feed.generator/fake', + 'at://bob.com/app.bsky.feed.generator/fake2', + ], + saved: [ + 'at://bob.com/app.bsky.feed.generator/fake', + 'at://bob.com/app.bsky.feed.generator/fake2', + ], + }, + { + $type: 'app.bsky.actor.defs#savedFeedsPref', + pinned: [], + saved: [], + }, + { + $type: 'app.bsky.actor.defs#personalDetailsPref', + birthDate: '2023-09-11T18:05:42.556Z', + }, + { + $type: 'app.bsky.actor.defs#personalDetailsPref', + birthDate: '2021-09-11T18:05:42.556Z', + }, + ], + }) + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { + pinned: [], + saved: [], + }, + adultContentEnabled: true, + contentLabels: { + nsfw: 'warn', + }, + birthDate: new Date('2021-09-11T18:05:42.556Z'), + }) + + await agent.setAdultContentEnabled(false) + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { + pinned: [], + saved: [], + }, + adultContentEnabled: false, + contentLabels: { + nsfw: 'warn', + }, + birthDate: new Date('2021-09-11T18:05:42.556Z'), + }) + + await agent.setContentLabelPref('nsfw', 'hide') + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { + pinned: [], + saved: [], + }, + adultContentEnabled: false, + contentLabels: { + nsfw: 'hide', + }, + birthDate: new Date('2021-09-11T18:05:42.556Z'), + }) + + await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake') + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { + pinned: ['at://bob.com/app.bsky.feed.generator/fake'], + saved: ['at://bob.com/app.bsky.feed.generator/fake'], + }, + adultContentEnabled: false, + contentLabels: { + nsfw: 'hide', + }, + birthDate: new Date('2021-09-11T18:05:42.556Z'), + }) + + await agent.setPersonalDetails({ birthDate: '2023-09-11T18:05:42.556Z' }) + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { + pinned: ['at://bob.com/app.bsky.feed.generator/fake'], + saved: ['at://bob.com/app.bsky.feed.generator/fake'], + }, + adultContentEnabled: false, + contentLabels: { + nsfw: 'hide', + }, + birthDate: new Date('2023-09-11T18:05:42.556Z'), + }) + + const res = await agent.app.bsky.actor.getPreferences() + await expect(res.data.preferences).toStrictEqual([ + { + $type: 'app.bsky.actor.defs#adultContentPref', + enabled: false, + }, + { + $type: 'app.bsky.actor.defs#contentLabelPref', + label: 'nsfw', + visibility: 'hide', + }, + { + $type: 'app.bsky.actor.defs#savedFeedsPref', + pinned: ['at://bob.com/app.bsky.feed.generator/fake'], + saved: ['at://bob.com/app.bsky.feed.generator/fake'], + }, + { + $type: 'app.bsky.actor.defs#personalDetailsPref', + birthDate: '2023-09-11T18:05:42.556Z', + }, + ]) }) }) })