buzz-rss/README.md

157 lines
No EOL
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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).