diff --git a/packages/api/src/routers/svc/newsletters.ts b/packages/api/src/routers/svc/newsletters.ts index 2a668115b..463da1747 100644 --- a/packages/api/src/routers/svc/newsletters.ts +++ b/packages/api/src/routers/svc/newsletters.ts @@ -1,43 +1,13 @@ import express from 'express' -import { - createPubSubClient, - readPushSubscription, -} from '../../datalayer/pubsub' -import { - getNewsletterEmail, - updateConfirmationCode, -} from '../../services/newsletters' -import { - SaveContext, - saveEmail, - SaveEmailInput, -} from '../../services/save_email' -import { initModels } from '../../server' -import { kx } from '../../datalayer/knex_config' -import { analytics } from '../../utils/analytics' -import { env } from '../../env' -import { sendMulticastPushNotifications } from '../../utils/sendNotification' -import { getDeviceTokensByUserId } from '../../services/user_device_tokens' -import { messaging } from 'firebase-admin' -import { ContentReader } from '../../generated/graphql' -import { UserDeviceToken } from '../../entity/user_device_tokens' -import { UserArticleData } from '../../datalayer/links/model' -import { ArticleData } from '../../datalayer/article/model' -import MulticastMessage = messaging.MulticastMessage +import { readPushSubscription } from '../../datalayer/pubsub' +import { updateConfirmationCode } from '../../services/newsletters' +import { saveNewsletterEmail } from '../../services/save_newsletter_email' interface SetConfirmationCodeMessage { emailAddress: string confirmationCode: string } -interface NewsletterMessage { - email: string - content: string - url: string - title: string - author: string -} - export function newsletterServiceRouter() { const router = express.Router() @@ -111,7 +81,7 @@ export function newsletterServiceRouter() { try { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const data: NewsletterMessage = JSON.parse(message) + const data = JSON.parse(message) if ( !('email' in data) || @@ -124,79 +94,15 @@ export function newsletterServiceRouter() { return } - // get user from newsletter email - const newsletterEmail = await getNewsletterEmail(data.email) - - if (!newsletterEmail) { - console.log('newsletter email not found', data.email) - res.status(200).send('Not Found') - return - } - - analytics.track({ - userId: newsletterEmail.user.id, - event: 'newsletter_email_received', - properties: { - url: data.url, - title: data.title, - author: data.author, - env: env.server.apiEnv, - }, - }) - - const ctx: SaveContext = { - models: initModels(kx, false), - pubsub: createPubSubClient(), - } - const input: SaveEmailInput = { - url: data.url, - originalContent: data.content, - title: data.title, - author: data.author, - } - - const result = await saveEmail(ctx, newsletterEmail.user.id, input) + const result = await saveNewsletterEmail(data) if (!result) { - console.log('newsletter not created:', input) - res.status(200).send(result) + console.log('Error createing newsletter link from data', data) + res.status(500).send('Error creating newsletter link') return } - // send push notification - const deviceTokens = await getDeviceTokensByUserId( - newsletterEmail.user.id - ) - - if (!deviceTokens) { - console.log('Device tokens not set:', newsletterEmail.user.id) - res.status(200).send('Device token Not Found') - return - } - - const link = await ctx.models.userArticle.getForUser( - newsletterEmail.user.id, - result.articleId - ) - - if (!link) { - console.log( - 'Newsletter link not found:', - newsletterEmail.user.id, - result.articleId - ) - res.status(200).send(result) - return - } - - if (deviceTokens.length) { - const multicastMessage = messageForLink(link, deviceTokens) - await sendMulticastPushNotifications( - newsletterEmail.user.id, - multicastMessage, - 'newsletter' - ) - } - + // We always send 200 if it was a valid message + // because we don't want the res.status(200).send('newsletter created') } catch (e) { console.log(e) @@ -211,43 +117,3 @@ export function newsletterServiceRouter() { return router } - -const messageForLink = ( - link: ArticleData & UserArticleData, - deviceTokens: UserDeviceToken[] -): MulticastMessage => { - let title = '📫 - An article was added to your Omnivore Inbox' - - if (link.author) { - title = `📫 - ${link.author} has published a new article` - } - - const pushData = !link - ? undefined - : { - link: Buffer.from( - JSON.stringify({ - id: link.id, - url: link.url, - slug: link.slug, - title: link.title, - image: link.image, - author: link.author, - isArchived: link.isArchived, - contentReader: ContentReader.Web, - readingProgressPercent: link.articleReadingProgress, - readingProgressAnchorIndex: link.articleReadingProgressAnchorIndex, - }) - ).toString('base64'), - } - - return { - notification: { - title: title, - body: link.title, - imageUrl: link.image || undefined, - }, - data: pushData, - tokens: deviceTokens.map((token) => token.token), - } -} diff --git a/packages/api/src/services/save_newsletter_email.ts b/packages/api/src/services/save_newsletter_email.ts new file mode 100644 index 000000000..e00bacb91 --- /dev/null +++ b/packages/api/src/services/save_newsletter_email.ts @@ -0,0 +1,137 @@ +import { MulticastMessage } from 'firebase-admin/messaging' +import { ArticleData } from '../datalayer/article/model' +import { kx } from '../datalayer/knex_config' +import { UserArticleData } from '../datalayer/links/model' +import { createPubSubClient } from '../datalayer/pubsub' +import { UserDeviceToken } from '../entity/user_device_tokens' +import { env } from '../env' +import { ContentReader } from '../generated/graphql' +import { initModels } from '../server' +import { analytics } from '../utils/analytics' +import { sendMulticastPushNotifications } from '../utils/sendNotification' +import { getNewsletterEmail } from './newsletters' +import { SaveContext, saveEmail, SaveEmailInput } from './save_email' +import { getDeviceTokensByUserId } from './user_device_tokens' + +interface NewsletterMessage { + email: string + content: string + url: string + title: string + author: string +} + +// Returns true if the link was created successfully. Can still fail to +// send the push but that is ok and we wont retry in that case. +export const saveNewsletterEmail = async ( + data: NewsletterMessage +): Promise => { + // get user from newsletter email + const newsletterEmail = await getNewsletterEmail(data.email) + + if (!newsletterEmail) { + console.log('newsletter email not found', data.email) + return false + } + + analytics.track({ + userId: newsletterEmail.user.id, + event: 'newsletter_email_received', + properties: { + url: data.url, + title: data.title, + author: data.author, + env: env.server.apiEnv, + }, + }) + + const ctx: SaveContext = { + models: initModels(kx, false), + pubsub: createPubSubClient(), + } + const input: SaveEmailInput = { + url: data.url, + originalContent: data.content, + title: data.title, + author: data.author, + } + + const result = await saveEmail(ctx, newsletterEmail.user.id, input) + if (!result) { + console.log('newsletter not created:', input) + return false + } + + // send push notification + const deviceTokens = await getDeviceTokensByUserId(newsletterEmail.user.id) + + if (!deviceTokens) { + console.log('Device tokens not set:', newsletterEmail.user.id) + return true + } + + const link = await ctx.models.userArticle.getForUser( + newsletterEmail.user.id, + result.articleId + ) + + if (!link) { + console.log( + 'Newsletter link not found:', + newsletterEmail.user.id, + result.articleId + ) + return true + } + + if (deviceTokens.length) { + const multicastMessage = messageForLink(link, deviceTokens) + await sendMulticastPushNotifications( + newsletterEmail.user.id, + multicastMessage, + 'newsletter' + ) + } + + return true +} + +const messageForLink = ( + link: ArticleData & UserArticleData, + deviceTokens: UserDeviceToken[] +): MulticastMessage => { + let title = '📫 - An article was added to your Omnivore Inbox' + + if (link.author) { + title = `📫 - ${link.author} has published a new article` + } + + const pushData = !link + ? undefined + : { + link: Buffer.from( + JSON.stringify({ + id: link.id, + url: link.url, + slug: link.slug, + title: link.title, + image: link.image, + author: link.author, + isArchived: link.isArchived, + contentReader: ContentReader.Web, + readingProgressPercent: link.articleReadingProgress, + readingProgressAnchorIndex: link.articleReadingProgressAnchorIndex, + }) + ).toString('base64'), + } + + return { + notification: { + title: title, + body: link.title, + imageUrl: link.image || undefined, + }, + data: pushData, + tokens: deviceTokens.map((token) => token.token), + } +} diff --git a/packages/api/src/utils/parser.ts b/packages/api/src/utils/parser.ts index 51a7fe60e..8b13cd1ab 100644 --- a/packages/api/src/utils/parser.ts +++ b/packages/api/src/utils/parser.ts @@ -397,4 +397,4 @@ export const isProbablyNewsletter = (dom: DOMWindow): boolean => { } return false -}; +}