From a4e34537cee8e12a022238f054bee4fe22cc7325 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 29 Apr 2024 16:04:35 -0500 Subject: [PATCH] Send Bluesky feeds and suggested follows more data (#3695) * WIP * Fix constructors * Clean up * Tweak * Rm extra assignment * Narrow down the argument --------- Co-authored-by: Dan Abramov --- src/lib/api/feed/custom.ts | 10 +++++++++ src/lib/api/feed/home.ts | 11 +++++++++- src/lib/api/feed/merge.ts | 14 ++++++++++++ src/lib/api/feed/utils.ts | 21 ++++++++++++++++++ src/state/queries/post-feed.ts | 15 ++++++++++--- src/state/queries/suggested-follows.ts | 30 ++++++++++++++++++++------ 6 files changed, 91 insertions(+), 10 deletions(-) create mode 100644 src/lib/api/feed/utils.ts diff --git a/src/lib/api/feed/custom.ts b/src/lib/api/feed/custom.ts index 75182c41..87e45ceb 100644 --- a/src/lib/api/feed/custom.ts +++ b/src/lib/api/feed/custom.ts @@ -7,20 +7,25 @@ import { import {getContentLanguages} from '#/state/preferences/languages' import {FeedAPI, FeedAPIResponse} from './types' +import {createBskyTopicsHeader, isBlueskyOwnedFeed} from './utils' export class CustomFeedAPI implements FeedAPI { getAgent: () => BskyAgent params: GetCustomFeed.QueryParams + userInterests?: string constructor({ getAgent, feedParams, + userInterests, }: { getAgent: () => BskyAgent feedParams: GetCustomFeed.QueryParams + userInterests?: string }) { this.getAgent = getAgent this.params = feedParams + this.userInterests = userInterests } async peekLatest(): Promise { @@ -44,6 +49,8 @@ export class CustomFeedAPI implements FeedAPI { }): Promise { const contentLangs = getContentLanguages().join(',') const agent = this.getAgent() + const isBlueskyOwned = isBlueskyOwnedFeed(this.params.feed) + const res = agent.session ? await this.getAgent().app.bsky.feed.getFeed( { @@ -53,6 +60,9 @@ export class CustomFeedAPI implements FeedAPI { }, { headers: { + ...(isBlueskyOwned + ? createBskyTopicsHeader(this.userInterests) + : {}), 'Accept-Language': contentLangs, }, }, diff --git a/src/lib/api/feed/home.ts b/src/lib/api/feed/home.ts index 4a530834..270f3aac 100644 --- a/src/lib/api/feed/home.ts +++ b/src/lib/api/feed/home.ts @@ -32,14 +32,22 @@ export class HomeFeedAPI implements FeedAPI { discover: CustomFeedAPI usingDiscover = false itemCursor = 0 + userInterests?: string - constructor({getAgent}: {getAgent: () => BskyAgent}) { + constructor({ + userInterests, + getAgent, + }: { + userInterests?: string + getAgent: () => BskyAgent + }) { this.getAgent = getAgent this.following = new FollowingFeedAPI({getAgent}) this.discover = new CustomFeedAPI({ getAgent, feedParams: {feed: PROD_DEFAULT_FEED('whats-hot')}, }) + this.userInterests = userInterests } reset() { @@ -47,6 +55,7 @@ export class HomeFeedAPI implements FeedAPI { this.discover = new CustomFeedAPI({ getAgent: this.getAgent, feedParams: {feed: PROD_DEFAULT_FEED('whats-hot')}, + userInterests: this.userInterests, }) this.usingDiscover = false this.itemCursor = 0 diff --git a/src/lib/api/feed/merge.ts b/src/lib/api/feed/merge.ts index c85de030..b7ac8bce 100644 --- a/src/lib/api/feed/merge.ts +++ b/src/lib/api/feed/merge.ts @@ -9,11 +9,13 @@ import {feedUriToHref} from 'lib/strings/url-helpers' import {FeedTuner} from '../feed-manip' import {FeedTunerFn} from '../feed-manip' import {FeedAPI, FeedAPIResponse, ReasonFeedSource} from './types' +import {createBskyTopicsHeader, isBlueskyOwnedFeed} from './utils' const REQUEST_WAIT_MS = 500 // 500ms const POST_AGE_CUTOFF = 60e3 * 60 * 24 // 24hours export class MergeFeedAPI implements FeedAPI { + userInterests?: string getAgent: () => BskyAgent params: FeedParams feedTuners: FeedTunerFn[] @@ -27,14 +29,17 @@ export class MergeFeedAPI implements FeedAPI { getAgent, feedParams, feedTuners, + userInterests, }: { getAgent: () => BskyAgent feedParams: FeedParams feedTuners: FeedTunerFn[] + userInterests?: string }) { this.getAgent = getAgent this.params = feedParams this.feedTuners = feedTuners + this.userInterests = userInterests this.following = new MergeFeedSource_Following({ getAgent: this.getAgent, feedTuners: this.feedTuners, @@ -58,6 +63,7 @@ export class MergeFeedAPI implements FeedAPI { getAgent: this.getAgent, feedUri, feedTuners: this.feedTuners, + userInterests: this.userInterests, }), ), ) @@ -254,15 +260,18 @@ class MergeFeedSource_Custom extends MergeFeedSource { getAgent: () => BskyAgent minDate: Date feedUri: string + userInterests?: string constructor({ getAgent, feedUri, feedTuners, + userInterests, }: { getAgent: () => BskyAgent feedUri: string feedTuners: FeedTunerFn[] + userInterests?: string }) { super({ getAgent, @@ -270,6 +279,7 @@ class MergeFeedSource_Custom extends MergeFeedSource { }) this.getAgent = getAgent this.feedUri = feedUri + this.userInterests = userInterests this.sourceInfo = { $type: 'reasonFeedSource', uri: feedUri, @@ -284,6 +294,7 @@ class MergeFeedSource_Custom extends MergeFeedSource { ): Promise { try { const contentLangs = getContentLanguages().join(',') + const isBlueskyOwned = isBlueskyOwnedFeed(this.feedUri) const res = await this.getAgent().app.bsky.feed.getFeed( { cursor, @@ -292,6 +303,9 @@ class MergeFeedSource_Custom extends MergeFeedSource { }, { headers: { + ...(isBlueskyOwned + ? createBskyTopicsHeader(this.userInterests) + : {}), 'Accept-Language': contentLangs, }, }, diff --git a/src/lib/api/feed/utils.ts b/src/lib/api/feed/utils.ts new file mode 100644 index 00000000..50162ed2 --- /dev/null +++ b/src/lib/api/feed/utils.ts @@ -0,0 +1,21 @@ +import {AtUri} from '@atproto/api' + +import {BSKY_FEED_OWNER_DIDS} from '#/lib/constants' +import {UsePreferencesQueryResponse} from '#/state/queries/preferences' + +export function createBskyTopicsHeader(userInterests?: string) { + return { + 'X-Bsky-Topics': userInterests || '', + } +} + +export function aggregateUserInterests( + preferences?: UsePreferencesQueryResponse, +) { + return preferences?.interests?.tags?.join(',') || '' +} + +export function isBlueskyOwnedFeed(feedUri: string) { + const uri = new AtUri(feedUri) + return BSKY_FEED_OWNER_DIDS.includes(uri.host) +} diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index 747dba02..c265cecd 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -15,6 +15,7 @@ import { } from '@tanstack/react-query' import {HomeFeedAPI} from '#/lib/api/feed/home' +import {aggregateUserInterests} from '#/lib/api/feed/utils' import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' import {logger} from '#/logger' import {STALE} from '#/state/queries' @@ -31,7 +32,7 @@ import {FeedTuner, FeedTunerFn, NoopFeedTuner} from 'lib/api/feed-manip' import {BSKY_FEED_OWNER_DIDS} from 'lib/constants' import {KnownError} from '#/view/com/posts/FeedErrorMessage' import {useFeedTuners} from '../preferences/feed-tuners' -import {useModerationOpts} from './preferences' +import {useModerationOpts, usePreferencesQuery} from './preferences' import {embedViewRecordToPostView, getEmbeddedPost} from './util' type ActorDid = string @@ -102,8 +103,11 @@ export function usePostFeedQuery( ) { const feedTuners = useFeedTuners(feedDesc) const moderationOpts = useModerationOpts() + const {data: preferences} = usePreferencesQuery() + const enabled = + opts?.enabled !== false && Boolean(moderationOpts) && Boolean(preferences) + const userInterests = aggregateUserInterests(preferences) const {getAgent} = useAgent() - const enabled = opts?.enabled !== false && Boolean(moderationOpts) const lastRun = useRef<{ data: InfiniteData args: typeof selectArgs @@ -141,6 +145,7 @@ export function usePostFeedQuery( feedDesc, feedParams: params || {}, feedTuners, + userInterests, // Not in the query key because they don't change. getAgent, }), cursor: undefined, @@ -371,11 +376,13 @@ function createApi({ feedDesc, feedParams, feedTuners, + userInterests, getAgent, }: { feedDesc: FeedDescriptor feedParams: FeedParams feedTuners: FeedTunerFn[] + userInterests?: string getAgent: () => BskyAgent }) { if (feedDesc === 'home') { @@ -384,9 +391,10 @@ function createApi({ getAgent, feedParams, feedTuners, + userInterests, }) } else { - return new HomeFeedAPI({getAgent}) + return new HomeFeedAPI({getAgent, userInterests}) } } else if (feedDesc === 'following') { return new FollowingFeedAPI({getAgent}) @@ -401,6 +409,7 @@ function createApi({ return new CustomFeedAPI({ getAgent, feedParams: {feed}, + userInterests, }) } else if (feedDesc.startsWith('list')) { const [_, list] = feedDesc.split('|') diff --git a/src/state/queries/suggested-follows.ts b/src/state/queries/suggested-follows.ts index 936912ab..3338130f 100644 --- a/src/state/queries/suggested-follows.ts +++ b/src/state/queries/suggested-follows.ts @@ -12,8 +12,16 @@ import { useQuery, } from '@tanstack/react-query' +import { + aggregateUserInterests, + createBskyTopicsHeader, +} from '#/lib/api/feed/utils' +import {getContentLanguages} from '#/state/preferences/languages' import {STALE} from '#/state/queries' -import {useModerationOpts} from '#/state/queries/preferences' +import { + useModerationOpts, + usePreferencesQuery, +} from '#/state/queries/preferences' import {useAgent, useSession} from '#/state/session' const suggestedFollowsQueryKeyRoot = 'suggested-follows' @@ -29,6 +37,7 @@ export function useSuggestedFollowsQuery() { const {currentAccount} = useSession() const {getAgent} = useAgent() const moderationOpts = useModerationOpts() + const {data: preferences} = usePreferencesQuery() return useInfiniteQuery< AppBskyActorGetSuggestions.OutputSchema, @@ -37,14 +46,23 @@ export function useSuggestedFollowsQuery() { QueryKey, string | undefined >({ - enabled: !!moderationOpts, + enabled: !!moderationOpts && !!preferences, staleTime: STALE.HOURS.ONE, queryKey: suggestedFollowsQueryKey, queryFn: async ({pageParam}) => { - const res = await getAgent().app.bsky.actor.getSuggestions({ - limit: 25, - cursor: pageParam, - }) + const contentLangs = getContentLanguages().join(',') + const res = await getAgent().app.bsky.actor.getSuggestions( + { + limit: 25, + cursor: pageParam, + }, + { + headers: { + ...createBskyTopicsHeader(aggregateUserInterests(preferences)), + 'Accept-Language': contentLangs, + }, + }, + ) res.data.actors = res.data.actors .filter(