- Rust 93.4%
- Dockerfile 4.3%
- Shell 2.3%
|
|
||
|---|---|---|
| docs | ||
| scripts | ||
| specs | ||
| src | ||
| tests | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .woodpecker.yml | ||
| 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, 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-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
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.3also publish1,1.2,1.2.3, andlatest
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 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/CI.md— Woodpecker pipeline and container image publishingdocs/REMOTE_DEPLOYMENT.md— deploying the built container to a server without Rust/Cargo