buzz-rss/WHATSAPP_DOORSTUREN_PLAN.md

13 KiB

buzz-rss: nieuwsfeed-URLs doorsturen naar WhatsApp-groep "OV in de media"

Context

De maubot-plugin nl.eddydevink.rss (bron: /Users/eddy/src/buzz-rss, draait als instance rss in matrix-bot-maubot op nextcloud) filtert nieuwsfeeds op OV-trefwoorden (match_filter) en plaatst ze per feed-subscription in Matrix-rooms. De wens: alleen de link van dezelfde geplaatste berichten óók plaatsen in de WhatsApp-groep "OV in de media" in de community "EBS Klets en Diensten opvraag", via de bestaande wwebjs-api op nextcloud (:3001).

Doelgroep-ID (vastgesteld via de DevTools-chatcollectie van de Eddy-sessie-browser): 120363429643087130@g.us (type LINKED_SUBGROUP, titel "OV in de media"). De Verkeersberichten-groep (120363427013472489@g.us) is een andere groep en blijft voor EBS-verkeersberichten.

Read-only-route voor de dedup-check: fetchMessages, getChats en getChatById falen in de aangepaste container met {"success":false,"error":"r"} (fout uit de browser-evaluate. De werkende read-route is POST /client/searchMessages/{session} — getest tegen de doelgroep: substring-query op de bericht-body; de response bevat messages[] met _data.body (berichttekst) en _data.id.remote (chat-id van het bericht. Alleen deze read-route en sendMessage worden gebruikt. Een searchMessages-query zonder match retourneert {"success":true,"messages":[]}.

. Deze foutsymptomen en workaround zijn gedocumenteerd in de Obsidian-vault: [[Whatsapp rest-api]] → sectie Troubleshooting: error r op read-endpoints. Raadpleeg die notitie bij het tegenkomen van deze fout (of client/searchMessages ineens faalt).

Hard verbod tijdens uitvoering: de wwebjs-api container NOOIT herstarten en geen WhatsApp-sessie beëindigen/restarten. Alleen de maubot-instance rss mag herladen (eigen container, geen WhatsApp-sessies.) WhatsApp-kanaal = uitsluitend de wwebjs-api HTTP-API op http://192.168.5.252:3001 (bereikbaarheid vanuit de maubot-container is geverifieerd: /ping → pong). Read-only container-inspectie (docker exec cat/logs, curl GET/POST tegen de API) is toegestaan; sessie-/container-management niet.

Approach

Alle wijzigingen in /Users/eddy/src/buzz-rss. Er is geen test-suite in de repo; er is geen bestaande WhatsApp-forward-functie (de EBS-verkeersberichten-bot is een los Node-script, geen herbruikbare bibliotheek — NIET importeren, alleen als voorbeeld van de wwebjs-API-call).

  1. Config-keys toevoegen — buzzrss/bot.py, class Config.do_update (rond regel 44-56): vóór helper.copy("admins") toevoegen:

    helper.copy("whatsapp_api")
    helper.copy("whatsapp_session")
    helper.copy("whatsapp_chat_id")
    

    Koppel daaraan in base-config.yaml (achter run_log_file, vóór exclude_paywalled — volgorde in YAML maakt niet uit, wel alle drie toevoegen):

    # Doorsturen naar WhatsApp (wwebjs-api op nextcloud :3001). Leeg whatsapp_api/whatsapp_chat_id = uit. Alleen de link wordt gepost (geen titel.
    whatsapp_api: http://192.168.5.252:3001
    whatsapp_session: EdGpt
    whatsapp_chat_id: 120363429643087130@g.us
    

    Geen whatsapp_notification_template: het WhatsApp-bericht is uitsluitend de onbewerkte entry.link (WhatsApp maakt daar automatisch link-preview van; geen Markdown-links. Een ander formaat later = code-wijziging nodig.

  2. Nieuwe methode _send_whatsapp in RSSBot (buzzrss/bot.py, naast _send, rond regel 200). Dedup-check is verplicht vóór elke post; faalt de check, dan wordt niet gepost. self.http is de bestaande aiohttp-sessie (self.client.api.session, al gebruikt voor feed-fetches met absolute URL's). Foutafhandeling: API-error of netwerkfout → log + False, geen crash; niet-geconfigureerd → False zonder fout:

    async def _send_whatsapp(self, feed: Feed, entry: Entry) -> bool:
        """Post entry.link naar de WhatsApp-groep via wwebjs-api (met dedup-check). True = nieuw geplaatst."""
        api = self.config.get("whatsapp_api", "")
        chat_id = self.config.get("whatsapp_chat_id", "")
        if not api or not chat_id:
            self.log.debug("WhatsApp forwarding disabled")
            return False
        link = entry.link
        if not link:
            self.log.warning(f"No link for {entry.id}, skipping WhatsApp")
            return False
        session = self.config.get("whatsapp_session", "EdGpt")
        base = api.rstrip("/")
        # Dedup: sla over als de URL al in de doelgroep staat. Search is globaal
        # over alle chats; filter op chat-id van het gevonden bericht.
    
        try:
            resp = await self.http.post(
                f"{base}/client/searchMessages/{session}",
                json={"query": link, "options": {"limit": 1}},
                timeout=aiohttp.ClientTimeout(total=20),
            )
            data = await resp.json()
            if data.get("success"):
                for msg in data.get("messages", []):
                    msg_data = msg.get("_data", {}).get("id", {})
                    if (
                        msg_data.get("remote") == chat_id
                        and link in msg.get("_data", {}).get("body", "")
                    ):
                        self.log.info(f"{entry.id} already in WhatsApp group, skipping")
                        return False
        except Exception as e:
            self.log.warning(f"WhatsApp dedup check failed for {entry.id}: {e}")
            return False
        try:
            resp = await self.http.post(
                f"{base}/client/sendMessage/{session}",
                json={"chatId": chat_id, "content": link, "contentType": "string"},
                timeout=aiohttp.ClientTimeout(total=20),
            )
            data = await resp.json()
            ok = bool(data.get("success"))
            self.log.info(f"WhatsApp post {entry.id} to {chat_id}: {'ok' if ok else 'failed: ' + str(data)}")
            return ok
        except Exception as e:
            self.log.warning(f"Failed to send {entry.id} to WhatsApp {chat_id}: {e}")
            return False
    
  3. _broadcast uitbreiden (regel ~235): ná de bestaande filter-check en ná de matrix-verzendlus, vlak vóór de return, één WhatsApp-send per entry (niet per subscription):

    sent = sum(1 for resultin results if result is not None)
    whatsapp_ok = await self._send_whatsapp(feed, entry)
    return (1 if (sent or whatsapp_ok) else 0, 0, len(results) - sent)
    

    Concreet:de huidige regel return (1 if sent else 0, 0, len(results) - sent) vervangen door de drie regels hierboven. Docstring-regel "placed = 1 when at least one room received the message" aanpassen naar "placed = 1 when the entry was posted to at least one channel (Matrix room or newly posted WhatsApp link)". De failed-component blijft het aantal gefaalde Matrix-subscriptions. WhatsApp-skip (URL al aanwezig) en WhatsApp-fouten loggen zelf; ze mogen de bestaande telling niet vertekenen. Filter-semantiek ongewijzigd: alleen entries die de filter-pas halen komen in _send_whatsapp terecht.

  4. Versie bump — maubot.yaml: version: 1.5.4 → version: 1.6.0.

Critical files & anchors

  • buzzrss/bot.py — Config.do_update (regel ~44), _send (~200), _broadcast (~225-250). Imports reeds aanwezig (Template, aiohttp, attr, Feed, Entry; Template blijft voor de Matrix-notification-template. Implementer herleest de regio's vóór het editen.

  • base-config.yaml — extra_files van het mbp; ontbreken = BaseMissingError bij start.

  • maubot.yaml — versie voor de mbp-naam en uploadlaar.

Verification

Volgorde na elke stap doorvoeren; wacht NIET op project-brede test (geen suite}). Werk: /Users/eddy/src/buzz-rss. Nooit wwebjs/sessie-acties — alleen maubot-management-API en de wwebjs HTTP-API (searchMessages/sendMessage).

  1. Compile-check na de code-wijziging: python3 -m py_compile buzzrss/bot.py → exit 0.

  2. Deploy volgens maubot-skill-recept (skill maubot lezen vóór uitvoeren; MAUBOT_ADMIN_PASSWORD uit vault-notitie "Matrix Synapse en MAS.md" → secrets-tabel, NOOIT in chat/plan):

    • Bronmap (met buzzrss/, maubot.yaml, base-config.yaml) via tar+base64 naar de container brengen in /data/deploytmp/; mbc build in die werkmap (géén -o); verifieer .mbp-inhoud: unzip -l … | grep base-config.
    • Upload (POST /plugins/upload?allow_override=true) → POST /plugin/nl.eddydevink.rss/reload → PUT /instance/rss met {"enabled": true, "started": true} → GET /plugin/nl.eddydevink.rss toont versie 1.6.0 en beide true.
  3. Poll draait: na reload binnen ~60 s één verse regel run complete … in /matrix/maubot/data/buzzrss-runs.log (ssh aiaiaiaiagent@nextcloud 'sudo -A tail …'); géén verdubbelde regels met dezelfde timestamp. Bewijst dat de nieuwe code startte (start-fout zou de instance op enabled=False zetten + geen run-log.).

  4. End-to-end WhatsApp-kanaal (nieuw gedrag, concrete input → observable output): throwaway-script (lokaal, daarna verwijderen) dat EXACT dezelfde volgorde doet als _send_whatsapp, tegen dezelfde API en sessie:

    • 4a — dedup-check (lege verwachting): POST http://192.168.5.252:3001/client/searchMessages/EdGpt met body {"query": "https://www.ovpro.nl/", "options": {"limit": 1}}}. Verwacht: {"success":true,"messages":[]} (URL nog niet in de groep.) Als die er WEL al instaat: stap 4b slaat het posten over en is de dedup-check daarmee bewezen — ga door naar stap 5.
    • 4b — post alleen de link: POST …/client/sendMessage/EdGpt met body {"chatId": "120363429643087130@g.us", "content": "https://www.ovpro.nl/", "contentType": "string"}. Verwacht: {"success":true}; het bericht (alleen de URL) is zichtbaar in de groep (gebruiker kan bevestigen. Geen titel in het bericht. .
    • 4c — dedup-check (verse post vindbaar): herhaal 4a → verwacht exact 1 match met _data.id.remote == "120363429643087130@g.us" en _data.body die de URL bevat. Bewijst dat de zoekfunctie een net geposte link direct vindt (geen index-vertraging. Vindt 4c de verse post niet direct, dan is de dedup-check onbetrouwbaar → stoppen en overleggen met de gebruiker vóór verdere uitvoering (geen post zonder werkende check.).
    • Contingentie (pre-gedecideerd): als de call faalt met een lidmaatschapsfout (EdGpt kan niet posten in die subgroep), zet dan in de maubot-instance-config whatsapp_session: Eddy (in de web-UI config-editor of PUT /config/…), herstart daarna alleen de instance rss, en herhaal deze stap. De sessie-Eddy heeft de groep in zijn chatcollectie (bewezen tijdens onderzoek).
  5. Plugin-pad zelf: de eerstvolgende poll die een nieuw OV-match-item vindt produceert in maubot-logs de regel WhatsApp post … ok (of … already in WhatsApp group, skipping bij een al geposte URL) én in de Matrix-room. Komt er (nog) geen nieuw item, dan is stap 4 het bewijs van de koppeling; de plugin-code is door stap 1-3 geverifieerd. Verwijder hierna het throwaway-script.

Assumptions & contingencies

  • Berichtformaat: uitsluitend de onbewerkte entry.link; geen titel, geen template-config. WhatsApp maakt automatisch link-preview van de URL. Een ander formaat later = code-wijziging nodig (bewuste keuze volgend uit de eis "Alleen de link moet worden gepost, niet de titel".

  • Dedup-check: verplicht vóór elke post. De search (client/searchMessages) doorzoekt globaal alle chats en matcht substring op de bericht-body; de filter op _data.id.remote == chat_id (plus entry.link in body als extra beveiliging) beperkt tot de doelgroep. Exacte-string-semantiek: een eerdere post met een variant van dezelfde link (bijv. andere scheme, www, trailing slash, utm-params) wordt niet als duplicaat herkend, omdat de body die variant bevat en de query (entry.link) geen substring is van die variant. Acceptabel: we posten exact dezelfde string die we zoeken; een eerdere post van dezelfde entry heeft dezelfde link. .

  • Check-fout → geen post: faalt de dedup-search (netwerk/API-fout), dan wordt de entry niet naar WhatsApp gepost (return False, warning-log). Dit volgt uit de vereiste "alvorens te posten moet worden gecheckt of de url nog niet in de whatsappgroep geplaatst is".

  • Sessie: default EdGpt (de bot-sessie, zelfde als andere EBS-bots). Valt de test-send in stap 4 uit op lidmaatschap van de subgroep → zie stap 4-contingentie (sessie Eddy).

  • Filter: WhatsApp-forward krijgt exact dezelfde filter-pas als Matrix — er wordt niets extra's gefilterd of doorgelaten; geen aparte whatsapp-filter.

  • Herstarten/beëindigen van de wwebjs-container of WhatsApp-sessies is ten strengste verboden; alles verloopt via HTTP-API (searchMessages + sendMessage) en de maubot-management-API.