157 lines
No EOL
7.9 KiB
Markdown
157 lines
No EOL
7.9 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.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). |