Skip to content

Commit

Permalink
Merge pull request #3261 from omnivore-app/feature/add-state-for-non-…
Browse files Browse the repository at this point in the history
…fetch-content

feature/add state for non fetch content
  • Loading branch information
sywhb authored Dec 19, 2023
2 parents 0c8bce3 + 92aa11b commit e4b6cba
Show file tree
Hide file tree
Showing 11 changed files with 221 additions and 45 deletions.
1 change: 1 addition & 0 deletions packages/api/src/entity/library_item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export enum LibraryItemState {
Succeeded = 'SUCCEEDED',
Deleted = 'DELETED',
Archived = 'ARCHIVED',
ContentNotFetched = 'CONTENT_NOT_FETCHED',
}

export enum ContentReaderType {
Expand Down
49 changes: 49 additions & 0 deletions packages/api/src/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ export type ArticleSavingRequestResult = ArticleSavingRequestError | ArticleSavi

export enum ArticleSavingRequestStatus {
Archived = 'ARCHIVED',
ContentNotFetched = 'CONTENT_NOT_FETCHED',
Deleted = 'DELETED',
Failed = 'FAILED',
Processing = 'PROCESSING',
Expand Down Expand Up @@ -819,6 +820,23 @@ export type FeedsSuccess = {
pageInfo: PageInfo;
};

export type FetchContentError = {
__typename?: 'FetchContentError';
errorCodes: Array<FetchContentErrorCode>;
};

export enum FetchContentErrorCode {
BadRequest = 'BAD_REQUEST',
Unauthorized = 'UNAUTHORIZED'
}

export type FetchContentResult = FetchContentError | FetchContentSuccess;

export type FetchContentSuccess = {
__typename?: 'FetchContentSuccess';
success: Scalars['Boolean'];
};

export type Filter = {
__typename?: 'Filter';
category?: Maybe<Scalars['String']>;
Expand Down Expand Up @@ -1334,6 +1352,7 @@ export type Mutation = {
deleteNewsletterEmail: DeleteNewsletterEmailResult;
deleteRule: DeleteRuleResult;
deleteWebhook: DeleteWebhookResult;
fetchContent: FetchContentResult;
generateApiKey: GenerateApiKeyResult;
googleLogin: LoginResult;
googleSignup: GoogleSignupResult;
Expand Down Expand Up @@ -1466,6 +1485,11 @@ export type MutationDeleteWebhookArgs = {
};


export type MutationFetchContentArgs = {
id: Scalars['ID'];
};


export type MutationGenerateApiKeyArgs = {
input: GenerateApiKeyInput;
};
Expand Down Expand Up @@ -3623,6 +3647,10 @@ export type ResolversTypes = {
FeedsInput: FeedsInput;
FeedsResult: ResolversTypes['FeedsError'] | ResolversTypes['FeedsSuccess'];
FeedsSuccess: ResolverTypeWrapper<FeedsSuccess>;
FetchContentError: ResolverTypeWrapper<FetchContentError>;
FetchContentErrorCode: FetchContentErrorCode;
FetchContentResult: ResolversTypes['FetchContentError'] | ResolversTypes['FetchContentSuccess'];
FetchContentSuccess: ResolverTypeWrapper<FetchContentSuccess>;
Filter: ResolverTypeWrapper<Filter>;
FiltersError: ResolverTypeWrapper<FiltersError>;
FiltersErrorCode: FiltersErrorCode;
Expand Down Expand Up @@ -4118,6 +4146,9 @@ export type ResolversParentTypes = {
FeedsInput: FeedsInput;
FeedsResult: ResolversParentTypes['FeedsError'] | ResolversParentTypes['FeedsSuccess'];
FeedsSuccess: FeedsSuccess;
FetchContentError: FetchContentError;
FetchContentResult: ResolversParentTypes['FetchContentError'] | ResolversParentTypes['FetchContentSuccess'];
FetchContentSuccess: FetchContentSuccess;
Filter: Filter;
FiltersError: FiltersError;
FiltersResult: ResolversParentTypes['FiltersError'] | ResolversParentTypes['FiltersSuccess'];
Expand Down Expand Up @@ -4986,6 +5017,20 @@ export type FeedsSuccessResolvers<ContextType = ResolverContext, ParentType exte
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};

export type FetchContentErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['FetchContentError'] = ResolversParentTypes['FetchContentError']> = {
errorCodes?: Resolver<Array<ResolversTypes['FetchContentErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};

export type FetchContentResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['FetchContentResult'] = ResolversParentTypes['FetchContentResult']> = {
__resolveType: TypeResolveFn<'FetchContentError' | 'FetchContentSuccess', ParentType, ContextType>;
};

export type FetchContentSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['FetchContentSuccess'] = ResolversParentTypes['FetchContentSuccess']> = {
success?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};

export type FilterResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['Filter'] = ResolversParentTypes['Filter']> = {
category?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
createdAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
Expand Down Expand Up @@ -5376,6 +5421,7 @@ export type MutationResolvers<ContextType = ResolverContext, ParentType extends
deleteNewsletterEmail?: Resolver<ResolversTypes['DeleteNewsletterEmailResult'], ParentType, ContextType, RequireFields<MutationDeleteNewsletterEmailArgs, 'newsletterEmailId'>>;
deleteRule?: Resolver<ResolversTypes['DeleteRuleResult'], ParentType, ContextType, RequireFields<MutationDeleteRuleArgs, 'id'>>;
deleteWebhook?: Resolver<ResolversTypes['DeleteWebhookResult'], ParentType, ContextType, RequireFields<MutationDeleteWebhookArgs, 'id'>>;
fetchContent?: Resolver<ResolversTypes['FetchContentResult'], ParentType, ContextType, RequireFields<MutationFetchContentArgs, 'id'>>;
generateApiKey?: Resolver<ResolversTypes['GenerateApiKeyResult'], ParentType, ContextType, RequireFields<MutationGenerateApiKeyArgs, 'input'>>;
googleLogin?: Resolver<ResolversTypes['LoginResult'], ParentType, ContextType, RequireFields<MutationGoogleLoginArgs, 'input'>>;
googleSignup?: Resolver<ResolversTypes['GoogleSignupResult'], ParentType, ContextType, RequireFields<MutationGoogleSignupArgs, 'input'>>;
Expand Down Expand Up @@ -6558,6 +6604,9 @@ export type Resolvers<ContextType = ResolverContext> = {
FeedsError?: FeedsErrorResolvers<ContextType>;
FeedsResult?: FeedsResultResolvers<ContextType>;
FeedsSuccess?: FeedsSuccessResolvers<ContextType>;
FetchContentError?: FetchContentErrorResolvers<ContextType>;
FetchContentResult?: FetchContentResultResolvers<ContextType>;
FetchContentSuccess?: FetchContentSuccessResolvers<ContextType>;
Filter?: FilterResolvers<ContextType>;
FiltersError?: FiltersErrorResolvers<ContextType>;
FiltersResult?: FiltersResultResolvers<ContextType>;
Expand Down
17 changes: 17 additions & 0 deletions packages/api/src/generated/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ union ArticleSavingRequestResult = ArticleSavingRequestError | ArticleSavingRequ

enum ArticleSavingRequestStatus {
ARCHIVED
CONTENT_NOT_FETCHED
DELETED
FAILED
PROCESSING
Expand Down Expand Up @@ -727,6 +728,21 @@ type FeedsSuccess {
pageInfo: PageInfo!
}

type FetchContentError {
errorCodes: [FetchContentErrorCode!]!
}

enum FetchContentErrorCode {
BAD_REQUEST
UNAUTHORIZED
}

union FetchContentResult = FetchContentError | FetchContentSuccess

type FetchContentSuccess {
success: Boolean!
}

type Filter {
category: String
createdAt: Date!
Expand Down Expand Up @@ -1197,6 +1213,7 @@ type Mutation {
deleteNewsletterEmail(newsletterEmailId: ID!): DeleteNewsletterEmailResult!
deleteRule(id: ID!): DeleteRuleResult!
deleteWebhook(id: ID!): DeleteWebhookResult!
fetchContent(id: ID!): FetchContentResult!
generateApiKey(input: GenerateApiKeyInput!): GenerateApiKeyResult!
googleLogin(input: GoogleLoginInput!): LoginResult!
googleSignup(input: GoogleSignupInput!): GoogleSignupResult!
Expand Down
51 changes: 50 additions & 1 deletion packages/api/src/resolvers/article/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@ import {
CreateArticleError,
CreateArticleErrorCode,
CreateArticleSuccess,
FetchContentError,
FetchContentErrorCode,
FetchContentSuccess,
MoveToFolderError,
MoveToFolderErrorCode,
MoveToFolderSuccess,
MutationBulkActionArgs,
MutationCreateArticleArgs,
MutationFetchContentArgs,
MutationMoveToFolderArgs,
MutationSaveArticleReadingProgressArgs,
MutationSetBookmarkArticleArgs,
Expand Down Expand Up @@ -67,6 +71,7 @@ import {
} from '../../services/labels'
import {
createLibraryItem,
findLibraryItemById,
findLibraryItemByUrl,
findLibraryItemsByPrefix,
searchLibraryItems,
Expand Down Expand Up @@ -955,7 +960,7 @@ export const moveToFolderResolver = authorized<
)

// if the content is not fetched yet, create a page save request
if (!item.readableContent) {
if (item.state === LibraryItemState.ContentNotFetched) {
try {
await createPageSaveRequest({
userId: uid,
Expand All @@ -981,6 +986,50 @@ export const moveToFolderResolver = authorized<
}
})

export const fetchContentResolver = authorized<
FetchContentSuccess,
FetchContentError,
MutationFetchContentArgs
>(async (_, { id }, { uid, log, pubsub }) => {
analytics.track({
userId: uid,
event: 'fetch_content',
properties: {
id,
},
})

const item = await findLibraryItemById(id, uid)
if (!item) {
return {
errorCodes: [FetchContentErrorCode.Unauthorized],
}
}

// if the content is not fetched yet, create a page save request
if (item.state === LibraryItemState.ContentNotFetched) {
try {
await createPageSaveRequest({
userId: uid,
url: item.originalUrl,
articleSavingRequestId: id,
priority: 'high',
pubsub,
})
} catch (error) {
log.error('fetchContentResolver error', error)

return {
errorCodes: [FetchContentErrorCode.BadRequest],
}
}
}

return {
success: true,
}
})

const getUpdateReason = (libraryItem: LibraryItem, since: Date) => {
if (libraryItem.deletedAt) {
return UpdateReason.Deleted
Expand Down
75 changes: 68 additions & 7 deletions packages/api/src/routers/svc/following.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
import express from 'express'
import {
ArticleSavingRequestStatus,
PreparedDocumentInput,
} from '../../generated/graphql'
import { createAndSaveLabelsInLibraryItem } from '../../services/labels'
import { saveFeedItemInFollowing } from '../../services/library_item'
import { createLibraryItem } from '../../services/library_item'
import { parsedContentToLibraryItem } from '../../services/save_page'
import { cleanUrl, generateSlug } from '../../utils/helpers'
import { createThumbnailUrl } from '../../utils/imageproxy'
import { logger } from '../../utils/logger'
import {
ParsedContentPuppeteer,
parsePreparedContent,
} from '../../utils/parser'

type SourceOfFollowing = 'feed' | 'newsletter' | 'user'

Expand Down Expand Up @@ -35,6 +46,8 @@ function isSaveFollowingItemRequest(
)
}

const FOLDER = 'following'

export function followingServiceRouter() {
const router = express.Router()

Expand All @@ -58,20 +71,68 @@ export function followingServiceRouter() {
const userId = req.body.userIds[0]
logger.info('saving feed item', userId)

const result = await saveFeedItemInFollowing(req.body, userId)
if (result.identifiers.length === 0) {
logger.error('error saving feed item in following')
return res.status(500).send('ERROR_SAVING_FEED_ITEM')
const feedUrl = req.body.addedToFollowingBy
const thumbnail =
req.body.thumbnail && createThumbnailUrl(req.body.thumbnail)
const url = cleanUrl(req.body.url)

const preparedDocument: PreparedDocumentInput = {
document: req.body.previewContent || '',
pageInfo: {
title: req.body.title,
author: req.body.author,
canonicalUrl: url,
contentType: req.body.previewContentType,
description: req.body.description,
previewImage: thumbnail,
},
}
let parsedResult: ParsedContentPuppeteer | undefined

// parse the content if we have a preview content
if (req.body.previewContent) {
parsedResult = await parsePreparedContent(url, preparedDocument)
}

const { pathname } = new URL(url)
const croppedPathname = decodeURIComponent(
pathname
.split('/')
[pathname.split('/').length - 1].split('.')
.slice(0, -1)
.join('.')
).replace(/_/gi, ' ')

const slug = generateSlug(
parsedResult?.parsedContent?.title || croppedPathname
)
const itemToSave = parsedContentToLibraryItem({
url,
title: req.body.title,
parsedContent: parsedResult?.parsedContent || null,
userId,
slug,
croppedPathname,
originalHtml: req.body.previewContent,
itemType: parsedResult?.pageType || 'unknown',
canonicalUrl: url,
folder: FOLDER,
rssFeedUrl: feedUrl,
preparedDocument,
savedAt: req.body.savedAt,
publishedAt: req.body.publishedAt,
state: ArticleSavingRequestStatus.ContentNotFetched,
})

const newItem = await createLibraryItem(itemToSave, userId)
logger.info('feed item saved in following')

// save RSS label in the item
await createAndSaveLabelsInLibraryItem(
result.identifiers[0].id,
newItem.id,
userId,
[{ name: 'RSS' }],
req.body.addedToFollowingBy
feedUrl
)

logger.info('RSS label added to the item')
Expand Down
17 changes: 17 additions & 0 deletions packages/api/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,7 @@ const schema = gql`
FAILED
DELETED
ARCHIVED
CONTENT_NOT_FETCHED
}
type ArticleSavingRequest {
Expand Down Expand Up @@ -2721,6 +2722,21 @@ const schema = gql`
BAD_REQUEST
}
union FetchContentResult = FetchContentSuccess | FetchContentError
type FetchContentSuccess {
success: Boolean!
}
type FetchContentError {
errorCodes: [FetchContentErrorCode!]!
}
enum FetchContentErrorCode {
UNAUTHORIZED
BAD_REQUEST
}
# Mutations
type Mutation {
googleLogin(input: GoogleLoginInput!): LoginResult!
Expand Down Expand Up @@ -2828,6 +2844,7 @@ const schema = gql`
input: UpdateSubscriptionInput!
): UpdateSubscriptionResult!
moveToFolder(id: ID!, folder: String!): MoveToFolderResult!
fetchContent(id: ID!): FetchContentResult!
}
# FIXME: remove sort from feedArticles after all cached tabs are closed
Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/services/create_page_save_request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export const createPageSaveRequest = async ({
pubsub
)
}
// reset state to processing if not in following
// reset state to processing
if (libraryItem.state !== LibraryItemState.Processing) {
libraryItem = await updateLibraryItem(
libraryItem.id,
Expand Down
Loading

1 comment on commit e4b6cba

@vercel
Copy link

@vercel vercel bot commented on e4b6cba Dec 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.