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

7.3 KiB
Raw Permalink Blame History

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

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

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:

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