diff --git a/__tests__/__snapshots__/settings.ts.snap b/__tests__/__snapshots__/settings.ts.snap index 8bb8f5e34f..7f571a122b 100644 --- a/__tests__/__snapshots__/settings.ts.snap +++ b/__tests__/__snapshots__/settings.ts.snap @@ -11,6 +11,7 @@ Object { "flags": Object { "clickbaitShieldEnabled": null, "defaultWriteTab": null, + "noAiFeedEnabled": null, "sidebarBookmarksExpanded": null, "sidebarCustomFeedsExpanded": null, "sidebarOtherExpanded": null, @@ -48,6 +49,7 @@ Object { "flags": Object { "clickbaitShieldEnabled": null, "defaultWriteTab": null, + "noAiFeedEnabled": null, "sidebarBookmarksExpanded": null, "sidebarCustomFeedsExpanded": null, "sidebarOtherExpanded": null, @@ -87,6 +89,7 @@ Object { "flags": Object { "clickbaitShieldEnabled": null, "defaultWriteTab": null, + "noAiFeedEnabled": null, "sidebarBookmarksExpanded": null, "sidebarCustomFeedsExpanded": null, "sidebarOtherExpanded": null, @@ -126,6 +129,7 @@ Object { "flags": Object { "clickbaitShieldEnabled": null, "defaultWriteTab": null, + "noAiFeedEnabled": null, "sidebarBookmarksExpanded": null, "sidebarCustomFeedsExpanded": null, "sidebarOtherExpanded": null, diff --git a/__tests__/boot.ts b/__tests__/boot.ts index 48ac054597..c5910cf572 100644 --- a/__tests__/boot.ts +++ b/__tests__/boot.ts @@ -1364,6 +1364,7 @@ describe('boot misc', () => { sidebarSquadExpanded: true, sidebarBookmarksExpanded: true, clickbaitShieldEnabled: true, + noAiFeedEnabled: false, }, }); }); diff --git a/__tests__/feeds.ts b/__tests__/feeds.ts index acd528729c..7a0c7a32b3 100644 --- a/__tests__/feeds.ts +++ b/__tests__/feeds.ts @@ -20,6 +20,7 @@ import { PostTag, PostType, SharePost, + Settings, Source, SourceMember, SourceType, @@ -728,6 +729,100 @@ describe('query feed', () => { expect(res.data.feed.edges.length).toEqual(2); }); + it('should include no-ai blocked tags and title words when the saved setting is enabled', async () => { + loggedUser = '1'; + await saveFeedFixtures(); + await saveFixtures(con, Settings, [ + { + userId: '1', + flags: { + noAiFeedEnabled: true, + }, + }, + ]); + nock('http://localhost:6002') + .post('/config') + .reply(200, { + user_id: '1', + config: { + providers: {}, + }, + }); + nock('http://localhost:6000') + .post('/feed.json', (body) => { + expect(body.blocked_tags).toEqual( + expect.arrayContaining(['golang', 'ai', 'openai']), + ); + expect(body.blocked_title_words).toEqual( + expect.arrayContaining(['Claude', 'Elon Musk']), + ); + + return true; + }) + .reply(200, { + data: [{ post_id: 'p1' }, { post_id: 'p4' }], + cursor: 'b', + }); + + const res = await client.query(QUERY, { + variables: { + ...variables, + version: 20, + }, + }); + + expect(res.errors).toBeFalsy(); + expect(res.data.feed.edges.length).toEqual(2); + }); + + it('should include no-ai blocked tags and title words for TIME ranking when the saved setting is enabled', async () => { + loggedUser = '1'; + await saveFeedFixtures(); + await saveFixtures(con, Settings, [ + { + userId: '1', + flags: { + noAiFeedEnabled: true, + }, + }, + ]); + nock('http://localhost:6002') + .post('/config') + .reply(200, { + user_id: '1', + config: { + providers: {}, + }, + }); + nock('http://localhost:6000') + .post('/feed.json', (body) => { + expect(body.blocked_tags).toEqual( + expect.arrayContaining(['golang', 'ai', 'openai']), + ); + expect(body.blocked_title_words).toEqual( + expect.arrayContaining(['Claude', 'Elon Musk']), + ); + expect(body.feed_config_name).toBe('for_you_by_date'); + + return true; + }) + .reply(200, { + data: [{ post_id: 'p1' }, { post_id: 'p4' }], + cursor: 'b', + }); + + const res = await client.query(QUERY, { + variables: { + ...variables, + ranking: Ranking.TIME, + version: 20, + }, + }); + + expect(res.errors).toBeFalsy(); + expect(res.data.feed.edges.length).toEqual(2); + }); + describe('youtube content', () => { beforeEach(async () => { await saveFixtures(con, YouTubePost, videoPostsFixture); diff --git a/__tests__/settings.ts b/__tests__/settings.ts index 6b344dfc4f..2a9f09f179 100644 --- a/__tests__/settings.ts +++ b/__tests__/settings.ts @@ -121,6 +121,7 @@ describe('mutation updateUserSettings', () => { sidebarSquadExpanded sidebarBookmarksExpanded clickbaitShieldEnabled + noAiFeedEnabled defaultWriteTab } } @@ -157,6 +158,7 @@ describe('mutation updateUserSettings', () => { sidebarSquadExpanded: null, sidebarBookmarksExpanded: null, clickbaitShieldEnabled: null, + noAiFeedEnabled: null, defaultWriteTab: null, }); }); @@ -176,6 +178,7 @@ describe('mutation updateUserSettings', () => { sidebarSquadExpanded: null, sidebarBookmarksExpanded: null, clickbaitShieldEnabled: null, + noAiFeedEnabled: null, defaultWriteTab: null, }); @@ -312,6 +315,7 @@ describe('mutation updateUserSettings', () => { sidebarSquadExpanded: null, sidebarBookmarksExpanded: null, clickbaitShieldEnabled: null, + noAiFeedEnabled: null, defaultWriteTab: null, }); }); @@ -347,10 +351,51 @@ describe('mutation updateUserSettings', () => { sidebarSquadExpanded: null, sidebarBookmarksExpanded: null, clickbaitShieldEnabled: null, + noAiFeedEnabled: null, defaultWriteTab: null, }); }); + it('should update the no ai feed flag', async () => { + loggedUser = '1'; + + const repo = con.getRepository(Settings); + await repo.save( + repo.create({ + userId: '1', + flags: { + noAiFeedEnabled: false, + }, + }), + ); + + const res = await client.mutate(MUTATION, { + variables: { + data: { + flags: { + noAiFeedEnabled: true, + }, + }, + }, + }); + + expect(res.data.updateUserSettings.flags).toEqual({ + sidebarCustomFeedsExpanded: null, + sidebarOtherExpanded: null, + sidebarResourcesExpanded: null, + sidebarSquadExpanded: null, + sidebarBookmarksExpanded: null, + clickbaitShieldEnabled: null, + noAiFeedEnabled: true, + defaultWriteTab: null, + }); + + const updated = await repo.findOneByOrFail({ userId: '1' }); + expect(updated.flags).toEqual({ + noAiFeedEnabled: true, + }); + }); + it('should update opt out companion settings', async () => { loggedUser = '1'; diff --git a/src/common/flags.ts b/src/common/flags.ts index ca10bde502..2d31cf94bb 100644 --- a/src/common/flags.ts +++ b/src/common/flags.ts @@ -8,6 +8,7 @@ export const transformSettingFlags = ({ flags }: Pick) => { sidebarResourcesExpanded: flags?.sidebarResourcesExpanded ?? true, sidebarBookmarksExpanded: flags?.sidebarBookmarksExpanded ?? true, clickbaitShieldEnabled: flags?.clickbaitShieldEnabled ?? true, + noAiFeedEnabled: flags?.noAiFeedEnabled ?? false, ...flags, }; }; diff --git a/src/entity/Settings.ts b/src/entity/Settings.ts index ce612209b9..c13e107e6e 100644 --- a/src/entity/Settings.ts +++ b/src/entity/Settings.ts @@ -33,6 +33,7 @@ export type SettingsFlags = Partial<{ sidebarResourcesExpanded: boolean; sidebarBookmarksExpanded: boolean; clickbaitShieldEnabled: boolean; + noAiFeedEnabled: boolean; prompt: object; timezoneMismatchIgnore: string; lastPrompt: string; @@ -47,6 +48,7 @@ export type SettingsFlagsPublic = Pick< | 'sidebarResourcesExpanded' | 'sidebarBookmarksExpanded' | 'clickbaitShieldEnabled' + | 'noAiFeedEnabled' | 'prompt' | 'timezoneMismatchIgnore' | 'lastPrompt' @@ -163,6 +165,7 @@ export const SETTINGS_DEFAULT = { sidebarResourcesExpanded: true, sidebarBookmarksExpanded: true, clickbaitShieldEnabled: true, + noAiFeedEnabled: false, defaultWriteTab: DefaultWriteTab.NewPost, }, }; diff --git a/src/schema/feeds.ts b/src/schema/feeds.ts index 061fe0334a..1042e8ad3f 100644 --- a/src/schema/feeds.ts +++ b/src/schema/feeds.ts @@ -9,6 +9,7 @@ import { FeedType, Post, PostType, + Settings, Source, UserPost, } from '../entity'; @@ -88,6 +89,7 @@ import { SourceMemberRoles } from '../roles'; import { ContentPreferenceKeyword } from '../entity/contentPreference/ContentPreferenceKeyword'; import { briefingPostIdsMaxItems } from '../common/brief'; import { NO_AI_BLOCKED_TAGS, NO_AI_BLOCKED_WORDS } from '../common/noAiFilter'; +import { queryReadReplica } from '../common/queryReadReplica'; interface GQLTagsCategory { id: string; @@ -1392,6 +1394,16 @@ const wrapGeneratorWithNoAi = (generator: FeedGenerator): FeedGenerator => }, })); +const isSavedNoAiEnabled = async (ctx: AuthContext): Promise => { + const settings = await queryReadReplica(ctx.con, ({ queryRunner }) => + queryRunner.manager.getRepository(Settings).findOneBy({ + userId: ctx.userId, + }), + ); + + return settings?.flags?.noAiFeedEnabled ?? false; +}; + const feedResolverV1: IFieldResolver = feedResolver( ( @@ -1591,32 +1603,40 @@ export const resolvers: IResolvers = { } return anonymousFeedResolverV1(source, args, ctx, info); }, - feed: (source, args: ConfiguredFeedArgs, ctx: Context, info) => { - if (args.version >= 2 && args.ranking === Ranking.POPULARITY) { - const generator = versionToFeedGenerator(args.version); - - return feedResolverCursor( - source, - { - ...(args as FeedArgs), - generator: args.noAi ? wrapGeneratorWithNoAi(generator) : generator, - }, - ctx, - info, - ); - } - if (args.version >= 2 && args.ranking === Ranking.TIME) { - const generator = versionToTimeFeedGenerator(args.version); - - return feedResolverCursor( - source, - { - ...(args as FeedArgs), - generator: args.noAi ? wrapGeneratorWithNoAi(generator) : generator, - }, - ctx, - info, - ); + feed: async (source, args: ConfiguredFeedArgs, ctx: AuthContext, info) => { + if (args.version >= 2) { + const shouldApplyNoAi = args.noAi || (await isSavedNoAiEnabled(ctx)); + const getGeneratorWithNoAi = ( + generator: FeedGenerator, + ): FeedGenerator => + shouldApplyNoAi ? wrapGeneratorWithNoAi(generator) : generator; + + if (args.ranking === Ranking.POPULARITY) { + return feedResolverCursor( + source, + { + ...(args as FeedArgs), + generator: getGeneratorWithNoAi( + versionToFeedGenerator(args.version), + ), + }, + ctx, + info, + ); + } + if (args.ranking === Ranking.TIME) { + return feedResolverCursor( + source, + { + ...(args as FeedArgs), + generator: getGeneratorWithNoAi( + versionToTimeFeedGenerator(args.version), + ), + }, + ctx, + info, + ); + } } return feedResolverV1(source, args, ctx, info); }, diff --git a/src/schema/settings.ts b/src/schema/settings.ts index d52869a599..00738a0a7c 100644 --- a/src/schema/settings.ts +++ b/src/schema/settings.ts @@ -72,6 +72,7 @@ export const typeDefs = /* GraphQL */ ` sidebarResourcesExpanded: Boolean sidebarBookmarksExpanded: Boolean clickbaitShieldEnabled: Boolean + noAiFeedEnabled: Boolean timezoneMismatchIgnore: String lastPrompt: String defaultWriteTab: DefaultWriteTab @@ -84,6 +85,7 @@ export const typeDefs = /* GraphQL */ ` sidebarResourcesExpanded: Boolean sidebarBookmarksExpanded: Boolean clickbaitShieldEnabled: Boolean + noAiFeedEnabled: Boolean prompt: JSONObject timezoneMismatchIgnore: String lastPrompt: String