From e392af48003dbe0718feafb2dade48aa33ddc924 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 11 Jan 2023 15:35:05 +0800 Subject: [PATCH 01/23] Update matter import handler to use archives instead of just history files --- packages/import-handler/package.json | 11 +- packages/import-handler/src/csv.ts | 15 +- packages/import-handler/src/index.ts | 81 +++++-- packages/import-handler/src/matterHistory.ts | 220 +++++++++++++++++- packages/import-handler/src/readability.d.ts | 108 +++++++++ packages/import-handler/test/csv/csv.test.ts | 12 +- .../test/matter/data/Archive.zip | Bin 0 -> 96906 bytes .../test/matter/matter_importer.test.ts | 43 +++- packages/import-handler/test/util.ts | 22 ++ packages/import-handler/tsconfig.json | 9 +- 10 files changed, 473 insertions(+), 48 deletions(-) create mode 100644 packages/import-handler/src/readability.d.ts create mode 100644 packages/import-handler/test/matter/data/Archive.zip create mode 100644 packages/import-handler/test/util.ts diff --git a/packages/import-handler/package.json b/packages/import-handler/package.json index fec6d9d45..609cd51b9 100644 --- a/packages/import-handler/package.json +++ b/packages/import-handler/package.json @@ -20,8 +20,10 @@ "deploy": "yarn build && yarn gcloud-deploy" }, "devDependencies": { - "@types/node": "^14.11.2", + "@types/fs-extra": "^11.0.1", "@types/jsonwebtoken": "^8.5.0", + "@types/node": "^14.11.2", + "@types/unzip-stream": "^0.3.1", "eslint-plugin-prettier": "^4.0.0" }, "dependencies": { @@ -29,9 +31,14 @@ "@google-cloud/functions-framework": "3.1.2", "@google-cloud/storage": "^5.18.1", "@google-cloud/tasks": "^3.0.5", + "@omnivore/content-handler": "1.0.0", + "@omnivore/readability": "1.0.0", "@types/express": "^4.17.13", "csv-parser": "^3.0.0", + "dompurify": "^2.4.3", + "fs-extra": "^11.1.0", "jsonwebtoken": "^8.5.1", - "nodemon": "^2.0.15" + "nodemon": "^2.0.15", + "unzip-stream": "^0.3.1" } } diff --git a/packages/import-handler/src/csv.ts b/packages/import-handler/src/csv.ts index 7f60abadd..80f15f72f 100644 --- a/packages/import-handler/src/csv.ts +++ b/packages/import-handler/src/csv.ts @@ -5,24 +5,19 @@ import { parse } from '@fast-csv/parse' import { Stream } from 'stream' +import { ImportContext } from '.' -export type UrlHandler = (url: URL) => Promise - -export const importCsv = async ( - stream: Stream, - handler: UrlHandler -): Promise => { +export const importCsv = async (stream: Stream, ctx: ImportContext) => { const parser = parse() stream.pipe(parser) - let count = 0 for await (const row of parser) { try { const url = new URL(row[0]) - await handler(url) + await ctx.urlHandler(ctx, url) + ctx.countImported += 1 } catch (error) { console.log('invalid url', row, error) + ctx.countFailed += 1 } - count++ } - return count } diff --git a/packages/import-handler/src/index.ts b/packages/import-handler/src/index.ts index 0b37cab30..20898c45e 100644 --- a/packages/import-handler/src/index.ts +++ b/packages/import-handler/src/index.ts @@ -3,30 +3,50 @@ import { CloudFunctionsContext, } from '@google-cloud/functions-framework/build/src/functions' import { Storage } from '@google-cloud/storage' -import { importCsv, UrlHandler } from './csv' +import { importCsv } from './csv' import * as path from 'path' -import { importMatterHistory } from './matterHistory' +import { importMatterArchive, importMatterHistoryCsv } from './matterHistory' import { Stream } from 'node:stream' import { v4 as uuid } from 'uuid' import { CONTENT_FETCH_URL, createCloudTask, EMAIL_USER_URL } from './task' import { promisify } from 'util' import * as jwt from 'jsonwebtoken' +import { Readability } from '@omnivore/readability' const signToken = promisify(jwt.sign) const storage = new Storage() +const CONTENT_TYPES = ['text/csv', 'application/zip'] + interface StorageEventData { bucket: string name: string contentType: string } +export type UrlHandler = (ctx: ImportContext, url: URL) => Promise +export type ContentHandler = ( + ctx: ImportContext, + url: URL, + title: string, + originalContent: string, + parseResult: Readability.ParseResult +) => Promise + +export type ImportContext = { + userId: string + countImported: number + countFailed: number + urlHandler: UrlHandler + contentHandler: ContentHandler +} + type importHandlerFunc = ( stream: Stream, - handler: UrlHandler -) => Promise + handler: ImportContext +) => Promise const shouldHandle = (data: StorageEventData, ctx: CloudFunctionsContext) => { console.log('deciding to handle', ctx, data) @@ -35,7 +55,7 @@ const shouldHandle = (data: StorageEventData, ctx: CloudFunctionsContext) => { } if ( !data.name.startsWith('imports/') || - data.contentType.toLowerCase() != 'text/csv' + CONTENT_TYPES.indexOf(data.contentType.toLocaleLowerCase()) == -1 ) { return false } @@ -93,7 +113,7 @@ const sendImportCompletedEmail = async ( const handlerForFile = (name: string): importHandlerFunc | undefined => { const fileName = path.parse(name).name if (fileName.startsWith('MATTER')) { - return importMatterHistory + return importMatterArchive } else if (fileName.startsWith('URL_LIST')) { return importCsv } @@ -101,6 +121,35 @@ const handlerForFile = (name: string): importHandlerFunc | undefined => { return undefined } +const urlHandler = async (ctx: ImportContext, url: URL): Promise => { + try { + // Imports are stored in the format imports//-.csv + const result = await importURL(ctx.userId, url, 'csv-importer') + if (result) { + ctx.countImported += 1 + } + } catch (err) { + console.log('error importing url', err) + } +} + +const contentHandler = async ( + ctx: ImportContext, + url: URL, + title: string, + originalContent: string, + parseResult: Readability.ParseResult +): Promise => { + // const apiResponse = await sendSavePageMutation(userId, { + // url: finalUrl, + // clientRequestId: articleSavingRequestId, + // title, + // originalContent: content, + // parseResult: readabilityResult, + // }) + return Promise.resolve() +} + export const importHandler: EventFunction = async (event, context) => { const data = event as StorageEventData const ctx = context as CloudFunctionsContext @@ -131,18 +180,14 @@ export const importHandler: EventFunction = async (event, context) => { return } - let countFailed = 0 - let countImported = 0 - await handler(stream, async (url): Promise => { - try { - // Imports are stored in the format imports//-.csv - const result = await importURL(userId, url, 'csv-importer') - console.log('import url result', result) - countImported = countImported + 1 - } catch (err) { - console.log('error importing url', err) - countFailed = countFailed + 1 - } + const countFailed = 0 + const countImported = 0 + await handler(stream, { + userId, + countImported: 0, + countFailed: 0, + urlHandler, + contentHandler, }) if (countImported <= 1) { diff --git a/packages/import-handler/src/matterHistory.ts b/packages/import-handler/src/matterHistory.ts index 8342735b5..f0b314183 100644 --- a/packages/import-handler/src/matterHistory.ts +++ b/packages/import-handler/src/matterHistory.ts @@ -5,28 +5,234 @@ import { parse } from '@fast-csv/parse' import { Stream } from 'stream' +import unzip from 'unzip-stream' +import fs from 'fs' +import path from 'path' +import * as fsExtra from 'fs-extra' + +import { parseHTML } from 'linkedom' +import { Readability } from '@omnivore/readability' +import createDOMPurify, { SanitizeElementHookEvent } from 'dompurify' + +import { encode } from 'urlsafe-base64' +import crypto from 'crypto' +import { ImportContext } from '.' export type UrlHandler = (url: URL) => Promise -export const importMatterHistory = async ( +export const importMatterHistoryCsv = async ( stream: Stream, - handler: UrlHandler -): Promise => { + ctx: ImportContext +): Promise => { const parser = parse({ headers: true, strictColumnHandling: false, }) stream.pipe(parser) - let count = 0 for await (const row of parser) { try { const url = new URL(row['URL']) - await handler(url) + await ctx.urlHandler(ctx, url) + ctx.countImported += 1 } catch (error) { console.log('invalid url', row, error) + ctx.countFailed += 1 + } + } +} + +const DOM_PURIFY_CONFIG = { + ADD_TAGS: ['iframe'], + ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling'], + FORBID_ATTR: [ + 'data-ml-dynamic', + 'data-ml-dynamic-type', + 'data-orig-url', + 'data-ml-id', + 'data-ml', + 'data-xid', + 'data-feature', + ], +} + +function domPurifySanitizeHook(node: Element, data: SanitizeElementHookEvent) { + if (data.tagName === 'iframe') { + const urlRegex = /^(https?:)?\/\/www\.youtube(-nocookie)?\.com\/embed\//i + const src = node.getAttribute('src') || '' + const dataSrc = node.getAttribute('data-src') || '' + + if (src && urlRegex.test(src)) { + return + } + + if (dataSrc && urlRegex.test(dataSrc)) { + node.setAttribute('src', dataSrc) + return + } + + node.parentNode?.removeChild(node) + } +} + +function getPurifiedContent(html: string) { + const newWindow = parseHTML('') + const DOMPurify = createDOMPurify(newWindow) + DOMPurify.addHook('uponSanitizeElement', domPurifySanitizeHook) + const clean = DOMPurify.sanitize(html, DOM_PURIFY_CONFIG) + return parseHTML(clean).document +} + +function createImageProxyUrl(url: string, width = 0, height = 0) { + if (process.env.IMAGE_PROXY_URL && process.env.IMAGE_PROXY_SECRET) { + const urlWithOptions = `${url}#${width}x${height}` + const signature = signImageProxyUrl(urlWithOptions) + + return `${process.env.IMAGE_PROXY_URL}/${width}x${height},s${signature}/${url}` + } + return url +} + +function signImageProxyUrl(url: string) { + if (process.env.IMAGE_PROXY_SECRET) { + return encode( + crypto + .createHmac('sha256', process.env.IMAGE_PROXY_SECRET) + .update(url) + .digest() + ) + } + return url +} + +async function getReadabilityResult(url: string, originalContent: string) { + const document = getPurifiedContent(originalContent) + + try { + const article = await new Readability(document, { + createImageProxyUrl, + url, + }).parse() + + if (article) { + return article + } + } catch (error) { + console.log('parsing error for url', url, error) + } + + return null +} + +const unarchive = async (stream: Stream): Promise => { + const archiveDir = `./archive-${Date.now().toString(16)}` + await fsExtra.emptyDir(archiveDir) + + return new Promise((resolve, reject) => { + stream + .pipe(unzip.Extract({ path: archiveDir })) + .on('close', () => { + resolve(archiveDir) + }) + .on('error', reject) + }) +} + +const getMatterHistoryContent = ( + archiveDir: string, + row: Record +) => { + try { + const contentKey = row['File Id'] + const contentPath = path.join(archiveDir, contentKey) + const content = fs.readFileSync(contentPath).toString() + + return content + } catch (err) { + console.log('error getting matter history content: ', { row, err }) + } + return undefined +} + +const getURL = (str: string | undefined) => { + if (!str) { + return undefined + } + + try { + const url = new URL(str) + return url + } catch (err) { + console.log('error parsing url', { str, err }) + } + + return undefined +} + +const handleMatterHistoryRow = async ( + ctx: ImportContext, + archiveDir: string, + row: Record +) => { + const title = row['Title'] + const urlStr = row['URL'] + const url = getURL(urlStr) + + if (!url) { + ctx.countFailed += 1 + return + } + + const originalContent = getMatterHistoryContent(archiveDir, row) + const readabilityResult = originalContent + ? await getReadabilityResult(urlStr, originalContent) + : null + + if (originalContent && readabilityResult) { + await ctx.contentHandler( + ctx, + url, + title, + originalContent, + readabilityResult + ) + } else { + await ctx.urlHandler(ctx, url) + } +} + +export const importMatterArchive = async ( + stream: Stream, + ctx: ImportContext +): Promise => { + const archiveDir = await unarchive(stream) + + try { + const historyFile = path.join(archiveDir, '_matter_history.csv') + + const parser = parse({ + headers: true, + strictColumnHandling: false, + }) + + fs.createReadStream(historyFile).pipe(parser) + + for await (const row of parser) { + try { + await handleMatterHistoryRow(ctx, archiveDir, row) + ctx.countImported += 1 + } catch (error) { + console.log('invalid url', row, error) + ctx.countFailed += 1 + } + } + } catch (err) { + console.log('error handling archive: ', { err }) + } finally { + try { + await fsExtra.rm(archiveDir, { recursive: true, force: true }) + } catch (err) { + console.log('Error removing archive directory', { err }) } - count++ } - return count } diff --git a/packages/import-handler/src/readability.d.ts b/packages/import-handler/src/readability.d.ts new file mode 100644 index 000000000..55bfc74d1 --- /dev/null +++ b/packages/import-handler/src/readability.d.ts @@ -0,0 +1,108 @@ +// Type definitions for non-npm package mozilla-readability 0.2 +// Project: https://github.com/mozilla/readability +// Definitions by: Charles Vandevoorde , Alex Wendland +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.2 + +declare module '@omnivore/readability' { + class Readability { + constructor(doc: Document, options?: Readability.Options) + + async parse(): Promise + } + + namespace Readability { + interface Options { + /** + * Control whether log messages are sent to the console + */ + debug?: boolean + + /** + * Set a maximum size on the documents that will be processed. This size is + * checked before any parsing operations occur. If the number of elements in + * the document exceeds this threshold then an Error will be thrown. + * + * See implementation details at https://github.com/mozilla/readability/blob/52ab9b5c8916c306a47b2119270dcdabebf9d203/Readability.js#L2019 + */ + maxElemsToParse?: number + + nbTopCandidates?: number + + /** + * Minimum number of characters in the extracted textContent in order to + * consider the article correctly identified. If the threshold is not met then + * the extraction process will automatically run again with different flags. + * + * See implementation details at https://github.com/mozilla/readability/blob/52ab9b5c8916c306a47b2119270dcdabebf9d203/Readability.js#L1208 + * + * Changed from wordThreshold in https://github.com/mozilla/readability/commit/3ff9a166fb27928f222c4c0722e730eda412658a + */ + charThreshold?: number + + /** + * parse() removes the class="" attribute from every element in the given + * subtree, except those that match CLASSES_TO_PRESERVE and + * the classesToPreserve array from the options object. + */ + classesToPreserve?: string[] + + /** + * By default Readability will strip all classes from the HTML elements in the + * processed article. By setting this to `true` the classes will be retained. + * + * This is a blanket alternative to `classesToPreserve`. + * + * Added in https://github.com/mozilla/readability/commit/2982216913af2c66b0690e88606b03116553ad92 + */ + + keepClasses?: boolean + url?: string + + /** + * Function that converts a regular image url into imageproxy url + * @param url string + */ + createImageProxyUrl?: ( + url: string, + width?: number, + height?: number + ) => string + + /** + * By default, Readability will clean all tables from the HTML elements in the + * processed article. But newsletters in emails use tables to display their content. + * By setting this to `true`, these tables will be retained. + */ + keepTables?: boolean + } + + interface ParseResult { + /** Article title */ + title: string + /** Author metadata */ + byline?: string | null + /** Content direction */ + dir?: string | null + /** HTML string of processed article content */ + content: string + /** non-HTML version of `content` */ + textContent: string + /** Length of an article, in characters */ + length: number + /** Article description, or short excerpt from the content */ + excerpt: string + /** Article site name */ + siteName?: string | null + /** Article site icon */ + siteIcon?: string | null + /** Article preview image */ + previewImage?: string | null + /** Article published date */ + publishedDate?: Date | null + language?: string | null + } + } + + export { Readability } +} diff --git a/packages/import-handler/test/csv/csv.test.ts b/packages/import-handler/test/csv/csv.test.ts index 4ea3ef705..7dfff7ea8 100644 --- a/packages/import-handler/test/csv/csv.test.ts +++ b/packages/import-handler/test/csv/csv.test.ts @@ -4,6 +4,8 @@ import { expect } from 'chai' import chaiString from 'chai-string' import * as fs from 'fs' import { importCsv } from '../../src/csv' +import { ImportContext } from '../../src' +import { stubImportCtx } from '../util' chai.use(chaiString) @@ -11,11 +13,15 @@ describe('Load a simple CSV file', () => { it('should call the handler for each URL', async () => { const urls: URL[] = [] const stream = fs.createReadStream('./test/csv/data/simple.csv') - const count = await importCsv(stream, (url): Promise => { + const stub = stubImportCtx() + stub.urlHandler = (ctx: ImportContext, url): Promise => { urls.push(url) return Promise.resolve() - }) - expect(count).to.equal(2) + } + + await importCsv(stream, stub) + expect(stub.countFailed).to.equal(0) + expect(stub.countImported).to.equal(2) expect(urls).to.eql([ new URL('https://omnivore.app'), new URL('https://google.com'), diff --git a/packages/import-handler/test/matter/data/Archive.zip b/packages/import-handler/test/matter/data/Archive.zip new file mode 100644 index 0000000000000000000000000000000000000000..f6301248439434d425b21791bf9cc885e9eefc31 GIT binary patch literal 96906 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

)KcHYYJt+ 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 { it('should find the URL of each row', async () => { const urls: URL[] = [] const stream = fs.createReadStream('./test/matter/data/_matter_history.csv') - const count = await importMatterHistory(stream, (url): Promise => { + const stub = stubImportCtx() + stub.urlHandler = (ctx: ImportContext, url): Promise => { urls.push(url) return Promise.resolve() - }) - expect(count).to.equal(1) + } + + await importMatterHistoryCsv(stream, stub) + expect(stub.countFailed).to.equal(0) + expect(stub.countImported).to.equal(1) + expect(urls).to.eql([ + new URL('https://www.bloomberg.com/features/2022-the-crypto-story/'), + ]) + }) +}) + +describe('Load archive file', () => { + it('should find the URL of each row', async () => { + const urls: URL[] = [] + const stream = fs.createReadStream('./test/matter/data/Archive.zip') + const stub = stubImportCtx() + stub.contentHandler = ( + ctx: ImportContext, + url: URL, + title: string, + originalContent: string, + parseResult: Readability.ParseResult + ): Promise => { + urls.push(url) + return Promise.resolve() + } + + await importMatterArchive(stream, stub) + expect(stub.countFailed).to.equal(0) + expect(stub.countImported).to.equal(1) expect(urls).to.eql([ new URL('https://www.bloomberg.com/features/2022-the-crypto-story/'), ]) diff --git a/packages/import-handler/test/util.ts b/packages/import-handler/test/util.ts new file mode 100644 index 000000000..58cd9dd10 --- /dev/null +++ b/packages/import-handler/test/util.ts @@ -0,0 +1,22 @@ +import { Readability } from '@omnivore/readability' +import { ImportContext } from '../src' + +export const stubImportCtx = () => { + return { + userId: '', + countImported: 0, + countFailed: 0, + urlHandler: (ctx: ImportContext, url: URL): Promise => { + return Promise.resolve() + }, + contentHandler: ( + ctx: ImportContext, + url: URL, + title: string, + originalContent: string, + parseResult: Readability.ParseResult + ): Promise => { + return Promise.resolve() + }, + } +} diff --git a/packages/import-handler/tsconfig.json b/packages/import-handler/tsconfig.json index f450acf38..5c4d5c778 100644 --- a/packages/import-handler/tsconfig.json +++ b/packages/import-handler/tsconfig.json @@ -1,9 +1,10 @@ { - "extends": "@tsconfig/node14/tsconfig.json", + "extends": "./../../tsconfig.json", + "ts-node": { + "files": true + }, "compilerOptions": { - "outDir": "build", - "rootDir": ".", - "lib": ["dom"] + "outDir": "dist" }, "include": ["src", "test"] } From 903d14ead3d62caa9bca2ffb552274c7afd35cb5 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 11 Jan 2023 15:50:10 +0800 Subject: [PATCH 02/23] Add Dockerfile --- packages/import-handler/Dockerfile | 56 ++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 packages/import-handler/Dockerfile diff --git a/packages/import-handler/Dockerfile b/packages/import-handler/Dockerfile new file mode 100644 index 000000000..84730b9d2 --- /dev/null +++ b/packages/import-handler/Dockerfile @@ -0,0 +1,56 @@ +FROM node:14.18-alpine + +# Installs latest Chromium (92) package. +RUN apk add --no-cache \ + chromium \ + nss \ + freetype \ + harfbuzz \ + ca-certificates \ + ttf-freefont \ + nodejs \ + yarn \ + g++ \ + make \ + python3 + +# Add user so we don't need --no-sandbox. +RUN addgroup -S pptruser && adduser -S -g pptruser pptruser \ + && mkdir -p /home/pptruser/Downloads /app \ + && chown -R pptruser:pptruser /home/pptruser \ + && chown -R pptruser:pptruser /app + +# Run everything after as non-privileged user. +WORKDIR /app + +ENV CHROMIUM_PATH /usr/bin/chromium-browser +ENV LAUNCH_HEADLESS=true +ENV PORT 9090 + +COPY package.json . +COPY yarn.lock . +COPY tsconfig.json . +COPY .prettierrc . +COPY .eslintrc . + +COPY /packages/readabilityjs/package.json ./packages/readabilityjs/package.json +COPY /packages/content-handler/package.json ./packages/content-handler/package.json +COPY /packages/import-handler/package.json ./packages/import-handler/package.json + + +RUN yarn install --pure-lockfile + +ADD /packages/content-handler ./packages/content-handler +ADD /packages/import-handler ./packages/import-handler +ADD /packages/readabilityjs ./packages/readabilityjs +RUN yarn workspace @omnivore/import-handler build + +# After building, fetch the production dependencies +RUN rm -rf /app/packages/import-handler/node_modules +RUN rm -rf /app/node_modules +RUN yarn install --pure-lockfile --production + +EXPOSE 9090 + +# USER pptruser +ENTRYPOINT ["yarn", "workspace", "@omnivore/import-handler", "start_gcf"] From 9e83f0f11cca79f7d8870982c110e0f2fc67b55b Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 11 Jan 2023 16:22:31 +0800 Subject: [PATCH 03/23] Packaging for the import handler --- packages/import-handler/Dockerfile | 30 +-------- packages/import-handler/package.json | 10 ++- yarn.lock | 93 +++++++++++++++++++++++++++- 3 files changed, 103 insertions(+), 30 deletions(-) diff --git a/packages/import-handler/Dockerfile b/packages/import-handler/Dockerfile index 84730b9d2..f4a167620 100644 --- a/packages/import-handler/Dockerfile +++ b/packages/import-handler/Dockerfile @@ -1,30 +1,10 @@ FROM node:14.18-alpine -# Installs latest Chromium (92) package. -RUN apk add --no-cache \ - chromium \ - nss \ - freetype \ - harfbuzz \ - ca-certificates \ - ttf-freefont \ - nodejs \ - yarn \ - g++ \ - make \ - python3 - -# Add user so we don't need --no-sandbox. -RUN addgroup -S pptruser && adduser -S -g pptruser pptruser \ - && mkdir -p /home/pptruser/Downloads /app \ - && chown -R pptruser:pptruser /home/pptruser \ - && chown -R pptruser:pptruser /app - -# Run everything after as non-privileged user. WORKDIR /app -ENV CHROMIUM_PATH /usr/bin/chromium-browser -ENV LAUNCH_HEADLESS=true +ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true +RUN apk add g++ make python3 + ENV PORT 9090 COPY package.json . @@ -34,13 +14,10 @@ COPY .prettierrc . COPY .eslintrc . COPY /packages/readabilityjs/package.json ./packages/readabilityjs/package.json -COPY /packages/content-handler/package.json ./packages/content-handler/package.json COPY /packages/import-handler/package.json ./packages/import-handler/package.json - RUN yarn install --pure-lockfile -ADD /packages/content-handler ./packages/content-handler ADD /packages/import-handler ./packages/import-handler ADD /packages/readabilityjs ./packages/readabilityjs RUN yarn workspace @omnivore/import-handler build @@ -52,5 +29,4 @@ RUN yarn install --pure-lockfile --production EXPOSE 9090 -# USER pptruser ENTRYPOINT ["yarn", "workspace", "@omnivore/import-handler", "start_gcf"] diff --git a/packages/import-handler/package.json b/packages/import-handler/package.json index 609cd51b9..75b4b85bd 100644 --- a/packages/import-handler/package.json +++ b/packages/import-handler/package.json @@ -20,10 +20,16 @@ "deploy": "yarn build && yarn gcloud-deploy" }, "devDependencies": { + "@types/chai": "^4.3.4", + "@types/chai-string": "^1.4.2", + "@types/dompurify": "^2.4.0", "@types/fs-extra": "^11.0.1", "@types/jsonwebtoken": "^8.5.0", + "@types/mocha": "^10.0.1", "@types/node": "^14.11.2", "@types/unzip-stream": "^0.3.1", + "@types/urlsafe-base64": "^1.0.28", + "@types/uuid": "^9.0.0", "eslint-plugin-prettier": "^4.0.0" }, "dependencies": { @@ -31,7 +37,6 @@ "@google-cloud/functions-framework": "3.1.2", "@google-cloud/storage": "^5.18.1", "@google-cloud/tasks": "^3.0.5", - "@omnivore/content-handler": "1.0.0", "@omnivore/readability": "1.0.0", "@types/express": "^4.17.13", "csv-parser": "^3.0.0", @@ -39,6 +44,7 @@ "fs-extra": "^11.1.0", "jsonwebtoken": "^8.5.1", "nodemon": "^2.0.15", - "unzip-stream": "^0.3.1" + "unzip-stream": "^0.3.1", + "urlsafe-base64": "^1.0.0" } } diff --git a/yarn.lock b/yarn.lock index d469f24e8..8fccfb80d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7865,6 +7865,11 @@ resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.21.tgz#9f35a5643129df132cf3b5c1ec64046ea1af0650" integrity sha512-yd+9qKmJxm496BOV9CMNaey8TWsikaZOwMRwPHQIjcOJM9oV+fi9ZMNw3JsVnbEEbo2gRTDnGEBv8pjyn67hNg== +"@types/chai@^4.3.4": + version "4.3.4" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.4.tgz#e913e8175db8307d78b4e8fa690408ba6b65dee4" + integrity sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw== + "@types/chrome@^0.0.197": version "0.0.197" resolved "https://registry.yarnpkg.com/@types/chrome/-/chrome-0.0.197.tgz#c1b50cdb72ee40f9bc1411506031a9f8a925ab35" @@ -7948,6 +7953,13 @@ dependencies: "@types/trusted-types" "*" +"@types/dompurify@^2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.4.0.tgz#fd9706392a88e0e0e6d367f3588482d817df0ab9" + integrity sha512-IDBwO5IZhrKvHFUl+clZxgf3hn2b/lU6H1KaBShPkQyGJUQ0xwebezIPSuiyGwfz1UzJWQl4M7BDxtHtCCPlTg== + dependencies: + "@types/trusted-types" "*" + "@types/duplexify@^3.6.0": version "3.6.0" resolved "https://registry.yarnpkg.com/@types/duplexify/-/duplexify-3.6.0.tgz#dfc82b64bd3a2168f5bd26444af165bf0237dcd8" @@ -8058,6 +8070,14 @@ dependencies: "@types/node" "*" +"@types/fs-extra@^11.0.1": + version "11.0.1" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-11.0.1.tgz#f542ec47810532a8a252127e6e105f487e0a6ea5" + integrity sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA== + dependencies: + "@types/jsonfile" "*" + "@types/node" "*" + "@types/glob@*": version "7.2.0" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb" @@ -8225,6 +8245,13 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/jsonfile@*": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@types/jsonfile/-/jsonfile-6.1.1.tgz#ac84e9aefa74a2425a0fb3012bdea44f58970f1b" + integrity sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png== + dependencies: + "@types/node" "*" + "@types/jsonwebtoken@^8.5.0": version "8.5.0" resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz#2531d5e300803aa63279b232c014acf780c981c5" @@ -8313,6 +8340,11 @@ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== +"@types/mocha@^10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.1.tgz#2f4f65bb08bc368ac39c96da7b2f09140b26851b" + integrity sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q== + "@types/mocha@^8.2.2": version "8.2.3" resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.3.tgz#bbeb55fbc73f28ea6de601fbfa4613f58d785323" @@ -8653,10 +8685,17 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== +"@types/unzip-stream@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@types/unzip-stream/-/unzip-stream-0.3.1.tgz#a57b8365be3d7f89960c755c8cc37c83223dc7d0" + integrity sha512-RlE3qaqvu4XaMwxkG/zR1gIunCbqXvNrmZ4BCG7OiQ8QUactFUPxm0TTrOCRJZQfPW3T6XBH7PcHQiiqkdcijw== + dependencies: + "@types/node" "*" + "@types/urlsafe-base64@^1.0.28": version "1.0.28" resolved "https://registry.yarnpkg.com/@types/urlsafe-base64/-/urlsafe-base64-1.0.28.tgz#2cf2098518e98c730a7e00de79a455e0269a3f4d" - integrity sha1-LPIJhRjpjHMKfgDeeaRV4CaaP00= + integrity sha512-TG5dKbqx75FUTXfiARIPvLvMCImVYJbKM+Fvy9HgpxkunHnMHNAn78xpvcZxIbPITyRzf0b2Gl8fnd1Ja3p1eQ== dependencies: "@types/node" "*" @@ -8665,6 +8704,11 @@ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== +"@types/uuid@^9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.0.tgz#53ef263e5239728b56096b0a869595135b7952d2" + integrity sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q== + "@types/voca@^1.4.0": version "1.4.2" resolved "https://registry.yarnpkg.com/@types/voca/-/voca-1.4.2.tgz#4d2ceef3582c2a1f6a1574f71f0897400d910583" @@ -10494,6 +10538,14 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== +binary@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" + integrity sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg== + dependencies: + buffers "~0.1.1" + chainsaw "~0.1.0" + binaryextensions@^2.1.2: version "2.3.0" resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.3.0.tgz#1d269cbf7e6243ea886aa41453c3651ccbe13c22" @@ -10793,6 +10845,11 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" +buffers@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" + integrity sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ== + bufrw@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/bufrw/-/bufrw-1.3.0.tgz#28d6cfdaf34300376836310f5c31d57eeb40c8fa" @@ -11089,6 +11146,13 @@ chai@^4.3.6: pathval "^1.1.1" type-detect "^4.0.5" +chainsaw@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" + integrity sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ== + dependencies: + traverse ">=0.3.0 <0.4" + chalk@^1.0.0, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -13062,6 +13126,11 @@ dompurify@^2.4.1: resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.1.tgz#f9cb1a275fde9af6f2d0a2644ef648dd6847b631" integrity sha512-ewwFzHzrrneRjxzmK6oVz/rZn9VWspGFRDb4/rRtIsM1n36t9AKma/ye8syCpcw+XJ25kOK/hOG7t1j2I2yBqA== +dompurify@^2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.3.tgz#f4133af0e6a50297fc8874e2eaedc13a3c308c03" + integrity sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ== + domutils@^2.0.0, domutils@^2.5.2: version "2.7.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.7.0.tgz#8ebaf0c41ebafcf55b0b72ec31c56323712c5442" @@ -14783,6 +14852,15 @@ fs-extra@^10.0.0: jsonfile "^6.0.1" universalify "^2.0.0" +fs-extra@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.0.tgz#5784b102104433bb0e090f48bfc4a30742c357ed" + integrity sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^9.0.0, fs-extra@^9.0.1, fs-extra@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" @@ -25566,6 +25644,11 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== +"traverse@>=0.3.0 <0.4": + version "0.3.9" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" + integrity sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ== + tree-kill@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" @@ -26151,6 +26234,14 @@ untildify@^4.0.0: resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== +unzip-stream@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/unzip-stream/-/unzip-stream-0.3.1.tgz#2333b5cd035d29db86fb701ca212cf8517400083" + integrity sha512-RzaGXLNt+CW+T41h1zl6pGz3EaeVhYlK+rdAap+7DxW5kqsqePO8kRtWPaCiVqdhZc86EctSPVYNix30YOMzmw== + dependencies: + binary "^0.3.0" + mkdirp "^0.5.1" + upath@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" From 6aabd0311f5ba0a7d2ac69b217d76c282bf42f90 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 11 Jan 2023 16:30:33 +0800 Subject: [PATCH 04/23] Fix scripts in import-handler package --- packages/import-handler/package.json | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/import-handler/package.json b/packages/import-handler/package.json index 75b4b85bd..71689c7f1 100644 --- a/packages/import-handler/package.json +++ b/packages/import-handler/package.json @@ -14,10 +14,8 @@ "lint": "eslint src --ext ts,js,tsx,jsx", "compile": "tsc", "build": "tsc", - "start": "functions-framework --source=build/src/ --target=importHandler", - "dev": "concurrently \"tsc -w\" \"nodemon --watch ./build/ --exec npm run start\"", - "gcloud-deploy": "gcloud functions deploy importHandler --region=$npm_config_region --runtime nodejs14 --trigger-bucket=$npm_config_bucket --env-vars-file=../gcf-shared/env-$npm_config_env.yaml", - "deploy": "yarn build && yarn gcloud-deploy" + "start_gcf": "npx functions-framework --port=9090 --target=puppeteer", + "dev": "concurrently \"tsc -w\" \"nodemon --watch ./build/ --exec npm run start\"" }, "devDependencies": { "@types/chai": "^4.3.4", @@ -47,4 +45,4 @@ "unzip-stream": "^0.3.1", "urlsafe-base64": "^1.0.0" } -} +} \ No newline at end of file From e31a806fcb360ffed6a5e268ac872fadf283fbb3 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 11 Jan 2023 16:57:00 +0800 Subject: [PATCH 05/23] Start http server to run in Cloud Run --- packages/import-handler/package.json | 5 ++- packages/import-handler/src/index.ts | 13 ++++++ yarn.lock | 60 +++++++++++++++++++++++++++- 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/packages/import-handler/package.json b/packages/import-handler/package.json index 71689c7f1..14cd0bfd4 100644 --- a/packages/import-handler/package.json +++ b/packages/import-handler/package.json @@ -14,7 +14,7 @@ "lint": "eslint src --ext ts,js,tsx,jsx", "compile": "tsc", "build": "tsc", - "start_gcf": "npx functions-framework --port=9090 --target=puppeteer", + "start_gcf": "npx functions-framework --port=9090 --target=httpServer", "dev": "concurrently \"tsc -w\" \"nodemon --watch ./build/ --exec npm run start\"" }, "devDependencies": { @@ -36,6 +36,7 @@ "@google-cloud/storage": "^5.18.1", "@google-cloud/tasks": "^3.0.5", "@omnivore/readability": "1.0.0", + "@sentry/serverless": "^7.30.0", "@types/express": "^4.17.13", "csv-parser": "^3.0.0", "dompurify": "^2.4.3", @@ -45,4 +46,4 @@ "unzip-stream": "^0.3.1", "urlsafe-base64": "^1.0.0" } -} \ No newline at end of file +} diff --git a/packages/import-handler/src/index.ts b/packages/import-handler/src/index.ts index 20898c45e..c68e0b395 100644 --- a/packages/import-handler/src/index.ts +++ b/packages/import-handler/src/index.ts @@ -14,6 +14,13 @@ import { promisify } from 'util' import * as jwt from 'jsonwebtoken' import { Readability } from '@omnivore/readability' +import Sentry from '@sentry/serverless' + +Sentry.GCPFunction.init({ + dsn: process.env.SENTRY_DSN, + tracesSampleRate: 0, +}) + const signToken = promisify(jwt.sign) const storage = new Storage() @@ -197,3 +204,9 @@ export const importHandler: EventFunction = async (event, context) => { } } } + +export const httpServer = Sentry.GCPFunction.wrapHttpFunction( + async (req, res) => { + res.send('ok') + } +) diff --git a/yarn.lock b/yarn.lock index 8fccfb80d..076209800 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5526,6 +5526,15 @@ "@sentry/utils" "6.19.6" tslib "^1.9.3" +"@sentry/core@7.30.0": + version "7.30.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.30.0.tgz#02d2e8747484ba64b6d712e8afe6736250efdc26" + integrity sha512-NeLigkBlpcK63ymM63GoIHurml6V3BUe1Vi+trwm4/qqOTzT7PQhvdJCX+o3+atzRBH+zdb6kd4VWx44Oye3KA== + dependencies: + "@sentry/types" "7.30.0" + "@sentry/utils" "7.30.0" + tslib "^1.9.3" + "@sentry/hub@5.30.0": version "5.30.0" resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.30.0.tgz#2453be9b9cb903404366e198bd30c7ca74cdc100" @@ -5652,6 +5661,19 @@ lru_map "^0.3.3" tslib "^1.9.3" +"@sentry/node@7.30.0": + version "7.30.0" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.30.0.tgz#42ef5b29d065f6de6ac5556a56aca20d3b9073e1" + integrity sha512-YYasu6C3I0HBP4N1oc/ed2nunxhGJgtAWaKwq3lo8uk3uF6cB1A8+2e0CpjzU5ejhbaFPUBxHyj4th39Bvku/w== + dependencies: + "@sentry/core" "7.30.0" + "@sentry/types" "7.30.0" + "@sentry/utils" "7.30.0" + cookie "^0.4.1" + https-proxy-agent "^5.0.0" + lru_map "^0.3.3" + tslib "^1.9.3" + "@sentry/node@^5.26.0": version "5.30.0" resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.30.0.tgz#4ca479e799b1021285d7fe12ac0858951c11cd48" @@ -5693,6 +5715,19 @@ "@types/express" "^4.17.2" tslib "^1.9.3" +"@sentry/serverless@^7.30.0": + version "7.30.0" + resolved "https://registry.yarnpkg.com/@sentry/serverless/-/serverless-7.30.0.tgz#f639c835043de2c3dec10f3667fcf8545a557e24" + integrity sha512-Fs6fuyDR+RJ5yg/m4M9Ids+jVc9FJbE8XBxOGZkpYkjZFLWVETMI3tJt4cF31LOQF/irftr6pKW/blxHAknBrg== + dependencies: + "@sentry/node" "7.30.0" + "@sentry/tracing" "7.30.0" + "@sentry/types" "7.30.0" + "@sentry/utils" "7.30.0" + "@types/aws-lambda" "^8.10.62" + "@types/express" "^4.17.14" + tslib "^1.9.3" + "@sentry/tracing@5.30.0": version "5.30.0" resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-5.30.0.tgz#501d21f00c3f3be7f7635d8710da70d9419d4e1f" @@ -5726,6 +5761,16 @@ "@sentry/utils" "6.19.6" tslib "^1.9.3" +"@sentry/tracing@7.30.0": + version "7.30.0" + resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.30.0.tgz#b4576fbaf81ce418f1b7c37e7e5f4f6cf19a3c3b" + integrity sha512-bjGeDeKhpGAmLcWcrXFT/xOfHVwp/j0L1aRHzYHnqgTjVzD0NXcooPu/Nz8vF0paxz+hPD5bJwb8kz/ggJzGWQ== + dependencies: + "@sentry/core" "7.30.0" + "@sentry/types" "7.30.0" + "@sentry/utils" "7.30.0" + tslib "^1.9.3" + "@sentry/tracing@^7.9.0": version "7.9.0" resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.9.0.tgz#0cbbf5b61ee76b934d2e4160a0ad3daf0001237b" @@ -5756,6 +5801,11 @@ resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.10.0.tgz#c91d634768336238ac30ed750fa918326c384cbb" integrity sha512-1UBwdbS0xXzANzp63g4eNQly/qKIXp0swP5OTKWoADvKBtL4anroLUA/l8ADMtuwFZYtVANc8WRGxM2+YmaXtg== +"@sentry/types@7.30.0": + version "7.30.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.30.0.tgz#fc2baeb5b0e1ecc4d52b07b056fcba54449cd9ce" + integrity sha512-l4A86typvt/SfWh5JffpdxNGkg5EEA8m35BzpIcKmCAQZUDmnb4b478r8jdD2uuOjLmPNmZr1tifdRW4NCLuxQ== + "@sentry/types@7.9.0": version "7.9.0" resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.9.0.tgz#8fa952865fda76f7c7c7fc6c84043979a22043ae" @@ -5793,6 +5843,14 @@ "@sentry/types" "7.10.0" tslib "^1.9.3" +"@sentry/utils@7.30.0": + version "7.30.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.30.0.tgz#1d83145399c65e31f725c1b6ae02f451a990f326" + integrity sha512-tSlBhr5u/LdE2emxIDTDmjmyRr99GnZGIAh5GwRxUgeDQ3VEfNUFlyFodBCbZ6yeYTYd6PWNih5xoHn1+Rf3Sw== + dependencies: + "@sentry/types" "7.30.0" + tslib "^1.9.3" + "@sentry/utils@7.9.0": version "7.9.0" resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.9.0.tgz#497c41efe1b32974208ca68570e42c853b874f77" @@ -8021,7 +8079,7 @@ dependencies: "@types/express" "*" -"@types/express@*", "@types/express@^4.16.0", "@types/express@^4.17.13", "@types/express@^4.17.2", "@types/express@^4.17.7": +"@types/express@*", "@types/express@^4.16.0", "@types/express@^4.17.13", "@types/express@^4.17.14", "@types/express@^4.17.2", "@types/express@^4.17.7": version "4.17.15" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.15.tgz#9290e983ec8b054b65a5abccb610411953d417ff" integrity sha512-Yv0k4bXGOH+8a+7bELd2PqHQsuiANB+A8a4gnQrkRWzrkKlb6KHaVvyXhqs04sVW/OWlbPyYxRgYlIXLfrufMQ== From b6f9a8bab5e5ff61c238bc25e2f313810f82074c Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 11 Jan 2023 17:06:04 +0800 Subject: [PATCH 06/23] Add uuid package to import-handler --- packages/import-handler/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/import-handler/package.json b/packages/import-handler/package.json index 14cd0bfd4..2413bf556 100644 --- a/packages/import-handler/package.json +++ b/packages/import-handler/package.json @@ -44,6 +44,7 @@ "jsonwebtoken": "^8.5.1", "nodemon": "^2.0.15", "unzip-stream": "^0.3.1", - "urlsafe-base64": "^1.0.0" + "urlsafe-base64": "^1.0.0", + "uuid": "^9.0.0" } } From dea4ffae3386e5e373b1a4a5cf160b4aed84d19c Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 11 Jan 2023 18:10:22 +0800 Subject: [PATCH 07/23] Fixes for dockerizing the import-handler service --- packages/import-handler/Dockerfile | 5 ++--- packages/import-handler/package.json | 9 +++++---- packages/import-handler/src/index.ts | 10 +++++----- packages/import-handler/src/task.ts | 11 ++++++----- packages/import-handler/tsconfig.json | 9 +++++---- yarn.lock | 20 ++++++++++++++++++++ 6 files changed, 43 insertions(+), 21 deletions(-) diff --git a/packages/import-handler/Dockerfile b/packages/import-handler/Dockerfile index f4a167620..83a66b834 100644 --- a/packages/import-handler/Dockerfile +++ b/packages/import-handler/Dockerfile @@ -5,12 +5,11 @@ WORKDIR /app ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true RUN apk add g++ make python3 -ENV PORT 9090 +ENV PORT 8080 COPY package.json . COPY yarn.lock . COPY tsconfig.json . -COPY .prettierrc . COPY .eslintrc . COPY /packages/readabilityjs/package.json ./packages/readabilityjs/package.json @@ -27,6 +26,6 @@ RUN rm -rf /app/packages/import-handler/node_modules RUN rm -rf /app/node_modules RUN yarn install --pure-lockfile --production -EXPOSE 9090 +EXPOSE 8080 ENTRYPOINT ["yarn", "workspace", "@omnivore/import-handler", "start_gcf"] diff --git a/packages/import-handler/package.json b/packages/import-handler/package.json index 2413bf556..2ca868a17 100644 --- a/packages/import-handler/package.json +++ b/packages/import-handler/package.json @@ -3,18 +3,17 @@ "version": "1.0.0", "description": "", "main": "build/src/index.js", - "types": "build/src/index.d.ts", "files": [ "build/src" ], - "license": "Apache-2.0", "keywords": [], + "license": "Apache-2.0", "scripts": { "test": "yarn mocha -r ts-node/register --config mocha-config.json", "lint": "eslint src --ext ts,js,tsx,jsx", "compile": "tsc", "build": "tsc", - "start_gcf": "npx functions-framework --port=9090 --target=httpServer", + "start": "functions-framework --target=importHandler", "dev": "concurrently \"tsc -w\" \"nodemon --watch ./build/ --exec npm run start\"" }, "devDependencies": { @@ -38,13 +37,15 @@ "@omnivore/readability": "1.0.0", "@sentry/serverless": "^7.30.0", "@types/express": "^4.17.13", + "axios": "^1.2.2", "csv-parser": "^3.0.0", "dompurify": "^2.4.3", "fs-extra": "^11.1.0", "jsonwebtoken": "^8.5.1", + "linkedom": "^0.14.21", "nodemon": "^2.0.15", "unzip-stream": "^0.3.1", "urlsafe-base64": "^1.0.0", "uuid": "^9.0.0" } -} +} \ No newline at end of file diff --git a/packages/import-handler/src/index.ts b/packages/import-handler/src/index.ts index c68e0b395..778c0f705 100644 --- a/packages/import-handler/src/index.ts +++ b/packages/import-handler/src/index.ts @@ -8,13 +8,13 @@ import * as path from 'path' import { importMatterArchive, importMatterHistoryCsv } from './matterHistory' import { Stream } from 'node:stream' import { v4 as uuid } from 'uuid' -import { CONTENT_FETCH_URL, createCloudTask, EMAIL_USER_URL } from './task' +import { CONTENT_FETCH_URL, createCloudTask, emailUserUrl } from './task' import { promisify } from 'util' import * as jwt from 'jsonwebtoken' import { Readability } from '@omnivore/readability' -import Sentry from '@sentry/serverless' +import * as Sentry from '@sentry/serverless' Sentry.GCPFunction.init({ dsn: process.env.SENTRY_DSN, @@ -96,7 +96,7 @@ const createEmailCloudTask = async (userId: string, payload: unknown) => { Cookie: `auth=${authToken}`, } - return createCloudTask(EMAIL_USER_URL, payload, headers) + return createCloudTask(emailUserUrl(), payload, headers) } const sendImportFailedEmail = async (userId: string) => { @@ -157,7 +157,7 @@ const contentHandler = async ( return Promise.resolve() } -export const importHandler: EventFunction = async (event, context) => { +export const gcsEventHandler: EventFunction = async (event, context) => { const data = event as StorageEventData const ctx = context as CloudFunctionsContext @@ -205,7 +205,7 @@ export const importHandler: EventFunction = async (event, context) => { } } -export const httpServer = Sentry.GCPFunction.wrapHttpFunction( +export const importHandler = Sentry.GCPFunction.wrapHttpFunction( async (req, res) => { res.send('ok') } diff --git a/packages/import-handler/src/task.ts b/packages/import-handler/src/task.ts index f35c31a2d..16f66b08d 100644 --- a/packages/import-handler/src/task.ts +++ b/packages/import-handler/src/task.ts @@ -3,12 +3,13 @@ import { CloudTasksClient, protos } from '@google-cloud/tasks' const cloudTask = new CloudTasksClient() -export const EMAIL_USER_URL = (() => { - if (!process.env.INTERNAL_SVC_ENDPOINT) { - throw `Environment not configured correctly, no SVC endpoint` +export const emailUserUrl = () => { + const envar = process.env.INTERNAL_SVC_ENDPOINT + if (envar) { + return envar + 'api/user/email' } - return (process.env.INTERNAL_SVC_ENDPOINT ?? '') + 'api/user/email' -})() + throw 'INTERNAL_SVC_ENDPOINT not set' +} export const CONTENT_FETCH_URL = process.env.CONTENT_FETCH_GCF_URL diff --git a/packages/import-handler/tsconfig.json b/packages/import-handler/tsconfig.json index 5c4d5c778..8c12cd3a8 100644 --- a/packages/import-handler/tsconfig.json +++ b/packages/import-handler/tsconfig.json @@ -1,10 +1,11 @@ { "extends": "./../../tsconfig.json", - "ts-node": { - "files": true - }, "compilerOptions": { - "outDir": "dist" + "outDir": "build", + "rootDir": ".", + "lib": ["dom"], + // Generate d.ts files + "declaration": true }, "include": ["src", "test"] } diff --git a/yarn.lock b/yarn.lock index 076209800..8a8741388 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10266,6 +10266,15 @@ axios@^1.2.0: form-data "^4.0.0" proxy-from-env "^1.1.0" +axios@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.2.2.tgz#72681724c6e6a43a9fea860fc558127dbe32f9f1" + integrity sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axobject-query@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" @@ -18550,6 +18559,17 @@ linkedom@^0.14.16: htmlparser2 "^8.0.1" uhyphen "^0.1.0" +linkedom@^0.14.21: + version "0.14.21" + resolved "https://registry.yarnpkg.com/linkedom/-/linkedom-0.14.21.tgz#878e1e5e88028cb1d57bc6262f84484a41a37497" + integrity sha512-V+c0AAFMTVJA2iAhrdd+u44lL0TjL6hBenVB061VQ6BHqTAHtXw1v5F1/CHGKtwg0OHm+hrGbepb9ZSFJ7lJkg== + dependencies: + css-select "^5.1.0" + cssom "^0.5.0" + html-escaper "^3.0.3" + htmlparser2 "^8.0.1" + uhyphen "^0.1.0" + linkedom@^0.14.9: version "0.14.9" resolved "https://registry.yarnpkg.com/linkedom/-/linkedom-0.14.9.tgz#34c6f15eddc809406f42d8ee48cd30b0222eccb0" From c75a558bd400637633e3dd75d96091ae4143b46a Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 11 Jan 2023 18:24:55 +0800 Subject: [PATCH 08/23] Fix entrypoint in Dockerfile --- packages/import-handler/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/import-handler/Dockerfile b/packages/import-handler/Dockerfile index 83a66b834..f1e4d0dd2 100644 --- a/packages/import-handler/Dockerfile +++ b/packages/import-handler/Dockerfile @@ -28,4 +28,4 @@ RUN yarn install --pure-lockfile --production EXPOSE 8080 -ENTRYPOINT ["yarn", "workspace", "@omnivore/import-handler", "start_gcf"] +ENTRYPOINT ["yarn", "workspace", "@omnivore/import-handler", "start"] From 50e1dbda5a772e227e020c7d012ff4cece76907a Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 11 Jan 2023 18:36:37 +0800 Subject: [PATCH 09/23] Add debug for Eventarc --- packages/import-handler/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/import-handler/src/index.ts b/packages/import-handler/src/index.ts index 778c0f705..f7053fd78 100644 --- a/packages/import-handler/src/index.ts +++ b/packages/import-handler/src/index.ts @@ -207,6 +207,7 @@ export const gcsEventHandler: EventFunction = async (event, context) => { export const importHandler = Sentry.GCPFunction.wrapHttpFunction( async (req, res) => { + console.log('received: ', req.body) res.send('ok') } ) From 154109bb9df511df043674f864302170d206dca6 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 11 Jan 2023 19:46:12 +0800 Subject: [PATCH 10/23] Log headers --- packages/import-handler/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/import-handler/src/index.ts b/packages/import-handler/src/index.ts index f7053fd78..bcf4a2da8 100644 --- a/packages/import-handler/src/index.ts +++ b/packages/import-handler/src/index.ts @@ -207,7 +207,7 @@ export const gcsEventHandler: EventFunction = async (event, context) => { export const importHandler = Sentry.GCPFunction.wrapHttpFunction( async (req, res) => { - console.log('received: ', req.body) + console.log('received: ', req.body, req.headers) res.send('ok') } ) From 62db9da1212228b135d14af7812a558fb8c39984 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 11 Jan 2023 20:07:54 +0800 Subject: [PATCH 11/23] Parse pubsub message --- packages/import-handler/src/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/import-handler/src/index.ts b/packages/import-handler/src/index.ts index bcf4a2da8..7a3e971a5 100644 --- a/packages/import-handler/src/index.ts +++ b/packages/import-handler/src/index.ts @@ -208,6 +208,15 @@ export const gcsEventHandler: EventFunction = async (event, context) => { export const importHandler = Sentry.GCPFunction.wrapHttpFunction( async (req, res) => { console.log('received: ', req.body, req.headers) + const pubSubMessage = req.body.message + if (pubSubMessage) { + console.log( + 'pubsub message: ' + + Buffer.from(pubSubMessage.data, 'base64').toString().trim() + ) + } else { + console.log('no pubsub message') + } res.send('ok') } ) From 6623d5a43af69086bd5654a2b01650dad82c9237 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 11 Jan 2023 20:59:41 +0800 Subject: [PATCH 12/23] Handle pubsub event parsing --- packages/import-handler/src/index.ts | 53 ++++++++++++++++------------ 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/packages/import-handler/src/index.ts b/packages/import-handler/src/index.ts index 7a3e971a5..b4e4bda1b 100644 --- a/packages/import-handler/src/index.ts +++ b/packages/import-handler/src/index.ts @@ -27,12 +27,6 @@ const storage = new Storage() const CONTENT_TYPES = ['text/csv', 'application/zip'] -interface StorageEventData { - bucket: string - name: string - contentType: string -} - export type UrlHandler = (ctx: ImportContext, url: URL) => Promise export type ContentHandler = ( ctx: ImportContext, @@ -55,11 +49,20 @@ type importHandlerFunc = ( handler: ImportContext ) => Promise -const shouldHandle = (data: StorageEventData, ctx: CloudFunctionsContext) => { - console.log('deciding to handle', ctx, data) - if (ctx.eventType !== 'google.storage.object.finalize') { - return false +interface StorageEvent { + name: string + bucket: string + contentType: string +} + +function isStorageEvent(event: any): event is StorageEvent { + if ('name' in event && 'bucket' in event && 'contentType' in event) { + return true } + return false +} + +const shouldHandle = (data: StorageEvent) => { if ( !data.name.startsWith('imports/') || CONTENT_TYPES.indexOf(data.contentType.toLocaleLowerCase()) == -1 @@ -157,11 +160,8 @@ const contentHandler = async ( return Promise.resolve() } -export const gcsEventHandler: EventFunction = async (event, context) => { - const data = event as StorageEventData - const ctx = context as CloudFunctionsContext - - if (shouldHandle(data, ctx)) { +const handleEvent = async (data: StorageEvent) => { + if (shouldHandle(data)) { console.log('handling csv data', data) const stream = storage @@ -205,15 +205,24 @@ export const gcsEventHandler: EventFunction = async (event, context) => { } } +function isPubsubMessage(event: any): event is StorageEvent { + if ('name' in event && 'bucket' in event && 'contentType' in event) { + return true + } + return false +} + export const importHandler = Sentry.GCPFunction.wrapHttpFunction( async (req, res) => { - console.log('received: ', req.body, req.headers) - const pubSubMessage = req.body.message - if (pubSubMessage) { - console.log( - 'pubsub message: ' + - Buffer.from(pubSubMessage.data, 'base64').toString().trim() - ) + /* eslint-disable @typescript-eslint/no-unsafe-member-access */ + if ('message' in req.body && 'data' in req.body.message) { + /* eslint-disable @typescript-eslint/no-unsafe-member-access */ + const pubSubMessage = req.body.message.data as string + const str = Buffer.from(pubSubMessage, 'base64').toString().trim() + const obj = JSON.parse(str) as unknown + if (isStorageEvent(obj)) { + await handleEvent(obj) + } } else { console.log('no pubsub message') } From 04375d685600678f653db52d05f6b1b09c3b0ee9 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 11 Jan 2023 22:42:37 +0800 Subject: [PATCH 13/23] Add some extra debugging --- packages/import-handler/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/import-handler/src/index.ts b/packages/import-handler/src/index.ts index b4e4bda1b..a86048f70 100644 --- a/packages/import-handler/src/index.ts +++ b/packages/import-handler/src/index.ts @@ -157,6 +157,7 @@ const contentHandler = async ( // originalContent: content, // parseResult: readabilityResult, // }) + console.log('content handler: ', url, title) return Promise.resolve() } From 0bc87deba01e628d95e83d72865fd8bb8177da05 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 11 Jan 2023 22:55:04 +0800 Subject: [PATCH 14/23] Change order of ctx arguments --- packages/import-handler/src/csv.ts | 2 +- packages/import-handler/src/index.ts | 21 +++++++------------ packages/import-handler/src/matterHistory.ts | 8 +++---- packages/import-handler/test/csv/csv.test.ts | 2 +- .../test/matter/matter_importer.test.ts | 2 +- 5 files changed, 14 insertions(+), 21 deletions(-) diff --git a/packages/import-handler/src/csv.ts b/packages/import-handler/src/csv.ts index 80f15f72f..c505cc39a 100644 --- a/packages/import-handler/src/csv.ts +++ b/packages/import-handler/src/csv.ts @@ -7,7 +7,7 @@ import { parse } from '@fast-csv/parse' import { Stream } from 'stream' import { ImportContext } from '.' -export const importCsv = async (stream: Stream, ctx: ImportContext) => { +export const importCsv = async (ctx: ImportContext, stream: Stream) => { const parser = parse() stream.pipe(parser) for await (const row of parser) { diff --git a/packages/import-handler/src/index.ts b/packages/import-handler/src/index.ts index a86048f70..dbce23fbc 100644 --- a/packages/import-handler/src/index.ts +++ b/packages/import-handler/src/index.ts @@ -1,7 +1,3 @@ -import { - EventFunction, - CloudFunctionsContext, -} from '@google-cloud/functions-framework/build/src/functions' import { Storage } from '@google-cloud/storage' import { importCsv } from './csv' import * as path from 'path' @@ -44,10 +40,7 @@ export type ImportContext = { contentHandler: ContentHandler } -type importHandlerFunc = ( - stream: Stream, - handler: ImportContext -) => Promise +type importHandlerFunc = (ctx: ImportContext, stream: Stream) => Promise interface StorageEvent { name: string @@ -188,20 +181,20 @@ const handleEvent = async (data: StorageEvent) => { return } - const countFailed = 0 - const countImported = 0 - await handler(stream, { + const ctx = { userId, countImported: 0, countFailed: 0, urlHandler, contentHandler, - }) + } - if (countImported <= 1) { + await handler(ctx, stream) + + if (ctx.countImported <= 1) { await sendImportFailedEmail(userId) } else { - await sendImportCompletedEmail(userId, countImported, countFailed) + await sendImportCompletedEmail(userId, ctx.countImported, ctx.countFailed) } } } diff --git a/packages/import-handler/src/matterHistory.ts b/packages/import-handler/src/matterHistory.ts index f0b314183..7d339d624 100644 --- a/packages/import-handler/src/matterHistory.ts +++ b/packages/import-handler/src/matterHistory.ts @@ -21,8 +21,8 @@ import { ImportContext } from '.' export type UrlHandler = (url: URL) => Promise export const importMatterHistoryCsv = async ( - stream: Stream, - ctx: ImportContext + ctx: ImportContext, + stream: Stream ): Promise => { const parser = parse({ headers: true, @@ -202,8 +202,8 @@ const handleMatterHistoryRow = async ( } export const importMatterArchive = async ( - stream: Stream, - ctx: ImportContext + ctx: ImportContext, + stream: Stream ): Promise => { const archiveDir = await unarchive(stream) diff --git a/packages/import-handler/test/csv/csv.test.ts b/packages/import-handler/test/csv/csv.test.ts index 7dfff7ea8..0f695d69e 100644 --- a/packages/import-handler/test/csv/csv.test.ts +++ b/packages/import-handler/test/csv/csv.test.ts @@ -19,7 +19,7 @@ describe('Load a simple CSV file', () => { return Promise.resolve() } - await importCsv(stream, stub) + await importCsv(stub, stream) expect(stub.countFailed).to.equal(0) expect(stub.countImported).to.equal(2) expect(urls).to.eql([ diff --git a/packages/import-handler/test/matter/matter_importer.test.ts b/packages/import-handler/test/matter/matter_importer.test.ts index e160c7be3..75b2a9cc8 100644 --- a/packages/import-handler/test/matter/matter_importer.test.ts +++ b/packages/import-handler/test/matter/matter_importer.test.ts @@ -23,7 +23,7 @@ describe('Load a simple _matter_history file', () => { return Promise.resolve() } - await importMatterHistoryCsv(stream, stub) + await importMatterHistoryCsv(stub, stream) expect(stub.countFailed).to.equal(0) expect(stub.countImported).to.equal(1) expect(urls).to.eql([ From ee50b237c63cb97fc82dc427629ae05215b362dc Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 11 Jan 2023 22:56:10 +0800 Subject: [PATCH 15/23] Fix order of args --- packages/import-handler/test/matter/matter_importer.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/import-handler/test/matter/matter_importer.test.ts b/packages/import-handler/test/matter/matter_importer.test.ts index 75b2a9cc8..90600b16a 100644 --- a/packages/import-handler/test/matter/matter_importer.test.ts +++ b/packages/import-handler/test/matter/matter_importer.test.ts @@ -48,7 +48,7 @@ describe('Load archive file', () => { return Promise.resolve() } - await importMatterArchive(stream, stub) + await importMatterArchive(stub, stream) expect(stub.countFailed).to.equal(0) expect(stub.countImported).to.equal(1) expect(urls).to.eql([ From fa866f1d29e92c738b993650a2c61e5e3dfb5dad Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 11 Jan 2023 23:08:30 +0800 Subject: [PATCH 16/23] Make function more clear --- packages/import-handler/src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/import-handler/src/index.ts b/packages/import-handler/src/index.ts index dbce23fbc..4c5957765 100644 --- a/packages/import-handler/src/index.ts +++ b/packages/import-handler/src/index.ts @@ -191,10 +191,10 @@ const handleEvent = async (data: StorageEvent) => { await handler(ctx, stream) - if (ctx.countImported <= 1) { - await sendImportFailedEmail(userId) - } else { + if (ctx.countImported > 0) { await sendImportCompletedEmail(userId, ctx.countImported, ctx.countFailed) + } else { + await sendImportFailedEmail(userId) } } } From ee8bad424b185014cb0175d4e931753e7ddeea9f Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 11 Jan 2023 23:19:59 +0800 Subject: [PATCH 17/23] Fix typo --- packages/import-handler/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/import-handler/src/index.ts b/packages/import-handler/src/index.ts index 4c5957765..3411f07d1 100644 --- a/packages/import-handler/src/index.ts +++ b/packages/import-handler/src/index.ts @@ -109,7 +109,7 @@ const sendImportCompletedEmail = async ( ) => { return createEmailCloudTask(userId, { subject: 'Your Omnivore import has completed processing', - body: `${urlsEnqueued} URLs have been pcoessed and should be available in your library. ${urlsFailed} URLs failed to be parsed.`, + body: `${urlsEnqueued} URLs have been processed and should be available in your library. ${urlsFailed} URLs failed to be parsed.`, }) } From 7263e91d6b3466d414dedaeec7249d849ed4f70c Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 11 Jan 2023 23:59:27 +0800 Subject: [PATCH 18/23] Enable ts-node files to pull in readability.d.ts --- packages/import-handler/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/import-handler/tsconfig.json b/packages/import-handler/tsconfig.json index 8c12cd3a8..ea8c4d3ef 100644 --- a/packages/import-handler/tsconfig.json +++ b/packages/import-handler/tsconfig.json @@ -1,5 +1,6 @@ { "extends": "./../../tsconfig.json", + "ts-node": { "files": true }, "compilerOptions": { "outDir": "build", "rootDir": ".", From c240f1d2c81619e17ec8f49b6147411eae04d292 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 12 Jan 2023 10:50:06 +0800 Subject: [PATCH 19/23] Make SavePage API call if we have content provided --- packages/import-handler/src/index.ts | 65 +++++++++++++++++++++------- 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/packages/import-handler/src/index.ts b/packages/import-handler/src/index.ts index 3411f07d1..262dbf138 100644 --- a/packages/import-handler/src/index.ts +++ b/packages/import-handler/src/index.ts @@ -6,6 +6,7 @@ import { Stream } from 'node:stream' import { v4 as uuid } from 'uuid' import { CONTENT_FETCH_URL, createCloudTask, emailUserUrl } from './task' +import axios from 'axios' import { promisify } from 'util' import * as jwt from 'jsonwebtoken' import { Readability } from '@omnivore/readability' @@ -136,6 +137,44 @@ const urlHandler = async (ctx: ImportContext, url: URL): Promise => { } } +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 + const response = await axios.post(`${REST_BACKEND_ENDPOINT}/graphql`, data, { + headers: { + Cookie: `auth=${auth};`, + 'Content-Type': 'application/json', + }, + }) + console.log('save page response: ', response) + + /* eslint-disable @typescript-eslint/no-unsafe-member-access */ + return !!response.data.data.savePage +} + const contentHandler = async ( ctx: ImportContext, url: URL, @@ -143,14 +182,17 @@ const contentHandler = async ( originalContent: string, parseResult: Readability.ParseResult ): Promise => { - // const apiResponse = await sendSavePageMutation(userId, { - // url: finalUrl, - // clientRequestId: articleSavingRequestId, - // title, - // originalContent: content, - // parseResult: readabilityResult, - // }) - console.log('content handler: ', url, title) + const requestId = uuid() + const apiResponse = await sendSavePageMutation(ctx.userId, { + url, + clientRequestId: requestId, + title, + originalContent, + parseResult, + }) + if (!apiResponse) { + return Promise.reject() + } return Promise.resolve() } @@ -199,13 +241,6 @@ const handleEvent = async (data: StorageEvent) => { } } -function isPubsubMessage(event: any): event is StorageEvent { - if ('name' in event && 'bucket' in event && 'contentType' in event) { - return true - } - return false -} - export const importHandler = Sentry.GCPFunction.wrapHttpFunction( async (req, res) => { /* eslint-disable @typescript-eslint/no-unsafe-member-access */ From 824899760a59eb956c66461ada44a0360b4b07fa Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 12 Jan 2023 14:00:46 +0800 Subject: [PATCH 20/23] Update some style, add Matter archive importer tool --- .../importers/uploadImportFileResolver.ts | 4 +- .../templates/ConfirmProfileModal.tsx | 17 +- .../components/templates/ProfileLayout.tsx | 7 +- .../templates/auth/EmailForgotPassword.tsx | 8 +- .../components/templates/auth/EmailLogin.tsx | 8 +- .../templates/auth/EmailResetPassword.tsx | 8 +- .../components/templates/auth/EmailSignup.tsx | 13 +- packages/web/pages/invite/[inviteCode].tsx | 8 +- packages/web/pages/tools/import/file.tsx | 3 +- .../web/pages/tools/import/matter-archive.tsx | 218 ++++++++++++++++++ 10 files changed, 262 insertions(+), 32 deletions(-) create mode 100644 packages/web/pages/tools/import/matter-archive.tsx diff --git a/packages/api/src/resolvers/importers/uploadImportFileResolver.ts b/packages/api/src/resolvers/importers/uploadImportFileResolver.ts index 544f97dc6..4b37cfb2f 100644 --- a/packages/api/src/resolvers/importers/uploadImportFileResolver.ts +++ b/packages/api/src/resolvers/importers/uploadImportFileResolver.ts @@ -18,7 +18,7 @@ import { v4 as uuidv4 } from 'uuid' import { buildLogger } from '../../utils/logger' const MAX_DAILY_UPLOADS = 4 -const VALID_CONTENT_TYPES = ['text/csv'] +const VALID_CONTENT_TYPES = ['text/csv', 'application/zip'] const logger = buildLogger('app.dispatch') @@ -26,6 +26,8 @@ const extensionForContentType = (contentType: string) => { switch (contentType) { case 'text/csv': return 'csv' + case 'application/zip': + return 'zip' } return '.unknown' } diff --git a/packages/web/components/templates/ConfirmProfileModal.tsx b/packages/web/components/templates/ConfirmProfileModal.tsx index 72a939947..a61766932 100644 --- a/packages/web/components/templates/ConfirmProfileModal.tsx +++ b/packages/web/components/templates/ConfirmProfileModal.tsx @@ -15,9 +15,8 @@ export function ConfirmProfileModal(): JSX.Element { const [username, setUsername] = useState('') const [debouncedUsername, setDebouncedUsername] = useState('') const [bio, setBio] = useState('') - const [errorMessage, setErrorMessage] = useState( - undefined - ) + const [errorMessage, setErrorMessage] = + useState(undefined) const { isUsernameValid, usernameErrorMessage } = useValidateUsernameQuery({ username: debouncedUsername, @@ -110,8 +109,9 @@ export function ConfirmProfileModal(): JSX.Element { @@ -139,7 +139,12 @@ export function ConfirmProfileModal(): JSX.Element { {isUsernameValid && ( Username is available. diff --git a/packages/web/components/templates/ProfileLayout.tsx b/packages/web/components/templates/ProfileLayout.tsx index 874b30c89..e2d3d6b1a 100644 --- a/packages/web/components/templates/ProfileLayout.tsx +++ b/packages/web/components/templates/ProfileLayout.tsx @@ -12,7 +12,12 @@ export function ProfileLayout(props: ProfileLayoutProps): JSX.Element { {props.children} diff --git a/packages/web/components/templates/auth/EmailForgotPassword.tsx b/packages/web/components/templates/auth/EmailForgotPassword.tsx index b2fca77da..c97a222fc 100644 --- a/packages/web/components/templates/auth/EmailForgotPassword.tsx +++ b/packages/web/components/templates/auth/EmailForgotPassword.tsx @@ -12,9 +12,8 @@ import { parseErrorCodes } from '../../../lib/queryParamParser' export function EmailForgotPassword(): JSX.Element { const router = useRouter() const [email, setEmail] = useState('') - const [errorMessage, setErrorMessage] = useState( - undefined - ) + const [errorMessage, setErrorMessage] = + useState(undefined) useEffect(() => { if (!router.isReady) return @@ -36,7 +35,8 @@ export function EmailForgotPassword(): JSX.Element { width: '70vw', maxWidth: '576px', borderRadius: '8px', - boxShadow: 'rgb(224 224 224) 9px 9px 9px -9px', + border: '1px solid #3D3D3D', + boxShadow: '#B1B1B1 9px 9px 9px -9px', }} > diff --git a/packages/web/components/templates/auth/EmailLogin.tsx b/packages/web/components/templates/auth/EmailLogin.tsx index 6d3533cb9..8175f329f 100644 --- a/packages/web/components/templates/auth/EmailLogin.tsx +++ b/packages/web/components/templates/auth/EmailLogin.tsx @@ -15,9 +15,8 @@ export function EmailLogin(): JSX.Element { const router = useRouter() const [email, setEmail] = useState(undefined) const [password, setPassword] = useState(undefined) - const [errorMessage, setErrorMessage] = useState( - undefined - ) + const [errorMessage, setErrorMessage] = + useState(undefined) useEffect(() => { if (!router.isReady) return @@ -39,7 +38,8 @@ export function EmailLogin(): JSX.Element { width: '70vw', maxWidth: '576px', borderRadius: '8px', - boxShadow: 'rgb(224 224 224) 9px 9px 9px -9px', + border: '1px solid #3D3D3D', + boxShadow: '#B1B1B1 9px 9px 9px -9px', }} > diff --git a/packages/web/components/templates/auth/EmailResetPassword.tsx b/packages/web/components/templates/auth/EmailResetPassword.tsx index eafc284b2..10f378c8b 100644 --- a/packages/web/components/templates/auth/EmailResetPassword.tsx +++ b/packages/web/components/templates/auth/EmailResetPassword.tsx @@ -13,9 +13,8 @@ export function EmailResetPassword(): JSX.Element { const router = useRouter() const [token, setToken] = useState(undefined) const [password, setPassword] = useState('') - const [errorMessage, setErrorMessage] = useState( - undefined - ) + const [errorMessage, setErrorMessage] = + useState(undefined) useEffect(() => { if (!router.isReady) return @@ -45,7 +44,8 @@ export function EmailResetPassword(): JSX.Element { width: '70vw', maxWidth: '576px', borderRadius: '8px', - boxShadow: 'rgb(224 224 224) 9px 9px 9px -9px', + border: '1px solid #3D3D3D', + boxShadow: '#B1B1B1 9px 9px 9px -9px', }} > (undefined) const [fullname, setFullname] = useState(undefined) const [username, setUsername] = useState(undefined) - const [debouncedUsername, setDebouncedUsername] = useState< - string | undefined - >(undefined) - const [errorMessage, setErrorMessage] = useState( - undefined - ) + const [debouncedUsername, setDebouncedUsername] = + useState(undefined) + const [errorMessage, setErrorMessage] = + useState(undefined) useEffect(() => { if (!router.isReady) return @@ -60,7 +58,8 @@ export function EmailSignup(): JSX.Element { width: '70vw', maxWidth: '576px', borderRadius: '8px', - boxShadow: 'rgb(224 224 224) 9px 9px 9px -9px', + border: '1px solid #3D3D3D', + boxShadow: '#B1B1B1 9px 9px 9px -9px', }} > diff --git a/packages/web/pages/invite/[inviteCode].tsx b/packages/web/pages/invite/[inviteCode].tsx index 45e23276f..c95a7365d 100644 --- a/packages/web/pages/invite/[inviteCode].tsx +++ b/packages/web/pages/invite/[inviteCode].tsx @@ -17,9 +17,8 @@ export default function InvitePage(): JSX.Element { const router = useRouter() const { viewerData, viewerDataError, isLoading } = useGetViewerQuery() const { inviteCode } = router.query - const [errorMessage, setErrorMessage] = useState( - undefined - ) + const [errorMessage, setErrorMessage] = + useState(undefined) // Check if the user is logged in and display an error message if they are not useEffect(() => { @@ -77,7 +76,8 @@ export default function InvitePage(): JSX.Element { width: '70vw', maxWidth: '576px', borderRadius: '8px', - boxShadow: 'rgb(224 224 224) 9px 9px 9px -9px', + border: '1px solid #3D3D3D', + boxShadow: '#B1B1B1 9px 9px 9px -9px', }} > diff --git a/packages/web/pages/tools/import/file.tsx b/packages/web/pages/tools/import/file.tsx index d49b551b7..44096d324 100644 --- a/packages/web/pages/tools/import/file.tsx +++ b/packages/web/pages/tools/import/file.tsx @@ -96,7 +96,8 @@ export default function ImportUploader(): JSX.Element { width: '70vw', maxWidth: '576px', borderRadius: '8px', - boxShadow: 'rgb(224 224 224) 9px 9px 9px -9px', + border: '1px solid #3D3D3D', + boxShadow: '#B1B1B1 9px 9px 9px -9px', }} > () + const [file, setFile] = useState() + const [type, setType] = useState() + const [uploadState, setUploadState] = useState('none') + const { acceptedFiles, getRootProps, getInputProps } = useDropzone() + + const onDropAccepted = async (acceptedFiles: File[]) => { + const contentType = 'application/zip' + const file = acceptedFiles.find(() => true) + if (!file) { + setErrorMessage('No file selected.') + return + } + + setUploadState('uploading') + + try { + const result = await uploadImportFileRequestMutation( + UploadImportFileType.MATTER, + contentType + ) + + if (result && result.uploadSignedUrl) { + const uploadRes = await fetch(result.uploadSignedUrl, { + method: 'PUT', + body: file, + headers: { + 'content-type': contentType, + 'content-length': `${file.size}`, + }, + }) + setUploadState('completed') + } else { + setErrorMessage( + 'Unable to create file upload. Please ensure you are logged in.' + ) + setUploadState('none') + } + } catch (error) { + console.log('caught error', error) + if (error == 'UPLOAD_DAILY_LIMIT_EXCEEDED') { + setErrorMessage('You have exceeded your maximum daily upload limit.') + } + setUploadState('none') + } + } + + return ( + + + + Import Matter Archive + + + Omnivore supports uploading the Archive.zip file generated by + exporting your data from the Matter app. + + + To export your data from Matter, go to My Account, and choose Export + data, this will send you an email with your data in a file + Archive.zip. Upload that file using the uploader on this page. + + + More info + + + Note: Please note you are limited to three import uploads per a + day, and the maximum file size is 10MB. + + + {uploadState == 'completed' ? ( + + Your upload has completed. Please note that it can take some time + for your library to be updated. You will be sent an email when the + process completes. + + ) : ( + <> + + + {({ getRootProps, getInputProps }) => ( +

+ + + {uploadState == 'uploading' ? ( + + ) : ( + <> + + + Click or Drag Archive.zip file here to upload + + + )} + +
+ )} + + + + )} + + {uploadState == 'completed' && ( + + + + )} + + {errorMessage && ( + {errorMessage} + )} + + + + ) +} From 444d4a69eb7d8293891c25b054c382cc718a8a6c Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 12 Jan 2023 15:37:50 +0800 Subject: [PATCH 21/23] i guess this was my first rodeo --- packages/import-handler/src/index.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/import-handler/src/index.ts b/packages/import-handler/src/index.ts index 262dbf138..ddd8a7beb 100644 --- a/packages/import-handler/src/index.ts +++ b/packages/import-handler/src/index.ts @@ -50,10 +50,7 @@ interface StorageEvent { } function isStorageEvent(event: any): event is StorageEvent { - if ('name' in event && 'bucket' in event && 'contentType' in event) { - return true - } - return false + return 'name' in event && 'bucket' in event && 'contentType' in event } const shouldHandle = (data: StorageEvent) => { From 6f588e217034d2c4dc749e7b861ae682c14bbd9f Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 12 Jan 2023 15:48:54 +0800 Subject: [PATCH 22/23] Create read stream after all early return checks --- packages/import-handler/src/index.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/import-handler/src/index.ts b/packages/import-handler/src/index.ts index ddd8a7beb..d1acf0a9e 100644 --- a/packages/import-handler/src/index.ts +++ b/packages/import-handler/src/index.ts @@ -195,13 +195,6 @@ const contentHandler = async ( const handleEvent = async (data: StorageEvent) => { if (shouldHandle(data)) { - console.log('handling csv data', data) - - const stream = storage - .bucket(data.bucket) - .file(data.name) - .createReadStream() - const handler = handlerForFile(data.name) if (!handler) { console.log('no handler for file:', data.name) @@ -220,6 +213,11 @@ const handleEvent = async (data: StorageEvent) => { return } + const stream = storage + .bucket(data.bucket) + .file(data.name) + .createReadStream() + const ctx = { userId, countImported: 0, From 6845696f23171893d1d02f13be6248b83b172d7b Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 12 Jan 2023 16:10:25 +0800 Subject: [PATCH 23/23] Handle exceptions from JSON.parse, in this case just return ok so the request isnt retried --- packages/import-handler/src/index.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/import-handler/src/index.ts b/packages/import-handler/src/index.ts index d1acf0a9e..3333d56df 100644 --- a/packages/import-handler/src/index.ts +++ b/packages/import-handler/src/index.ts @@ -236,15 +236,26 @@ const handleEvent = async (data: StorageEvent) => { } } +const getStorageEvent = (pubSubMessage: string): StorageEvent | undefined => { + try { + const str = Buffer.from(pubSubMessage, 'base64').toString().trim() + const obj = JSON.parse(str) as unknown + if (isStorageEvent(obj)) { + return obj + } + } catch (err) { + console.log('error deserializing event: ', { pubSubMessage, err }) + } + return undefined +} + export const importHandler = Sentry.GCPFunction.wrapHttpFunction( async (req, res) => { /* eslint-disable @typescript-eslint/no-unsafe-member-access */ if ('message' in req.body && 'data' in req.body.message) { - /* eslint-disable @typescript-eslint/no-unsafe-member-access */ const pubSubMessage = req.body.message.data as string - const str = Buffer.from(pubSubMessage, 'base64').toString().trim() - const obj = JSON.parse(str) as unknown - if (isStorageEvent(obj)) { + const obj = getStorageEvent(pubSubMessage) + if (obj) { await handleEvent(obj) } } else {