Merge pull request #3567 from omnivore-app/feature/feed-summary
feature/feed summary
This commit is contained in:
@ -19,6 +19,11 @@ export enum HighlightType {
|
||||
Note = 'NOTE', // to be deleted in favor of note on library item
|
||||
}
|
||||
|
||||
export enum RepresentationType {
|
||||
Content = 'CONTENT',
|
||||
FeedContent = 'FEED_CONTENT',
|
||||
}
|
||||
|
||||
@Entity({ name: 'highlight' })
|
||||
export class Highlight {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
@ -87,4 +92,10 @@ export class Highlight {
|
||||
inverseJoinColumn: { name: 'label_id' },
|
||||
})
|
||||
labels?: Label[]
|
||||
|
||||
@Column('enum', {
|
||||
enum: RepresentationType,
|
||||
default: RepresentationType.Content,
|
||||
})
|
||||
representation!: RepresentationType
|
||||
}
|
||||
|
||||
@ -191,7 +191,7 @@ export class LibraryItem {
|
||||
links?: any | null
|
||||
|
||||
@Column('text')
|
||||
previewContent?: string | null
|
||||
feedContent?: string | null
|
||||
|
||||
@Column('text')
|
||||
previewContentType?: string | null
|
||||
|
||||
@ -23,6 +23,12 @@ export enum SubscriptionType {
|
||||
Rss = 'RSS',
|
||||
}
|
||||
|
||||
export enum FetchContentType {
|
||||
Always = 'ALWAYS',
|
||||
Never = 'NEVER',
|
||||
WhenEmpty = 'WHEN_EMPTY',
|
||||
}
|
||||
|
||||
@Entity({ name: 'subscriptions' })
|
||||
export class Subscription {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
@ -98,6 +104,12 @@ export class Subscription {
|
||||
@Column('boolean')
|
||||
fetchContent!: boolean
|
||||
|
||||
@Column('enum', {
|
||||
enum: FetchContentType,
|
||||
default: FetchContentType.Always,
|
||||
})
|
||||
fetchContentType!: FetchContentType
|
||||
|
||||
@Column('text')
|
||||
folder?: string | null
|
||||
}
|
||||
|
||||
@ -94,6 +94,7 @@ export type Article = {
|
||||
contentReader: ContentReader;
|
||||
createdAt: Scalars['Date'];
|
||||
description?: Maybe<Scalars['String']>;
|
||||
feedContent?: Maybe<Scalars['String']>;
|
||||
folder: Scalars['String'];
|
||||
hasContent?: Maybe<Scalars['Boolean']>;
|
||||
hash: Scalars['String'];
|
||||
@ -367,6 +368,7 @@ export type CreateHighlightInput = {
|
||||
patch?: InputMaybe<Scalars['String']>;
|
||||
prefix?: InputMaybe<Scalars['String']>;
|
||||
quote?: InputMaybe<Scalars['String']>;
|
||||
representation?: InputMaybe<RepresentationType>;
|
||||
sharedAt?: InputMaybe<Scalars['Date']>;
|
||||
shortId: Scalars['String'];
|
||||
suffix?: InputMaybe<Scalars['String']>;
|
||||
@ -853,6 +855,12 @@ export type FetchContentSuccess = {
|
||||
success: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export enum FetchContentType {
|
||||
Always = 'ALWAYS',
|
||||
Never = 'NEVER',
|
||||
WhenEmpty = 'WHEN_EMPTY'
|
||||
}
|
||||
|
||||
export type Filter = {
|
||||
__typename?: 'Filter';
|
||||
category?: Maybe<Scalars['String']>;
|
||||
@ -1017,6 +1025,7 @@ export type Highlight = {
|
||||
quote?: Maybe<Scalars['String']>;
|
||||
reactions: Array<Reaction>;
|
||||
replies: Array<HighlightReply>;
|
||||
representation: RepresentationType;
|
||||
sharedAt?: Maybe<Scalars['Date']>;
|
||||
shortId: Scalars['String'];
|
||||
suffix?: Maybe<Scalars['String']>;
|
||||
@ -1274,6 +1283,7 @@ export type MergeHighlightInput = {
|
||||
patch: Scalars['String'];
|
||||
prefix?: InputMaybe<Scalars['String']>;
|
||||
quote: Scalars['String'];
|
||||
representation?: InputMaybe<RepresentationType>;
|
||||
shortId: Scalars['ID'];
|
||||
suffix?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
@ -2171,6 +2181,11 @@ export enum ReportType {
|
||||
Spam = 'SPAM'
|
||||
}
|
||||
|
||||
export enum RepresentationType {
|
||||
Content = 'CONTENT',
|
||||
FeedContent = 'FEED_CONTENT'
|
||||
}
|
||||
|
||||
export type RevokeApiKeyError = {
|
||||
__typename?: 'RevokeApiKeyError';
|
||||
errorCodes: Array<RevokeApiKeyErrorCode>;
|
||||
@ -2397,6 +2412,7 @@ export type SearchItem = {
|
||||
contentReader: ContentReader;
|
||||
createdAt: Scalars['Date'];
|
||||
description?: Maybe<Scalars['String']>;
|
||||
feedContent?: Maybe<Scalars['String']>;
|
||||
folder: Scalars['String'];
|
||||
highlights?: Maybe<Array<Highlight>>;
|
||||
id: Scalars['ID'];
|
||||
@ -2409,7 +2425,6 @@ export type SearchItem = {
|
||||
ownedByViewer?: Maybe<Scalars['Boolean']>;
|
||||
pageId?: Maybe<Scalars['ID']>;
|
||||
pageType: PageType;
|
||||
previewContent?: Maybe<Scalars['String']>;
|
||||
previewContentType?: Maybe<Scalars['String']>;
|
||||
publishedAt?: Maybe<Scalars['Date']>;
|
||||
quote?: Maybe<Scalars['String']>;
|
||||
@ -2818,6 +2833,7 @@ export enum SubscribeErrorCode {
|
||||
export type SubscribeInput = {
|
||||
autoAddToLibrary?: InputMaybe<Scalars['Boolean']>;
|
||||
fetchContent?: InputMaybe<Scalars['Boolean']>;
|
||||
fetchContentType?: InputMaybe<FetchContentType>;
|
||||
folder?: InputMaybe<Scalars['String']>;
|
||||
isPrivate?: InputMaybe<Scalars['Boolean']>;
|
||||
subscriptionType?: InputMaybe<SubscriptionType>;
|
||||
@ -2839,6 +2855,7 @@ export type Subscription = {
|
||||
description?: Maybe<Scalars['String']>;
|
||||
failedAt?: Maybe<Scalars['Date']>;
|
||||
fetchContent: Scalars['Boolean'];
|
||||
fetchContentType: FetchContentType;
|
||||
folder: Scalars['String'];
|
||||
icon?: Maybe<Scalars['String']>;
|
||||
id: Scalars['ID'];
|
||||
@ -3214,6 +3231,7 @@ export type UpdateSubscriptionInput = {
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
failedAt?: InputMaybe<Scalars['Date']>;
|
||||
fetchContent?: InputMaybe<Scalars['Boolean']>;
|
||||
fetchContentType?: InputMaybe<FetchContentType>;
|
||||
folder?: InputMaybe<Scalars['String']>;
|
||||
id: Scalars['ID'];
|
||||
isPrivate?: InputMaybe<Scalars['Boolean']>;
|
||||
@ -3711,6 +3729,7 @@ export type ResolversTypes = {
|
||||
FetchContentErrorCode: FetchContentErrorCode;
|
||||
FetchContentResult: ResolversTypes['FetchContentError'] | ResolversTypes['FetchContentSuccess'];
|
||||
FetchContentSuccess: ResolverTypeWrapper<FetchContentSuccess>;
|
||||
FetchContentType: FetchContentType;
|
||||
Filter: ResolverTypeWrapper<Filter>;
|
||||
FiltersError: ResolverTypeWrapper<FiltersError>;
|
||||
FiltersErrorCode: FiltersErrorCode;
|
||||
@ -3860,6 +3879,7 @@ export type ResolversTypes = {
|
||||
ReportItemInput: ReportItemInput;
|
||||
ReportItemResult: ResolverTypeWrapper<ReportItemResult>;
|
||||
ReportType: ReportType;
|
||||
RepresentationType: RepresentationType;
|
||||
RevokeApiKeyError: ResolverTypeWrapper<RevokeApiKeyError>;
|
||||
RevokeApiKeyErrorCode: RevokeApiKeyErrorCode;
|
||||
RevokeApiKeyResult: ResolversTypes['RevokeApiKeyError'] | ResolversTypes['RevokeApiKeySuccess'];
|
||||
@ -4585,6 +4605,7 @@ export type ArticleResolvers<ContextType = ResolverContext, ParentType extends R
|
||||
contentReader?: Resolver<ResolversTypes['ContentReader'], ParentType, ContextType>;
|
||||
createdAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
|
||||
description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
feedContent?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
folder?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
hasContent?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
|
||||
hash?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
@ -5245,6 +5266,7 @@ export type HighlightResolvers<ContextType = ResolverContext, ParentType extends
|
||||
quote?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
reactions?: Resolver<Array<ResolversTypes['Reaction']>, ParentType, ContextType>;
|
||||
replies?: Resolver<Array<ResolversTypes['HighlightReply']>, ParentType, ContextType>;
|
||||
representation?: Resolver<ResolversTypes['RepresentationType'], ParentType, ContextType>;
|
||||
sharedAt?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
|
||||
shortId?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
suffix?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
@ -5931,6 +5953,7 @@ export type SearchItemResolvers<ContextType = ResolverContext, ParentType extend
|
||||
contentReader?: Resolver<ResolversTypes['ContentReader'], ParentType, ContextType>;
|
||||
createdAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
|
||||
description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
feedContent?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
folder?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
highlights?: Resolver<Maybe<Array<ResolversTypes['Highlight']>>, ParentType, ContextType>;
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
@ -5943,7 +5966,6 @@ export type SearchItemResolvers<ContextType = ResolverContext, ParentType extend
|
||||
ownedByViewer?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
|
||||
pageId?: Resolver<Maybe<ResolversTypes['ID']>, ParentType, ContextType>;
|
||||
pageType?: Resolver<ResolversTypes['PageType'], ParentType, ContextType>;
|
||||
previewContent?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
previewContentType?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
publishedAt?: Resolver<Maybe<ResolversTypes['Date']>, ParentType, ContextType>;
|
||||
quote?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
@ -6197,6 +6219,7 @@ export type SubscriptionResolvers<ContextType = ResolverContext, ParentType exte
|
||||
description?: SubscriptionResolver<Maybe<ResolversTypes['String']>, "description", ParentType, ContextType>;
|
||||
failedAt?: SubscriptionResolver<Maybe<ResolversTypes['Date']>, "failedAt", ParentType, ContextType>;
|
||||
fetchContent?: SubscriptionResolver<ResolversTypes['Boolean'], "fetchContent", ParentType, ContextType>;
|
||||
fetchContentType?: SubscriptionResolver<ResolversTypes['FetchContentType'], "fetchContentType", ParentType, ContextType>;
|
||||
folder?: SubscriptionResolver<ResolversTypes['String'], "folder", ParentType, ContextType>;
|
||||
icon?: SubscriptionResolver<Maybe<ResolversTypes['String']>, "icon", ParentType, ContextType>;
|
||||
id?: SubscriptionResolver<ResolversTypes['ID'], "id", ParentType, ContextType>;
|
||||
|
||||
@ -69,6 +69,7 @@ type Article {
|
||||
contentReader: ContentReader!
|
||||
createdAt: Date!
|
||||
description: String
|
||||
feedContent: String
|
||||
folder: String!
|
||||
hasContent: Boolean
|
||||
hash: String!
|
||||
@ -318,6 +319,7 @@ input CreateHighlightInput {
|
||||
patch: String
|
||||
prefix: String
|
||||
quote: String
|
||||
representation: RepresentationType
|
||||
sharedAt: Date
|
||||
shortId: String!
|
||||
suffix: String
|
||||
@ -757,6 +759,12 @@ type FetchContentSuccess {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
enum FetchContentType {
|
||||
ALWAYS
|
||||
NEVER
|
||||
WHEN_EMPTY
|
||||
}
|
||||
|
||||
type Filter {
|
||||
category: String
|
||||
createdAt: Date!
|
||||
@ -905,6 +913,7 @@ type Highlight {
|
||||
quote: String
|
||||
reactions: [Reaction!]!
|
||||
replies: [HighlightReply!]!
|
||||
representation: RepresentationType!
|
||||
sharedAt: Date
|
||||
shortId: String!
|
||||
suffix: String
|
||||
@ -1141,6 +1150,7 @@ input MergeHighlightInput {
|
||||
patch: String!
|
||||
prefix: String
|
||||
quote: String!
|
||||
representation: RepresentationType
|
||||
shortId: ID!
|
||||
suffix: String
|
||||
}
|
||||
@ -1615,6 +1625,11 @@ enum ReportType {
|
||||
SPAM
|
||||
}
|
||||
|
||||
enum RepresentationType {
|
||||
CONTENT
|
||||
FEED_CONTENT
|
||||
}
|
||||
|
||||
type RevokeApiKeyError {
|
||||
errorCodes: [RevokeApiKeyErrorCode!]!
|
||||
}
|
||||
@ -1825,6 +1840,7 @@ type SearchItem {
|
||||
contentReader: ContentReader!
|
||||
createdAt: Date!
|
||||
description: String
|
||||
feedContent: String
|
||||
folder: String!
|
||||
highlights: [Highlight!]
|
||||
id: ID!
|
||||
@ -1837,7 +1853,6 @@ type SearchItem {
|
||||
ownedByViewer: Boolean
|
||||
pageId: ID
|
||||
pageType: PageType!
|
||||
previewContent: String
|
||||
previewContentType: String
|
||||
publishedAt: Date
|
||||
quote: String
|
||||
@ -2216,6 +2231,7 @@ enum SubscribeErrorCode {
|
||||
input SubscribeInput {
|
||||
autoAddToLibrary: Boolean
|
||||
fetchContent: Boolean
|
||||
fetchContentType: FetchContentType
|
||||
folder: String
|
||||
isPrivate: Boolean
|
||||
subscriptionType: SubscriptionType
|
||||
@ -2235,6 +2251,7 @@ type Subscription {
|
||||
description: String
|
||||
failedAt: Date
|
||||
fetchContent: Boolean!
|
||||
fetchContentType: FetchContentType!
|
||||
folder: String!
|
||||
icon: String
|
||||
id: ID!
|
||||
@ -2581,6 +2598,7 @@ input UpdateSubscriptionInput {
|
||||
description: String
|
||||
failedAt: Date
|
||||
fetchContent: Boolean
|
||||
fetchContentType: FetchContentType
|
||||
folder: String
|
||||
id: ID!
|
||||
isPrivate: Boolean
|
||||
|
||||
@ -31,7 +31,7 @@ export const refreshAllFeeds = async (db: DataSource): Promise<boolean> => {
|
||||
ARRAY_AGG(s.most_recent_item_date) AS "mostRecentItemDates",
|
||||
ARRAY_AGG(coalesce(s.scheduled_at, NOW())) AS "scheduledDates",
|
||||
ARRAY_AGG(s.last_fetched_checksum) AS checksums,
|
||||
ARRAY_AGG(s.fetch_content) AS "fetchContents",
|
||||
JSON_AGG(s.fetch_content_type) AS "fetchContentTypes",
|
||||
ARRAY_AGG(coalesce(s.folder, $3)) AS folders
|
||||
FROM
|
||||
omnivore.subscriptions s
|
||||
@ -106,7 +106,7 @@ const updateSubscriptionGroup = async (
|
||||
timestamp.getTime()
|
||||
), // unix timestamp in milliseconds
|
||||
userIds: group.userIds,
|
||||
fetchContents: group.fetchContents,
|
||||
fetchContentTypes: group.fetchContentTypes,
|
||||
folders: group.folders,
|
||||
}
|
||||
|
||||
|
||||
@ -2,14 +2,20 @@ import axios from 'axios'
|
||||
import crypto from 'crypto'
|
||||
import { parseHTML } from 'linkedom'
|
||||
import Parser, { Item } from 'rss-parser'
|
||||
import { FetchContentType } from '../../entity/subscription'
|
||||
import { env } from '../../env'
|
||||
import { ArticleSavingRequestStatus } from '../../generated/graphql'
|
||||
import { redisDataSource } from '../../redis_data_source'
|
||||
import { validateUrl } from '../../services/create_page_save_request'
|
||||
import { savePage } from '../../services/save_page'
|
||||
import {
|
||||
updateSubscription,
|
||||
updateSubscriptions,
|
||||
} from '../../services/update_subscription'
|
||||
import { findActiveUser } from '../../services/user'
|
||||
import createHttpTaskWithToken from '../../utils/createTask'
|
||||
import { cleanUrl } from '../../utils/helpers'
|
||||
import { createThumbnailUrl } from '../../utils/imageproxy'
|
||||
import { logger } from '../../utils/logger'
|
||||
import { RSSRefreshContext } from './refreshAllFeeds'
|
||||
|
||||
@ -22,7 +28,7 @@ interface RefreshFeedRequest {
|
||||
scheduledTimestamps: number[] // unix timestamp in milliseconds
|
||||
lastFetchedChecksums: string[]
|
||||
userIds: string[]
|
||||
fetchContents: boolean[]
|
||||
fetchContentTypes: FetchContentType[]
|
||||
folders: FolderType[]
|
||||
refreshContext?: RSSRefreshContext
|
||||
}
|
||||
@ -35,7 +41,7 @@ export const isRefreshFeedRequest = (data: any): data is RefreshFeedRequest => {
|
||||
'scheduledTimestamps' in data &&
|
||||
'userIds' in data &&
|
||||
'lastFetchedChecksums' in data &&
|
||||
'fetchContents' in data &&
|
||||
'fetchContentTypes' in data &&
|
||||
'folders' in data
|
||||
)
|
||||
}
|
||||
@ -263,7 +269,7 @@ const createTask = async (
|
||||
userId: string,
|
||||
feedUrl: string,
|
||||
item: RssFeedItem,
|
||||
fetchContent: boolean,
|
||||
fetchContentType: FetchContentType,
|
||||
folder: FolderType
|
||||
) => {
|
||||
const isRecentlySaved = await isItemRecentlySaved(userId, item.link)
|
||||
@ -272,8 +278,12 @@ const createTask = async (
|
||||
return true
|
||||
}
|
||||
|
||||
if (folder === 'following' && !fetchContent) {
|
||||
return createItemWithPreviewContent(userId, feedUrl, item)
|
||||
const feedContent = item.content || item.contentSnippet || item.summary
|
||||
if (
|
||||
fetchContentType === FetchContentType.Never ||
|
||||
(fetchContentType === FetchContentType.WhenEmpty && feedContent)
|
||||
) {
|
||||
return createItemWithFeedContent(userId, feedUrl, item, folder, feedContent)
|
||||
}
|
||||
|
||||
logger.info(`adding fetch content task ${userId} ${item.link.trim()}`)
|
||||
@ -309,44 +319,60 @@ const fetchContentAndCreateItem = async (
|
||||
}
|
||||
}
|
||||
|
||||
const createItemWithPreviewContent = async (
|
||||
const createItemWithFeedContent = async (
|
||||
userId: string,
|
||||
feedUrl: string,
|
||||
item: RssFeedItem
|
||||
item: RssFeedItem,
|
||||
folder: FolderType,
|
||||
feedContent?: string
|
||||
) => {
|
||||
const input = {
|
||||
userIds: [userId],
|
||||
url: item.link,
|
||||
title: item.title,
|
||||
author: item.creator,
|
||||
description: item.summary,
|
||||
addedToFollowingFrom: 'feed',
|
||||
previewContent: item.content || item.contentSnippet || item.summary,
|
||||
addedToFollowingBy: feedUrl,
|
||||
savedAt: item.isoDate,
|
||||
publishedAt: item.isoDate,
|
||||
previewContentType: 'text/html', // TODO: get content type from feed
|
||||
thumbnail: getThumbnail(item),
|
||||
}
|
||||
|
||||
try {
|
||||
const serviceBaseUrl = process.env.INTERNAL_API_URL
|
||||
const token = process.env.PUBSUB_VERIFICATION_TOKEN
|
||||
if (!serviceBaseUrl || !token) {
|
||||
throw 'Environment not configured correctly'
|
||||
logger.info('saving feed item with feed content', {
|
||||
userId,
|
||||
feedUrl,
|
||||
item,
|
||||
folder,
|
||||
})
|
||||
|
||||
const thumbnail = getThumbnail(item)
|
||||
const previewImage = thumbnail && createThumbnailUrl(thumbnail)
|
||||
const url = cleanUrl(item.link)
|
||||
|
||||
const user = await findActiveUser(userId)
|
||||
if (!user) {
|
||||
logger.error('User not found', { userId })
|
||||
return false
|
||||
}
|
||||
|
||||
// save page
|
||||
const taskHandlerUrl = `${serviceBaseUrl}/svc/following/save?token=${token}`
|
||||
const task = await createHttpTaskWithToken({
|
||||
queue: env.queue.name,
|
||||
priority: 'low',
|
||||
taskHandlerUrl: taskHandlerUrl,
|
||||
payload: input,
|
||||
})
|
||||
return !!task
|
||||
const result = await savePage(
|
||||
{
|
||||
url,
|
||||
feedContent,
|
||||
title: item.title,
|
||||
folder,
|
||||
rssFeedUrl: feedUrl,
|
||||
savedAt: item.isoDate,
|
||||
publishedAt: item.isoDate,
|
||||
originalContent: feedContent || '',
|
||||
source: 'rss-feeder',
|
||||
state: ArticleSavingRequestStatus.ContentNotFetched,
|
||||
clientRequestId: '',
|
||||
author: item.creator,
|
||||
previewImage,
|
||||
},
|
||||
user
|
||||
)
|
||||
|
||||
if (result.__typename === 'SaveError') {
|
||||
logger.error(
|
||||
`Error while saving feed item with feed content: ${result.errorCodes[0]}`
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
logger.error('Error while creating task', error)
|
||||
logger.error('Error while saving feed item with feed content', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -456,7 +482,7 @@ const processSubscription = async (
|
||||
mostRecentItemDate: number,
|
||||
scheduledAt: number,
|
||||
lastFetchedChecksum: string,
|
||||
fetchContent: boolean,
|
||||
fetchContentType: FetchContentType,
|
||||
folder: FolderType,
|
||||
feed: RssFeed
|
||||
) => {
|
||||
@ -547,7 +573,7 @@ const processSubscription = async (
|
||||
userId,
|
||||
feedUrl,
|
||||
feedItem,
|
||||
fetchContent,
|
||||
fetchContentType,
|
||||
folder
|
||||
)
|
||||
if (!created) {
|
||||
@ -580,7 +606,7 @@ const processSubscription = async (
|
||||
userId,
|
||||
feedUrl,
|
||||
lastValidItem,
|
||||
fetchContent,
|
||||
fetchContentType,
|
||||
folder
|
||||
)
|
||||
if (!created) {
|
||||
@ -626,7 +652,7 @@ export const _refreshFeed = async (request: RefreshFeedRequest) => {
|
||||
scheduledTimestamps,
|
||||
userIds,
|
||||
lastFetchedChecksums,
|
||||
fetchContents,
|
||||
fetchContentTypes,
|
||||
folders,
|
||||
refreshContext,
|
||||
} = request
|
||||
@ -666,6 +692,9 @@ export const _refreshFeed = async (request: RefreshFeedRequest) => {
|
||||
// process each subscription sequentially
|
||||
for (let i = 0; i < subscriptionIds.length; i++) {
|
||||
const subscriptionId = subscriptionIds[i]
|
||||
const fetchContentType = allowFetchContent
|
||||
? fetchContentTypes[i]
|
||||
: FetchContentType.Never
|
||||
|
||||
try {
|
||||
await processSubscription(
|
||||
@ -677,7 +706,7 @@ export const _refreshFeed = async (request: RefreshFeedRequest) => {
|
||||
mostRecentItemDates[i],
|
||||
scheduledTimestamps[i],
|
||||
lastFetchedChecksums[i],
|
||||
fetchContents[i] && allowFetchContent,
|
||||
fetchContentType,
|
||||
folders[i],
|
||||
feed
|
||||
)
|
||||
|
||||
@ -38,7 +38,7 @@ import {
|
||||
generateDownloadSignedUrl,
|
||||
generateUploadFilePathName,
|
||||
} from '../utils/uploads'
|
||||
import { emptyTrashResolver } from './article'
|
||||
import { emptyTrashResolver, fetchContentResolver } from './article'
|
||||
import { optInFeatureResolver } from './features'
|
||||
import { uploadImportFileResolver } from './importers/uploadImportFileResolver'
|
||||
import {
|
||||
@ -296,6 +296,7 @@ export const functionResolvers = {
|
||||
moveToFolder: moveToFolderResolver,
|
||||
updateNewsletterEmail: updateNewsletterEmailResolver,
|
||||
emptyTrash: emptyTrashResolver,
|
||||
fetchContent: fetchContentResolver,
|
||||
},
|
||||
Query: {
|
||||
me: getMeUserResolver,
|
||||
@ -623,4 +624,5 @@ export const functionResolvers = {
|
||||
...resultResolveTypeResolver('MoveToFolder'),
|
||||
...resultResolveTypeResolver('UpdateNewsletterEmail'),
|
||||
...resultResolveTypeResolver('EmptyTrash'),
|
||||
...resultResolveTypeResolver('FetchContent'),
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import { DeepPartial } from 'typeorm'
|
||||
import {
|
||||
Highlight as HighlightData,
|
||||
HighlightType,
|
||||
RepresentationType,
|
||||
} from '../../entity/highlight'
|
||||
import { Label } from '../../entity/label'
|
||||
import { env } from '../../env'
|
||||
@ -34,8 +35,8 @@ import {
|
||||
updateHighlight,
|
||||
} from '../../services/highlights'
|
||||
import { analytics } from '../../utils/analytics'
|
||||
import { highlightDataToHighlight } from '../../utils/helpers'
|
||||
import { authorized } from '../../utils/gql-utils'
|
||||
import { highlightDataToHighlight } from '../../utils/helpers'
|
||||
|
||||
export const createHighlightResolver = authorized<
|
||||
CreateHighlightSuccess,
|
||||
@ -51,6 +52,7 @@ export const createHighlightResolver = authorized<
|
||||
highlightType: input.type || HighlightType.Highlight,
|
||||
highlightPositionAnchorIndex: input.highlightPositionAnchorIndex || 0,
|
||||
highlightPositionPercent: input.highlightPositionPercent || 0,
|
||||
representation: input.representation || RepresentationType.Content,
|
||||
},
|
||||
input.articleId,
|
||||
uid,
|
||||
@ -129,6 +131,7 @@ export const mergeHighlightResolver = authorized<
|
||||
libraryItem: { id: input.articleId },
|
||||
highlightPositionAnchorIndex: input.highlightPositionAnchorIndex || 0,
|
||||
highlightPositionPercent: input.highlightPositionPercent || 0,
|
||||
representation: input.representation || RepresentationType.Content,
|
||||
}
|
||||
|
||||
const newHighlight = await mergeHighlights(
|
||||
|
||||
@ -3,6 +3,7 @@ import { parseHTML } from 'linkedom'
|
||||
import { Brackets, In } from 'typeorm'
|
||||
import {
|
||||
DEFAULT_SUBSCRIPTION_FOLDER,
|
||||
FetchContentType,
|
||||
Subscription,
|
||||
SubscriptionStatus,
|
||||
SubscriptionType,
|
||||
@ -226,7 +227,9 @@ export const subscribeResolver = authorized<
|
||||
// re-subscribe
|
||||
const updatedSubscription = await getRepository(Subscription).save({
|
||||
...existingSubscription,
|
||||
fetchContent: input.fetchContent ?? undefined,
|
||||
fetchContentType: input.fetchContentType
|
||||
? (input.fetchContentType as FetchContentType)
|
||||
: undefined,
|
||||
folder: input.folder ?? undefined,
|
||||
isPrivate: input.isPrivate,
|
||||
status: SubscriptionStatus.Active,
|
||||
@ -240,7 +243,7 @@ export const subscribeResolver = authorized<
|
||||
scheduledDates: [new Date()], // fetch immediately
|
||||
mostRecentItemDates: [updatedSubscription.mostRecentItemDate || null],
|
||||
checksums: [updatedSubscription.lastFetchedChecksum || null],
|
||||
fetchContents: [updatedSubscription.fetchContent],
|
||||
fetchContentTypes: [updatedSubscription.fetchContentType],
|
||||
folders: [updatedSubscription.folder || DEFAULT_SUBSCRIPTION_FOLDER],
|
||||
})
|
||||
|
||||
@ -254,7 +257,7 @@ export const subscribeResolver = authorized<
|
||||
|
||||
// limit number of rss subscriptions to max
|
||||
const results = (await getRepository(Subscription).query(
|
||||
`insert into omnivore.subscriptions (name, url, description, type, user_id, icon, is_private, fetch_content, folder)
|
||||
`insert into omnivore.subscriptions (name, url, description, type, user_id, icon, is_private, fetch_content_type, folder)
|
||||
select $1, $2, $3, $4, $5, $6, $7, $8, $9 from omnivore.subscriptions
|
||||
where user_id = $5 and type = 'RSS' and status = 'ACTIVE'
|
||||
having count(*) < $10
|
||||
@ -267,7 +270,7 @@ export const subscribeResolver = authorized<
|
||||
uid,
|
||||
feed.thumbnail,
|
||||
input.isPrivate,
|
||||
input.fetchContent ?? true,
|
||||
input.fetchContentType ?? FetchContentType.Always,
|
||||
input.folder ?? 'following',
|
||||
MAX_RSS_SUBSCRIPTIONS,
|
||||
]
|
||||
@ -290,7 +293,7 @@ export const subscribeResolver = authorized<
|
||||
scheduledDates: [new Date()], // fetch immediately
|
||||
mostRecentItemDates: [null],
|
||||
checksums: [null],
|
||||
fetchContents: [newSubscription.fetchContent],
|
||||
fetchContentTypes: [newSubscription.fetchContentType],
|
||||
folders: [newSubscription.folder || DEFAULT_SUBSCRIPTION_FOLDER],
|
||||
})
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@ export interface SaveFollowingItemRequest {
|
||||
author?: string
|
||||
description?: string
|
||||
links?: any
|
||||
previewContent?: string
|
||||
feedContent?: string
|
||||
previewContentType?: string
|
||||
publishedAt?: Date
|
||||
savedAt?: Date
|
||||
@ -76,7 +76,7 @@ export function followingServiceRouter() {
|
||||
const url = cleanUrl(req.body.url)
|
||||
|
||||
const preparedDocument: PreparedDocumentInput = {
|
||||
document: req.body.previewContent || '',
|
||||
document: req.body.feedContent || '',
|
||||
pageInfo: {
|
||||
title: req.body.title,
|
||||
author: req.body.author,
|
||||
@ -89,7 +89,7 @@ export function followingServiceRouter() {
|
||||
let parsedResult: ParsedContentPuppeteer | undefined
|
||||
|
||||
// parse the content if we have a preview content
|
||||
if (req.body.previewContent) {
|
||||
if (req.body.feedContent) {
|
||||
parsedResult = await parsePreparedContent(url, preparedDocument)
|
||||
}
|
||||
|
||||
@ -112,7 +112,7 @@ export function followingServiceRouter() {
|
||||
userId,
|
||||
slug,
|
||||
croppedPathname,
|
||||
originalHtml: req.body.previewContent,
|
||||
originalHtml: req.body.feedContent,
|
||||
itemType: parsedResult?.pageType || PageType.Unknown,
|
||||
canonicalUrl: url,
|
||||
folder: FOLDER,
|
||||
|
||||
@ -397,6 +397,7 @@ const schema = gql`
|
||||
recommendations: [Recommendation!]
|
||||
wordsCount: Int
|
||||
folder: String!
|
||||
feedContent: String
|
||||
}
|
||||
|
||||
# Query: article
|
||||
@ -703,6 +704,11 @@ const schema = gql`
|
||||
NOTE
|
||||
}
|
||||
|
||||
enum RepresentationType {
|
||||
CONTENT
|
||||
FEED_CONTENT
|
||||
}
|
||||
|
||||
# Highlight
|
||||
type Highlight {
|
||||
id: ID!
|
||||
@ -728,6 +734,7 @@ const schema = gql`
|
||||
type: HighlightType!
|
||||
html: String
|
||||
color: String
|
||||
representation: RepresentationType!
|
||||
}
|
||||
|
||||
input CreateHighlightInput {
|
||||
@ -745,6 +752,7 @@ const schema = gql`
|
||||
type: HighlightType
|
||||
html: String
|
||||
color: String
|
||||
representation: RepresentationType
|
||||
}
|
||||
|
||||
type CreateHighlightSuccess {
|
||||
@ -779,6 +787,7 @@ const schema = gql`
|
||||
highlightPositionAnchorIndex: Int
|
||||
html: String
|
||||
color: String
|
||||
representation: RepresentationType
|
||||
}
|
||||
|
||||
type MergeHighlightSuccess {
|
||||
@ -1634,7 +1643,7 @@ const schema = gql`
|
||||
wordsCount: Int
|
||||
content: String
|
||||
archivedAt: Date
|
||||
previewContent: String
|
||||
feedContent: String
|
||||
previewContentType: String
|
||||
links: JSON
|
||||
folder: String!
|
||||
@ -1670,6 +1679,12 @@ const schema = gql`
|
||||
NEWSLETTER
|
||||
}
|
||||
|
||||
enum FetchContentType {
|
||||
ALWAYS
|
||||
NEVER
|
||||
WHEN_EMPTY
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
id: ID!
|
||||
name: String!
|
||||
@ -1688,6 +1703,7 @@ const schema = gql`
|
||||
isPrivate: Boolean
|
||||
autoAddToLibrary: Boolean
|
||||
fetchContent: Boolean!
|
||||
fetchContentType: FetchContentType!
|
||||
folder: String!
|
||||
mostRecentItemDate: Date
|
||||
refreshedAt: Date
|
||||
@ -2597,6 +2613,7 @@ const schema = gql`
|
||||
isPrivate: Boolean
|
||||
autoAddToLibrary: Boolean
|
||||
fetchContent: Boolean
|
||||
fetchContentType: FetchContentType
|
||||
folder: String
|
||||
}
|
||||
|
||||
@ -2610,6 +2627,7 @@ const schema = gql`
|
||||
isPrivate: Boolean
|
||||
autoAddToLibrary: Boolean
|
||||
fetchContent: Boolean
|
||||
fetchContentType: FetchContentType
|
||||
folder: String
|
||||
refreshedAt: Date
|
||||
mostRecentItemDate: Date
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
SavePageInput,
|
||||
SaveResult,
|
||||
} from '../generated/graphql'
|
||||
import { Merge } from '../util'
|
||||
import { enqueueThumbnailJob } from '../utils/createTask'
|
||||
import {
|
||||
cleanUrl,
|
||||
@ -61,10 +62,13 @@ const shouldParseInBackend = (input: SavePageInput): boolean => {
|
||||
)
|
||||
}
|
||||
|
||||
export type SavePageArgs = Merge<
|
||||
SavePageInput,
|
||||
{ feedContent?: string; previewImage?: string; author?: string }
|
||||
>
|
||||
|
||||
export const savePage = async (
|
||||
input: SavePageInput & {
|
||||
finalUrl?: string
|
||||
},
|
||||
input: SavePageArgs,
|
||||
user: User
|
||||
): Promise<SaveResult> => {
|
||||
const [slug, croppedPathname] = createSlug(input.url, input.title)
|
||||
@ -100,6 +104,8 @@ export const savePage = async (
|
||||
pageInfo: {
|
||||
title: input.title,
|
||||
canonicalUrl: input.url,
|
||||
previewImage: input.previewImage,
|
||||
author: input.author,
|
||||
},
|
||||
})
|
||||
|
||||
@ -119,6 +125,7 @@ export const savePage = async (
|
||||
state: input.state || undefined,
|
||||
rssFeedUrl: input.rssFeedUrl,
|
||||
folder: input.folder,
|
||||
feedContent: input.feedContent,
|
||||
})
|
||||
const isImported =
|
||||
input.source === 'csv-importer' || input.source === 'pocket'
|
||||
@ -196,6 +203,7 @@ export const parsedContentToLibraryItem = ({
|
||||
state,
|
||||
rssFeedUrl,
|
||||
folder,
|
||||
feedContent,
|
||||
}: {
|
||||
url: string
|
||||
userId: string
|
||||
@ -215,6 +223,7 @@ export const parsedContentToLibraryItem = ({
|
||||
state?: ArticleSavingRequestStatus | null
|
||||
rssFeedUrl?: string | null
|
||||
folder?: string | null
|
||||
feedContent?: string | null
|
||||
}): DeepPartial<LibraryItem> & { originalUrl: string } => {
|
||||
logger.info('save_page', { url, state, itemId })
|
||||
return {
|
||||
@ -257,5 +266,6 @@ export const parsedContentToLibraryItem = ({
|
||||
archivedAt:
|
||||
state === ArticleSavingRequestStatus.Archived ? new Date() : null,
|
||||
deletedAt: state === ArticleSavingRequestStatus.Deleted ? new Date() : null,
|
||||
feedContent,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { Subscription, SubscriptionStatus } from '../entity/subscription'
|
||||
import {
|
||||
FetchContentType,
|
||||
Subscription,
|
||||
SubscriptionStatus,
|
||||
} from '../entity/subscription'
|
||||
import { getRepository } from '../repository'
|
||||
|
||||
const ensureOwns = async (userId: string, subscriptionId: string) => {
|
||||
@ -16,7 +20,7 @@ const ensureOwns = async (userId: string, subscriptionId: string) => {
|
||||
type UpdateSubscriptionData = {
|
||||
autoAddToLibrary?: boolean | null
|
||||
description?: string | null
|
||||
fetchContent?: boolean | null
|
||||
fetchContentType?: FetchContentType | null
|
||||
folder?: string | null
|
||||
isPrivate?: boolean | null
|
||||
mostRecentItemDate?: Date | null
|
||||
@ -48,7 +52,7 @@ export const updateSubscription = async (
|
||||
failedAt: newData.failedAt || undefined,
|
||||
autoAddToLibrary: newData.autoAddToLibrary ?? undefined,
|
||||
isPrivate: newData.isPrivate ?? undefined,
|
||||
fetchContent: newData.fetchContent ?? undefined,
|
||||
fetchContentType: newData.fetchContentType || undefined,
|
||||
folder: newData.folder ?? undefined,
|
||||
})
|
||||
|
||||
@ -75,7 +79,7 @@ export const updateSubscriptions = async (
|
||||
failedAt: newData.failedAt || undefined,
|
||||
autoAddToLibrary: newData.autoAddToLibrary ?? undefined,
|
||||
isPrivate: newData.isPrivate ?? undefined,
|
||||
fetchContent: newData.fetchContent ?? undefined,
|
||||
fetchContentType: newData.fetchContentType ?? undefined,
|
||||
folder: newData.folder ?? undefined,
|
||||
}))
|
||||
)
|
||||
|
||||
@ -9,6 +9,7 @@ import { DeepPartial } from 'typeorm'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import { ImportItemState } from '../entity/integration'
|
||||
import { Recommendation } from '../entity/recommendation'
|
||||
import { FetchContentType } from '../entity/subscription'
|
||||
import { env } from '../env'
|
||||
import {
|
||||
ArticleSavingRequestStatus,
|
||||
@ -625,7 +626,7 @@ export interface RssSubscriptionGroup {
|
||||
mostRecentItemDates: (Date | null)[]
|
||||
scheduledDates: Date[]
|
||||
checksums: (string | null)[]
|
||||
fetchContents: boolean[]
|
||||
fetchContentTypes: FetchContentType[]
|
||||
folders: string[]
|
||||
}
|
||||
|
||||
@ -648,7 +649,7 @@ export const enqueueRssFeedFetch = async (
|
||||
timestamp.getTime()
|
||||
), // unix timestamp in milliseconds
|
||||
userIds: subscriptionGroup.userIds,
|
||||
fetchContents: subscriptionGroup.fetchContents,
|
||||
fetchContentTypes: subscriptionGroup.fetchContentTypes,
|
||||
folders: subscriptionGroup.folders,
|
||||
}
|
||||
|
||||
|
||||
14
packages/db/migrations/0165.do.rename_preview_content_in_library_item.sql
Executable file
14
packages/db/migrations/0165.do.rename_preview_content_in_library_item.sql
Executable file
@ -0,0 +1,14 @@
|
||||
-- Type: DO
|
||||
-- Name: rename_preview_content_in_library_item
|
||||
-- Description: Rename preview_content column in library_item table to feed_content
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE omnivore.library_item RENAME COLUMN preview_content TO feed_content;
|
||||
|
||||
CREATE TYPE fetch_content_enum AS ENUM ('ALWAYS', 'NEVER', 'WHEN_EMPTY');
|
||||
ALTER TABLE omnivore.subscriptions ADD COLUMN fetch_content_type fetch_content_enum NOT NULL DEFAULT 'ALWAYS'::fetch_content_enum;
|
||||
|
||||
CREATE TYPE representation_type AS ENUM ('CONTENT', 'FEED_CONTENT');
|
||||
ALTER TABLE omnivore.highlight ADD COLUMN representation representation_type NOT NULL DEFAULT 'CONTENT'::representation_type;
|
||||
COMMIT;
|
||||
15
packages/db/migrations/0165.undo.rename_preview_content_in_library_item.sql
Executable file
15
packages/db/migrations/0165.undo.rename_preview_content_in_library_item.sql
Executable file
@ -0,0 +1,15 @@
|
||||
-- Type: UNDO
|
||||
-- Name: rename_preview_content_in_library_item
|
||||
-- Description: Rename preview_content column in library_item table to feed_content
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE omnivore.highlight DROP COLUMN representation;
|
||||
DROP TYPE representation_type;
|
||||
|
||||
ALTER TABLE omnivore.subscriptions DROP COLUMN fetch_content_type;
|
||||
DROP TYPE fetch_content_enum;
|
||||
|
||||
ALTER TABLE omnivore.library_item RENAME COLUMN feed_content TO preview_content;
|
||||
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user