- Python 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| buzzrss | ||
| .gitignore | ||
| AGENTS.md | ||
| base-config.yaml | ||
| maubot.yaml | ||
| README.md | ||
| validate_filter.py | ||
| WHATSAPP_DOORSTUREN_PLAN.md | ||
buzz-rss — RSS/Atom → Matrix, gefilterd op OV-relevantie
Maubot-plugin nl.eddydevink.rss (fork van xyz.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(ziebuzzrss/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.linkwordt gepost (vooruit-links, zonder titel), met dedup-check tegen de doelgroep. Uit zolangwhatsapp_api/whatsapp_chat_idleeg 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:
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:
- 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. - 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.
- 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
python3 validate_filter.py
Vaste invarianten + regressie (momenteel 90 cases):
- dubbelzinnige woorden → alleen zwak (1), nooit whitelist;
- onmiskenbare OV-termen → sterk (2) of whitelist;
- alle families compileren, gewichten in {1, 2};
- 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:
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+idPK-dedup) → cross-feed titel-dedup →_filter_passes→ broadcast naar alle subscriptions + optioneel WhatsApp (_send_whatsapp). - Persistente tabel
whatsapp_postedvoor WhatsApp-dedup. - DB: PostgreSQL-schema
mbp_rss(feed,entry,subscription,whatsapp_posted); migraties via maubot'sUpgradeTable(buzzrss/migrations.py).
Licentie
AGPL-3.0-or-later (upstream: maubot/rss, © Tulir Asokan).