diff --git a/packages/content-handler/src/newsletters/cooper-press-handler.ts b/packages/content-handler/src/newsletters/cooper-press-handler.ts new file mode 100644 index 000000000..d0f39f4c2 --- /dev/null +++ b/packages/content-handler/src/newsletters/cooper-press-handler.ts @@ -0,0 +1,38 @@ +import { ContentHandler } from '../content-handler' + +export class CooperPressHandler extends ContentHandler { + constructor() { + super() + this.name = 'cooper-press' + } + + findNewsletterHeaderHref(dom: Document): string | undefined { + const readOnline = dom.querySelectorAll('a') + let res: string | undefined = undefined + readOnline.forEach((e) => { + if (e.textContent === 'Read on the Web') { + res = e.getAttribute('href') || undefined + } + }) + return res + } + + async isNewsletter(input: { + postHeader: string + from: string + unSubHeader: string + dom: Document + }): Promise { + const dom = input.dom + return Promise.resolve( + dom.querySelectorAll('a[href*="cooperpress.com"]').length > 0 + ) + } + + async parseNewsletterUrl( + postHeader: string, + html: string + ): Promise { + return this.findNewsletterUrl(html) + } +} diff --git a/packages/content-handler/test/apple-news-handler.test.ts b/packages/content-handler/test/apple-news-handler.test.ts index 1584f9e28..058ece4f4 100644 --- a/packages/content-handler/test/apple-news-handler.test.ts +++ b/packages/content-handler/test/apple-news-handler.test.ts @@ -1,6 +1,11 @@ import { AppleNewsHandler } from '../src/websites/apple-news-handler' +import nock from 'nock' describe('open a simple web page', () => { + before(() => { + nock('https://apple.news').get('/AxjzaZaPvSn23b67LhXI5EQ').reply(200, '') + }) + it('should return a response', async () => { const response = await new AppleNewsHandler().preHandle( 'https://apple.news/AxjzaZaPvSn23b67LhXI5EQ' diff --git a/packages/content-handler/test/data/node-weekly-newsletter.html b/packages/content-handler/test/data/node-weekly-newsletter.html new file mode 100644 index 000000000..01173902f --- /dev/null +++ b/packages/content-handler/test/data/node-weekly-newsletter.html @@ -0,0 +1,250 @@ + + + + + + + + + + + + +
Plus Node 16.18.0, an IP info database, turning cron expressions into English, and 2FA with Twilio. |
+ + + +
+
+ + + +

#​458 — October 13, 2022

Read on the Web

+ + +
+ + + + + +
Together with  + + Userfront + +
+
+
Node.js Weekly
+
+ +
+ +
+ +

njt: Quick Navigation to npm Package Resources — Provides a rapid way to jump to various destinations related to npm packages (such as a project’s homepage, repo, issues, or even a package cost estimation). You can install it for use in your terminal, as a Chrome or Firefox search, via VS Code’s command palette (via LaunchX) or you can even use it directly on the Web here. – GitHub repo.

+

Alexander Kachkaev

+
+ +
+ +

Knip: Find Unused Files, Dependencies and Exports in TypeScript Projects — Knip’s creator tells us it’s Dutch for “cut” which is quite appropriate as it’s a new tool for trimming away things that aren’t being used in your project. If you just want to compare it to similar existing tools, there’s a handy comparison chart.

+

Lars Kappert

+
+ +
+ +

Node Authentication, Simplified — In this article, we lay out a new approach to authentication (plus access control & SSO) in Node.js applications.

+

Userfront sponsor

+
+ +
+ +

Node v16.18.0 (LTS) Released — Largely backported fixes and tweaks – no big headlines here.

+

Juan José (Node Core Team)

+
+ +
+ +

How to Write CommonJS Exports That Can Be Name-Imported from ESM — If you’ve ever got tangled up between using CommonJS and ES modules (I sure have!) Dr. Axel clears up a key cross-compatibility concern here.

+

Dr. Axel Rauschmayer

+
+ +
+ +

Adding Observability to Jest Tests — A look at how to get a bit more out of your Jest-based testing by keeping an eye on things.

+

Eliran Maman (Sprkl)

+
+ +
+ +

🔐  Node.js Authentication with Twilio Verify — If you’re happy using a third party service, bringing two-factor auth into your Express.js app needn’t be too hard. The author demonstrates the creation of a simple app that authenticates users using password-based authentication with an extra layer of OTPs (One-Time Passcodes) powered by Twilio’s Verify service.

+

Alexander Godwin

+
+
+ +

🛠 Code & Tools

+
+ +
+ +
+ +

IP Index: A Fast IP Lookup Web Service + Library — Returns blacklist status, detects VPN/hosting and shows geo and ASN info. The repo gets updated every day too.

+

Mykhailo Gorianskyi

+
+ +
+ +

cRonstrue: Library to Convert cron Expressions into Human Readable Form — Love the project name! The idea is given something like */10 * * * *, it will return “Every 10 minutes”. No dependencies.

+

Brady Holt

+
+ +
+ +

Dynaboard: The Pro-Code Web App Builder Made for Developers — Build high performance public and private web apps in a collaborative — code forward — WYSIWYG environment.

+

Dynaboard sponsor

+
+ +
+ +

Whoiser: A WHOIS Client for Node.js — Given a domain name, TLD, or IP address, it queries online WHOIS databases for info.

+

Andrei Igna

+
+ +
+ +

Print Ready: A JS-Powered CLI for Converting HTML Into PDFs — Uses Paged.js to render your HTML file inside Puppeteer, then exports a PDF from Puppeteer.

+

Nicholas C. Zakas

+
+ +
+ +

Check HTML Links: A Fast Checker for Broken Links/References in HTML — An npm package you can run on static pages to find broken links in href, src, and srcset, and can process 500-1000 documents in seconds.

+

Modern Web

+
+ +
+ +

human-signals: Human-Friendly Process Signal Info — Basically a JavaScript object that contains info about the various POSIX signals (SIGHUP, SIGINT, et al.)

+

ehmicky

+
+ +
+ +

Need to Upgrade Your Node.js App? Hire Us to Do It for You

+

UpgradeJS․com - The JS Upgrade Service by OmbuLabs sponsor

+
+ +
+

Flyweight: A Brand New ORM for SQLite — Early days but provides some extra abstraction around SQLite you might appreciate. +
Andrew Jones +

+
+
+
    +
  • +

    AdminJS 6.4
    + ↳ Admin panel / UI for Node apps.

    +
  • +
  • +

    Faker 7.6
    + ↳ Generate large amounts of fake data.

    +
  • +
  • +

    Middy 3.6
    + ↳ Node middleware engine for AWS Lambda.

    +
  • +
  • +

    quagga2 1.7.5
    + ↳ Advanced barcode scanning for browser and Node.

    +
  • +
  • +

    node-jira-client 8.2
    + ↳ Node wrapper for Jira's REST API.

    +
  • +
  • +

    RedisSMQ 7.1.1
    + ↳ High-performance Redis message queue.

    +
  • +
+
+
+ +

💻 Jobs

+ +
+

Full-Stack Engineer (NYC / Remote) — 100M+ devices, 100B+ API calls. Radar is looking for Product Engineers to build geospatial dev tools. +
Radar +

+
+ +
+

Find Tech Jobs with Hired — Create a profile on Hired to connect with hiring managers at growing startups and Fortune 500 companies. It's free for job-seekers. +
Hired +

+
+
+
+
+ +
+
+ + +n + diff --git a/packages/content-handler/test/newsletter.test.ts b/packages/content-handler/test/newsletter.test.ts index d4a1f2562..11777c420 100644 --- a/packages/content-handler/test/newsletter.test.ts +++ b/packages/content-handler/test/newsletter.test.ts @@ -15,6 +15,7 @@ import { BeehiivHandler } from '../src/newsletters/beehiiv-handler' import { ConvertkitHandler } from '../src/newsletters/convertkit-handler' import { parseHTML } from 'linkedom' import { GhostHandler } from '../src/newsletters/ghost-handler' +import { CooperPressHandler } from '../src/newsletters/cooper-press-handler' chai.use(chaiAsPromised) chai.use(chaiString) @@ -179,6 +180,18 @@ describe('Newsletter email test', () => { }) ).to.eventually.be.true }) + it('returns true for node-weekly newsletter', async () => { + const html = load('./test/data/node-weekly-newsletter.html') + const dom = parseHTML(html).document + await expect( + new CooperPressHandler().isNewsletter({ + dom, + postHeader: '', + from: '', + unSubHeader: '', + }) + ).to.eventually.be.true + }) }) describe('findNewsletterUrl', async () => { @@ -188,16 +201,13 @@ describe('Newsletter email test', () => { .head( '/c/eJxNkk2TojAQhn-N3KTyQfg4cGDGchdnYcsZx9K5UCE0EMVAkTiKv36iHnarupNUd7rfVJ4W3EDTj1M89No496Uw0wCxgovuwBgYnbOGsZBVjDHzKPWYU8VehUMWOlIX9Qhw4rKLzXgGZziXnRTcyF7dK0iIGMVOG_OS1aTmKPRDilgVhTQUPCQIcE0x-MFTmJ8rCUpA3KtuenR2urg1ZtAzmszI0tq_Z7m66y-ilQo0uAqMTQ7WRX8auJKg56blZg7WB-iHDuYEBzO6NP0R1IwuYFphQbbTjnTH9NBfs80nym4Zyj8uUvyKbtUyGr5eUz9fNDQ7JCxfJDo9dW1lY9lmj_JNivPbGmf2Pt_lN9tDit9b-WeTetni85Z9pDpVOd7L1E_Vy7egayNO23ZP34eSeLJeux1b0rer_xaZ7ykS78nuSjMY-nL98rparNZNcv07JCjN06_EkTFBxBqOUMACErnELUNMSxTUjLDQZwzcqa4bRjCfeejUEFefS224OLr2S5wxPtij7lVrs80d2CNseRV2P52VNFMBipcdVE-U5jkRD7hFAwpGOylVwU2Mfc9qBh7DoR89yVnWXhgQFHnIsbpVb6tU_B-hH_2yzWY' ) - .reply(302, undefined, { + .reply(301, undefined, { Location: 'https://newsletter.slowchinese.net/p/companies-that-eat-people-217', }) - nock('https://newsletter.slowchinese.net') - .head('/p/companies-that-eat-people-217') - .reply(200, '') - }) - after(() => { - nock.restore() + nock('https://newsletter.slowchinese.net') + .head('/p/companies-that-eat-people-217') + .reply(200, '') }) it('gets the URL from the header', async () => { @@ -216,24 +226,21 @@ describe('Newsletter email test', () => { .head( '/ss/c/AX1lEgEQaxtvFxLaVo0GBo_geajNrlI1TGeIcmMViR3pL3fEDZnbbkoeKcaY62QZk0KPFudUiUXc_uMLerV4nA/3k5/3TFZmreTR0qKSCgowABnVg/h30/zzLik7UXd1H_n4oyd5W8Xu639AYQQB2UXz-CsssSnno' ) - .reply(302, undefined, { + .reply(301, undefined, { Location: 'https://www.milkroad.com/p/talked-guy-spent-30m-beeple', }) - .get('/p/talked-guy-spent-30m-beeple') + nock('https://www.milkroad.com') + .head('/p/talked-guy-spent-30m-beeple') .reply(200, '') }) - after(() => { - nock.restore() - }) - it('gets the URL from the header', async () => { const html = load('./test/data/beehiiv-newsletter.html') const url = await new BeehiivHandler().findNewsletterUrl(html) expect(url).to.startWith( 'https://www.milkroad.com/p/talked-guy-spent-30m-beeple' ) - }).timeout(10000) + }) }) context('when email is from convertkit', () => { @@ -242,22 +249,19 @@ describe('Newsletter email test', () => { .head( '/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7wDTJvdVU1ACmSJ753YuhScf71JWthxqM8RnVh-2FZG0rYzrbR04P99S2ld2OkTtQmrx2FDwArpYdk5N0jVpN9dLBZ-2BdPNqkRHxNvuygY8-2F-2FtRNFoPjxjtTuyWM6L3tcYDYnAnL2xCueddWcFlUNrQWsvLotmgvC-2BrQc7bxsZhW0pUBmS_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nzSBnnaDN-2FNHWDodWnbUOPZ063v3w3z8QtcaPpE1qNu8xYkNJJFb-2F1uZEG-2BzsLfyDkjvvVX5zYs5OyyRYlhMOlXDJcr4-2FtMrFwii0uFAvwbhxDdnTxEpi-2F7maufyH39AEO-2BtCeSUg5V4FM43UpI1zUSXeWK-2Fh5JumSmR5XhrrRAig-3D-3D' ) - .reply(302, undefined, { + .reply(301, undefined, { Location: 'https://fs.blog/brain-food/october-16-2022/', }) - .get('/brain-food/october-16-2022/') + nock('https://fs.blog') + .head('/brain-food/october-16-2022/') .reply(200, '') }) - after(() => { - nock.restore() - }) - it('gets the URL from the header', async () => { const html = load('./test/data/convertkit-newsletter.html') const url = await new ConvertkitHandler().findNewsletterUrl(html) expect(url).to.startWith('https://fs.blog/brain-food/october-16-2022/') - }).timeout(10000) + }) }) it('returns undefined if it is not a newsletter', async () => { @@ -272,22 +276,36 @@ describe('Newsletter email test', () => { .head( '/ls/click?upn=MnmHBiCwIPe9TmIJeskmA9nRLefEmmgrd5xWS-2Bc39wxPBpwDRny1FmWt1H0FpgKAz1dv_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nzulL-2F3YyC-2FHgJV0JnOPtjNvgjHSaQVfisQ15hPQtnlo4t73zgTQL4QnDoer4qJ3-2F2Lf-2F2ElFMF3NyoUD4eqWCWwUM0w4P9Feaeo-2BolkySAB611BySXRt6V3Z-2F7mQcpcRX3D9zV-2B-2FdRY0Vn30aR-2BKY8qpTFuivxzF19UkQGjK5srg-3D-3D' ) - .reply(302, undefined, { + .reply(301, undefined, { Location: 'https://www.openml.fyi/2022-10-14/', }) - .get('/2022-10-14/') - .reply(200, '') - }) - - after(() => { - nock.restore() + nock('https://www.openml.fyi').head('/2022-10-14/').reply(200, '') }) it('gets the URL from the header', async () => { const html = load('./test/data/ghost-newsletter.html') const url = await new GhostHandler().findNewsletterUrl(html) expect(url).to.startWith('https://www.openml.fyi/2022-10-14/') - }).timeout(10000) + }) + }) + + context('when email is from cooper press', () => { + before(() => { + nock('https://u25184427.ct.sendgrid.net') + .head( + '/ls/click?upn=MnmHBiCwIPe9TmIJeskmA7mFdqmsIs-2B5Xs-2FNpSIs56o0z9xhskaXR4aYohHPLtwRHfml_vVXscVLXlj5UtQe3aqo5RMTdTq2PepdZjP86UOmA8nxtQVfuqJiLh7Fio3fEtt5ouN4IH56AfszUQpxY-2FQ233kp0bjSZhBBVWAB43dgKumQkDW-2BxDFnQIUpvhmEgzSJq-2FMRG00GM7fkZVuPU-2BX8cdg8AGRHUU9Qhw6W67XEMkJVygTdm70Mo9ypNi8N33hgmhM3F6un9s7p1K1Gq-2FunslA-3D-3D' + ) + .reply(301, undefined, { + Location: 'https://nodeweekly.com/issues/458', + }) + nock('https://nodeweekly.com').head('/issues/458').reply(200, '') + }) + + it('gets the URL from the header', async () => { + const html = load('./test/data/node-weekly-newsletter.html') + const url = await new CooperPressHandler().findNewsletterUrl(html) + expect(url).to.startWith('https://nodeweekly.com/issues/458') + }) }) })