diff --git a/packages/api/src/resolvers/features/index.ts b/packages/api/src/resolvers/features/index.ts index cb879d71f..a5226ef48 100644 --- a/packages/api/src/resolvers/features/index.ts +++ b/packages/api/src/resolvers/features/index.ts @@ -40,7 +40,7 @@ export const optInFeatureResolver = authorized< } } - const token = signFeatureToken(optIn) + const token = signFeatureToken(optIn, claims.uid) return { feature: { diff --git a/packages/api/src/routers/text_to_speech.ts b/packages/api/src/routers/text_to_speech.ts index e0796a76d..4e89954f6 100644 --- a/packages/api/src/routers/text_to_speech.ts +++ b/packages/api/src/routers/text_to_speech.ts @@ -16,6 +16,7 @@ import { enqueueTextToSpeech } from '../utils/createTask' import { htmlToSpeechFile } from '@omnivore/text-to-speech-handler' import { UserPersonalization } from '../entity/user_personalization' import { ArticleSavingRequestStatus } from '../elastic/types' +import { FeatureName, getFeature } from '../services/features' const logger = buildLogger('app.dispatch') @@ -80,6 +81,11 @@ export function textToSpeechRouter() { }, }) + const feature = await getFeature( + FeatureName.UltraRealisticVoice, + userId + ) + for (const utterance of speechFile.utterances) { // enqueue a task to convert text to speech const taskName = await enqueueTextToSpeech({ @@ -91,6 +97,8 @@ export function textToSpeechRouter() { isUltraRealisticVoice: true, language: speechFile.language, rate: userPersonalization?.speechRate || '1.1', + featureName: feature?.name, + grantedAt: feature?.grantedAt, }) logger.info('Start Text to speech task', { taskName }) } diff --git a/packages/api/src/services/features.ts b/packages/api/src/services/features.ts index b9dd19605..99d53e194 100644 --- a/packages/api/src/services/features.ts +++ b/packages/api/src/services/features.ts @@ -4,7 +4,7 @@ import * as jwt from 'jsonwebtoken' import { env } from '../env' import { IsNull, Not } from 'typeorm' -enum FeatureName { +export enum FeatureName { UltraRealisticVoice = 'ultra-realistic-voice', } @@ -56,10 +56,16 @@ const optInUltraRealisticVoice = async (uid: string): Promise => { }) } -export const signFeatureToken = (feature: Feature): string => { +export const signFeatureToken = ( + feature: { + name?: string + grantedAt?: Date | null + }, + userId: string +): string => { return jwt.sign( { - uid: feature.user.id, + uid: userId, featureName: feature.name, grantedAt: feature.grantedAt ? feature.grantedAt.getTime() / 1000 : null, }, @@ -67,3 +73,26 @@ export const signFeatureToken = (feature: Feature): string => { { expiresIn: '1y' } ) } + +export const isOptedIn = async ( + name: FeatureName, + uid: string +): Promise => { + const feature = await getRepository(Feature).findOneBy({ + user: { id: uid }, + name, + grantedAt: Not(IsNull()), + }) + + return !!feature +} + +export const getFeature = async ( + name: FeatureName, + uid: string +): Promise => { + return getRepository(Feature).findOneBy({ + user: { id: uid }, + name, + }) +} diff --git a/packages/api/src/services/speech.ts b/packages/api/src/services/speech.ts index 380a14a36..784d56840 100644 --- a/packages/api/src/services/speech.ts +++ b/packages/api/src/services/speech.ts @@ -1,6 +1,7 @@ import { searchPages } from '../elastic/pages' import { Page, PageType } from '../elastic/types' import { SortBy, SortOrder } from '../utils/search' +import { FeatureName, isOptedIn } from './features' /* * We should not synthesize the page when: @@ -16,7 +17,7 @@ export const shouldSynthesize = async ( return false } - if (process.env.TEXT_TO_SPEECH_BETA_TEST) { + if (await isOptedIn(FeatureName.UltraRealisticVoice, userId)) { return true } diff --git a/packages/api/src/utils/createTask.ts b/packages/api/src/utils/createTask.ts index af9b0e3ad..51d18b592 100644 --- a/packages/api/src/utils/createTask.ts +++ b/packages/api/src/utils/createTask.ts @@ -11,6 +11,7 @@ import { google } from '@google-cloud/tasks/build/protos/protos' import { IntegrationType } from '../entity/integration' import { promisify } from 'util' import * as jwt from 'jsonwebtoken' +import { signFeatureToken } from '../services/features' import View = google.cloud.tasks.v2.Task.View const logger = buildLogger('app.dispatch') @@ -347,6 +348,8 @@ export const enqueueTextToSpeech = async ({ isUltraRealisticVoice = false, language, rate, + featureName, + grantedAt, }: { userId: string speechId: string @@ -360,6 +363,8 @@ export const enqueueTextToSpeech = async ({ isUltraRealisticVoice?: boolean language?: string rate?: string + featureName?: string + grantedAt?: Date | null }): Promise => { const { GOOGLE_CLOUD_PROJECT } = process.env const payload = { @@ -372,11 +377,7 @@ export const enqueueTextToSpeech = async ({ language, rate, } - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const token = await signToken({ uid: userId }, env.server.jwtSecret, { - expiresIn: '1h', - }) + const token = signFeatureToken({ name: featureName, grantedAt }, userId) const taskHandlerUrl = `${env.queue.textToSpeechTaskHandlerUrl}?token=${token}` // If there is no Google Cloud Project Id exposed, it means that we are in local environment if (env.dev.isLocal || !GOOGLE_CLOUD_PROJECT) {