From 4171c04ad81c5734a4558bc41fa1c4f3a1aba18c Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 25 Jan 2024 12:05:10 -0600 Subject: [PATCH] Add interest tags to preferences (#2086) * Add interestsPref * Codegen * Update lex * Add method to agent, test * Codegen * Format * Remove console * Update lex, codegen, update tests * Update tests * Format * Add changeset * Update property name --- .changeset/real-bikes-share.md | 6 + lexicons/app/bsky/actor/defs.json | 15 ++- packages/api/src/bsky-agent.ts | 27 +++++ packages/api/src/client/lexicons.ts | 18 +++ .../src/client/types/app/bsky/actor/defs.ts | 19 +++ packages/api/src/types.ts | 9 ++ packages/api/tests/bsky-agent.test.ts | 109 ++++++++++++++++++ packages/bsky/src/lexicon/lexicons.ts | 18 +++ .../src/lexicon/types/app/bsky/actor/defs.ts | 19 +++ packages/ozone/src/lexicon/lexicons.ts | 18 +++ .../src/lexicon/types/app/bsky/actor/defs.ts | 19 +++ packages/pds/src/lexicon/lexicons.ts | 18 +++ .../src/lexicon/types/app/bsky/actor/defs.ts | 19 +++ 13 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 .changeset/real-bikes-share.md diff --git a/.changeset/real-bikes-share.md b/.changeset/real-bikes-share.md new file mode 100644 index 00000000000..2ae2ddb5a95 --- /dev/null +++ b/.changeset/real-bikes-share.md @@ -0,0 +1,6 @@ +--- +'@atproto/api': patch +--- + +Add `setInterestsPref` method to BskyAgent, and `interests` prop to +`getPreferences` response. diff --git a/lexicons/app/bsky/actor/defs.json b/lexicons/app/bsky/actor/defs.json index 913957f1291..9f8e2ea97c8 100644 --- a/lexicons/app/bsky/actor/defs.json +++ b/lexicons/app/bsky/actor/defs.json @@ -104,7 +104,8 @@ "#savedFeedsPref", "#personalDetailsPref", "#feedViewPref", - "#threadViewPref" + "#threadViewPref", + "#interestsPref" ] } }, @@ -199,6 +200,18 @@ "description": "Show followed users at the top of all replies." } } + }, + "interestsPref": { + "type": "object", + "required": ["tags"], + "properties": { + "tags": { + "type": "array", + "maxLength": 100, + "items": { "type": "string", "maxLength": 640, "maxGraphemes": 64 }, + "description": "A list of tags which describe the account owner's interests gathered during onboarding." + } + } } } } diff --git a/packages/api/src/bsky-agent.ts b/packages/api/src/bsky-agent.ts index ce166a59490..606e06dcda8 100644 --- a/packages/api/src/bsky-agent.ts +++ b/packages/api/src/bsky-agent.ts @@ -11,6 +11,7 @@ import { BskyLabelPreference, BskyFeedViewPreference, BskyThreadViewPreference, + BskyInterestsPreference, } from './types' const FEED_VIEW_PREF_DEFAULTS = { @@ -323,6 +324,9 @@ export class BskyAgent extends AtpAgent { adultContentEnabled: false, contentLabels: {}, birthDate: undefined, + interests: { + tags: [], + }, } const res = await this.app.bsky.actor.getPreferences({}) for (const pref of res.data.preferences) { @@ -369,6 +373,13 @@ export class BskyAgent extends AtpAgent { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { $type, ...v } = pref prefs.threadViewPrefs = { ...prefs.threadViewPrefs, ...v } + } else if ( + AppBskyActorDefs.isInterestsPref(pref) && + AppBskyActorDefs.validateInterestsPref(pref).success + ) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { $type, ...v } = pref + prefs.interests = { ...prefs.interests, ...v } } } return prefs @@ -521,6 +532,22 @@ export class BskyAgent extends AtpAgent { .concat([{ ...pref, $type: 'app.bsky.actor.defs#threadViewPref' }]) }) } + + async setInterestsPref(pref: Partial) { + await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => { + const existing = prefs.findLast( + (pref) => + AppBskyActorDefs.isInterestsPref(pref) && + AppBskyActorDefs.validateInterestsPref(pref).success, + ) + if (existing) { + pref = { ...existing, ...pref } + } + return prefs + .filter((p) => !AppBskyActorDefs.isInterestsPref(p)) + .concat([{ ...pref, $type: 'app.bsky.actor.defs#interestsPref' }]) + }) + } } /** diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index fea624e9a04..38d565f2571 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -4615,6 +4615,7 @@ export const schemaDict = { 'lex:app.bsky.actor.defs#personalDetailsPref', 'lex:app.bsky.actor.defs#feedViewPref', 'lex:app.bsky.actor.defs#threadViewPref', + 'lex:app.bsky.actor.defs#interestsPref', ], }, }, @@ -4718,6 +4719,23 @@ export const schemaDict = { }, }, }, + interestsPref: { + type: 'object', + required: ['tags'], + properties: { + tags: { + type: 'array', + maxLength: 100, + items: { + type: 'string', + maxLength: 640, + maxGraphemes: 64, + }, + description: + "A list of tags which describe the account owner's interests gathered during onboarding.", + }, + }, + }, }, }, AppBskyActorGetPreferences: { diff --git a/packages/api/src/client/types/app/bsky/actor/defs.ts b/packages/api/src/client/types/app/bsky/actor/defs.ts index 0cb22ea5797..5c1791e6130 100644 --- a/packages/api/src/client/types/app/bsky/actor/defs.ts +++ b/packages/api/src/client/types/app/bsky/actor/defs.ts @@ -112,6 +112,7 @@ export type Preferences = ( | PersonalDetailsPref | FeedViewPref | ThreadViewPref + | InterestsPref | { $type: string; [k: string]: unknown } )[] @@ -233,3 +234,21 @@ export function isThreadViewPref(v: unknown): v is ThreadViewPref { export function validateThreadViewPref(v: unknown): ValidationResult { return lexicons.validate('app.bsky.actor.defs#threadViewPref', v) } + +export interface InterestsPref { + /** A list of tags which describe the account owner's interests gathered during onboarding. */ + tags: string[] + [k: string]: unknown +} + +export function isInterestsPref(v: unknown): v is InterestsPref { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#interestsPref' + ) +} + +export function validateInterestsPref(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#interestsPref', v) +} diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index 45684a3ef27..3d6f73baa33 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -97,6 +97,14 @@ export interface BskyThreadViewPreference { [key: string]: any } +/** + * Bluesky interests preferences + */ +export interface BskyInterestsPreference { + tags: string[] + [key: string]: any +} + /** * Bluesky preferences */ @@ -110,4 +118,5 @@ export interface BskyPreferences { adultContentEnabled: boolean contentLabels: Record birthDate: Date | undefined + interests: BskyInterestsPreference } diff --git a/packages/api/tests/bsky-agent.test.ts b/packages/api/tests/bsky-agent.test.ts index 5582f7ac021..5f850b19e91 100644 --- a/packages/api/tests/bsky-agent.test.ts +++ b/packages/api/tests/bsky-agent.test.ts @@ -236,6 +236,9 @@ describe('agent', () => { sort: 'oldest', prioritizeFollowedUsers: true, }, + interests: { + tags: [], + }, }) await agent.setAdultContentEnabled(true) @@ -257,6 +260,9 @@ describe('agent', () => { sort: 'oldest', prioritizeFollowedUsers: true, }, + interests: { + tags: [], + }, }) await agent.setAdultContentEnabled(false) @@ -278,6 +284,9 @@ describe('agent', () => { sort: 'oldest', prioritizeFollowedUsers: true, }, + interests: { + tags: [], + }, }) await agent.setContentLabelPref('impersonation', 'warn') @@ -301,6 +310,9 @@ describe('agent', () => { sort: 'oldest', prioritizeFollowedUsers: true, }, + interests: { + tags: [], + }, }) await agent.setContentLabelPref('spam', 'show') // will convert to 'ignore' @@ -326,6 +338,9 @@ describe('agent', () => { sort: 'oldest', prioritizeFollowedUsers: true, }, + interests: { + tags: [], + }, }) await agent.addSavedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -353,6 +368,9 @@ describe('agent', () => { sort: 'oldest', prioritizeFollowedUsers: true, }, + interests: { + tags: [], + }, }) await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -380,6 +398,9 @@ describe('agent', () => { sort: 'oldest', prioritizeFollowedUsers: true, }, + interests: { + tags: [], + }, }) await agent.removePinnedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -407,6 +428,9 @@ describe('agent', () => { sort: 'oldest', prioritizeFollowedUsers: true, }, + interests: { + tags: [], + }, }) await agent.removeSavedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -434,6 +458,9 @@ describe('agent', () => { sort: 'oldest', prioritizeFollowedUsers: true, }, + interests: { + tags: [], + }, }) await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -461,6 +488,9 @@ describe('agent', () => { sort: 'oldest', prioritizeFollowedUsers: true, }, + interests: { + tags: [], + }, }) await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake2') @@ -494,6 +524,9 @@ describe('agent', () => { sort: 'oldest', prioritizeFollowedUsers: true, }, + interests: { + tags: [], + }, }) await agent.removeSavedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -521,6 +554,9 @@ describe('agent', () => { sort: 'oldest', prioritizeFollowedUsers: true, }, + interests: { + tags: [], + }, }) await agent.setPersonalDetails({ birthDate: '2023-09-11T18:05:42.556Z' }) @@ -548,6 +584,9 @@ describe('agent', () => { sort: 'oldest', prioritizeFollowedUsers: true, }, + interests: { + tags: [], + }, }) await agent.setFeedViewPrefs('home', { hideReplies: true }) @@ -575,6 +614,9 @@ describe('agent', () => { sort: 'oldest', prioritizeFollowedUsers: true, }, + interests: { + tags: [], + }, }) await agent.setFeedViewPrefs('home', { hideReplies: false }) @@ -602,6 +644,9 @@ describe('agent', () => { sort: 'oldest', prioritizeFollowedUsers: true, }, + interests: { + tags: [], + }, }) await agent.setFeedViewPrefs('other', { hideReplies: true }) @@ -636,6 +681,9 @@ describe('agent', () => { sort: 'oldest', prioritizeFollowedUsers: true, }, + interests: { + tags: [], + }, }) await agent.setThreadViewPrefs({ sort: 'random' }) @@ -670,6 +718,9 @@ describe('agent', () => { sort: 'random', prioritizeFollowedUsers: true, }, + interests: { + tags: [], + }, }) await agent.setThreadViewPrefs({ sort: 'oldest' }) @@ -704,6 +755,46 @@ describe('agent', () => { sort: 'oldest', prioritizeFollowedUsers: true, }, + interests: { + tags: [], + }, + }) + + await agent.setInterestsPref({ tags: ['foo', 'bar'] }) + 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'), + feedViewPrefs: { + home: { + hideReplies: false, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, + }, + other: { + hideReplies: true, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, + }, + }, + threadViewPrefs: { + sort: 'oldest', + prioritizeFollowedUsers: true, + }, + interests: { + tags: ['foo', 'bar'], + }, }) }) @@ -827,6 +918,9 @@ describe('agent', () => { sort: 'newest', prioritizeFollowedUsers: false, }, + interests: { + tags: [], + }, }) await agent.setAdultContentEnabled(false) @@ -853,6 +947,9 @@ describe('agent', () => { sort: 'newest', prioritizeFollowedUsers: false, }, + interests: { + tags: [], + }, }) await agent.setContentLabelPref('nsfw', 'hide') @@ -879,6 +976,9 @@ describe('agent', () => { sort: 'newest', prioritizeFollowedUsers: false, }, + interests: { + tags: [], + }, }) await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -905,6 +1005,9 @@ describe('agent', () => { sort: 'newest', prioritizeFollowedUsers: false, }, + interests: { + tags: [], + }, }) await agent.setPersonalDetails({ birthDate: '2023-09-11T18:05:42.556Z' }) @@ -931,6 +1034,9 @@ describe('agent', () => { sort: 'newest', prioritizeFollowedUsers: false, }, + interests: { + tags: [], + }, }) await agent.setFeedViewPrefs('home', { @@ -968,6 +1074,9 @@ describe('agent', () => { sort: 'oldest', prioritizeFollowedUsers: true, }, + interests: { + tags: [], + }, }) const res = await agent.app.bsky.actor.getPreferences() diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index fea624e9a04..38d565f2571 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -4615,6 +4615,7 @@ export const schemaDict = { 'lex:app.bsky.actor.defs#personalDetailsPref', 'lex:app.bsky.actor.defs#feedViewPref', 'lex:app.bsky.actor.defs#threadViewPref', + 'lex:app.bsky.actor.defs#interestsPref', ], }, }, @@ -4718,6 +4719,23 @@ export const schemaDict = { }, }, }, + interestsPref: { + type: 'object', + required: ['tags'], + properties: { + tags: { + type: 'array', + maxLength: 100, + items: { + type: 'string', + maxLength: 640, + maxGraphemes: 64, + }, + description: + "A list of tags which describe the account owner's interests gathered during onboarding.", + }, + }, + }, }, }, AppBskyActorGetPreferences: { diff --git a/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts b/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts index c20177ca50e..8cdcafcb72f 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts @@ -112,6 +112,7 @@ export type Preferences = ( | PersonalDetailsPref | FeedViewPref | ThreadViewPref + | InterestsPref | { $type: string; [k: string]: unknown } )[] @@ -233,3 +234,21 @@ export function isThreadViewPref(v: unknown): v is ThreadViewPref { export function validateThreadViewPref(v: unknown): ValidationResult { return lexicons.validate('app.bsky.actor.defs#threadViewPref', v) } + +export interface InterestsPref { + /** A list of tags which describe the account owner's interests gathered during onboarding. */ + tags: string[] + [k: string]: unknown +} + +export function isInterestsPref(v: unknown): v is InterestsPref { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#interestsPref' + ) +} + +export function validateInterestsPref(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#interestsPref', v) +} diff --git a/packages/ozone/src/lexicon/lexicons.ts b/packages/ozone/src/lexicon/lexicons.ts index fea624e9a04..38d565f2571 100644 --- a/packages/ozone/src/lexicon/lexicons.ts +++ b/packages/ozone/src/lexicon/lexicons.ts @@ -4615,6 +4615,7 @@ export const schemaDict = { 'lex:app.bsky.actor.defs#personalDetailsPref', 'lex:app.bsky.actor.defs#feedViewPref', 'lex:app.bsky.actor.defs#threadViewPref', + 'lex:app.bsky.actor.defs#interestsPref', ], }, }, @@ -4718,6 +4719,23 @@ export const schemaDict = { }, }, }, + interestsPref: { + type: 'object', + required: ['tags'], + properties: { + tags: { + type: 'array', + maxLength: 100, + items: { + type: 'string', + maxLength: 640, + maxGraphemes: 64, + }, + description: + "A list of tags which describe the account owner's interests gathered during onboarding.", + }, + }, + }, }, }, AppBskyActorGetPreferences: { diff --git a/packages/ozone/src/lexicon/types/app/bsky/actor/defs.ts b/packages/ozone/src/lexicon/types/app/bsky/actor/defs.ts index c20177ca50e..8cdcafcb72f 100644 --- a/packages/ozone/src/lexicon/types/app/bsky/actor/defs.ts +++ b/packages/ozone/src/lexicon/types/app/bsky/actor/defs.ts @@ -112,6 +112,7 @@ export type Preferences = ( | PersonalDetailsPref | FeedViewPref | ThreadViewPref + | InterestsPref | { $type: string; [k: string]: unknown } )[] @@ -233,3 +234,21 @@ export function isThreadViewPref(v: unknown): v is ThreadViewPref { export function validateThreadViewPref(v: unknown): ValidationResult { return lexicons.validate('app.bsky.actor.defs#threadViewPref', v) } + +export interface InterestsPref { + /** A list of tags which describe the account owner's interests gathered during onboarding. */ + tags: string[] + [k: string]: unknown +} + +export function isInterestsPref(v: unknown): v is InterestsPref { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#interestsPref' + ) +} + +export function validateInterestsPref(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#interestsPref', v) +} diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index fea624e9a04..38d565f2571 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -4615,6 +4615,7 @@ export const schemaDict = { 'lex:app.bsky.actor.defs#personalDetailsPref', 'lex:app.bsky.actor.defs#feedViewPref', 'lex:app.bsky.actor.defs#threadViewPref', + 'lex:app.bsky.actor.defs#interestsPref', ], }, }, @@ -4718,6 +4719,23 @@ export const schemaDict = { }, }, }, + interestsPref: { + type: 'object', + required: ['tags'], + properties: { + tags: { + type: 'array', + maxLength: 100, + items: { + type: 'string', + maxLength: 640, + maxGraphemes: 64, + }, + description: + "A list of tags which describe the account owner's interests gathered during onboarding.", + }, + }, + }, }, }, AppBskyActorGetPreferences: { diff --git a/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts b/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts index c20177ca50e..8cdcafcb72f 100644 --- a/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts +++ b/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts @@ -112,6 +112,7 @@ export type Preferences = ( | PersonalDetailsPref | FeedViewPref | ThreadViewPref + | InterestsPref | { $type: string; [k: string]: unknown } )[] @@ -233,3 +234,21 @@ export function isThreadViewPref(v: unknown): v is ThreadViewPref { export function validateThreadViewPref(v: unknown): ValidationResult { return lexicons.validate('app.bsky.actor.defs#threadViewPref', v) } + +export interface InterestsPref { + /** A list of tags which describe the account owner's interests gathered during onboarding. */ + tags: string[] + [k: string]: unknown +} + +export function isInterestsPref(v: unknown): v is InterestsPref { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#interestsPref' + ) +} + +export function validateInterestsPref(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#interestsPref', v) +}