Merge pull request #3744 from omnivore-app/fix/notion
fix: allow max 1000 users to use notion feature
This commit is contained in:
@ -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>;
|
||||
|
||||
@ -2984,6 +2984,7 @@ enum UploadImportFileType {
|
||||
|
||||
type User {
|
||||
email: String
|
||||
featureList: [Feature!]
|
||||
features: [String]
|
||||
followersCount: Int
|
||||
friendsCount: Int
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -89,6 +89,7 @@ const schema = gql`
|
||||
source: String
|
||||
intercomHash: String
|
||||
features: [String]
|
||||
featureList: [Feature!]
|
||||
}
|
||||
|
||||
type Profile {
|
||||
|
||||
@ -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()),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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())
|
||||
)
|
||||
}
|
||||
|
||||
21
packages/web/lib/networking/fragments/featureFragment.ts
Normal file
21
packages/web/lib/networking/fragments/featureFragment.ts
Normal 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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
)
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user