- Rust 95.6%
- Dockerfile 2.6%
- Shell 1.8%
|
|
||
|---|---|---|
| docs | ||
| scripts | ||
| specs | ||
| src | ||
| tests | ||
| .env.example | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| Dockerfile | ||
| README.md | ||
storied-colors-bot
A small daemon that watches the Storied Colors homepage for the daily "Read today's entry" link and posts it to a Mastodon account when it changes.
What this does
Once a day, Storied Colors features a new color on its homepage under "Today's Color," with a "Read today's entry" link to that color's full page. This bot watches for that link and posts it — and only it — to a configured Mastodon account, once per day, with no duplicate posts across restarts.
The bot runs as a long-lived Docker container. It polls the homepage every 2 hours, compares the current entry URL against the last one it posted, and posts the bare URL if it is new. Mastodon's own link preview generation creates the visual card.
Why this scrapes the homepage instead of using RSS
storiedcolors.com publishes an RSS feed at /rss.xml, but that feed is known
to lag behind the live site and miss recent entries. This bot deliberately
scrapes the homepage's "Read today's entry" link directly instead, since that's
the reliable source of the current day's pick.
Respecting robots.txt
This bot only ever fetches the homepage (/). It never touches /proofs/,
/journal/, or /rss.xml. It identifies itself with the User-Agent string in
src/scraper.rs — please update the contact URL in that string to your own
before deploying, so the site owner has a way to reach you if needed.
How it works
- Runs as a long-lived process in Docker (
docker run --restart unless-stopped). - Checks the homepage immediately on startup, then every 2 hours.
- Compares the current "today's entry" URL against the last one posted, tracked in a JSON state file on a persistent volume.
- Posts a short prefix plus the entry URL to Mastodon if it is new.
- Logs structured JSON to stdout; use
docker logs -f storied-colors-botto watch it. - Handles SIGTERM/SIGINT gracefully so
docker stopdoes not kill an in-flight request.
Setup
1. Get a Mastodon access token
- Log in to your Mastodon instance in a web browser.
- Open Preferences → Development → New Application (the exact path varies by instance; look for "Development" or "Applications" under the settings gear).
- Fill in the application form:
- Application name:
Storied Colors Bot(or any name you will recognize). - Application website: optional; you can leave it blank or use a contact URL.
- Redirect URI: leave blank, or set it to
urn:ietf:wg:oauth:2.0:oobif a value is required.
- Application name:
- Under Scopes, enable only
write:statuses. The bot does not needreadorwrite:mediapermissions. - Submit the form, open the newly created application, and copy the Access token.
- Paste the token into your
.envfile asMASTODON_ACCESS_TOKEN=....
Treat the access token like a password. If it leaks, revoke it in the same Development settings page and generate a new one.
2. Configure the bot
cp .env.example .env
# edit .env with your MASTODON_INSTANCE_URL and MASTODON_ACCESS_TOKEN
3. Build and run
# Build the image
docker build -t storied-colors-bot:latest .
# Start the daemon
./scripts/run.sh
Or run the long command directly:
mkdir -p data
docker run -d \
--name storied-colors-bot \
--env-file .env \
-v "$(pwd)/data:/data" \
--restart unless-stopped \
storied-colors-bot:latest
4. Verify (without posting)
To preview what the bot would post without touching Mastodon:
# Using cargo
cargo run -- --dry-run
# Or inside the container
./scripts/dry-run.sh
Output will be one of:
would post: Today's featured color from Storied Colors: https://storiedcolors.com/color/<slug>
nothing new: Today's featured color from Storied Colors: https://storiedcolors.com/color/<slug>
Then start the daemon and watch logs:
docker logs -f storied-colors-bot
Configuration reference
| Variable | Required | Default | Notes |
|---|---|---|---|
MASTODON_INSTANCE_URL |
yes | — | e.g. https://mastodon.social, no trailing slash, HTTPS only |
MASTODON_ACCESS_TOKEN |
yes | — | needs write:statuses scope |
STATE_PATH |
no | /data/state.json |
path to the JSON state file |
Status format
Each post uses a consistent prefix plus the entry URL:
Today's featured color from Storied Colors: https://storiedcolors.com/color/confederate-grey
Mastodon's link preview generation handles the visual card.
If the site's markup changes
The entry-extraction logic matches on the link text "Read today's entry." If
the site changes that wording or restructures the homepage significantly, the bot
will log a warning each cycle instead of posting. Check the logs, update the
parser in src/scraper.rs if needed, and add a new fixture to the test suite.
Development
Prerequisites: a recent stable Rust toolchain (the current local target is 1.95) and Docker.
# Run the test suite
cargo test
# Check formatting and lints
cargo fmt --check
cargo clippy -- -D warnings
# Build the Docker image
docker build -t storied-colors-bot:latest .
Non-goals
This bot intentionally does not: use the site's RSS feed, attach images to
posts, backfill missed days, scrape any page other than the homepage, or provide
a web UI. See docs/IMPLEMENTATION_PLAN.md and docs/DECISIONS.md for the
reasoning behind these choices.
Project documentation
docs/IMPLEMENTATION_PLAN.md— phased build plandocs/ARCHITECTURE.md— component and data-flow designdocs/TESTING.md— test strategy and red-to-green workflowdocs/DECISIONS.md— architecture decision recordsdocs/GAP_ANALYSIS.md— adversarial review of the implementationdocs/ACCEPTANCE.md— acceptance-criteria verification logdocs/REMOTE_DEPLOYMENT.md— deploying the built container to a server without Rust/Cargo