Eigen maubot RSS-fork met whitelist-filter (titel+summary) voor ov-only nieuws — Buzz rss bot
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-13 09:01:03 +02:00
buzzrss v1.14.0: refactor OV-filter naar driestapsmodel — één regel i.p.v. drie uitzonderingstakken 2026-09-13 08:57:49 +02:00
.gitignore docs: README voor de repo — filter v1.14.0, commando's, config en deploy 2026-09-13 09:01:03 +02:00
AGENTS.md AGENTS.md: compleetheids-audit verplicht bij elke filterwijziging (max-2-stelsel, besluit 2026-09-12) 2026-09-12 19:58:41 +02:00
base-config.yaml v1.9.9: één OV-familie telt 1x per artikel — herhaling en beide velden wegen niet dubbel 2026-09-11 09:28:09 +02:00
maubot.yaml v1.14.0: refactor OV-filter naar driestapsmodel — één regel i.p.v. drie uitzonderingstakken 2026-09-13 08:57:49 +02:00
README.md docs: README voor de repo — filter v1.14.0, commando's, config en deploy 2026-09-13 09:01:03 +02:00
validate_filter.py v1.14.0: refactor OV-filter naar driestapsmodel — één regel i.p.v. drie uitzonderingstakken 2026-09-13 08:57:49 +02:00
WHATSAPP_DOORSTUREN_PLAN.md v1.8.1: idempotente start() — guard voorkomt dubbele poll-loop bij maubot double-start 2026-09-09 23:46:22 +02:00

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 (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:

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

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:

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