diff --git a/.gitignore b/.gitignore index 7a60b85..dee8344 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ __pycache__/ *.pyc + +# Taakbestanden die geen onderdeel zijn van de repo +Prompt voor agent*.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..8e6f504 --- /dev/null +++ b/README.md @@ -0,0 +1,157 @@ +# 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 ` | `s`, `sub` | Deze room op een feed abonneren (nieuwe feed wordt direct geïndexeerd) | +| `!rss unsubscribe ` | `u`, `unsub` | Abonnement van deze room opzeggen | +| `!rss subscriptions` | `ls`, `list`, `subs` | Abonnementen in deze room + huidige filter | +| `!rss template [sjabloon]` | `t`, `tpl` | Meldingssjabloon bekijken/wijzigen (variabelen: `$title`, `$link`, `$summary`, `$feed_title`, …) | +| `!rss notice ` | `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). \ No newline at end of file