fresh readme

This commit is contained in:
sakrecoer 2026-10-03 12:08:32 +02:00
parent f6558efa1f
commit 6e48b5516d

262
README.md
View file

@ -1,54 +1,260 @@
# nostr-calendar # nostr-calendar
Fetch events from an ICS + RSS feed and publish them as NIP-52 calendar Fetch events from an ICS + RSS feed and publish them as [NIP-52](https://github.com/nostr-protocol/nips/blob/master/52.md)
events (kind 31923) to Nostr relays, requesting inclusion in a kind 31924 calendar events (kind `31923`) to Nostr relays, requesting inclusion in a
calendar. 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 ## Install
Clone this repository to `/opt/nostr-calendar`: Clone to `/opt/nostr-calendar`:
sudo git clone <repo-url> /opt/nostr-calendar ```bash
sudo chown -R "$USER:$USER" /opt/nostr-calendar sudo git clone <repo-url> /opt/nostr-calendar
cd /opt/nostr-calendar sudo chown -R "$USER:$USER" /opt/nostr-calendar
./install.sh cd /opt/nostr-calendar
./install.sh
```
The installer creates a virtualenv, installs dependencies, and copies The installer:
`config.toml.example` to `config.toml`.
- 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 ## Configure
Edit `config.toml` and set at minimum: Edit `config.toml`:
- `secret_key` — your Nostr secret key (`nsec1...` or hex) ```toml
- `relays` — relays to publish to secret_key = "nsec1..." # or 64-char hex
- `calendar.d`, `calendar.title`, `calendar.description` — your calendar
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:<your-pubkey>:<calendar.d>
```
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 ## 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 ```bash
published; subsequent runs only publish new or changed events (dedupe /opt/nostr-calendar/venv/bin/python /opt/nostr-calendar/publish.py --dry-run
is tracked in `events.db`). ```
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 ## Schedule with cron
crontab -e ```bash
crontab -e
```
Add: 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`. ## Files
- The only place the absolute install path appears is the crontab entry.
| 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 <copyright holder>
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 <https://www.gnu.org/licenses/>.