From abd59f05277b4772645b22f4e00aa7cd6e9f7676 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Fri, 7 Jul 2023 21:48:16 +0800 Subject: [PATCH] save each item in a feed --- packages/rss-handler/package.json | 1 + packages/rss-handler/src/index.ts | 114 ++++++++++++++++++++++++++++-- 2 files changed, 108 insertions(+), 7 deletions(-) diff --git a/packages/rss-handler/package.json b/packages/rss-handler/package.json index e24080ae1..82b87db8c 100644 --- a/packages/rss-handler/package.json +++ b/packages/rss-handler/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@google-cloud/functions-framework": "3.1.2", + "@google-cloud/tasks": "^3.0.5", "@sentry/serverless": "^6.16.1", "axios": "^1.4.0", "dotenv": "^16.0.1", diff --git a/packages/rss-handler/src/index.ts b/packages/rss-handler/src/index.ts index c6459bbff..895af2855 100644 --- a/packages/rss-handler/src/index.ts +++ b/packages/rss-handler/src/index.ts @@ -1,6 +1,70 @@ import * as Sentry from '@sentry/serverless' +import axios from 'axios' import * as dotenv from 'dotenv' // see https://github.com/motdotla/dotenv#how-do-i-use-dotenv-with-import +import * as jwt from 'jsonwebtoken' import Parser from 'rss-parser' +import { promisify } from 'util' + +interface RssFeedRequest { + subscriptionId: string + userId: string + feedUrl: string +} + +function isRssFeedRequest(body: any): body is RssFeedRequest { + return 'subscriptionId' in body && 'userId' in body && 'feedUrl' in body +} + +const sendSavePageMutation = async (userId: string, input: unknown) => { + const JWT_SECRET = process.env.JWT_SECRET + const REST_BACKEND_ENDPOINT = process.env.REST_BACKEND_ENDPOINT + + if (!JWT_SECRET || !REST_BACKEND_ENDPOINT) { + throw 'Environment not configured correctly' + } + + const data = JSON.stringify({ + query: `mutation SavePage ($input: SavePageInput!){ + savePage(input:$input){ + ... on SaveSuccess{ + url + clientRequestId + } + ... on SaveError{ + errorCodes + } + } + }`, + variables: { + input: Object.assign({}, input, { source: 'puppeteer-parse' }), + }, + }) + + const auth = (await signToken({ uid: userId }, JWT_SECRET)) as string + try { + const response = await axios.post( + `${REST_BACKEND_ENDPOINT}/graphql`, + data, + { + headers: { + Cookie: `auth=${auth};`, + 'Content-Type': 'application/json', + }, + timeout: 30000, // 30s + } + ) + + /* eslint-disable @typescript-eslint/no-unsafe-member-access */ + return !!response.data.data.savePage + } catch (error) { + if (axios.isAxiosError(error)) { + console.error('save page mutation error', error.message) + } else { + console.error(error) + } + return false + } +} dotenv.config() Sentry.GCPFunction.init({ @@ -8,18 +72,54 @@ Sentry.GCPFunction.init({ tracesSampleRate: 0, }) +const signToken = promisify(jwt.sign) const parser = new Parser() export const rssHandler = Sentry.GCPFunction.wrapHttpFunction( async (req, res) => { - try { - const feed = await parser.parseURL('https://www.reddit.com/.rss') - console.log(feed.title) + if (!process.env.JWT_SECRET) { + console.error('Missing JWT_SECRET in environment') + return res.status(500).send('INTERNAL_SERVER_ERROR') + } - feed.items.forEach((item) => { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - console.log(`${item.title}:${item.link}`) - }) + try { + if (!isRssFeedRequest(req.body)) { + console.error('Invalid request body', req.body) + return res.status(400).send('INVALID_REQUEST_BODY') + } + + const { userId, feedUrl } = req.body + // fetch feed + const feed = await parser.parseURL(feedUrl) + console.debug(feed.title) + + // save each item in the feed + await Promise.all( + feed.items.map((item) => { + if (!item.link || !item.title || !item.content) { + console.log('Invalid feed item', item) + return + } + + const input = { + source: 'rss-feeder', + url: item.link, + saveRequestId: '', + labels: [{ name: 'RSS' }], + title: item.title, + originalContent: item.content, + } + + try { + // save page + return sendSavePageMutation(userId, input) + } catch (error) { + console.error('Error while saving page', error) + } + }) + ) + + // TODO: update subscription lastFetchedAt res.send('ok') } catch (e) {