From 6592ac6a86a8fcbc2e27d99ab8e0cb3a9bc13a17 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Fri, 3 May 2024 11:12:19 +0800 Subject: [PATCH 01/12] add digest_config column to the user_personalization table --- ...0174.do.add_digest_config_to_user_personalization.sql | 9 +++++++++ ...74.undo.add_digest_config_to_user_personalization.sql | 9 +++++++++ 2 files changed, 18 insertions(+) create mode 100755 packages/db/migrations/0174.do.add_digest_config_to_user_personalization.sql create mode 100755 packages/db/migrations/0174.undo.add_digest_config_to_user_personalization.sql diff --git a/packages/db/migrations/0174.do.add_digest_config_to_user_personalization.sql b/packages/db/migrations/0174.do.add_digest_config_to_user_personalization.sql new file mode 100755 index 000000000..63c0a6f81 --- /dev/null +++ b/packages/db/migrations/0174.do.add_digest_config_to_user_personalization.sql @@ -0,0 +1,9 @@ +-- Type: DO +-- Name: add_digest_config_to_user_personalization +-- Description: Add digest_config json column to the user_personalization table + +BEGIN; + +ALTER TABLE omnivore.user_personalization ADD COLUMN digest_config jsonb; + +COMMIT; diff --git a/packages/db/migrations/0174.undo.add_digest_config_to_user_personalization.sql b/packages/db/migrations/0174.undo.add_digest_config_to_user_personalization.sql new file mode 100755 index 000000000..14e19be15 --- /dev/null +++ b/packages/db/migrations/0174.undo.add_digest_config_to_user_personalization.sql @@ -0,0 +1,9 @@ +-- Type: UNDO +-- Name: add_digest_config_to_user_personalization +-- Description: Add digest_config json column to the user_personalization table + +BEGIN; + +ALTER TABLE omnivore.user_personalization DROP COLUMN digest_config; + +COMMIT; From 0639a526763830771e9869a28ac0cc84f427c644 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Fri, 3 May 2024 11:13:24 +0800 Subject: [PATCH 02/12] add digestConfig to the userPersonalization entity --- packages/api/src/entity/user_personalization.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/api/src/entity/user_personalization.ts b/packages/api/src/entity/user_personalization.ts index 788b7050b..b891535fc 100644 --- a/packages/api/src/entity/user_personalization.ts +++ b/packages/api/src/entity/user_personalization.ts @@ -56,4 +56,7 @@ export class UserPersonalization { @Column('json') fields?: any | null + + @Column('jsonb') + digestConfig?: any } From 3e84c2f1df09223a41cb2446239483ee09c4e3d6 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Fri, 3 May 2024 12:17:07 +0800 Subject: [PATCH 03/12] fix tests --- .../api/src/entity/user_personalization.ts | 2 +- .../api/src/services/user_personalization.ts | 8 +++--- .../resolvers/user_personalization.test.ts | 28 ++++++------------- 3 files changed, 14 insertions(+), 24 deletions(-) diff --git a/packages/api/src/entity/user_personalization.ts b/packages/api/src/entity/user_personalization.ts index b891535fc..d421b3963 100644 --- a/packages/api/src/entity/user_personalization.ts +++ b/packages/api/src/entity/user_personalization.ts @@ -55,7 +55,7 @@ export class UserPersonalization { updatedAt!: Date @Column('json') - fields?: any | null + fields?: any @Column('jsonb') digestConfig?: any diff --git a/packages/api/src/services/user_personalization.ts b/packages/api/src/services/user_personalization.ts index cc9cd82c4..40e39e459 100644 --- a/packages/api/src/services/user_personalization.ts +++ b/packages/api/src/services/user_personalization.ts @@ -2,22 +2,22 @@ import { DeepPartial } from 'typeorm' import { UserPersonalization } from '../entity/user_personalization' import { authTrx } from '../repository' -export const findUserPersonalization = async (id: string, userId: string) => { +export const findUserPersonalization = async (userId: string) => { return authTrx( (t) => t.getRepository(UserPersonalization).findOneBy({ - id, + user: { id: userId }, }), undefined, userId ) } -export const deleteUserPersonalization = async (id: string, userId: string) => { +export const deleteUserPersonalization = async (userId: string) => { return authTrx( (t) => t.getRepository(UserPersonalization).delete({ - id, + user: { id: userId }, }), undefined, userId diff --git a/packages/api/test/resolvers/user_personalization.test.ts b/packages/api/test/resolvers/user_personalization.test.ts index 4f940d05f..b8e93d942 100644 --- a/packages/api/test/resolvers/user_personalization.test.ts +++ b/packages/api/test/resolvers/user_personalization.test.ts @@ -22,7 +22,7 @@ describe('User Personalization API', () => { .post('/local/debug/fake-user-login') .send({ fakeEmail: user.email }) - authToken = res.body.authToken + authToken = res.body.authToken as string }) after(async () => { @@ -61,25 +61,17 @@ describe('User Personalization API', () => { res.body.data.setUserPersonalization.updatedUserPersonalization.fields ).to.eql(fields) - const userPersonalization = await findUserPersonalization( - res.body.data.setUserPersonalization.updatedUserPersonalization.id, - user.id - ) + const userPersonalization = await findUserPersonalization(user.id) expect(userPersonalization).to.not.be.null // clean up - await deleteUserPersonalization( - res.body.data.setUserPersonalization.updatedUserPersonalization.id, - user.id - ) + await deleteUserPersonalization(user.id) }) }) context('when user personalization exists', () => { - let existingUserPersonalization: UserPersonalization - before(async () => { - existingUserPersonalization = await saveUserPersonalization(user.id, { + await saveUserPersonalization(user.id, { user: { id: user.id }, fields: { testField: 'testValue', @@ -89,7 +81,7 @@ describe('User Personalization API', () => { after(async () => { // clean up - await deleteUserPersonalization(existingUserPersonalization.id, user.id) + await deleteUserPersonalization(user.id) }) it('updates the user personalization', async () => { @@ -106,7 +98,6 @@ describe('User Personalization API', () => { ).to.eql(newFields) const updatedUserPersonalization = await findUserPersonalization( - existingUserPersonalization.id, user.id ) expect(updatedUserPersonalization?.fields).to.eql(newFields) @@ -128,7 +119,7 @@ describe('User Personalization API', () => { after(async () => { // clean up - await deleteUserPersonalization(existingUserPersonalization.id, user.id) + await deleteUserPersonalization(user.id) }) const query = ` @@ -150,10 +141,9 @@ describe('User Personalization API', () => { it('returns the user personalization', async () => { const res = await graphqlRequest(query, authToken).expect(200) - expect(res.body.data.getUserPersonalization.userPersonalization).to.eql({ - id: existingUserPersonalization.id, - fields: existingUserPersonalization.fields, - }) + expect( + res.body.data.getUserPersonalization.userPersonalization.fields + ).to.eql(existingUserPersonalization.fields) }) }) }) From 7060ae771c7d5544ef5f263a1d0d9c29701ea258 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Fri, 3 May 2024 14:17:22 +0800 Subject: [PATCH 04/12] send notifications to the channels defined in digest config --- packages/api/src/entity/user.ts | 2 +- packages/api/src/jobs/ai/create_digest.ts | 110 +++++++++++++++++----- packages/api/src/services/user.ts | 14 +++ 3 files changed, 103 insertions(+), 23 deletions(-) diff --git a/packages/api/src/entity/user.ts b/packages/api/src/entity/user.ts index 83526f946..0df30e6ac 100644 --- a/packages/api/src/entity/user.ts +++ b/packages/api/src/entity/user.ts @@ -73,5 +73,5 @@ export class User { () => UserPersonalization, (userPersonalization) => userPersonalization.user ) - userPersonalization!: UserPersonalization + userPersonalization?: UserPersonalization } diff --git a/packages/api/src/jobs/ai/create_digest.ts b/packages/api/src/jobs/ai/create_digest.ts index db5650ccb..3a289b5a2 100644 --- a/packages/api/src/jobs/ai/create_digest.ts +++ b/packages/api/src/jobs/ai/create_digest.ts @@ -12,6 +12,8 @@ import showdown from 'showdown' import { v4 as uuid } from 'uuid' import yaml from 'yaml' import { LibraryItem } from '../../entity/library_item' +import { User } from '../../entity/user' +import { env } from '../../env' import { TaskState } from '../../generated/graphql' import { redisDataSource } from '../../redis_data_source' import { Digest, writeDigest } from '../../services/digest' @@ -19,12 +21,15 @@ import { findLibraryItemsByIds, searchLibraryItems, } from '../../services/library_item' -import { findDeviceTokensByUserId } from '../../services/user_device_tokens' +import { + findUserAndPersonalization, + sendPushNotifications, +} from '../../services/user' +import { enqueueSendEmail } from '../../utils/createTask' import { wordsCount } from '../../utils/helpers' import { logger } from '../../utils/logger' import { htmlToMarkdown } from '../../utils/parser' -import { sendMulticastPushNotifications } from '../../utils/sendNotification' -import { generateUploadFilePathName, uploadToBucket } from '../../utils/uploads' +import { uploadToBucket } from '../../utils/uploads' export type CreateDigestJobSchedule = 'daily' | 'weekly' @@ -77,6 +82,8 @@ interface RankedTitle { title: string } +type Channel = 'push' | 'email' + export const CREATE_DIGEST_JOB = 'create-digest' export const CRON_PATTERNS = { // every day at 10:30 UTC @@ -546,14 +553,86 @@ const uploadSummary = async ( console.timeEnd('uploadSummary') } +const sendPushNotification = async (userId: string, digest: Digest) => { + const notification = { + title: digest.title ?? 'Omnivore digest', + body: 'Your digest is ready to listen', + } + + await sendPushNotifications(userId, notification, 'reminder') +} + +const sendEmail = async (user: User, digest: Digest) => { + const title = digest.title ?? 'Omnivore digest' + const html = ` + ${title}

+ +

Transcript

+

${digest.content ?? 'Transcript not available'}

+ ` + + await enqueueSendEmail({ + to: user.email, + from: env.sender.message, + subject: title, + html, + }) +} + +const sendNotifications = async ( + user: User, + channels: Channel[], + digest: Digest +) => { + await Promise.all( + channels.map(async (channel) => { + switch (channel) { + case 'push': + return sendPushNotification(user.id, digest) + case 'email': + return sendEmail(user, digest) + default: + } + }) + ) +} + export const createDigest = async (jobData: CreateDigestData) => { console.time('createDigestJob') // generate a unique id for the digest if not provided for scheduled jobs const digestId = jobData.id ?? uuid() + + const user = await findUserAndPersonalization(jobData.userId) + if (!user) { + logger.error('User not found', { userId: jobData.userId }) + return writeDigest(jobData.userId, { + id: digestId, + jobState: TaskState.Failed, + }) + } + + const personalization = user.userPersonalization + if (!personalization) { + logger.info('User personalization not found') + } + + const config = personalization + ? (personalization.digestConfig as { + model?: string + channels?: Channel[] + }) + : undefined + + // default digest + let digest: Digest = { + id: digestId, + jobState: TaskState.Succeeded, + } + try { digestDefinition = await fetchDigestDefinition() - const model = selectModel(digestDefinition.model) + const model = selectModel(config?.model || digestDefinition.model) logger.info(`model: ${model}`) const candidates = await getCandidatesList( @@ -562,11 +641,7 @@ export const createDigest = async (jobData: CreateDigestData) => { ) if (candidates.length === 0) { logger.info('No candidates found') - return writeDigest(jobData.userId, { - id: digestId, - jobState: TaskState.Succeeded, - title: 'No articles found', - }) + return writeDigest(jobData.userId, digest) } // const userProfile = await findOrCreateUserProfile(jobData.userId) @@ -591,7 +666,7 @@ export const createDigest = async (jobData: CreateDigestData) => { secondaryVoice: jobData.voices?.[1], }) const title = generateTitle(summaries) - const digest: Digest = { + digest = { id: digestId, title, content: generateContent(summaries), @@ -630,19 +705,10 @@ export const createDigest = async (jobData: CreateDigestData) => { jobState: TaskState.Failed, }) } finally { + // default to push notification + const channels = config?.channels ?? ['push'] // send notification - const tokens = await findDeviceTokensByUserId(jobData.userId) - if (tokens.length > 0) { - const message = { - notification: { - title: 'Digest ready', - body: 'Your digest is ready to listen', - }, - tokens: tokens.map((token) => token.token), - } - - await sendMulticastPushNotifications(jobData.userId, message, 'reminder') - } + await sendNotifications(user, channels, digest) console.timeEnd('createDigestJob') } diff --git a/packages/api/src/services/user.ts b/packages/api/src/services/user.ts index 52efa08e9..3eabe002a 100644 --- a/packages/api/src/services/user.ts +++ b/packages/api/src/services/user.ts @@ -150,3 +150,17 @@ export const sendPushNotifications = async ( return sendMulticastPushNotifications(userId, message, notificationType) } + +export const findUserAndPersonalization = async (id: string) => { + return authTrx( + (t) => + t.getRepository(User).findOne({ + where: { id }, + relations: { + userPersonalization: true, + }, + }), + undefined, + id + ) +} From 20408277d2354dae11407d0237f1bacc87aae872 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Fri, 3 May 2024 14:35:55 +0800 Subject: [PATCH 05/12] add digestConfig to the userPersonalization API request data --- packages/api/src/entity/user_personalization.ts | 4 ++-- packages/api/src/generated/graphql.ts | 3 +++ packages/api/src/generated/schema.graphql | 2 ++ packages/api/src/schema.ts | 2 ++ packages/api/test/resolvers/user_personalization.test.ts | 6 +++--- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/api/src/entity/user_personalization.ts b/packages/api/src/entity/user_personalization.ts index d421b3963..7fa36b470 100644 --- a/packages/api/src/entity/user_personalization.ts +++ b/packages/api/src/entity/user_personalization.ts @@ -55,8 +55,8 @@ export class UserPersonalization { updatedAt!: Date @Column('json') - fields?: any + fields?: any | null @Column('jsonb') - digestConfig?: any + digestConfig?: any | null } diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index a632e26a8..a63988225 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -3054,6 +3054,7 @@ export enum SetUserPersonalizationErrorCode { } export type SetUserPersonalizationInput = { + digestConfig?: InputMaybe; fields?: InputMaybe; fontFamily?: InputMaybe; fontSize?: InputMaybe; @@ -3767,6 +3768,7 @@ export enum UserErrorCode { export type UserPersonalization = { __typename?: 'UserPersonalization'; + digestConfig?: Maybe; fields?: Maybe; fontFamily?: Maybe; fontSize?: Maybe; @@ -7230,6 +7232,7 @@ export type UserErrorResolvers = { + digestConfig?: Resolver, ParentType, ContextType>; fields?: Resolver, ParentType, ContextType>; fontFamily?: Resolver, ParentType, ContextType>; fontSize?: Resolver, ParentType, ContextType>; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index 815fc9502..1c0800f5b 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -2385,6 +2385,7 @@ enum SetUserPersonalizationErrorCode { } input SetUserPersonalizationInput { + digestConfig: JSON fields: JSON fontFamily: String fontSize: Int @@ -3044,6 +3045,7 @@ enum UserErrorCode { } type UserPersonalization { + digestConfig: JSON fields: JSON fontFamily: String fontSize: Int diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 8d112d509..30f13320a 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -1080,6 +1080,7 @@ const schema = gql` speechRate: String speechVolume: String fields: JSON + digestConfig: JSON } # Query: UserPersonalization @@ -1122,6 +1123,7 @@ const schema = gql` speechRate: String speechVolume: String fields: JSON + digestConfig: JSON } # Type: ArticleSavingRequest diff --git a/packages/api/test/resolvers/user_personalization.test.ts b/packages/api/test/resolvers/user_personalization.test.ts index b8e93d942..fe04e93f3 100644 --- a/packages/api/test/resolvers/user_personalization.test.ts +++ b/packages/api/test/resolvers/user_personalization.test.ts @@ -73,8 +73,8 @@ describe('User Personalization API', () => { before(async () => { await saveUserPersonalization(user.id, { user: { id: user.id }, - fields: { - testField: 'testValue', + digestConfig: { + channels: ['email'], }, }) }) @@ -86,7 +86,7 @@ describe('User Personalization API', () => { it('updates the user personalization', async () => { const newFields = { - testField: 'testValue1', + channels: ['push', 'email'], } const res = await graphqlRequest(query, authToken, { From 0692ab04546055d505ee0bf2fcfb8d53a55f0a0d Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Fri, 3 May 2024 14:39:12 +0800 Subject: [PATCH 06/12] add chapters to the digest email --- packages/api/src/jobs/ai/create_digest.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/api/src/jobs/ai/create_digest.ts b/packages/api/src/jobs/ai/create_digest.ts index 3a289b5a2..98ee0488f 100644 --- a/packages/api/src/jobs/ai/create_digest.ts +++ b/packages/api/src/jobs/ai/create_digest.ts @@ -564,10 +564,24 @@ const sendPushNotification = async (userId: string, digest: Digest) => { const sendEmail = async (user: User, digest: Digest) => { const title = digest.title ?? 'Omnivore digest' + const chapters = digest.chapters ?? [] const html = ` - ${title}

+

${title}

-

Transcript

+

Chapters

+
    + ${chapters + .map( + (chapter) => ` +
  • + ${chapter.title} +
  • + ` + ) + .join('')} +
+ +

Transcript

${digest.content ?? 'Transcript not available'}

` From a9dd2d0d4cbfcb9a2ebd21002e921a6f5d02ef0a Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Fri, 3 May 2024 14:43:18 +0800 Subject: [PATCH 07/12] dedupe channels --- packages/api/src/jobs/ai/create_digest.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/api/src/jobs/ai/create_digest.ts b/packages/api/src/jobs/ai/create_digest.ts index 98ee0488f..81c6512f8 100644 --- a/packages/api/src/jobs/ai/create_digest.ts +++ b/packages/api/src/jobs/ai/create_digest.ts @@ -598,8 +598,10 @@ const sendNotifications = async ( channels: Channel[], digest: Digest ) => { + const deduplicateChannels = [...new Set(channels)] + await Promise.all( - channels.map(async (channel) => { + deduplicateChannels.map(async (channel) => { switch (channel) { case 'push': return sendPushNotification(user.id, digest) From 7b2e336eca1556e70243cc047bfeb27b9850a7ed Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Fri, 3 May 2024 14:45:01 +0800 Subject: [PATCH 08/12] log unknown channel --- packages/api/src/jobs/ai/create_digest.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/api/src/jobs/ai/create_digest.ts b/packages/api/src/jobs/ai/create_digest.ts index 81c6512f8..87990ea4d 100644 --- a/packages/api/src/jobs/ai/create_digest.ts +++ b/packages/api/src/jobs/ai/create_digest.ts @@ -595,8 +595,8 @@ const sendEmail = async (user: User, digest: Digest) => { const sendNotifications = async ( user: User, - channels: Channel[], - digest: Digest + digest: Digest, + channels: Channel[] = ['push'] // default to push notification ) => { const deduplicateChannels = [...new Set(channels)] @@ -608,6 +608,8 @@ const sendNotifications = async ( case 'email': return sendEmail(user, digest) default: + logger.error('Unknown channel', { channel }) + return } }) ) @@ -721,10 +723,8 @@ export const createDigest = async (jobData: CreateDigestData) => { jobState: TaskState.Failed, }) } finally { - // default to push notification - const channels = config?.channels ?? ['push'] // send notification - await sendNotifications(user, channels, digest) + await sendNotifications(user, digest, config?.channels) console.timeEnd('createDigestJob') } From 9bac4798b1de55e0fec938ee2c63fe11eebaa797 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Fri, 3 May 2024 14:56:45 +0800 Subject: [PATCH 09/12] put omnivore url in each chapter --- packages/api/src/jobs/ai/create_digest.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/api/src/jobs/ai/create_digest.ts b/packages/api/src/jobs/ai/create_digest.ts index 87990ea4d..7c58936e2 100644 --- a/packages/api/src/jobs/ai/create_digest.ts +++ b/packages/api/src/jobs/ai/create_digest.ts @@ -19,6 +19,7 @@ import { redisDataSource } from '../../redis_data_source' import { Digest, writeDigest } from '../../services/digest' import { findLibraryItemsByIds, + getItemUrl, searchLibraryItems, } from '../../services/library_item' import { @@ -693,7 +694,7 @@ export const createDigest = async (jobData: CreateDigestData) => { chapters: filteredSummaries.map((item, index) => ({ title: item.libraryItem.title, id: item.libraryItem.id, - url: item.libraryItem.originalUrl, + url: getItemUrl(item.libraryItem.id), thumbnail: item.libraryItem.thumbnail ?? undefined, wordCount: speechFiles[index].wordCount, })), From 6a0ea1b0789a40f9e7202d0581623f379686b87a Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Fri, 3 May 2024 15:15:11 +0800 Subject: [PATCH 10/12] update gcs upload filename --- packages/api/src/jobs/ai/create_digest.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/api/src/jobs/ai/create_digest.ts b/packages/api/src/jobs/ai/create_digest.ts index 7c58936e2..7a86c392e 100644 --- a/packages/api/src/jobs/ai/create_digest.ts +++ b/packages/api/src/jobs/ai/create_digest.ts @@ -532,7 +532,7 @@ const uploadSummary = async ( console.time('uploadSummary') logger.info('uploading summaries to gcs') - const filename = `digest/${userId}/${digest.id}/summaries.json` + const filename = `digest/${userId}/${digest.id}.json` await uploadToBucket( filename, Buffer.from( @@ -589,7 +589,7 @@ const sendEmail = async (user: User, digest: Digest) => { await enqueueSendEmail({ to: user.email, from: env.sender.message, - subject: title, + subject: 'Omnivore digest', html, }) } @@ -684,11 +684,11 @@ export const createDigest = async (jobData: CreateDigestData) => { primaryVoice: jobData.voices?.[0], secondaryVoice: jobData.voices?.[1], }) - const title = generateTitle(summaries) + const title = generateTitle(filteredSummaries) digest = { id: digestId, title, - content: generateContent(summaries), + content: generateContent(filteredSummaries), jobState: TaskState.Succeeded, speechFiles, chapters: filteredSummaries.map((item, index) => ({ @@ -700,8 +700,8 @@ export const createDigest = async (jobData: CreateDigestData) => { })), createdAt: new Date(), description: '', - // description: generateDescription(summaries, rankedTopics), - byline: generateByline(summaries), + // description: generateDescription(filteredSummaries, rankedTopics), + byline: generateByline(filteredSummaries), urlsToAudio: [], model, } @@ -710,7 +710,7 @@ export const createDigest = async (jobData: CreateDigestData) => { // write the digest to redis writeDigest(jobData.userId, digest), // upload the summaries to GCS - uploadSummary(jobData.userId, digest, summaries).catch((error) => + uploadSummary(jobData.userId, digest, filteredSummaries).catch((error) => logger.error('uploadSummary error', error) ), ]) From e013963143a77a2c98122f258d34ec448819a29a Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Fri, 3 May 2024 15:56:19 +0800 Subject: [PATCH 11/12] update email content --- packages/api/src/jobs/ai/create_digest.ts | 64 ++++++++++++++--------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/packages/api/src/jobs/ai/create_digest.ts b/packages/api/src/jobs/ai/create_digest.ts index 7a86c392e..b7c23719a 100644 --- a/packages/api/src/jobs/ai/create_digest.ts +++ b/packages/api/src/jobs/ai/create_digest.ts @@ -8,6 +8,7 @@ import { SSMLOptions, } from '@omnivore/text-to-speech-handler' import axios from 'axios' +import { truncate } from 'lodash' import showdown from 'showdown' import { v4 as uuid } from 'uuid' import yaml from 'yaml' @@ -556,40 +557,50 @@ const uploadSummary = async ( const sendPushNotification = async (userId: string, digest: Digest) => { const notification = { - title: digest.title ?? 'Omnivore digest', - body: 'Your digest is ready to listen', + title: 'Omnivore Digest', + body: truncate(digest.title, { length: 100 }), } await sendPushNotifications(userId, notification, 'reminder') } -const sendEmail = async (user: User, digest: Digest) => { - const title = digest.title ?? 'Omnivore digest' +const sendEmail = async ( + user: User, + digest: Digest, + summaries: RankedItem[] +) => { + const createdAt = digest.createdAt ?? new Date() + + const title = 'Omnivore Digest' + const subject = `${title} ${createdAt.toLocaleDateString()}` + const subTitle = truncate(digest.title, { length: 200 }).slice( + title.length + 1 + ) + const chapters = digest.chapters ?? [] + const html = ` -

${title}

+
+

${subject}

+

${subTitle}

-

Chapters

-
    - ${chapters - .map( - (chapter) => ` -
  • - ${chapter.title} -
  • - ` - ) - .join('')} -
- -

Transcript

-

${digest.content ?? 'Transcript not available'}

- ` + ${chapters + .map( + (chapter, index) => ` +
+

${chapter.title} (${chapter.wordCount} words)

+
+ ${summaries[index].summary} +
+
` + ) + .join('')} +
` await enqueueSendEmail({ to: user.email, from: env.sender.message, - subject: 'Omnivore digest', + subject, html, }) } @@ -597,6 +608,7 @@ const sendEmail = async (user: User, digest: Digest) => { const sendNotifications = async ( user: User, digest: Digest, + summaries: RankedItem[], channels: Channel[] = ['push'] // default to push notification ) => { const deduplicateChannels = [...new Set(channels)] @@ -607,7 +619,7 @@ const sendNotifications = async ( case 'push': return sendPushNotification(user.id, digest) case 'email': - return sendEmail(user, digest) + return sendEmail(user, digest, summaries) default: logger.error('Unknown channel', { channel }) return @@ -648,6 +660,7 @@ export const createDigest = async (jobData: CreateDigestData) => { id: digestId, jobState: TaskState.Succeeded, } + let filteredSummaries: RankedItem[] = [] try { digestDefinition = await fetchDigestDefinition() @@ -677,7 +690,7 @@ export const createDigest = async (jobData: CreateDigestData) => { const summaries = await summarizeItems(model, selections) console.timeEnd('summarizeItems') - const filteredSummaries = filterSummaries(summaries) + filteredSummaries = filterSummaries(summaries) const speechFiles = generateSpeechFiles(filteredSummaries, { ...jobData, @@ -722,10 +735,11 @@ export const createDigest = async (jobData: CreateDigestData) => { await writeDigest(jobData.userId, { id: digestId, jobState: TaskState.Failed, + title: 'Failed to create digest', }) } finally { // send notification - await sendNotifications(user, digest, config?.channels) + await sendNotifications(user, digest, filteredSummaries, config?.channels) console.timeEnd('createDigestJob') } From 2fd3b930fe9a35ac7a14e405a9a483375ea54d4f Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Fri, 3 May 2024 16:06:50 +0800 Subject: [PATCH 12/12] use the shortened title as subject --- packages/api/src/jobs/ai/create_digest.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/api/src/jobs/ai/create_digest.ts b/packages/api/src/jobs/ai/create_digest.ts index b7c23719a..6ffdd96d4 100644 --- a/packages/api/src/jobs/ai/create_digest.ts +++ b/packages/api/src/jobs/ai/create_digest.ts @@ -571,17 +571,17 @@ const sendEmail = async ( ) => { const createdAt = digest.createdAt ?? new Date() - const title = 'Omnivore Digest' - const subject = `${title} ${createdAt.toLocaleDateString()}` + const prefix = 'Omnivore Digest' + const title = `${prefix} ${createdAt.toLocaleDateString()}` const subTitle = truncate(digest.title, { length: 200 }).slice( - title.length + 1 + prefix.length + 1 ) const chapters = digest.chapters ?? [] const html = `
-

${subject}

+

${title}

${subTitle}

${chapters @@ -600,7 +600,7 @@ const sendEmail = async ( await enqueueSendEmail({ to: user.email, from: env.sender.message, - subject, + subject: subTitle, html, }) }