- 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.
144 lines
No EOL
7.3 KiB
Markdown
144 lines
No EOL
7.3 KiB
Markdown
# 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). |