---
title: "Letters of John and Abigail Adams Twitterbot"
url: "https://github.com/mas-4/johnandabigail"
featureImage: "/projects/john_and_abbie.png"
short: "john"
category: "automation"
ai: ""
order: "20"
year: "2020"
tags:
[
"python",
"aws",
"twitterbot",
"netlify",
"beautifulsoup4",
"scraping",
"civics",
]
---
This project was inspired by a [photo][11] that made the rounds of Twitter in
the wake of the protests in June 2020. The photo was of John Trumbull's famous
painting of the signing of the Declaration of Independence with red dots over
the faces of all the slave-owning founding fathers. Only 8 or 9 of the several
dozen men in the photo could boast never having owned slaves and John Adams was
smack dab in the middle with his face as apparent as ever.
I've always been a bit of a John Adams fanboy so I decided to put my Python to
work making a twitterbot that would tweet lines from the letters of John and
Abigail Adams, a lovely repository of romantic correspondence.
The bot tweets out fairly regularized sentences in threads that correspond to
each letter in chronological order, and then repeats. I used tweepy for the
Twitter interfacing, AWS's Lambda to run the program, and AWS's DynamoDB to keep
track of where the bot is. The actual letters are hosted on a static site of
json files on Netlify. That simplified some of the deployment for various
reasons I can't recall now.
- Twitter handle [@john_and_abbie][10]
- Bot to tweet the corpus of letters between [John and Abigail Adams][0]
- Data scraped from NARA's [The Adams Papers][1] using [requests][2] and
[BeautifulSoup4][3]
- Use [AWS DynamoDB][6] to track place in corpus
- Use [Netlify][7] to host the letters as static text files
[0]: https://www.masshist.org/digitaladams/archive/letter/
[1]: https://founders.archives.gov/about/Adams
[2]: https://requests.readthedocs.io/en/master/
[3]: https://www.crummy.com/software/BeautifulSoup/
[4]: https://www.tweepy.org/
[5]: https://aws.amazon.com/lambda/
[6]: https://aws.amazon.com/dynamodb/
[7]: https://johnandabigail.netlify.app/1777-04-02-0-john.txt
[8]: https://urllib3.readthedocs.io/en/latest/
[9]: https://boto3.amazonaws.com/v1/documentation/api/latest/index.html
[10]: https://github.com/mas-4/johnandabigail
[11]:
https://hyperallergic.com/572035/historical-painting-is-altered-to-show-most-declaration-of-independence-signatories-were-enslavers/
<sky-template path="main.html" />
<sky-block name="title">${frontmatter.title} · </sky-block>
<sky-block name="content">
<article class="project">
<a class="back" href="/projects">← all projects</a>
<p class="pills"><span class="pill">${frontmatter.category}</span><span class="pill pill-ai">${frontmatter.ai}</span><span class="year">${frontmatter.year}</span></p>
<h1>${frontmatter.title}</h1>
<a class="visit" href="${frontmatter.url}">${frontmatter.url}</a>
<img class="feature" src="${frontmatter.featureImage}" alt="" />
<div class="markdown">
<sky-slot name="content" />
</div>
</article>
</sky-block>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script><sky-include path="theme.js" /></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><sky-slot name="title" />${meta.title}</title>
<meta name="description" content="${meta.description}" />
<meta name="theme-color" content="#142420" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="/style.css" />
</head>
<body>
<!-- Every page is a sheet of paper lying on the source it was built
from. This checkbox lifts the sheet; no JavaScript involved. -->
<input type="checkbox" id="xray" class="xray" aria-label="Show the source of this page" />
<div class="desk">
<div class="sheet">
<!-- Under the paper, the page's source, for the candle to show. -->
<div class="underside" aria-hidden="true"><sky-sources item="underside.html" /></div>
<div class="paper" aria-hidden="true"></div>
<svg class="flame" viewBox="0 0 24 44" aria-hidden="true">
<defs>
<radialGradient id="flame-body" cx="50%" cy="78%" r="62%">
<stop offset="0" stop-color="#fffbe8" />
<stop offset="0.35" stop-color="#ffe08a" />
<stop offset="0.7" stop-color="#ff9a2e" />
<stop offset="1" stop-color="#e2470f" stop-opacity="0" />
</radialGradient>
<radialGradient id="flame-core" cx="50%" cy="80%" r="55%">
<stop offset="0" stop-color="#ffffff" />
<stop offset="0.6" stop-color="#fff3c4" />
<stop offset="1" stop-color="#ffd166" stop-opacity="0" />
</radialGradient>
</defs>
<path class="flame-body" fill="url(#flame-body)" d="M12 1C15 11 22 18 21 29 20 37 16 43 12 43 8 43 4 37 3 29 2 18 9 11 12 1Z" />
<path class="flame-core" fill="url(#flame-core)" d="M12 15C14 21 17 26 16.5 32 16 37 14 40 12 40 10 40 8 37 7.5 32 7 26 10 21 12 15Z" />
<ellipse class="flame-base" cx="12" cy="40" rx="3.2" ry="2" fill="#5aa9ff" opacity="0.45" />
</svg>
<label for="xray" class="curl" title="Lift the page to see what it's made of" aria-hidden="true"></label>
<label for="xray" class="sheet-cover" title="Back to the page"></label>
<sky-include path="_nav.html" />
<main class="page">
<sky-slot name="content" />
</main>
<sky-include path="_footer.html" />
</div>
<aside class="ground" aria-label="The source of this page">
<header class="ground-head">
<p>
<a href="https://github.com/mas-4/skyblue">skyblue</a> built this
page from these files, in the order it used them.
</p>
<label for="xray" class="ground-close">back to the page →</label>
</header>
<sky-sources item="source.html" />
</aside>
</div>
<script><sky-include path="candle.js" /></script>
</body>
</html>
// The light switch. Until it's flipped, the page follows the system's
// light or dark preference (that part is CSS). Flipping it picks the other
// one and remembers it. This runs in <head>, so a saved choice applies
// before the page is drawn.
(() => {
const root = document.documentElement;
const system = matchMedia("(prefers-color-scheme: dark)");
try {
const saved = localStorage.getItem("theme");
if (saved === "light" || saved === "dark") root.dataset.theme = saved;
} catch {}
const isDark = () => (root.dataset.theme ? root.dataset.theme === "dark" : system.matches);
document.addEventListener("DOMContentLoaded", () => {
const button = document.querySelector(".theme-toggle");
if (!button) return;
const show = () => button.setAttribute("aria-checked", String(isDark()));
button.hidden = false;
show();
system.addEventListener("change", show);
button.addEventListener("click", () => {
root.dataset.theme = isDark() ? "light" : "dark";
try {
localStorage.setItem("theme", root.dataset.theme);
} catch {}
show();
});
});
})();
<header class="site-header">
<nav class="nav">
<a class="brand" href="/">
<svg class="brand-mark" viewBox="0 0 32 32" aria-hidden="true">
<path d="M2 13c4 0 4-3 7-3s3 3 7 3 4-3 7-3 3 3 7 3" />
<path d="M2 20c4 0 4-3 7-3s3 3 7 3 4-3 7-3 3 3 7 3" />
</svg>
${meta.title}
</a>
<div class="nav-links">
<a href="/">home</a>
<a href="/about">about</a>
<a href="/projects">projects</a>
<a href="/orbits">orbits</a>
<label for="xray" class="source-toggle" title="Lift the page to see what it's made of">source</label>
</div>
<button type="button" class="theme-toggle" role="switch" aria-checked="false" aria-label="Dark mode" title="Light or dark" hidden><span class="knob"></span></button>
</nav>
</header>
<footer class="site-footer">
<div class="social">
<a href="https://github.com/mas-4">
<svg
aria-labelledby="svg-inline--fa-title-Yppzrr98YU17"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 496 512"
>
<title id="svg-inline--fa-title-Yppzrr98YU17">GitHub</title>
<path
fill="currentColor"
d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"
></path>
</svg>
</a>
<a href="https://stackoverflow.com/users/9691276/mas">
<svg
aria-labelledby="svg-inline--fa-title-kGxnyVLHc64Z"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 384 512"
>
<title id="svg-inline--fa-title-kGxnyVLHc64Z">StackOverflow</title>
<path
fill="currentColor"
d="M290.7 311L95 269.7 86.8 309l195.7 41zm51-87L188.2 95.7l-25.5 30.8 153.5 128.3zm-31.2 39.7L129.2 179l-16.7 36.5L293.7 300zM262 32l-32 24 119.3 160.3 32-24zm20.5 328h-200v39.7h200zm39.7 80H42.7V320h-40v160h359.5V320h-40z"
></path>
</svg>
</a>
<a href="https://www.linkedin.com/in/michael-sendker">
<svg
aria-labelledby="svg-inline--fa-title-BaV39z6dqIIX"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512"
>
<title id="svg-inline--fa-title-BaV39z6dqIIX">LinkedIn</title>
<path
fill="currentColor"
d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z"
></path>
</svg>
</a>
<a href="https://raw.githubusercontent.com/mas-4/resume/master/main.pdf">
<svg
aria-labelledby="svg-inline--fa-title-fmQP80A0S9GF"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 384 512"
>
<title id="svg-inline--fa-title-fmQP80A0S9GF">Résumé</title>
<path
fill="currentColor"
d="M181.9 256.1c-5-16-4.9-46.9-2-46.9 8.4 0 7.6 36.9 2 46.9zm-1.7 47.2c-7.7 20.2-17.3 43.3-28.4 62.7 18.3-7 39-17.2 62.9-21.9-12.7-9.6-24.9-23.4-34.5-40.8zM86.1 428.1c0 .8 13.2-5.4 34.9-40.2-6.7 6.3-29.1 24.5-34.9 40.2zM248 160h136v328c0 13.3-10.7 24-24 24H24c-13.3 0-24-10.7-24-24V24C0 10.7 10.7 0 24 0h200v136c0 13.2 10.8 24 24 24zm-8 171.8c-20-12.2-33.3-29-42.7-53.8 4.5-18.5 11.6-46.6 6.2-64.2-4.7-29.4-42.4-26.5-47.8-6.8-5 18.3-.4 44.1 8.1 77-11.6 27.6-28.7 64.6-40.8 85.8-.1 0-.1.1-.2.1-27.1 13.9-73.6 44.5-54.5 68 5.6 6.9 16 10 21.5 10 17.9 0 35.7-18 61.1-61.8 25.8-8.5 54.1-19.1 79-23.2 21.7 11.8 47.1 19.5 64 19.5 29.2 0 31.2-32 19.7-43.4-13.9-13.6-54.3-9.7-73.6-7.2zM377 105L279 7c-4.5-4.5-10.6-7-17-7h-6v128h128v-6.1c0-6.3-2.5-12.4-7-16.9zm-74.1 255.3c4.1-2.7-2.5-11.9-42.8-9 37.1 15.8 42.8 9 42.8 9z"
></path>
</svg>
</a>
</div>
<!-- skyblue fills these in as it builds the site. -->
<pre class="receipt" aria-label="How this page was built"><span class="prompt">$</span> skyblue --version
skyblue <a href="https://github.com/mas-4/skyblue">${sky.version}</a> (compiled by cprime <a href="https://github.com/mas-4/cprime">${sky.compiler}</a>)
<span class="prompt">$</span> skyblue -i src -o build
skyblue: ${sky.pages} pages and ${sky.files} files -> build
<span class="note"># ${sky.date}, ${sky.time}, ${sky.ms} ms. Two small scripts: the candle and the light switch.</span></pre>
<p class="copyright">© 2022–2026 Michael Sendker</p>
</footer>
// The candle. Hold the pointer over the words and a flame lights above
// them: the paper warms and thins, and the page's source shows through
// from underneath. Only over the letters themselves, not blank paper.
// Without this script the paper simply stays opaque.
(() => {
const sheet = document.querySelector(".sheet");
if (!sheet || !matchMedia("(hover: hover) and (pointer: fine)").matches) return;
// Is (x, y) on a letter? Find the text position under the point, then
// check that the point is inside the box of the character on either side.
const range = document.createRange();
const overGlyph = (x, y) => {
let node, offset;
if (document.caretPositionFromPoint) {
const pos = document.caretPositionFromPoint(x, y);
if (pos) [node, offset] = [pos.offsetNode, pos.offset];
} else if (document.caretRangeFromPoint) {
const r = document.caretRangeFromPoint(x, y);
if (r) [node, offset] = [r.startContainer, r.startOffset];
}
if (!node || node.nodeType !== Node.TEXT_NODE || !sheet.contains(node)) return false;
for (const i of [offset - 1, offset]) {
if (i < 0 || i >= node.length || /\s/.test(node.data[i])) continue;
range.setStart(node, i);
range.setEnd(node, i + 1);
for (const box of range.getClientRects())
if (x >= box.left && x <= box.right && y >= box.top && y <= box.bottom) return true;
}
return false;
};
let x = 0, y = 0, queued = false, dousing = 0;
const draw = () => {
queued = false;
const box = sheet.getBoundingClientRect();
sheet.style.setProperty("--cx", x - box.left + "px");
sheet.style.setProperty("--cy", y - box.top + "px");
if (overGlyph(x, y)) {
clearTimeout(dousing);
dousing = 0;
sheet.classList.add("lit");
} else if (!dousing && sheet.classList.contains("lit")) {
// A moment's grace, so it doesn't strobe across the spaces between words
dousing = setTimeout(() => {
sheet.classList.remove("lit");
dousing = 0;
}, 350);
}
};
sheet.addEventListener("pointermove", (e) => {
[x, y] = [e.clientX, e.clientY];
if (!queued) {
queued = true;
requestAnimationFrame(draw);
}
});
sheet.addEventListener("pointerleave", () => {
clearTimeout(dousing);
dousing = 0;
sheet.classList.remove("lit");
});
})();
<section class="source">
<header class="source-head">
<a href="https://github.com/mas-4/standingwater-flat/blob/main/src/${source.path}">${source.path}</a>
<span>${source.lines} lines</span>
</header>
<pre class="listing"><code>${source.code}</code></pre>
</section>
<pre class="listing"><code>${source.code}</code></pre>