docs: README voor de repo — filter v1.14.0, commando's, config en deploy

This commit is contained in:
Eddy de Vink 2026-09-13 09:01:03 +02:00
commit 0c5a07071c
2 changed files with 160 additions and 0 deletions

3
.gitignore vendored
View file

@ -1,2 +1,5 @@
__pycache__/
*.pyc
# Taakbestanden die geen onderdeel zijn van de repo
Prompt voor agent*.md

157
README.md Normal file
View file

@ -0,0 +1,157 @@
# buzz-rss — RSS/Atom → Matrix, gefilterd op OV-relevantie
Maubot-plugin `nl.eddydevink.rss` (fork van [`xyz.maubot.rss`](https://github.com/maubot/rss))
die nieuwsfeeds pollt en **alleen openbaar-vervoer-gerelateerde berichten** doorplaatst
naar Matrix-rooms (optioneel ook alleen de link naar een WhatsApp-groep).
De filterlogica is **bewust vast in code** (niet via config) en volgt één businessregel:
> **Precision boven recall** — liever een relevant OV-artikel missen dan één
> niet-OV-artikel ten onrechte doorlaten. (Gebruikersbesluit 2026-09-10)
## Functionaliteit
- Abonneren op feeds per room, met per-feed meldingssjabloon en `m.notice`-optie.
- **Conditional fetch** (`ETag`/`Last-Modified`) en backoff bij faalende feeds.
- **HTML-scrape-modus**: bronnen zonder RSS/Atom/JSON worden als pagina gepolled via
per-feed `feed.scrape_config` (zie `buzzrss/html_scrape.py`; eerste productiefeed: CNV).
- **Paywall-uitsluiting** (`exclude_paywalled`) en **titel-dedup over feeds**
(`title_dedup_days`: zelfde titel van een andere site van dezelfde uitgever = niet dubbel posten).
- Per-run logfile met aantallen (`run_log_file`).
- **WhatsApp-forward**: alleen `entry.link` wordt gepost (vooruit-links, zonder titel),
met dedup-check tegen de doelgroep. Uit zolang `whatsapp_api`/`whatsapp_chat_id` leeg zijn.
## Commando's
Prefix uit config (`command_prefix`, default `rss`), eventueel zonder `!`:
| Commando | Aliassen | Werking |
|---|---|---|
| `!rss subscribe <url>` | `s`, `sub` | Deze room op een feed abonneren (nieuwe feed wordt direct geïndexeerd) |
| `!rss unsubscribe <feed-id>` | `u`, `unsub` | Abonnement van deze room opzeggen |
| `!rss subscriptions` | `ls`, `list`, `subs` | Abonnementen in deze room + huidige filter |
| `!rss template <feed-id> [sjabloon]` | `t`, `tpl` | Meldingssjabloon bekijken/wijzigen (variabelen: `$title`, `$link`, `$summary`, `$feed_title`, …) |
| `!rss notice <feed-id> <true\|false>` | `n` | Als `m.notice` versturen i.p.v. `m.text` |
| `!rss filter` | `f`, `set-filter`, `show-filter`, `get-filter` | Toont het vaste OV-matchfilter |
Bestuurders: admins uit de config, of gebruikers met een powerlevel ≥ dat van het
RSS-state-event in die room (default 50 als geen level is ingesteld).
Bij een room-tombstone worden abonnementen automatisch naar de vervangende room verplaatst.
## Het OV-matchfilter (v1.14.0)
De volledige classificatie + de werkwijze voor nieuwe woorden staat in
`buzzrss/bot.py`, boven `MATCH_FILTER_FAMILIES` — **dat blok is leidend** bij elke
wijziging. Samengevat:
```text
score = Σ gewichten van unieke OV-termen over titel + samenvatting
(één term = één signaal per artikel; overlappende familie-treffers wegen 1×
met het hoogste gewicht — span-merge, besluit 2026-09-13)
+ 2 als een whitelist-term óf een dienststoring-zin in de TITEL staat
doorlaten ⇔ score ≥ 3 én (titel-signaal ∨ geen zwakke term in de score)
```
Stappen:
1. **Sterk titel-signaal — door.** Een whitelist-term (NS, ProRail, spoorstoring,
perron, bushalte, ovpay, openbaar vervoer, …) of een dienststoring-zin
(‹vervoerswoord› `rijdt/rijden … niet`, bv. 'sprinters rijden niet', 'metro rijdt niet')
in de titel is voldoende.
2. **Context — som ≥ 3.** Gewichten: **sterk (2)** = onmiskenbaar OV
(vervoerders, spoorinfra, bus-samenstellingen, trein-storingsjargon, tram/metro/halte,
storingszin, …), **zwak (1)** = dubbelzinnig, elk met een eigen familie
(trein, bus, station(s), sprinter, reiziger, vervoerder, sein, overweg, breng, …)
en alleen als combinatie geldig. Max-2-stelsel: geen enkele familie weegt meer dan 2,
één sterke hit kan nooit solo doorlaten.
3. **Titel-anker.** Zonder enig OV-signaal in de titel (geen familie-hit, geen
whitelist-term, geen storingszin) mag de score geen zwak woord bevatten —
'station' + 'pendelbus' + 'bus' in een samenvatting is geen OV-bewijs.
Uitzondering die bewust blijft: **twee sterke termen in de samenvatting** (bv. de
delftopzondag-bereikbaarheidszin 'tram 1, halte Buitenhofdreef', besluit 2026-09-13).
Zwarte lijst (`MATCH_FILTER_BLACKLIST`): gematchte woorden zonder OV-waarde —
na de v1.14.0-opschoning alleen nog `treinstraat`/`treinweg`. De oudere
stations\*-vormen (`stationsstraat`, `stationair`, `stationcar`, `stationeren`, …)
zijn inert sinds de woordgrens-familie `\bstations?\b` en worden door tests bewaakt.
### Bekende bewust-geaccepteerde doorlatingen / weigeringen
- *Door*: 'met een tram en halte bereikbaar'-zinnen in lokale berichten (2 sterke
termen, samenvatting) — kurk op de fles is het max-2-stelsel + titel-anker.
- *Geweerd*: krant 'De Metro', café 'De Tram', atleet-'sprinter',
'VW-bus'/'schoolbus' (koppelteken- en woordgrensregels), werkwoord 'brengen',
idioom 'goed met elkaar overweg kunnen', 'een wijk opwaarderen',
brandweerjargon 'sein brandmeester', figuratief 'weer op de rails',
achternaam 'Hermes' (Margraten), 'restaurant/woning naast busstation'.
## Tests
```bash
python3 validate_filter.py
```
Vaste invarianten + regressie (momenteel **90 cases**):
1. dubbelzinnige woorden → alleen zwak (1), nooit whitelist;
2. onmiskenbare OV-termen → sterk (2) of whitelist;
3. alle families compileren, gewichten in {1, 2};
4. elke whitelist-term zit óók in een familie én elke zwartgelijste term wordt door
een familie gematcht (geen inert gewicht).
**Verplicht vóór elke filterwijziging** (zie `AGENTS.md`): het classificatiebeleid in
`buzzrss/bot.py` lezen, `validate_filter.py` groen, en de **compleetheids-audit**:
OV-gap-scan over de afgewezen artikelen van de specialist-feeds (SpoorPro,
Treinreiziger, Mobiliteit; query op `entry` in PostgreSQL-schema `mbp_rss`),
nieuwe woordkandidaten klasseer je volgens het beleid (minimaal één extra
taalmodel; enige stem 'TWIJFEL'/'NIET-OV' ⇒ zwak).
## Configuratie
Zie `base-config.yaml` (wordt in het gebouwde `.mbp` meegeleverd):
| Sleutel | Default | Werking |
|---|---|---|
| `update_interval` | 60 | Poll-interval in minuten |
| `max_backoff` | 7200 | Maximale backoff (minuten) bij faalende feeds |
| `poll_parallelism_limit` | 10 | Maximaal aantal feeds tegelijk pollend |
| `spam_sleep` | 2 | Pauze tussen berichten bij broadcast (‑1 = parallel) |
| `command_prefix` | `rss` | Commando-prefix |
| `notification_template` | `New post in $feed_title: [$title]($link)` | Standaard meldingssjabloon |
| `run_log_file` | `/data/buzzrss-runs.log` | Per-run samenvattingsregel (leeg = uit) |
| `whatsapp_api` / `whatsapp_session` / `whatsapp_chat_id` | – | WhatsApp-forward (leeg adapter = uit) |
| `exclude_paywalled` | true | Betaalmuur-artikelen overslaan |
| `title_dedup_days` | 30 | Cross-feed titel-dedup-venster (0 = uit) |
| `admins` | – | Gebruikers die room-permissies mogen omzeilen |
## Deploy
Productie draait het **geïnstalleerde `.mbp`** (maubot), niet deze werkdirectory —
een commit is pas actief na build + upload + restart. Exacte recepten staan in de
maubot-beheerskill (`skill://maubot`) en de Obsidian-vault (Matrix Synapse en MAS.md).
In het kort:
```text
1. mbc build uit de VOLLEDIGE bron (buzzrss/ + maubot.yaml + base-config.yaml —
zonder base-config.yaml faalt de instance met BaseMissingError)
2. POST /_matrix/maubot/v1/plugins/upload?allow_override=true (admin-login)
3. docker restart matrix-bot-maubot
```
**NOOIT maubot `reload` gebruiken** — die laat zombie-commandhandlers achter
(één commando → N antwoorden). Alleen een proces-herstart wist de handlers.
## Architectuur in het kort
- Poll-loop → per feed conditional-fetch of HTML-scrape → nieuwe entries
(`feed_id` + `id` PK-dedup) → cross-feed titel-dedup → `_filter_passes` →
broadcast naar alle subscriptions + optioneel WhatsApp (`_send_whatsapp`).
- Persistente tabel `whatsapp_posted` voor WhatsApp-dedup.
- DB: PostgreSQL-schema `mbp_rss` (`feed`, `entry`, `subscription`,
`whatsapp_posted`); migraties via maubot's `UpgradeTable` (`buzzrss/migrations.py`).
## Licentie
AGPL-3.0-or-later (upstream: maubot/rss, © Tulir Asokan).