buzz-rss/README.md
Eddy de Vink ac30575660 v1.15.0: filter vereenvoudigd naar drie regels — 30 semantische groepen i.p.v. families+whitelist+blacklist
- Strip de filter terug tot: (1) sterk signaal in de titel -> door;
  (2) score van unieke semantische groepen >= 3 met titel-signaal -> door;
  (3) anders weg. Geen whitelist (82 termen), geen blacklist, geen span-merge,
  geen dienststoring-titelanker meer; woordgrenzen/lookbehinds weren de
  bekende botsingen structureel (schoolbus/VW-bus/stationsstraat/stationair/
  treinstraat/treinweg).
- Lijsten 49 families + 82 whitelist + 2 blacklist = 133 -> 16 sterk + 14
  ambigu = 30 groepen; filterzone 524 -> ~292 regels; match-methodes 4 -> 2.
- Vijf historische uitzonderings-families geschrapt (busongeluk, sein, breng,
  hermes, inchecken, kaartje, rails, staking, e.d.) — die woorden leveren nu
  0 signalen op en de bekende false positives blijven geweerd.
- Completheids-audit ving de Maaslijn/Nedersaksenlijn/Kamperlijn/waterbus-
  regressie op (hersteld); resterende zero-hit-kandidaten geen wijziging.
- validate_filter.py: invarianten 1-3 + 113 regressie-cases (90 onveranderd
  groen + 23 nieuw); prompt-matrix 27/27.
- Gedragswijziging op 45 productiedagen: uitsluitend false positives extra
  geweerd (buitenlandse veerboot-rampen, Zwitsers busongeluk, delftopzondag
  tram+halte, Cox/RET, tram-blokkade); 0 recalls verloren.
- README/AGENTS.md/vault bijgewerkt.
2026-09-13 09:28:28 +02:00

144 lines
No EOL
7.3 KiB
Markdown
Raw Permalink 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.15)
De volledige classificatie + de werkwijze voor nieuwe woorden staat in
`buzzrss/bot.py`, boven `MATCH_FILTER_STRONG` — **dat blok is leidend** bij elke
wijziging. Samengevat, in drie regels:
```text
1. staat er een STERK signaal in de TITEL? -> PLAATSEN
2. score = som unieke groeps-gewichten ≥ 3 én de titel bevat een signaal? -> PLAATSEN
3. anders -> NIET PLAATSEN
```
- **Sterk (2 punten, titel = zelfstandig)**: vervoerders (NS, ProRail, Arriva,
RET, GVB, Qbuzz, Deutsche Bahn, TESO, ...), spoorjargon (spoorstoring, spoorlijn,
wisselstoring, treinstoring, ertms, perron, ...), bus-samenstellingen
(bushalte, buslijn, busbaan, buschauffeur), tramverkeer/tramlijn, metrolijn,
intercity/streekvervoer/veerdienst/buurtbus, conducteur, ov-chipkaart/ovpay,
openbaar vervoer, lightrail, 'lijn 12', ICE-frasen, rangeer... én de
dienststoring-zin ('sprinters rijden niet', 'metro rijdt niet').
- **Ambugue (1 punt; halte/tram/metro/spoorbrug 2)** — contextafhankelijk,
nooit zelfstandig voldoende, alleen als combinatie: `bus`, `trein`,
`station(s)`, `reiziger`/`passagier`, `vervoer(der)`, `sprinter`,
`pendelbus`/`shuttlebus`, `dienstregeling`, `ontsporen`, `spoor`.
- **Semantische groepen tellen één keer per artikel**: 'bus' + 'buslijn' +
'busvervoer' + 'busstation' is één bus-signaal (bus-samenstelling, bus,
station — nooit vier onafhankelijke signalen).
- **Geen whitelist, geen blacklist**: woordgrenzen en koppelteken-lookbehinds
weren structureel 'schoolbus'/'VW-bus', 'stationsstraat'/'stationair'/
'stationcar'/'treinstraat'/'treinweg'. Bekende false positives blijven geweerd:
krant 'De Metro', café 'De Tram', atleet-'sprinter', werkwoord 'brengen',
idioom 'goed met elkaar overweg kunnen', 'een wijk opwaarderen',
brandweerjargon 'sein brandmeester', figuratief 'weer op de rails',
achternaam 'Hermes', buitenlandse veerboot-/busrampen, 'restaurant/woning naast
busstation'.
## Tests
```bash
python3 validate_filter.py
```
Vaste invarianten + regressie (momenteel **110 cases**):
1. gewichten geldig (sterk 2; ambigu in {1, 2}), drempel 3;
2. trefwoorden matchen hooguit één groep per categorie (geen overlappende regexes);
3. semantische varianten ('bus' + 'buslijn' + 'busvervoer', 'station'-reeks)
leveren samen één signaal op;
4. regressie: bekende positive/negative/combinatie-cases (precision-first).
**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).