From 6e48b5516d3d597ad14d8bebe6f623f890fe5ca6 Mon Sep 17 00:00:00 2001 From: sakrecoer Date: Sat, 3 Oct 2026 12:08:32 +0200 Subject: [PATCH] fresh readme --- README.md | 262 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 234 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 0e2dd88..55c3ed4 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,260 @@ # nostr-calendar -Fetch events from an ICS + RSS feed and publish them as NIP-52 calendar -events (kind 31923) to Nostr relays, requesting inclusion in a kind 31924 -calendar. +Fetch events from an ICS + RSS feed and publish them as [NIP-52](https://github.com/nostr-protocol/nips/blob/master/52.md) +calendar events (kind `31923`) to Nostr relays, requesting inclusion in a +kind `31924` calendar. + +Built for [do.basspistol.org](https://do.basspistol.org) but configurable +for any ICS + RSS pair. + +--- + +## How it works + +Every run: + +1. Fetches the ICS feed (complete event list with UTC start/end times). +2. Fetches the RSS feed (the "announcement stream" — only the most recent + entries, with images and tags). +3. Merges them by event URL. The ICS is the source of truth for time, + location, UID and tags; the RSS contributes the image and, if the ICS + lacks categories, the tags. +4. Publishes each event as a kind `31923` Nostr event, referencing the + calendar (kind `31924`) via an `a` tag. +5. Skips events that haven't changed since the last run, using a content + hash stored in a local SQLite database. + +Events are **addressable** (`d` tag = ICS UID), so republishing replaces +the previous version on the relay rather than creating duplicates. + +### `rss_only` mode + +When `rss_only = true` (recommended), only events that appear in the RSS +feed are published. The RSS is treated as the "announcement stream": +newly-created events appear there, and recurring events reappear once the +current occurrence ends. This avoids back-filling the Nostr calendar with +every event in the ICS, including old ones that may be stale or cancelled. + +With `rss_only = false`, every event in the ICS is published. Events +without an RSS match simply have no image. + +--- + +## Requirements + +- Linux (tested on Raspberry Pi OS) +- Python 3.11+ (3.12 or 3.13 recommended — see the note on 3.14 below) +- Outbound internet access to the feed URLs and relays + +### Python 3.14 note + +`coincurve` (a `pynostr` dependency) has no prebuilt wheel for Python 3.14 +at the time of writing, so it must build from source. The installer pins +`coincurve==20.0.0` and constrains its build backend to +`scikit-build-core<0.10` to work around two upstream build bugs. This +works but is slow. **If you can, install Python 3.12 or 3.13** — prebuilt +wheels exist and installation is much faster. + +--- ## Install -Clone this repository to `/opt/nostr-calendar`: +Clone to `/opt/nostr-calendar`: - sudo git clone /opt/nostr-calendar - sudo chown -R "$USER:$USER" /opt/nostr-calendar - cd /opt/nostr-calendar - ./install.sh +```bash +sudo git clone /opt/nostr-calendar +sudo chown -R "$USER:$USER" /opt/nostr-calendar +cd /opt/nostr-calendar +./install.sh +``` -The installer creates a virtualenv, installs dependencies, and copies -`config.toml.example` to `config.toml`. +The installer: + +- creates a virtualenv in `venv/` +- installs dependencies with the build constraints for Python 3.14 +- copies `config.toml.example` to `config.toml` (if it doesn't exist) +- locks `config.toml` to mode `600` +- prints next steps + +--- ## Configure -Edit `config.toml` and set at minimum: +Edit `config.toml`: -- `secret_key` — your Nostr secret key (`nsec1...` or hex) -- `relays` — relays to publish to -- `calendar.d`, `calendar.title`, `calendar.description` — your calendar +```toml +secret_key = "nsec1..." # or 64-char hex -Then lock down the file: +relays = [ + "wss://basspistol.org", + "wss://nos.lol", +] - chmod 600 config.toml +ics_url = "https://do.basspistol.org/feed/ics?show_recurrent=true" +rss_url = "https://do.basspistol.org/feed/rss?show_recurrent=true" + +rss_only = true # only publish events advertised in RSS + +[calendar] +d = "basspistol-do" +title = "Basspistol Do 🗓️" +description = "Underground events promoted by the people, for the people." +``` + +Then: + +```bash +chmod 600 config.toml +``` + +`config.toml` contains your secret key and is **gitignored**. Never commit it. +`config.toml.example` is the committed template. + +### Calendar (kind 31924) + +The `[calendar]` block defines the kind `31924` event that your published +events request inclusion in, via an `a` tag of the form: + +``` +31924:: +``` + +You don't need to publish the calendar separately first — the script +publishes it on every run (idempotent, since it's addressable via `d`). + +--- ## Test - /opt/nostr-calendar/venv/bin/python /opt/nostr-calendar/publish.py +Always dry-run first. It fetches and parses the feeds, logs what would be +published (including images and tags), and touches neither the database +nor the relays: -Check the output and `publish.log`. On the first run, all events are -published; subsequent runs only publish new or changed events (dedupe -is tracked in `events.db`). +```bash +/opt/nostr-calendar/venv/bin/python /opt/nostr-calendar/publish.py --dry-run +``` + +Sample output: + +``` +INFO DRY RUN 17943: title='Blokes fantasma 33 anys resistint' image='https://...jpg' tags=['gràcia', 'concert', ...] start=1791021600 end=1791079200 +... +INFO DRY RUN complete. Would publish 104, skipped_no_rss=0 +``` + +Then run for real: + +```bash +/opt/nostr-calendar/venv/bin/python /opt/nostr-calendar/publish.py +``` + +Logs go to both stdout and `publish.log`. + +--- ## Schedule with cron - crontab -e +```bash +crontab -e +``` Add: - */30 * * * * /opt/nostr-calendar/run.sh >> /opt/nostr-calendar/cron.log 2>&1 +``` +*/30 * * * * /opt/nostr-calendar/run.sh >> /opt/nostr-calendar/cron.log 2>&1 +``` -This runs every 30 minutes. Adjust the `*/30` to taste. +`run.sh` resolves its own directory, `cd`s into it, and invokes the +virtualenv Python — so it's safe to call from cron regardless of the +starting working directory. -## Notes +Adjust `*/30` to taste. Every 15–30 minutes is plenty; the RSS feed is +truncated server-side, so polling more often doesn't recover older events. -- `config.toml` contains your secret key and is gitignored. Never commit it. -- The virtualenv is not relocatable — if you move the directory after - installing, recreate the venv by re-running `./install.sh`. -- The only place the absolute install path appears is the crontab entry. \ No newline at end of file +--- + +## Files + +| File | Purpose | +|---|---| +| `publish.py` | Main script. `--dry-run` for a no-op test. | +| `run.sh` | Cron wrapper. Resolves its own directory. | +| `install.sh` | Creates venv, installs deps, seeds config. | +| `requirements.txt` | Python dependencies. | +| `config.toml.example` | Configuration template (committed). | +| `config.toml` | Your configuration (gitignored, mode 600). | +| `events.db` | SQLite dedupe table (gitignored). | +| `publish.log` | Log file (gitignored). | +| `cron.log` | Cron output (gitignored). | + +--- + +## Dedupe behavior + +`events.db` stores, per event: the `d` tag, a content hash, the published +event id, and a timestamp. On each run: + +- If the event is absent → publish. +- If the `d` tag exists but the content hash differs → republish (addressable + event replaces the previous version on the relay). +- If both match → skip. + +The content hash covers title, start, end, location, URL, image, and tags. + +To force a full republish, delete `events.db`. Relays will replace the +existing events; nothing is duplicated. + +--- + +## Troubleshooting + +**`ModuleNotFoundError: No module named 'feedparser'`** +You're running the script with system Python. Use the virtualenv: + +```bash +/opt/nostr-calendar/venv/bin/python /opt/nostr-calendar/publish.py +``` + +or activate it first: + +```bash +source /opt/nostr-calendar/venv/bin/activate +./publish.py +``` + +**`coincurve` fails to build** +See the Python 3.14 note above. The installer handles it, but if you're +installing manually you need `PIP_CONSTRAINT=build-constraints.txt` with +`scikit-build-core<0.10` inside it. + +**Only some events have images** +Expected. Images come from the RSS feed, which only carries the most +recent entries. With `rss_only = true`, only RSS-advertised events are +published, and all of them get images. + +**Event times look wrong** +The ICS feed uses UTC (`DTSTART:...Z`) and is authoritative. The RSS +renders times in the feed server's local timezone — ignore it. + +**Relay doesn't accept writes** +Check `publish.log` for WebSocket errors. Some relays require paid access +or reject certain kinds; try adding a third relay as a fallback. + +--- + +## License + +Copyright (C) 2026 + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . \ No newline at end of file