Merge pull request #3744 from omnivore-app/fix/notion

fix: allow max 1000 users to use notion feature
This commit is contained in:
Hongbo Wu
2024-03-28 15:57:06 +08:00
committed by GitHub
12 changed files with 101 additions and 48 deletions

View File

@ -3696,6 +3696,7 @@ export enum UploadImportFileType {
export type User = {
__typename?: 'User';
email?: Maybe<Scalars['String']>;
featureList?: Maybe<Array<Feature>>;
features?: Maybe<Array<Maybe<Scalars['String']>>>;
followersCount?: Maybe<Scalars['Int']>;
friendsCount?: Maybe<Scalars['Int']>;
@ -7139,6 +7140,7 @@ export type UploadImportFileSuccessResolvers<ContextType = ResolverContext, Pare
export type UserResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User']> = {
email?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
featureList?: Resolver<Maybe<Array<ResolversTypes['Feature']>>, ParentType, ContextType>;
features?: Resolver<Maybe<Array<Maybe<ResolversTypes['String']>>>, ParentType, ContextType>;
followersCount?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
friendsCount?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;

View File

@ -2984,6 +2984,7 @@ enum UploadImportFileType {
type User {
email: String
featureList: [Feature!]
features: [String]
followersCount: Int
friendsCount: Int

View File

@ -1,21 +1,19 @@
import { logger } from '../utils/logger'
import { Storage } from '@google-cloud/storage'
import { PromptTemplate } from '@langchain/core/prompts'
import { OpenAI } from '@langchain/openai'
import { parseHTML } from 'linkedom'
import showdown from 'showdown'
import * as stream from 'stream'
import { Chapter, Client as YouTubeClient } from 'youtubei'
import { LibraryItem, LibraryItemState } from '../entity/library_item'
import { env } from '../env'
import { authTrx } from '../repository'
import { libraryItemRepository } from '../repository/library_item'
import { LibraryItem, LibraryItemState } from '../entity/library_item'
import { Chapter, Client as YouTubeClient } from 'youtubei'
import showdown from 'showdown'
import { parseHTML } from 'linkedom'
import { parsePreparedContent } from '../utils/parser'
import { OpenAI } from '@langchain/openai'
import { PromptTemplate } from '@langchain/core/prompts'
import { FeatureName, findGrantedFeatureByName } from '../services/features'
import { enqueueProcessYouTubeTranscript } from '../utils/createTask'
import { env } from '../env'
import * as stream from 'stream'
import { Storage } from '@google-cloud/storage'
import { stringToHash } from '../utils/helpers'
import { FeatureName, findFeatureByName } from '../services/features'
import { logger } from '../utils/logger'
import { parsePreparedContent } from '../utils/parser'
export interface ProcessYouTubeVideoJobData {
userId: string
@ -394,7 +392,10 @@ export const processYouTubeVideo = async (
}
if (
await findFeatureByName(FeatureName.YouTubeTranscripts, jobData.userId)
await findGrantedFeatureByName(
FeatureName.YouTubeTranscripts,
jobData.userId
)
) {
if ('getTranscript' in video && duration > 0 && duration < 1801) {
// If the video has a transcript available, put a placehold in and

View File

@ -3,7 +3,6 @@ import express from 'express'
import { RuleEventType } from './entity/rule'
import { env } from './env'
import { ReportType } from './generated/graphql'
import { FeatureName, findFeatureByName } from './services/features'
import {
enqueueExportItem,
enqueueProcessYouTubeVideo,
@ -87,12 +86,12 @@ export const createPubSubClient = (): PubsubClient => {
})
if (type === EntityType.PAGE) {
if (await findFeatureByName(FeatureName.AISummaries, userId)) {
// await enqueueAISummarizeJob({
// userId,
// libraryItemId,
// })
}
// if (await findGrantedFeatureByName(FeatureName.AISummaries, userId)) {
// await enqueueAISummarizeJob({
// userId,
// libraryItemId,
// })
// }
const isYoutubeVideo = (data: any): data is { originalUrl: string } => {
return 'originalUrl' in data

View File

@ -368,6 +368,17 @@ export const functionResolvers = {
}
return undefined
},
async featureList(
_: User,
__: Record<string, unknown>,
ctx: WithDataSourcesContext
) {
if (!ctx.claims?.uid) {
return undefined
}
return findUserFeatures(ctx.claims.uid)
},
async features(
user: User,
__: Record<string, unknown>,
@ -376,7 +387,8 @@ export const functionResolvers = {
if (!ctx.claims?.uid) {
return undefined
}
return findUserFeatures(ctx.claims.uid)
return (await findUserFeatures(ctx.claims.uid)).map((f) => f.name)
},
},
Article: {

View File

@ -89,6 +89,7 @@ const schema = gql`
source: String
intercomHash: String
features: [String]
featureList: [Feature!]
}
type Profile {

View File

@ -7,7 +7,8 @@ import { getRepository } from '../repository'
import { logger } from '../utils/logger'
const MAX_ULTRA_REALISTIC_USERS = 1500
const MAX_YOUTUBE_TRANSCRIPT_USERS = 100
const MAX_YOUTUBE_TRANSCRIPT_USERS = 500
const MAX_NOTION_USERS = 1000
export enum FeatureName {
AISummaries = 'ai-summaries',
@ -38,7 +39,7 @@ export const optInFeature = async (
MAX_YOUTUBE_TRANSCRIPT_USERS
)
case FeatureName.Notion:
return optInLimitedFeature(FeatureName.Notion, uid, 1)
return optInLimitedFeature(FeatureName.Notion, uid, MAX_NOTION_USERS)
default:
return undefined
}
@ -121,23 +122,20 @@ export const signFeatureToken = (
)
}
export const findUserFeatures = async (userId: string): Promise<string[]> => {
return (
await getRepository(Feature).find({
where: {
user: { id: userId },
},
})
).map((feature) => feature.name)
export const findUserFeatures = async (userId: string) => {
return getRepository(Feature).findBy({
user: { id: userId },
})
}
export const findFeatureByName = async (
export const findGrantedFeatureByName = async (
name: FeatureName,
userId: string
): Promise<Feature | null> => {
return await getRepository(Feature).findOneBy({
return getRepository(Feature).findOneBy({
name,
user: { id: userId },
grantedAt: Not(IsNull()),
})
}

View File

@ -7,5 +7,10 @@ export const userHasFeature = (
if (!user) {
return false
}
return user.features.includes(feature)
return user.featureList.some(
(f) =>
f.name === feature &&
f.grantedAt &&
(!f.expiresAt || new Date(f.expiresAt) > new Date())
)
}

View File

@ -0,0 +1,21 @@
import { gql } from 'graphql-request'
export const featureFragment = gql`
fragment FeatureFields on Feature {
id
name
createdAt
updatedAt
grantedAt
expiresAt
}
`
export interface Feature {
id: string
name: string
createdAt: Date
updatedAt?: Date
grantedAt?: Date
expiresAt?: Date
}

View File

@ -1,16 +1,17 @@
import { gql } from 'graphql-request'
import { Feature, featureFragment } from '../fragments/featureFragment'
import { gqlFetcher } from '../networkHelpers'
export interface OptInFeatureInput {
name: string
}
export interface OptInFeatureSuccess {
feature: { id: string }
export interface OptInFeatureResponse {
feature?: Feature
}
interface Response {
optInFeature: OptInFeatureSuccess
optInFeature: OptInFeatureResponse
}
export async function optInFeature(
@ -21,7 +22,7 @@ export async function optInFeature(
optInFeature(input: $input) {
... on OptInFeatureSuccess {
feature {
id
...FeatureFields
}
}
... on OptInFeatureError {
@ -29,6 +30,7 @@ export async function optInFeature(
}
}
}
${featureFragment}
`
try {
const data = await gqlFetcher(mutation, {
@ -37,8 +39,8 @@ export async function optInFeature(
const output = data as Response | undefined
if (
!output ||
!output.optInFeature ||
'errorCodes' in output?.optInFeature
!output.optInFeature.feature ||
!output.optInFeature.feature.grantedAt
) {
return false
}

View File

@ -1,5 +1,6 @@
import { gql } from 'graphql-request'
import useSWR from 'swr'
import { Feature, featureFragment } from '../fragments/featureFragment'
import { publicGqlFetcher } from '../networkHelpers'
type ViewerQueryResponse = {
@ -22,6 +23,7 @@ export type UserBasicData = {
source: string
intercomHash: string
features: string[]
featureList: Feature[]
}
export type UserProfile = {
@ -48,8 +50,12 @@ export function useGetViewerQuery(): ViewerQueryResponse {
source
intercomHash
features
featureList {
...FeatureFields
}
}
}
${featureFragment}
`
const { data, error, mutate } = useSWR(query, publicGqlFetcher)

View File

@ -6,6 +6,7 @@ import { HStack, VStack } from '../../../components/elements/LayoutPrimitives'
import { StyledText } from '../../../components/elements/StyledText'
import { SettingsLayout } from '../../../components/templates/SettingsLayout'
import { styled } from '../../../components/tokens/stitches.config'
import { userHasFeature } from '../../../lib/featureFlag'
import { optInFeature } from '../../../lib/networking/mutations/optIntoFeatureMutation'
import { useGetViewerQuery } from '../../../lib/networking/queries/useGetViewerQuery'
import { applyStoredTheme } from '../../../lib/themeUpdater'
@ -43,13 +44,13 @@ export default function Account(): JSX.Element {
)
const hasYouTube = useMemo(() => {
return (
(viewerData?.me?.features.indexOf('youtube-transcripts') ?? -1) !== -1
return viewerData?.me?.featureList?.some(
(f) => f.name === 'youtube-transcripts'
)
}, [viewerData])
const hasNotion = useMemo(() => {
return (viewerData?.me?.features.indexOf('notion') ?? -1) !== -1
return viewerData?.me?.featureList?.some((f) => f.name === 'notion')
}, [viewerData])
applyStoredTheme()
@ -89,7 +90,7 @@ export default function Account(): JSX.Element {
<StyledLabel>Enabled beta features</StyledLabel>
{!showSpinner ? (
<>
{viewerData?.me?.features.map((feature) => {
{viewerData?.me?.featureList.map((feature) => {
return (
<StyledText
key={`feature-${feature}`}
@ -103,10 +104,14 @@ export default function Account(): JSX.Element {
>
<input
type="checkbox"
checked={true}
checked={userHasFeature(viewerData?.me, feature.name)}
disabled={true}
></input>
{feature}
{`${feature.name}${
userHasFeature(viewerData?.me, feature.name)
? ''
: ' - Requested'
}`}
</StyledText>
)
})}