A bot that reads from https://storiedcolors.com/ and posts to daily@color.goodstuff.im
  • Rust 93.4%
  • Dockerfile 4.3%
  • Shell 2.3%
Find a file
David Pollak bbbbb60cee
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
Some permissions changes
Signed-off-by: David Pollak <feeder.of.the.bears@gmail.com>
2026-11-02 16:40:50 -05:00
docs Some permissions changes 2026-11-02 16:40:50 -05:00
scripts Set up for woodpecker CI 2026-11-02 16:36:52 -05:00
specs Updated to removed Docker compose 2026-06-21 11:12:47 -04:00
src Set up for woodpecker CI 2026-11-02 16:36:52 -05:00
tests Initial commit 2026-06-20 14:39:00 -04:00
.dockerignore Set up for woodpecker CI 2026-11-02 16:36:52 -05:00
.env.example Updated to removed Docker compose 2026-06-21 11:12:47 -04:00
.gitignore Initial commit 2026-06-20 14:39:00 -04:00
.woodpecker.yml Some permissions changes 2026-11-02 16:40:50 -05:00
Cargo.lock Set up for woodpecker CI 2026-11-02 16:36:52 -05:00
Cargo.toml Set up for woodpecker CI 2026-11-02 16:36:52 -05:00
Dockerfile Set up for woodpecker CI 2026-11-02 16:36:52 -05:00
README.md Some permissions changes 2026-11-02 16:40:50 -05:00

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, which points at this repository — update the contact URL there if you fork the project, 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-bot to watch it.
  • Handles SIGTERM/SIGINT gracefully so docker stop does not kill an in-flight request.

Setup

1. Get a Mastodon access token

  1. Log in to your Mastodon instance in a web browser.
  2. Open Preferences → Development → New Application (the exact path varies by instance; look for "Development" or "Applications" under the settings gear).
  3. 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:oob if a value is required.
  4. Under Scopes, enable only write:statuses. The bot does not need read or write:media permissions.
  5. Submit the form, open the newly created application, and copy the Access token.
  6. Paste the token into your .env file as MASTODON_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

CI publishes a ready-to-run image to the Forgejo container registry, so you can deploy without a local Rust toolchain:

# Optional: verify the image is reachable first
docker login git.dpp.me
docker pull git.dpp.me/dpp/dailycolor:latest

# Start the daemon from the published image
IMAGE=git.dpp.me/dpp/dailycolor:latest ./scripts/run.sh

Or build the image locally:

# Build the image
docker build -t storied-colors-bot:latest .

# Start the daemon
./scripts/run.sh

Or run the long command directly (substitute the locally built image name if you did not use CI):

mkdir -p data
docker run -d \
  --name storied-colors-bot \
  --env-file .env \
  -v "$(pwd)/data:/data" \
  --restart unless-stopped \
  git.dpp.me/dpp/dailycolor: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.

CI and image publishing

The repository uses Woodpecker CI to lint, test, and publish container images to the Forgejo registry. The pipeline lives in .woodpecker.yml and runs these steps:

Step What it does
format cargo fmt --all -- --check
clippy cargo clippy --all-targets --locked -- -D warnings
test cargo test --all-targets --locked
docker Builds and pushes git.dpp.me/dpp/dailycolor with the docker-buildx plugin

The first three steps run on every push, pull request, and tag. The docker step only runs on pushes to main/master/develop and on tags:

  • default branches publish latest
  • tag events published as v1.2.3 also publish 1, 1.2, 1.2.3, and latest

Required repository secret

Configure this in Woodpecker under Repository settings → Secrets:

Secret Value
registry_password A Forgejo token (or password) for the dpp user with permission to push to the dailycolor package registry

If the registry user is not dpp, update username and repo in .woodpecker.yml. The pipeline intentionally uses no host volumes, so the repository does not need Woodpecker's Trusted setting; without that setting, volumes: is rejected with Insufficient trust level to use volumes. If an admin marks the project trusted, see docs/CI.md for the optional /cache/cargo registry cache.

The publish step uses woodpeckerci/plugin-docker-buildx. Woodpecker v3 requires that plugin to be listed in the server's WOODPECKER_PLUGINS_PRIVILEGED setting; see docs/CI.md if the step fails with a privileged-plugin error.

See docs/CI.md for the full pipeline notes and troubleshooting.

Development

Prerequisites: a recent stable Rust toolchain (the current local target is 1.95) and Docker.

# Run the test suite
cargo test --all-targets

# Check formatting and lints
cargo fmt --all -- --check
cargo clippy --all-targets -- -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 plan
  • docs/ARCHITECTURE.md — component and data-flow design
  • docs/TESTING.md — test strategy and red-to-green workflow
  • docs/DECISIONS.md — architecture decision records
  • docs/GAP_ANALYSIS.md — adversarial review of the implementation
  • docs/ACCEPTANCE.md — acceptance-criteria verification log
  • docs/CI.md — Woodpecker pipeline and container image publishing
  • docs/REMOTE_DEPLOYMENT.md — deploying the built container to a server without Rust/Cargo