mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #1379 from omnivore-app/fix/newsletters
fix/newsletters
This commit is contained in:
commit
b7ec6ac5a8
7 changed files with 729 additions and 6 deletions
|
|
@ -27,6 +27,7 @@ import { RevueHandler } from './newsletters/revue-handler'
|
|||
import { GhostHandler } from './newsletters/ghost-handler'
|
||||
import { parseHTML } from 'linkedom'
|
||||
import { CooperPressHandler } from './newsletters/cooper-press-handler'
|
||||
import { HeyWorldHandler } from './newsletters/hey-world-handler'
|
||||
|
||||
const validateUrlString = (url: string) => {
|
||||
const u = new URL(url)
|
||||
|
|
@ -75,6 +76,7 @@ const newsletterHandlers: ContentHandler[] = [
|
|||
new RevueHandler(),
|
||||
new GhostHandler(),
|
||||
new CooperPressHandler(),
|
||||
new HeyWorldHandler(),
|
||||
]
|
||||
|
||||
export const preHandleContent = async (
|
||||
|
|
|
|||
|
|
@ -27,11 +27,21 @@ export class ConvertkitHandler extends ContentHandler {
|
|||
dom: Document
|
||||
}): Promise<boolean> {
|
||||
const dom = input.dom
|
||||
return Promise.resolve(
|
||||
dom.querySelectorAll(
|
||||
'img[src*="convertkit.com"], img[src*="convertkit-mail"]'
|
||||
).length > 0
|
||||
const icons = dom.querySelectorAll(
|
||||
'img[src*="convertkit.com"], img[src*="convertkit-mail"]'
|
||||
)
|
||||
if (icons.length === 0) {
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
// ignore newsletters that have a confirmation link to the newsletter in the body
|
||||
const links = dom.querySelectorAll(
|
||||
'a[href*="convertkit.com"], a[href*="convertkit-mail"]'
|
||||
)
|
||||
const isConfirmation = Array.from(links).some((e) => {
|
||||
return e.textContent === 'Confirm your subscription'
|
||||
})
|
||||
|
||||
return Promise.resolve(!isConfirmation)
|
||||
}
|
||||
|
||||
async parseNewsletterUrl(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
import { ContentHandler } from '../content-handler'
|
||||
|
||||
export class HeyWorldHandler extends ContentHandler {
|
||||
constructor() {
|
||||
super()
|
||||
this.name = 'hey-world'
|
||||
this.senderRegex = /<.+@world.hey.com>/
|
||||
}
|
||||
|
||||
findNewsletterHeaderHref(dom: Document): string | undefined {
|
||||
const readOnline = dom.querySelectorAll('a')
|
||||
let res: string | undefined = undefined
|
||||
readOnline.forEach((e) => {
|
||||
if (e.textContent === 'View this post online') {
|
||||
res = e.getAttribute('href') || undefined
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
async parseNewsletterUrl(
|
||||
postHeader: string,
|
||||
html: string
|
||||
): Promise<string | undefined> {
|
||||
return this.findNewsletterUrl(html)
|
||||
}
|
||||
}
|
||||
268
packages/content-handler/test/data/convertkit-confirmation.html
Normal file
268
packages/content-handler/test/data/convertkit-confirmation.html
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<style>
|
||||
email-button:hover {
|
||||
background-color: #0875c1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body
|
||||
style="
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
"
|
||||
>
|
||||
<div style="background-color: #efeff3">
|
||||
<table
|
||||
role="presentation"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
class="email"
|
||||
style="
|
||||
border-collapse: separate;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
background-color: #efeff3;
|
||||
width: 100%;
|
||||
"
|
||||
bgcolor="#efeff3"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="vertical-align: top">
|
||||
<div
|
||||
class="email-container"
|
||||
style="
|
||||
margin: 0 auto;
|
||||
padding: 30px 0;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
max-width: 100%;
|
||||
padding-top: 30px;
|
||||
padding-left: 20px;
|
||||
padding-bottom: 30px;
|
||||
padding-right: 20px;
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
"
|
||||
>
|
||||
<!--[if mso]><center><table cellpadding="0" cellspacing="0" align="center" style="" width="600" bgcolor="#ffffff"><tr><td><![endif]-->
|
||||
<div
|
||||
style="
|
||||
background-color: #ffffff;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
max-width: 600;
|
||||
margin: 0px auto;
|
||||
"
|
||||
>
|
||||
<table
|
||||
bgcolor="#ffffff"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
width="100%"
|
||||
style="
|
||||
border-collapse: separate;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="vertical-align: top; padding: 20px">
|
||||
<div
|
||||
class="email-body"
|
||||
style="background-color: #ffffff"
|
||||
>
|
||||
<div class="email-content">
|
||||
<p
|
||||
style="
|
||||
margin: 1em 0;
|
||||
font-size: 18px;
|
||||
line-height: 1.5em;
|
||||
font-family: Georgia, Times, 'Times New Roman',
|
||||
serif;
|
||||
color: #3d3d3d;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
"
|
||||
>
|
||||
Thanks for signing up for the
|
||||
<em>Weekend Upgrade</em> newsletter! Click the
|
||||
link below to confirm your subscription and
|
||||
you'll be on your way.
|
||||
</p>
|
||||
<table
|
||||
maxwidth="100"
|
||||
width="100%"
|
||||
style="
|
||||
border-collapse: separate;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
align="left"
|
||||
style="vertical-align: top"
|
||||
>
|
||||
<a
|
||||
class="email-button"
|
||||
href="https://click.convertkit-mail2.com/d0uo2xqod0a5fm76qdtm/reh8hohqg8ogd5i2/aHR0cHM6Ly9hcHAuY29udmVydGtpdC5jb20vZm9ybXMvY29uZmlybT9rZXk9MTY0OTkzZWJlM2ViY2Y2YWMxMjFiMjU0M2M2ODEwNzUwNGRlOGY4ZSZzaWQ9NTIwOTgzNTkzNg=="
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style="
|
||||
color: #0875c1;
|
||||
margin-bottom: 0.5em;
|
||||
border: 0 none;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
background-color: #0875c1;
|
||||
color: #ffffff;
|
||||
border-radius: 4px;
|
||||
color: #ffffff;
|
||||
display: inline-block;
|
||||
background: #d71c1c;
|
||||
padding: 6px 12px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
margin: 6px 0;
|
||||
"
|
||||
url-id="1496943150"
|
||||
>Confirm your subscription</a
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p
|
||||
style="
|
||||
margin: 1em 0;
|
||||
font-size: 18px;
|
||||
line-height: 1.5em;
|
||||
font-family: Georgia, Times, 'Times New Roman',
|
||||
serif;
|
||||
color: #3d3d3d;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
"
|
||||
>
|
||||
It's good to have you!
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="email-body-footer"
|
||||
style="
|
||||
border-top: 1px dotted #888;
|
||||
padding: 20px;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
color: #aaa;
|
||||
font-size: 12px;
|
||||
"
|
||||
>
|
||||
<p
|
||||
style="
|
||||
margin: 1em 0;
|
||||
font-size: 18px;
|
||||
line-height: 1.5em;
|
||||
font-family: Georgia, Times, 'Times New Roman',
|
||||
serif;
|
||||
color: #3d3d3d;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
<a
|
||||
href="https://unsubscribe.convertkit-mail2.com/d0uo2xqod0a5fm76qdtm"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style="color: #0875c1"
|
||||
>Unsubscribe</a
|
||||
>
|
||||
|
|
||||
<a
|
||||
href="https://preferences.convertkit-mail2.com/d0uo2xqod0a5fm76qdtm"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style="color: #0875c1"
|
||||
>Update your profile</a
|
||||
>
|
||||
| 714 Venture Dr. #185, Morgantown, WV 26508
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="built-with"
|
||||
style="
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
"
|
||||
>
|
||||
<a
|
||||
href="https://click.convertkit-mail2.com/d0uo2xqod0a5fm76qdtm/l2hehmhkgg99mnb6/aHR0cHM6Ly9jb252ZXJ0a2l0LmNvbS9mZWF0dXJlcy9lbWFpbC1tYXJrZXRpbmc_dXRtX2NhbXBhaWduPXBvd2VyZWRieSZ1dG1fY29udGVudD1lbWFpbCZ1dG1fbWVkaXVtPXJlZmVycmFsJnV0bV9zb3VyY2U9ZHluYW1pYw=="
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
style="
|
||||
color: #0875c1;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
"
|
||||
url-id="1337634290"
|
||||
><img
|
||||
src="https://cdn.convertkit.com/assets/light-built-with-badge.png"
|
||||
alt="Built with ConvertKit"
|
||||
style="
|
||||
display: block;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
max-width: 100%;
|
||||
border: 0 none;
|
||||
width: 190px;
|
||||
height: 36px;
|
||||
"
|
||||
width="190"
|
||||
height="36"
|
||||
/></a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso]></td></tr></table></center><![endif]-->
|
||||
</div>
|
||||
</td>
|
||||
<td style="vertical-align: top"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- -->
|
||||
<img
|
||||
src="https://open.convertkit-mail2.com/d0uo2xqod0a5fm76qdtm"
|
||||
alt=""
|
||||
style="
|
||||
border: 0 none;
|
||||
display: block;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
max-width: 100%;
|
||||
"
|
||||
/>
|
||||
</body>
|
||||
</html>
|
||||
274
packages/content-handler/test/data/hey-world-newsletter.html
Normal file
274
packages/content-handler/test/data/hey-world-newsletter.html
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
id="viewport"
|
||||
content="width=device-width,minimum-scale=1.0,maximum-scale=10.0,initial-scale=1.0"
|
||||
/>
|
||||
|
||||
<style>
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.3em;
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
}
|
||||
ul,
|
||||
ol {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
ul li,
|
||||
ol li,
|
||||
li li {
|
||||
margin: 0 0 0 36px;
|
||||
}
|
||||
[dir='rtl'] li {
|
||||
margin: 0 18px 0 0;
|
||||
}
|
||||
blockquote {
|
||||
border-color: #dfdee1;
|
||||
border-style: solid;
|
||||
border-width: 0 0 0 1px;
|
||||
margin: 0;
|
||||
padding: 0 0 0 1em;
|
||||
}
|
||||
[dir='rtl'] blockquote,
|
||||
blockquote[dir='rtl'] {
|
||||
border-width: 0 1px 0 0;
|
||||
padding: 0 1em 0 0;
|
||||
}
|
||||
pre {
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo,
|
||||
Courier, monospace;
|
||||
font-size: 0.9em;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
background-color: #f6f5f3;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow: visible;
|
||||
}
|
||||
.message-content {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol';
|
||||
line-height: 1.4;
|
||||
}
|
||||
.attachment {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.attachment__caption {
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.attachment__caption a[href] {
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
}
|
||||
.attachment--preview {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin: 0.625em 0;
|
||||
}
|
||||
.attachment--preview img {
|
||||
border: 1px solid #dfdee1;
|
||||
vertical-align: middle;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
max-height: 640px;
|
||||
}
|
||||
.attachment--preview .attachment__caption {
|
||||
color: #716d7b;
|
||||
font-size: 0.85em;
|
||||
margin-top: 0.625em;
|
||||
}
|
||||
.attachment--file {
|
||||
color: #282138;
|
||||
line-height: 1;
|
||||
margin: 0 2px 2px 0;
|
||||
padding: 0.4em 1em;
|
||||
border: 1px solid #dfdee1;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.permalink {
|
||||
color: inherit;
|
||||
}
|
||||
.txt--xx-small {
|
||||
font-size: 14px;
|
||||
}
|
||||
.flush {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.push--bottom {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.border--top {
|
||||
border-top: 1px solid #ece9e6;
|
||||
}
|
||||
.btn {
|
||||
padding: 0.2em 0.4em;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
border-radius: 3rem;
|
||||
white-space: nowrap;
|
||||
background: #5522fa;
|
||||
border-color: #5522fa;
|
||||
color: #fff;
|
||||
}
|
||||
.btn--email {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
font-size: 1em;
|
||||
text-decoration: none;
|
||||
border-radius: 2em;
|
||||
white-space: nowrap;
|
||||
background: #5522fa;
|
||||
border-color: #5522fa;
|
||||
color: #fff;
|
||||
border-top: 0.3em solid #5522fa;
|
||||
border-left: 1em solid #5522fa;
|
||||
border-bottom: 0.3em solid #5522fa;
|
||||
border-right: 1em solid #5522fa;
|
||||
}
|
||||
.shaded {
|
||||
padding: 1em;
|
||||
border-radius: 4px;
|
||||
background-color: #f6f5f3;
|
||||
border: 1px solid #dfdee1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="message-content">
|
||||
<div class="trix-content">
|
||||
<div>
|
||||
With Twitter under new ownership, it's a great time for us to imagine
|
||||
how this hellscape might actually get fixed. And by fixed, I mean be a
|
||||
place where people enjoy hanging out, where the debate over content
|
||||
moderation is settled, and where Elon Musk might make his money back.
|
||||
Easy peasy then!<br /><br />To start, here's my fundamental premise:
|
||||
The internet is full of the bland, the wonderful, the obscure, the
|
||||
sacrilegious, the offensive, the infuriating, and the nasty. The
|
||||
internet, as a whole, is governed by local laws on speech, not by a
|
||||
council of global content moderators, not by single individuals. This
|
||||
is a feature not a bug.<br /><br />An American can sue another
|
||||
American under US defamation laws for stuff posted on the internet.
|
||||
But the Thai government won't get an American extradited under its
|
||||
lèse-majesté laws if that person offends the country's monarch on the
|
||||
internet. Restrictions on speech are based on jurisdictions (and
|
||||
Twitter is thankfully liable in one of most liberal speech
|
||||
jurisdictions around). This is also a feature not a bug.<br /><br />Twitter
|
||||
should embrace these features, and accept its role as a fundamental
|
||||
protocol. Just like HTTP, just like SMTP, just like FTP. All protocols
|
||||
that can be used to transmit all sorts of disagreeable content, but
|
||||
where the moderation of said content is left to individuals, not some
|
||||
all-knowing overlords.<br /><br />This isn't possible today because
|
||||
Twitter shoves so much speech in front of you that you never asked to
|
||||
see. It allows strangers to invade popular threads with no protections
|
||||
available to the original author.<br /><br />You know that you can
|
||||
find the most vile, nasty shit on the internet if you search for the
|
||||
right words on Google. So most people just don't do that! They manage
|
||||
their own exposure by moderating their own behavior. This is again a
|
||||
feature, not a bug.<br /><br />This could be Twitter!<br /><br />If
|
||||
Twitter stopped shoving all this speech you never asked to see in
|
||||
front of you, there would be no need for an army of content
|
||||
moderators. The internet at large is not filtered through such an
|
||||
army, and it's all the better for it. Governments keep trying to
|
||||
impose such a filter, but so far only the most authoritarian regimes
|
||||
have succeeded (and up until a few minutes ago, there was broad
|
||||
consensus in the Western world that this was bad).<br /><br />This
|
||||
would mean that Twitter would have to change in a few fundamental
|
||||
ways:<br /><br />
|
||||
</div>
|
||||
<ol>
|
||||
<li>
|
||||
Only speech from people you've asked to follow should appear in your
|
||||
feed. If any of those people post something disagreeable to your
|
||||
tastes, you can moderate that content by unfollowing them.
|
||||
</li>
|
||||
<li>
|
||||
Authors should be deputized as content moderators of their own
|
||||
posts. Just like someone hosting a blog with comments on the
|
||||
internet is. You can either say you don't want comments at all, that
|
||||
comments need to be pre-approved, that you'll remove the bad ones,
|
||||
or that you'll just let it flow.
|
||||
</li>
|
||||
<li>
|
||||
All the promoted tweets that Twitter wants to showcase to the world
|
||||
should pass through a positive rather than negative editorial
|
||||
process. Meaning Twitter employees pick what they like to feature
|
||||
rather than ban what they don't.
|
||||
</li>
|
||||
</ol>
|
||||
<div>
|
||||
<br />Thus, like the internet at large, everyone gets to control the
|
||||
kind of speech they're exposed to. Different people will come to
|
||||
different conclusions on the kind speech they wish to be exposed to.
|
||||
This is the fundamental wisdom of the infamous Section 230 protection
|
||||
that has allowed the internet as we know it to exist.<br /><br />This
|
||||
curation should be something you can outsource to trusted delegates.
|
||||
Let me experience Twitter as a list of authors and threads maintained
|
||||
by someone I trust.<br /><br />It'll also mean, like the internet at
|
||||
large, that you'll be able to find Bad Words, Terrible Words, No Good
|
||||
Words, if you deliberately seek them out. Twitter will serve as a
|
||||
portal to this in the same way Google does. These words may be hurtful
|
||||
or harmful.<br /><br />But all the content moderators in the world
|
||||
couldn't put the utopia that a platform for hundreds of millions won't
|
||||
contain disagreeable speech together again. <br /><br />Accept that
|
||||
reality. Accept that the price we pay for an open, free internet is
|
||||
one which permits the darker sides of humanity. Lean on the local
|
||||
jurisdictions and laws, democratically controlled as they are, to
|
||||
police this reality. Like we've done with that beautiful bastard that
|
||||
is the internet.<br /><br />Let Twitter be free to ascend to sit
|
||||
amongst all the other fundamental protocols that constitutes the
|
||||
pantheon of the internet.<br /><br />You can do it, Mr Musk!
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<hr class="border--top" />
|
||||
|
||||
<p class="txt--xx-small flush push--bottom">
|
||||
<a
|
||||
class="permalink"
|
||||
href="https://world.hey.com/dhh/here-s-how-to-fix-twitter-79632ecb"
|
||||
>View this post online</a
|
||||
>
|
||||
</p>
|
||||
<p class="txt--xx-small flush push--bottom">
|
||||
You’re subscribed to
|
||||
<a class="permalink" href="https://world.hey.com/dhh"
|
||||
>David Heinemeier Hansson</a
|
||||
>
|
||||
on
|
||||
<a class="permalink" href="https://hey.com/world?source=hw-email"
|
||||
>HEY World</a
|
||||
>
|
||||
</p>
|
||||
<p class="txt--xx-small flush push--bottom">
|
||||
Don’t want these posts anymore?
|
||||
<a
|
||||
class="permalink"
|
||||
href="https://world.hey.com/dhh/subscribers/MtuoW9TvSJK9o5c7ohB72V2s/unsubscribe"
|
||||
>Unsubscribe</a
|
||||
>
|
||||
</p>
|
||||
<p class="txt--xx-small flush push--bottom">
|
||||
HEY: PO Box 803338, PMB 53289, Chicago, Illinois 60680
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
<div dir="ltr"><br><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">---------- Forwarded message ---------<br>From: <strong class="gmail_sendername" dir="auto">Tomasz Tunguz</strong> <span dir="auto"><<a href="mailto:blog@tomtunguz.com">blog@tomtunguz.com</a>></span><br>Date: Fri, 28 Oct 2022 at 19:15<br>Subject: Grey Skies in Cloud Earnings<br>To: <<a href="mailto:martin@doversten.se">martin@doversten.se</a>><br></div><br><br><div class="msg5509635934175298552"><u></u>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div style="margin:0;padding:0">
|
||||
<div style="background-color:#aaaaaa"><table role="presentation" cellpadding="0" cellspacing="0" style="border-collapse:separate;background-color:#ffffff;width:100%" bgcolor="#aaaaaa"><tbody><tr>
|
||||
<td style="vertical-align:top"><div style="margin:0 auto;padding:45px 0 30px;padding-left:20px;padding-right:20px;max-width:600px;padding-top:30px;padding-left:20px;padding-bottom:30px;padding-right:20px;margin:0 auto;max-width:600px">
|
||||
<div style="border-left:10px;border-style:solid;border-top:none;border-right:none;border-bottom:none;padding:3px 20px 1px 20px;margin:0 0 15px 0;border-color:#6495ed">
|
||||
<h1 style="font-weight:bold;font-style:normal;font-size:1em;margin:0;font-size:2em;font-weight:700;font-family:Arial,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',sans-serif;color:#281842;line-height:1.5">Tomasz Tunguz</h1>
|
||||
<p style="margin:1em 0;font-size:18px;line-height:1.5em;font-family:Arial,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',sans-serif;color:#4e4e4e;font-size:16px;line-height:1.5;text-align:left">Venture Capitalist at Redpoint</p>
|
||||
</div>
|
||||
<div><div style="padding:20px 30px">
|
||||
<p style="margin:1em 0;font-size:18px;line-height:1.5em;font-family:Arial,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',sans-serif;color:#4e4e4e;font-size:16px;line-height:1.5"><span style="color:#ababab"><em>If you were forwarded this newsletter, and you'd like to receive it in the future, </em></span><a href="https://click.convertkit-mail2.com/68ugeo9lmxa8hkwqe7dbo/dpheh0hql2pz8zam/aHR0cHM6Ly90dHVuZ3V6LmNrLnBhZ2UvYjcxZmQ5YTNkZA==" rel="noopener noreferrer" style="font-weight:bold;color:#000000" target="_blank"><span style="color:#ababab"><em>subscribe here.</em></span></a><span style="color:#ababab"><em></em></span></p>
|
||||
<h1 style="font-weight:bold;font-style:normal;font-size:1em;margin:0;font-size:2em;font-weight:700;font-family:Arial,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',sans-serif;color:#281842;line-height:1.5"><a href="https://click.convertkit-mail2.com/68ugeo9lmxa8hkwqe7dbo/e0hph7hk8dr9qpu8/aHR0cHM6Ly93d3cudG9tdHVuZ3V6LmNvbS9xMzIwMjItY2xvdWQtZWFybmluZ3Mv" rel="noopener noreferrer" style="font-weight:bold;color:#000000" target="_blank">Grey Skies in Cloud Earnings</a></h1>
|
||||
<p style="margin:1em 0;font-size:18px;line-height:1.5em;font-family:Arial,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',sans-serif;color:#4e4e4e;font-size:16px;line-height:1.5">
|
||||
</p>
|
||||
<p style="margin:1em 0;font-size:18px;line-height:1.5em;font-family:Arial,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',sans-serif;color:#4e4e4e;font-size:16px;line-height:1.5">I’m watching <a href="https://click.convertkit-mail2.com/68ugeo9lmxa8hkwqe7dbo/7qh7h8h0rxnvw6iz/aHR0cHM6Ly90b210dW5ndXouY29tL3RoZS1pbmRleC1vZi1zb2Z0d2FyZS1idXllcnMv" style="font-weight:bold;color:#000000" target="_blank">public company earnings to identify early weaknesses in the software market</a>. This week Microsoft, Google/ Alphabet, & Amazon reported their third quarter figures.</p>
|
||||
<table style="border-collapse:separate">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Company</th>
|
||||
<th style="text-align:right">Q-4 CAGR</th>
|
||||
<th style="text-align:right">Q-4 CAGR</th>
|
||||
<th style="text-align:right">Q-3 CAGR</th>
|
||||
<th style="text-align:right">Q-2 CAGR</th>
|
||||
<th style="text-align:right">Q-1 CAGR</th>
|
||||
<th style="text-align:right">Q0 CAGR</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="vertical-align:top">Microsoft Azure</td>
|
||||
<td style="vertical-align:top;text-align:right">50%</td>
|
||||
<td style="vertical-align:top;text-align:right">51%</td>
|
||||
<td style="vertical-align:top;text-align:right">46%</td>
|
||||
<td style="vertical-align:top;text-align:right">46%</td>
|
||||
<td style="vertical-align:top;text-align:right">40%</td>
|
||||
<td style="vertical-align:top;text-align:right">35%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="vertical-align:top">Google Cloud Platform</td>
|
||||
<td style="vertical-align:top;text-align:right">46%</td>
|
||||
<td style="vertical-align:top;text-align:right">54%</td>
|
||||
<td style="vertical-align:top;text-align:right">45%</td>
|
||||
<td style="vertical-align:top;text-align:right">51%</td>
|
||||
<td style="vertical-align:top;text-align:right">35%</td>
|
||||
<td style="vertical-align:top;text-align:right">38%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="vertical-align:top">Amazon Web Services</td>
|
||||
<td style="vertical-align:top;text-align:right">37%</td>
|
||||
<td style="vertical-align:top;text-align:right">39%</td>
|
||||
<td style="vertical-align:top;text-align:right">40%</td>
|
||||
<td style="vertical-align:top;text-align:right">40%</td>
|
||||
<td style="vertical-align:top;text-align:right">33%</td>
|
||||
<td style="vertical-align:top;text-align:right">27%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p style="margin:1em 0;font-size:18px;line-height:1.5em;font-family:Arial,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',sans-serif;color:#4e4e4e;font-size:16px;line-height:1.5">The meaningful decline in growth rates started four quarters ago is hard to ignore.</p>
|
||||
<p style="margin:1em 0;font-size:18px;line-height:1.5em;font-family:Arial,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',sans-serif;color:#4e4e4e;font-size:16px;line-height:1.5"><img src="https://res.cloudinary.com/dzawgnnlr/image/upload/w_700,c_scale/co68b8xfjofqhgyw2ome.png" alt="image" style="border:0 none;display:block;height:auto;line-height:100%;outline:none;text-decoration:none;max-width:100%">
|
||||
Let’s put these figures into context. Infrastructure revenue growth averaged 33% this quarter, which is astounding considering we’re talking about businesses that sum to more than $50b of revenue per quarter.</p>
|
||||
<p style="margin:1em 0;font-size:18px;line-height:1.5em;font-family:Arial,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',sans-serif;color:#4e4e4e;font-size:16px;line-height:1.5">Customers spent $12b more this quarter - an astounding figure. At a 7x multiple of revenue, that is another $84b of market cap creation, in theory.</p>
|
||||
<p style="margin:1em 0;font-size:18px;line-height:1.5em;font-family:Arial,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',sans-serif;color:#4e4e4e;font-size:16px;line-height:1.5">But, the change in slope is meaningful, too. A year ago, these business units grew at 44% annually. The current recession is evident in the charts: growth rates have declined by 25% in six months’ time.</p>
|
||||
<p style="margin:1em 0;font-size:18px;line-height:1.5em;font-family:Arial,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',sans-serif;color:#4e4e4e;font-size:16px;line-height:1.5">The kink downwards in the red line at Q-2 shows a sudden deceleration in AWS’ growth rate. GCP is more volatile, likely driven by the volatility of bigger deals closing on a smaller revenue base. Meanwhile, Azure has declined in a more steady cadence.</p>
|
||||
<p style="margin:1em 0;font-size:18px;line-height:1.5em;font-family:Arial,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',sans-serif;color:#4e4e4e;font-size:16px;line-height:1.5">The data suggests a broader slowdown in software spending as these companies are <a href="https://click.convertkit-mail2.com/68ugeo9lmxa8hkwqe7dbo/7qh7h8h0rxnvw6iz/aHR0cHM6Ly90b210dW5ndXouY29tL3RoZS1pbmRleC1vZi1zb2Z0d2FyZS1idXllcnMv" style="font-weight:bold;color:#000000" target="_blank">indices of software buyers’ behavior</a>.</p>
|
||||
<p style="margin:1em 0;font-size:18px;line-height:1.5em;font-family:Arial,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',sans-serif;color:#4e4e4e;font-size:16px;line-height:1.5">Public software multiples - <a href="https://click.convertkit-mail2.com/68ugeo9lmxa8hkwqe7dbo/z2hghnhoqml9prap/aHR0cHM6Ly90b210dW5ndXouY29tL3JhdGVzLWFuZC1tdWx0aXBsZXMtMjAyMi8=" style="font-weight:bold;color:#000000" target="_blank">4.9x as of last week</a>, not far from the decade low of 3.3x - should depress further given these results & forward guidance. Microsoft stock is down 10%; Amazon fell about 20%; Google about 12%.</p>
|
||||
<p style="margin:1em 0;font-size:18px;line-height:1.5em;font-family:Arial,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',sans-serif;color:#4e4e4e;font-size:16px;line-height:1.5">Lopping another 15% off multiples implies the median software company should trade at 4.2x forward revenues, a far cry from the 15x we observed not twelve months ago.</p>
|
||||
|
||||
|
||||
<p style="margin:1em 0;font-size:18px;line-height:1.5em;font-family:Arial,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',sans-serif;color:#4e4e4e;font-size:16px;line-height:1.5"></p>
|
||||
</div></div>
|
||||
<div style="margin:8px 0 5px;padding:0 0 0 30px"><span style="display:inline-block;border-top-style:solid;border-top-width:5px;width:40px;border-color:#6495ed"></span></div>
|
||||
<div style="font-size:14px;padding:20px 30px;font-family:Helvetica,Arial,sans-serif">
|
||||
<p style="margin:1em 0;font-size:18px;line-height:1.5em;font-family:Arial,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',sans-serif;color:#4e4e4e;font-size:16px;line-height:1.5;font-size:14px;color:#868686;text-align:left">If you didn’t enjoy the email you can <a href="https://unsubscribe.convertkit-mail2.com/68ugeo9lmxa8hkwqe7dbo" rel="noopener noreferrer" style="font-weight:bold;color:#000000" target="_blank">unsubscribe here</a>. </p>
|
||||
<p style="margin:1em 0;font-size:18px;line-height:1.5em;font-family:Arial,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',sans-serif;color:#4e4e4e;font-size:16px;line-height:1.5;font-size:14px;color:#868686;text-align:left">2969 Woodside Road, Woodside, CA 94062</p>
|
||||
</div>
|
||||
|
||||
</div></td>
|
||||
<td style="vertical-align:top"></td>
|
||||
</tr></tbody></table></div>
|
||||
|
||||
<img src="https://open.convertkit-mail2.com/68ugeo9lmxa8hkwqe7dbo" alt="" style="border:0 none;display:block;height:auto;line-height:100%;outline:none;text-decoration:none;max-width:100%">
|
||||
</div>
|
||||
|
||||
</div></div></div>
|
||||
|
|
@ -17,6 +17,7 @@ import { GhostHandler } from '../src/newsletters/ghost-handler'
|
|||
import { CooperPressHandler } from '../src/newsletters/cooper-press-handler'
|
||||
import { getNewsletterHandler } from '../src'
|
||||
import { parseHTML } from 'linkedom'
|
||||
import { HeyWorldHandler } from '../src/newsletters/hey-world-handler'
|
||||
|
||||
chai.use(chaiAsPromised)
|
||||
chai.use(chaiString)
|
||||
|
|
@ -144,7 +145,8 @@ describe('Newsletter email test', () => {
|
|||
})
|
||||
|
||||
it('fixes up static tweets in Substack newsletters', async () => {
|
||||
const url = 'https://astralcodexten.substack.com/p/nick-cammarata-on-jhana'
|
||||
const url =
|
||||
'https://astralcodexten.substack.com/p/nick-cammarata-on-jhana'
|
||||
const html = load(
|
||||
'./test/data/substack-with-static-tweets-newsletter.html'
|
||||
)
|
||||
|
|
@ -160,7 +162,9 @@ describe('Newsletter email test', () => {
|
|||
expect(handler?.shouldPreParse(url, dom)).to.be.true
|
||||
|
||||
const preparsed = await handler?.preParse(url, dom)
|
||||
const tweets = Array.from(preparsed?.querySelectorAll('div[class="_omnivore-static-tweet"]') ?? [])
|
||||
const tweets = Array.from(
|
||||
preparsed?.querySelectorAll('div[class="_omnivore-static-tweet"]') ?? []
|
||||
)
|
||||
|
||||
expect(tweets.length).to.eq(7)
|
||||
})
|
||||
|
|
@ -219,6 +223,40 @@ describe('Newsletter email test', () => {
|
|||
})
|
||||
expect(handler).to.be.instanceOf(CooperPressHandler)
|
||||
})
|
||||
|
||||
it('returns HeyWorldHandler for hey world newsletter', async () => {
|
||||
const html = load('./test/data/hey-world-newsletter.html')
|
||||
const handler = await getNewsletterHandler({
|
||||
html,
|
||||
postHeader: '',
|
||||
from: 'Hongbo Wu <hw@world.hey.com>',
|
||||
unSubHeader:
|
||||
'<https://world.hey.com/dhh/subscribers/MtuoW9TvSJK9o5c7ohB72V2s/unsubscribe>',
|
||||
})
|
||||
expect(handler).to.be.instanceOf(HeyWorldHandler)
|
||||
})
|
||||
|
||||
it('returns ConvertkitHandler for Tomasz Tunguz newsletter', async () => {
|
||||
const html = load('./test/data/tomasz-tunguz-newsletter.html')
|
||||
const handler = await getNewsletterHandler({
|
||||
html,
|
||||
postHeader: '',
|
||||
from: '',
|
||||
unSubHeader: '',
|
||||
})
|
||||
expect(handler).to.be.instanceOf(ConvertkitHandler)
|
||||
})
|
||||
|
||||
it('returns undefined for convertkit confirmation email', async () => {
|
||||
const html = load('./test/data/convertkit-confirmation.html')
|
||||
const handler = await getNewsletterHandler({
|
||||
html,
|
||||
postHeader: '',
|
||||
from: '',
|
||||
unSubHeader: '',
|
||||
})
|
||||
expect(handler).to.be.undefined
|
||||
})
|
||||
})
|
||||
|
||||
describe('findNewsletterUrl', async () => {
|
||||
|
|
@ -334,6 +372,22 @@ describe('Newsletter email test', () => {
|
|||
expect(url).to.startWith('https://nodeweekly.com/issues/459')
|
||||
})
|
||||
})
|
||||
|
||||
context('when email is from hey world', () => {
|
||||
before(() => {
|
||||
nock('https://world.hey.com')
|
||||
.head('/dhh/here-s-how-to-fix-twitter-79632ecb')
|
||||
.reply(200, '')
|
||||
})
|
||||
|
||||
it('gets the URL from the header', async () => {
|
||||
const html = load('./test/data/hey-world-newsletter.html')
|
||||
const url = await new HeyWorldHandler().findNewsletterUrl(html)
|
||||
expect(url).to.startWith(
|
||||
'https://world.hey.com/dhh/here-s-how-to-fix-twitter-79632ecb'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('generateUniqueUrl', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue