From 62a3f6d1d87ff4a7a24000d1f1c98b76e18dcae5 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Thu, 8 Dec 2022 11:37:35 +0800 Subject: [PATCH 1/3] Add recommendHighlights API --- packages/api/src/elastic/types.ts | 2 +- packages/api/src/generated/graphql.ts | 63 ++++++++++- packages/api/src/generated/schema.graphql | 27 ++++- .../api/src/resolvers/function_resolvers.ts | 3 + .../src/resolvers/recommendations/index.ts | 101 +++++++++++++++++- packages/api/src/schema.ts | 31 +++++- packages/api/src/utils/createTask.ts | 4 +- 7 files changed, 222 insertions(+), 9 deletions(-) diff --git a/packages/api/src/elastic/types.ts b/packages/api/src/elastic/types.ts index 9b7122e57..070cf39ec 100644 --- a/packages/api/src/elastic/types.ts +++ b/packages/api/src/elastic/types.ts @@ -208,7 +208,7 @@ export interface RecommendingUser { export interface Recommendation { id: string name: string - note: string | null + note?: string | null user: RecommendingUser recommendedAt: Date } diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index 8728bb795..299708f0b 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -1153,6 +1153,7 @@ export type Mutation = { moveLabel: MoveLabelResult; optInFeature: OptInFeatureResult; recommend: RecommendResult; + recommendHighlights: RecommendHighlightsResult; reportItem: ReportItemResult; revokeApiKey: RevokeApiKeyResult; saveArticleReadingProgress: SaveArticleReadingProgressResult; @@ -1332,6 +1333,11 @@ export type MutationRecommendArgs = { }; +export type MutationRecommendHighlightsArgs = { + input: RecommendHighlightsInput; +}; + + export type MutationReportItemArgs = { input: ReportItemInput; }; @@ -1785,17 +1791,43 @@ export enum RecommendErrorCode { Unauthorized = 'UNAUTHORIZED' } +export type RecommendHighlightsError = { + __typename?: 'RecommendHighlightsError'; + errorCodes: Array; +}; + +export enum RecommendHighlightsErrorCode { + BadRequest = 'BAD_REQUEST', + NotFound = 'NOT_FOUND', + Unauthorized = 'UNAUTHORIZED' +} + +export type RecommendHighlightsInput = { + groupIds: Array; + highlightIds: Array; + note?: InputMaybe; + pageId: Scalars['ID']; +}; + +export type RecommendHighlightsResult = RecommendHighlightsError | RecommendHighlightsSuccess; + +export type RecommendHighlightsSuccess = { + __typename?: 'RecommendHighlightsSuccess'; + success: Scalars['Boolean']; +}; + export type RecommendInput = { groupIds: Array; note?: InputMaybe; pageId: Scalars['ID']; + recommendedWithHighlights?: InputMaybe; }; export type RecommendResult = RecommendError | RecommendSuccess; export type RecommendSuccess = { __typename?: 'RecommendSuccess'; - taskNames: Array; + success: Scalars['Boolean']; }; export type Recommendation = { @@ -3236,6 +3268,11 @@ export type ResolversTypes = { RecentSearchesSuccess: ResolverTypeWrapper; RecommendError: ResolverTypeWrapper; RecommendErrorCode: RecommendErrorCode; + RecommendHighlightsError: ResolverTypeWrapper; + RecommendHighlightsErrorCode: RecommendHighlightsErrorCode; + RecommendHighlightsInput: RecommendHighlightsInput; + RecommendHighlightsResult: ResolversTypes['RecommendHighlightsError'] | ResolversTypes['RecommendHighlightsSuccess']; + RecommendHighlightsSuccess: ResolverTypeWrapper; RecommendInput: RecommendInput; RecommendResult: ResolversTypes['RecommendError'] | ResolversTypes['RecommendSuccess']; RecommendSuccess: ResolverTypeWrapper; @@ -3638,6 +3675,10 @@ export type ResolversParentTypes = { RecentSearchesResult: ResolversParentTypes['RecentSearchesError'] | ResolversParentTypes['RecentSearchesSuccess']; RecentSearchesSuccess: RecentSearchesSuccess; RecommendError: RecommendError; + RecommendHighlightsError: RecommendHighlightsError; + RecommendHighlightsInput: RecommendHighlightsInput; + RecommendHighlightsResult: ResolversParentTypes['RecommendHighlightsError'] | ResolversParentTypes['RecommendHighlightsSuccess']; + RecommendHighlightsSuccess: RecommendHighlightsSuccess; RecommendInput: RecommendInput; RecommendResult: ResolversParentTypes['RecommendError'] | ResolversParentTypes['RecommendSuccess']; RecommendSuccess: RecommendSuccess; @@ -4657,6 +4698,7 @@ export type MutationResolvers>; optInFeature?: Resolver>; recommend?: Resolver>; + recommendHighlights?: Resolver>; reportItem?: Resolver>; revokeApiKey?: Resolver>; saveArticleReadingProgress?: Resolver>; @@ -4837,12 +4879,26 @@ export type RecommendErrorResolvers; }; +export type RecommendHighlightsErrorResolvers = { + errorCodes?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type RecommendHighlightsResultResolvers = { + __resolveType: TypeResolveFn<'RecommendHighlightsError' | 'RecommendHighlightsSuccess', ParentType, ContextType>; +}; + +export type RecommendHighlightsSuccessResolvers = { + success?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type RecommendResultResolvers = { __resolveType: TypeResolveFn<'RecommendError' | 'RecommendSuccess', ParentType, ContextType>; }; export type RecommendSuccessResolvers = { - taskNames?: Resolver, ParentType, ContextType>; + success?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; @@ -5737,6 +5793,9 @@ export type Resolvers = { RecentSearchesResult?: RecentSearchesResultResolvers; RecentSearchesSuccess?: RecentSearchesSuccessResolvers; RecommendError?: RecommendErrorResolvers; + RecommendHighlightsError?: RecommendHighlightsErrorResolvers; + RecommendHighlightsResult?: RecommendHighlightsResultResolvers; + RecommendHighlightsSuccess?: RecommendHighlightsSuccessResolvers; RecommendResult?: RecommendResultResolvers; RecommendSuccess?: RecommendSuccessResolvers; Recommendation?: RecommendationResolvers; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index 89f6cbf6b..3b98f1c06 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -1029,6 +1029,7 @@ type Mutation { moveLabel(input: MoveLabelInput!): MoveLabelResult! optInFeature(input: OptInFeatureInput!): OptInFeatureResult! recommend(input: RecommendInput!): RecommendResult! + recommendHighlights(input: RecommendHighlightsInput!): RecommendHighlightsResult! reportItem(input: ReportItemInput!): ReportItemResult! revokeApiKey(id: ID!): RevokeApiKeyResult! saveArticleReadingProgress(input: SaveArticleReadingProgressInput!): SaveArticleReadingProgressResult! @@ -1248,16 +1249,40 @@ enum RecommendErrorCode { UNAUTHORIZED } +type RecommendHighlightsError { + errorCodes: [RecommendHighlightsErrorCode!]! +} + +enum RecommendHighlightsErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +input RecommendHighlightsInput { + groupIds: [ID!]! + highlightIds: [ID!]! + note: String + pageId: ID! +} + +union RecommendHighlightsResult = RecommendHighlightsError | RecommendHighlightsSuccess + +type RecommendHighlightsSuccess { + success: Boolean! +} + input RecommendInput { groupIds: [ID!]! note: String pageId: ID! + recommendedWithHighlights: Boolean } union RecommendResult = RecommendError | RecommendSuccess type RecommendSuccess { - taskNames: [String!]! + success: Boolean! } type Recommendation { diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index f7fdc9e44..1582856e2 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -64,6 +64,7 @@ import { moveFilterResolver, moveLabelResolver, newsletterEmailsResolver, + recommendHighlightsResolver, recommendResolver, reminderResolver, reportItemResolver, @@ -193,6 +194,7 @@ export const functionResolvers = { createGroup: createGroupResolver, recommend: recommendResolver, joinGroup: joinGroupResolver, + recommendHighlights: recommendHighlightsResolver, }, Query: { me: getMeUserResolver, @@ -646,4 +648,5 @@ export const functionResolvers = { ...resultResolveTypeResolver('Groups'), ...resultResolveTypeResolver('Recommend'), ...resultResolveTypeResolver('JoinGroup'), + ...resultResolveTypeResolver('RecommendHighlights'), } diff --git a/packages/api/src/resolvers/recommendations/index.ts b/packages/api/src/resolvers/recommendations/index.ts index d3e90df89..8074484fa 100644 --- a/packages/api/src/resolvers/recommendations/index.ts +++ b/packages/api/src/resolvers/recommendations/index.ts @@ -11,8 +11,12 @@ import { MutationCreateGroupArgs, MutationJoinGroupArgs, MutationRecommendArgs, + MutationRecommendHighlightsArgs, RecommendError, RecommendErrorCode, + RecommendHighlightsError, + RecommendHighlightsErrorCode, + RecommendHighlightsSuccess, RecommendSuccess, } from '../../generated/graphql' import { @@ -175,7 +179,7 @@ export const recommendResolver = authorized< const exp = Math.floor(Date.now() / 1000) + 60 * 60 * 24 // 1 day const auth = (await signToken({ uid, exp }, env.server.jwtSecret)) as string - const taskNames = await Promise.all( + await Promise.all( groups .map((group) => group.members @@ -204,10 +208,9 @@ export const recommendResolver = authorized< ) return { - taskNames, + success: true, } } catch (error) { - console.log('Error recommending: ', error) log.error('Error recommending', { error, labels: { @@ -268,3 +271,95 @@ export const joinGroupResolver = authorized< } } }) + +export const recommendHighlightsResolver = authorized< + RecommendHighlightsSuccess, + RecommendHighlightsError, + MutationRecommendHighlightsArgs +>(async (_, { input }, { claims: { uid }, log, signToken }) => { + log.info('Recommend highlights', { + input, + labels: { + source: 'resolver', + resolver: 'recommendHighlightsResolver', + uid, + }, + }) + + try { + const user = await getRepository(User).findOne({ + where: { id: uid }, + relations: ['profile'], + }) + if (!user) { + return { + errorCodes: [RecommendHighlightsErrorCode.Unauthorized], + } + } + + const groups = await getRepository(Group).find({ + where: { id: In(input.groupIds) }, + relations: ['members', 'members.user'], + }) + if (groups.length === 0) { + return { + errorCodes: [RecommendHighlightsErrorCode.NotFound], + } + } + + const page = await getPageByParam({ _id: input.pageId, userId: uid }) + if (!page) { + return { + errorCodes: [RecommendHighlightsErrorCode.NotFound], + } + } + + const exp = Math.floor(Date.now() / 1000) + 60 * 60 * 24 // 1 day + const auth = (await signToken({ uid, exp }, env.server.jwtSecret)) as string + await Promise.all( + groups + .map((group) => + group.members + .filter((member) => member.user.id !== uid) + .map((member) => + enqueueRecommendation( + member.user.id, + page.id, + { + id: group.id, + name: group.name, + note: input.note, + user: { + userId: user.id, + name: user.name, + username: user.profile.username, + profileImageURL: user.profile.pictureUrl, + }, + recommendedAt: new Date(), + }, + auth, + input.highlightIds + ) + ) + ) + .flat() + ) + + return { + success: true, + } + } catch (error) { + log.error('Error recommending highlights', { + error, + labels: { + source: 'resolver', + resolver: 'recommendHighlightsResolver', + uid, + }, + }) + + return { + errorCodes: [RecommendHighlightsErrorCode.BadRequest], + } + } +}) diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index de1a182e3..1e89a7ead 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -2204,12 +2204,13 @@ const schema = gql` pageId: ID! groupIds: [ID!]! note: String + recommendedWithHighlights: Boolean } union RecommendResult = RecommendSuccess | RecommendError type RecommendSuccess { - taskNames: [String!]! + success: Boolean! } type RecommendError { @@ -2238,6 +2239,31 @@ const schema = gql` NOT_FOUND } + input RecommendHighlightsInput { + pageId: ID! + highlightIds: [ID!]! + groupIds: [ID!]! + note: String + } + + union RecommendHighlightsResult = + RecommendHighlightsSuccess + | RecommendHighlightsError + + type RecommendHighlightsSuccess { + success: Boolean! + } + + type RecommendHighlightsError { + errorCodes: [RecommendHighlightsErrorCode!]! + } + + enum RecommendHighlightsErrorCode { + UNAUTHORIZED + BAD_REQUEST + NOT_FOUND + } + # Mutations type Mutation { googleLogin(input: GoogleLoginInput!): LoginResult! @@ -2318,6 +2344,9 @@ const schema = gql` createGroup(input: CreateGroupInput!): CreateGroupResult! recommend(input: RecommendInput!): RecommendResult! joinGroup(inviteCode: String!): JoinGroupResult! + recommendHighlights( + input: RecommendHighlightsInput! + ): RecommendHighlightsResult! } # FIXME: remove sort from feedArticles after all cached tabs are closed diff --git a/packages/api/src/utils/createTask.ts b/packages/api/src/utils/createTask.ts index 0d1256e56..8b90111ef 100644 --- a/packages/api/src/utils/createTask.ts +++ b/packages/api/src/utils/createTask.ts @@ -413,13 +413,15 @@ export const enqueueRecommendation = async ( userId: string, pageId: string, recommendation: Recommendation, - authToken: string + authToken: string, + highlightIds?: string[] ): Promise => { const { GOOGLE_CLOUD_PROJECT } = process.env const payload = { userId, pageId, recommendation, + highlightIds, } const headers = { From ef13cbf39a36f2d12f5821f45bffe3227c2f64a5 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Thu, 8 Dec 2022 12:33:05 +0800 Subject: [PATCH 2/3] Recommend highlights --- packages/api/src/elastic/recommendation.ts | 36 +++++++++++++++++----- packages/api/src/routers/page_router.ts | 6 ++-- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/packages/api/src/elastic/recommendation.ts b/packages/api/src/elastic/recommendation.ts index 924f444fe..19a0a3cd1 100644 --- a/packages/api/src/elastic/recommendation.ts +++ b/packages/api/src/elastic/recommendation.ts @@ -4,28 +4,50 @@ import { createPage, getPageByParam, updatePage } from './pages' export const addRecommendation = async ( ctx: PageContext, page: Page, - recommendation: Recommendation + recommendation: Recommendation, + highlightIds?: string[] ): Promise => { try { + const highlights = page.highlights?.filter((highlight) => + highlightIds?.includes(highlight.id) + ) + // check if the page is already recommended to the group const existingPage = await getPageByParam({ userId: ctx.uid, url: page.url, }) if (existingPage) { - if (existingPage.recommendations?.includes(recommendation)) { + const existingHighlights = existingPage.highlights || [] + + // remove duplicates + const newHighlights = + highlights?.filter( + (highlight) => + !existingHighlights.find( + (existingHighlight) => existingHighlight.quote === highlight.quote + ) + ) || [] + + const existingRecommendations = existingPage.recommendations || [] + const isRecommended = existingRecommendations.some( + (existingRecommendation) => + existingRecommendation.id === recommendation.id + ) + if (isRecommended && newHighlights.length === 0) { return existingPage._id } - // update recommendedBy in the existing page - const recommendations = (existingPage.recommendations || []).concat( - recommendation - ) + // update recommendations in the existing page + const recommendations = isRecommended + ? undefined + : existingRecommendations.concat(recommendation) await updatePage( existingPage.id, { recommendations, + highlights: existingHighlights.concat(newHighlights), }, ctx ) @@ -41,7 +63,7 @@ export const addRecommendation = async ( readingProgressPercent: 0, readingProgressAnchorIndex: 0, sharedAt: new Date(), - highlights: [], + highlights, readAt: undefined, labels: [], } diff --git a/packages/api/src/routers/page_router.ts b/packages/api/src/routers/page_router.ts index da74925fc..50bca8865 100644 --- a/packages/api/src/routers/page_router.ts +++ b/packages/api/src/routers/page_router.ts @@ -163,10 +163,11 @@ export function pageRouter() { } const claims = jwt.decode(token) as Claims - const { userId, pageId, recommendation } = req.body as { + const { userId, pageId, recommendation, highlightIds } = req.body as { userId: string pageId: string recommendation: Recommendation + highlightIds?: string[] } if (!userId || !pageId || !recommendation) { return res.status(400).send({ errorCode: 'BAD_DATA' }) @@ -188,7 +189,8 @@ export function pageRouter() { const recommendedPageId = await addRecommendation( ctx, page, - recommendation + recommendation, + highlightIds ) if (!recommendedPageId) { logger.error('Failed to add recommendation to page') From 4c92af1856ca81a599a1025e13e624436dc60023 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Thu, 8 Dec 2022 12:51:01 +0800 Subject: [PATCH 3/3] Allow recommended With Highlights --- packages/api/src/elastic/recommendation.ts | 3 ++- packages/api/src/resolvers/recommendations/index.ts | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/api/src/elastic/recommendation.ts b/packages/api/src/elastic/recommendation.ts index 19a0a3cd1..5c68e206a 100644 --- a/packages/api/src/elastic/recommendation.ts +++ b/packages/api/src/elastic/recommendation.ts @@ -35,7 +35,7 @@ export const addRecommendation = async ( existingRecommendation.id === recommendation.id ) if (isRecommended && newHighlights.length === 0) { - return existingPage._id + return existingPage.id } // update recommendations in the existing page @@ -51,6 +51,7 @@ export const addRecommendation = async ( }, ctx ) + return existingPage.id } diff --git a/packages/api/src/resolvers/recommendations/index.ts b/packages/api/src/resolvers/recommendations/index.ts index 8074484fa..c51be3c0c 100644 --- a/packages/api/src/resolvers/recommendations/index.ts +++ b/packages/api/src/resolvers/recommendations/index.ts @@ -177,6 +177,10 @@ export const recommendResolver = authorized< } } + const recommendedHighlightIds = input.recommendedWithHighlights + ? page.highlights?.map((h) => h.id) + : undefined + const exp = Math.floor(Date.now() / 1000) + 60 * 60 * 24 // 1 day const auth = (await signToken({ uid, exp }, env.server.jwtSecret)) as string await Promise.all( @@ -200,7 +204,8 @@ export const recommendResolver = authorized< }, recommendedAt: new Date(), }, - auth + auth, + recommendedHighlightIds ) ) )