Merge pull request #3567 from omnivore-app/feature/feed-summary

feature/feed summary
This commit is contained in:
Hongbo Wu
2024-02-26 12:19:57 +08:00
committed by GitHub
17 changed files with 231 additions and 68 deletions

View File

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

View File

@ -191,7 +191,7 @@ export class LibraryItem {
links?: any | null
@Column('text')
previewContent?: string | null
feedContent?: string | null
@Column('text')
previewContentType?: string | null

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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;

View 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;