Merge pull request #1516 from omnivore-app/recommend-highlight-api

recommend highlight api
This commit is contained in:
Jackson Harper 2022-12-08 15:30:23 +08:00 committed by GitHub
commit 79f739c04c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 263 additions and 20 deletions

View file

@ -4,31 +4,54 @@ import { createPage, getPageByParam, updatePage } from './pages'
export const addRecommendation = async (
ctx: PageContext,
page: Page,
recommendation: Recommendation
recommendation: Recommendation,
highlightIds?: string[]
): Promise<string | undefined> => {
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)) {
return existingPage._id
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
)
return existingPage.id
}
@ -41,7 +64,7 @@ export const addRecommendation = async (
readingProgressPercent: 0,
readingProgressAnchorIndex: 0,
sharedAt: new Date(),
highlights: [],
highlights,
readAt: undefined,
labels: [],
}

View file

@ -208,7 +208,7 @@ export interface RecommendingUser {
export interface Recommendation {
id: string
name: string
note: string | null
note?: string | null
user: RecommendingUser
recommendedAt: Date
}

View file

@ -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<RecommendHighlightsErrorCode>;
};
export enum RecommendHighlightsErrorCode {
BadRequest = 'BAD_REQUEST',
NotFound = 'NOT_FOUND',
Unauthorized = 'UNAUTHORIZED'
}
export type RecommendHighlightsInput = {
groupIds: Array<Scalars['ID']>;
highlightIds: Array<Scalars['ID']>;
note?: InputMaybe<Scalars['String']>;
pageId: Scalars['ID'];
};
export type RecommendHighlightsResult = RecommendHighlightsError | RecommendHighlightsSuccess;
export type RecommendHighlightsSuccess = {
__typename?: 'RecommendHighlightsSuccess';
success: Scalars['Boolean'];
};
export type RecommendInput = {
groupIds: Array<Scalars['ID']>;
note?: InputMaybe<Scalars['String']>;
pageId: Scalars['ID'];
recommendedWithHighlights?: InputMaybe<Scalars['Boolean']>;
};
export type RecommendResult = RecommendError | RecommendSuccess;
export type RecommendSuccess = {
__typename?: 'RecommendSuccess';
taskNames: Array<Scalars['String']>;
success: Scalars['Boolean'];
};
export type Recommendation = {
@ -3236,6 +3268,11 @@ export type ResolversTypes = {
RecentSearchesSuccess: ResolverTypeWrapper<RecentSearchesSuccess>;
RecommendError: ResolverTypeWrapper<RecommendError>;
RecommendErrorCode: RecommendErrorCode;
RecommendHighlightsError: ResolverTypeWrapper<RecommendHighlightsError>;
RecommendHighlightsErrorCode: RecommendHighlightsErrorCode;
RecommendHighlightsInput: RecommendHighlightsInput;
RecommendHighlightsResult: ResolversTypes['RecommendHighlightsError'] | ResolversTypes['RecommendHighlightsSuccess'];
RecommendHighlightsSuccess: ResolverTypeWrapper<RecommendHighlightsSuccess>;
RecommendInput: RecommendInput;
RecommendResult: ResolversTypes['RecommendError'] | ResolversTypes['RecommendSuccess'];
RecommendSuccess: ResolverTypeWrapper<RecommendSuccess>;
@ -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<ContextType = ResolverContext, ParentType extends
moveLabel?: Resolver<ResolversTypes['MoveLabelResult'], ParentType, ContextType, RequireFields<MutationMoveLabelArgs, 'input'>>;
optInFeature?: Resolver<ResolversTypes['OptInFeatureResult'], ParentType, ContextType, RequireFields<MutationOptInFeatureArgs, 'input'>>;
recommend?: Resolver<ResolversTypes['RecommendResult'], ParentType, ContextType, RequireFields<MutationRecommendArgs, 'input'>>;
recommendHighlights?: Resolver<ResolversTypes['RecommendHighlightsResult'], ParentType, ContextType, RequireFields<MutationRecommendHighlightsArgs, 'input'>>;
reportItem?: Resolver<ResolversTypes['ReportItemResult'], ParentType, ContextType, RequireFields<MutationReportItemArgs, 'input'>>;
revokeApiKey?: Resolver<ResolversTypes['RevokeApiKeyResult'], ParentType, ContextType, RequireFields<MutationRevokeApiKeyArgs, 'id'>>;
saveArticleReadingProgress?: Resolver<ResolversTypes['SaveArticleReadingProgressResult'], ParentType, ContextType, RequireFields<MutationSaveArticleReadingProgressArgs, 'input'>>;
@ -4837,12 +4879,26 @@ export type RecommendErrorResolvers<ContextType = ResolverContext, ParentType ex
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type RecommendHighlightsErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['RecommendHighlightsError'] = ResolversParentTypes['RecommendHighlightsError']> = {
errorCodes?: Resolver<Array<ResolversTypes['RecommendHighlightsErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type RecommendHighlightsResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['RecommendHighlightsResult'] = ResolversParentTypes['RecommendHighlightsResult']> = {
__resolveType: TypeResolveFn<'RecommendHighlightsError' | 'RecommendHighlightsSuccess', ParentType, ContextType>;
};
export type RecommendHighlightsSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['RecommendHighlightsSuccess'] = ResolversParentTypes['RecommendHighlightsSuccess']> = {
success?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type RecommendResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['RecommendResult'] = ResolversParentTypes['RecommendResult']> = {
__resolveType: TypeResolveFn<'RecommendError' | 'RecommendSuccess', ParentType, ContextType>;
};
export type RecommendSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['RecommendSuccess'] = ResolversParentTypes['RecommendSuccess']> = {
taskNames?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
success?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
@ -5737,6 +5793,9 @@ export type Resolvers<ContextType = ResolverContext> = {
RecentSearchesResult?: RecentSearchesResultResolvers<ContextType>;
RecentSearchesSuccess?: RecentSearchesSuccessResolvers<ContextType>;
RecommendError?: RecommendErrorResolvers<ContextType>;
RecommendHighlightsError?: RecommendHighlightsErrorResolvers<ContextType>;
RecommendHighlightsResult?: RecommendHighlightsResultResolvers<ContextType>;
RecommendHighlightsSuccess?: RecommendHighlightsSuccessResolvers<ContextType>;
RecommendResult?: RecommendResultResolvers<ContextType>;
RecommendSuccess?: RecommendSuccessResolvers<ContextType>;
Recommendation?: RecommendationResolvers<ContextType>;

View file

@ -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 {

View file

@ -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'),
}

View file

@ -11,8 +11,12 @@ import {
MutationCreateGroupArgs,
MutationJoinGroupArgs,
MutationRecommendArgs,
MutationRecommendHighlightsArgs,
RecommendError,
RecommendErrorCode,
RecommendHighlightsError,
RecommendHighlightsErrorCode,
RecommendHighlightsSuccess,
RecommendSuccess,
} from '../../generated/graphql'
import {
@ -173,9 +177,13 @@ 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
const taskNames = await Promise.all(
await Promise.all(
groups
.map((group) =>
group.members
@ -196,7 +204,8 @@ export const recommendResolver = authorized<
},
recommendedAt: new Date(),
},
auth
auth,
recommendedHighlightIds
)
)
)
@ -204,10 +213,9 @@ export const recommendResolver = authorized<
)
return {
taskNames,
success: true,
}
} catch (error) {
console.log('Error recommending: ', error)
log.error('Error recommending', {
error,
labels: {
@ -268,3 +276,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],
}
}
})

View file

@ -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')

View file

@ -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

View file

@ -413,13 +413,15 @@ export const enqueueRecommendation = async (
userId: string,
pageId: string,
recommendation: Recommendation,
authToken: string
authToken: string,
highlightIds?: string[]
): Promise<string> => {
const { GOOGLE_CLOUD_PROJECT } = process.env
const payload = {
userId,
pageId,
recommendation,
highlightIds,
}
const headers = {