diff --git a/.env.example b/.env.example index 96341f2..7ce2057 100644 --- a/.env.example +++ b/.env.example @@ -25,7 +25,7 @@ MAX_REQUESTS_PER_DAY=5 ACK_STUCK_TIMEOUT_MS=300000 # 5 min zonder ack 2 = stuck ACK_PRUNED_AFTER_MS=1800000 # 30 min: stale trackings opruimen (loop stopt dan) ACK_CHECK_INTERVAL_MS=30000 # checkinterval (alleen actief als er iets pending is) -MEDIA_SEND_DELAY_MS=500 # pauze tussen pagina's van een dienst (niet vóór de eerste) +MEDIA_SEND_DELAY_MS=1500 # wacht ná élke media-send (media-pipeline is één tab; voorkomt overlappende sends) # Admin notifications ADMIN_NUMBER=31612345678@c.us diff --git a/AGENTS.md b/AGENTS.md index 77b3228..c2204f1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,13 +10,18 @@ zag in de chat maar **één** bericht (ghost-sends: id geretourneerd, nooit echt - **`/chat/fetchMessages`-acks en de lokale message-store van wwebjs-api zijn een lokale registratie, géén bezorgbewijs.** Ze kunnen ack 1/2/3 tonen voor berichten die nooit aankwamen. -- **Enige geldige bewijzen van levering:** (1) de **live `message_ack`-progressie** zoals - die vlak ná verzending via de webhook binnenkomt; (2) de **zichtbare werkelijkheid op - het apparaat** van de ontvanger/betrokkene. +- **Ook de live `message_ack`-progressie is sinds 2026-09-26 géén bezorgbewijs meer:** + de staged-ghost van dienst 5219 blz 2 kreeg via de webhook een `ack=2` afgegeven voor een + bericht dat nooit getransmitteerd was (stond niet eens in de WhatsApp-app van de eigenaar). + De bridge resolvet media-sends **optimistisch op lokale staging** en fabriceert daarna + ack-events + delivery-info. Ack-tracking (stopt bij ack ≥ 2) kan deze klasse per ontwerp + niet vangen. +- **Enige geldige bewijzen van levering:** (1) de **zichtbare werkelijkheid op het + apparaat** van de ontvanger/betrokkene; (2) `getInfo` (`message.getInfo()`): bij een + staged-ghost blijft `read` leeg terwijl `delivery` gevuld kan zijn — het enige + betrouwbare bridge-signaal is `read`/`readRemaining`. - **Trek NOOIT een leveringsconclusie uit een later gelezen fetchMessages-ack.** Claim geen "bezorgd/gelezen" op basis van de store — dat geeft verkeerde adviezen. -- Dit is de reden dat ack-tracking bestaat: het hangt af van de live events, niet van de - store. ## Waar draait de bot? @@ -176,13 +181,47 @@ Sommige mediabestand-verzendingen keren `success` + message-ID terug maar worden - **Pruning** na `ACK_PRUNED_AFTER_MS` (30 min); de check-loop is **activity-driven** (draait alleen bij openstaande tracking, geen polling in rust). - Vereiste aan de bridge: `message_ack` staat sinds 2026-09-24 in wwebjs-api `.env` **niet** meer in `DISABLED_CALLBACKS`; container is daarvoor herstart. - Env-vars: `ACK_STUCK_TIMEOUT_MS` (default 300000), `ACK_PRUNED_AFTER_MS` (default 1800000), `ACK_CHECK_INTERVAL_MS` (default 30000). -- **Media-spreiding:** `MEDIA_SEND_DELAY_MS` (default **500**) pauzeert tussen de pagina's van één - dienst (niet vóór de eerste). Doel: de wwebjs media-pipeline (één chromium-tab) per-verzending - lucht geven en gelijktijdige storage/upload-conflicten verminderen. 0 = uitschakelen. +- **Media-spreiding:** `MEDIA_SEND_DELAY_MS` (default **500**, nu **1500**) pauzeert **na élke + media-send** (in `sendImageMessage`), niet alleen tussen pagina's van één dienst. De + wwebjs-media-pipeline is EEN chromium-tab; opeenvolgende media-sends — óók naar + verschillende ontvangers — overlappen anders en veroorzaken de staged-ghost-race. + 0 = uitschakelen. (Tijdelijke mitigatie; permanente fix = fork: send óf écht verzenden + óf zichtbaar falen, zie RCA.) Logregels: `[ack] track ( → )`, `[ack] delivered ack= `, `[ack] pruned stale entries`. -**Ack-semantiek (wwebjs 1.34.7):** ack 1 = verstuurd naar server (één grijs vinkje), ack 2 = bezorgd op apparaat (twee vinkjes), ack 3 = geopend/gelezen. Let op: eerdere notities die "één grijs vinkje (ack 2)" noemden, zaten er dus naast. +**Ack-semantiek (wwebjs 1.34.7):** ack 1 = verstuurd naar server (één grijs vinkje), ack 2 = bezorgd op apparaat (twee vinkjes), ack 3 = geopend/gelezen. Let op: eerdere notities die "één grijs vinkje (ack 2)" noemden, zaten er dus naast. **Let op 2 (2026-09-26):** óók deze acks zijn geen leveringsbewijs — zie RCA hieronder. + +### RCA 2026-09-26: staged-ghost = race-conditie, géén bestandprobleem + +Casus: dienst 5219 (2 pagina's), 09:29 gevraagd in de groep, bot stuurt beide pagina's +prive. App toont alleen blz 1; **blz 2 stond in het geheel niet in de WhatsApp-app van de +eigenaar** (geen transmissie). Toch: bridge retourneerde success + message-ID (`3EB040AD…`), +store toont het bericht, live `message_ack` gaf `ack=2`, `getInfo` toonde `delivery` gevuld +maar `read` leeg. Geen enkele fout in de container-logs. + +Mechanisme (bewezen uit fork-broncode + logs): +- De send loopt via `WAWebSendMsgChatAction.addAndSendMsgToChat(chat, message)` → + `[msgPromise, sendMsgResultPromise]`. De bridge zet `waitUntilMsgSent: true` + (stock wwebjs-api, `clientController.js`), maar de promise resolve **ook optimistisch op + lokale staging** — een echte serverbevestiging wordt nooit afgewacht. HTTP-success + + message-ID betekenen dus **niet** "verzonden". +- **Patroon (gebruiker, consistent met logs):** de ghost treedt alleen op bij **2+ media + direct achter elkaar**, en dan altijd de **2e** van het paar; enkele sends falen nooit. +- Verklaring: de bot wacht per pagina op het HTTP-signaal (staging), niet op echte + transmissie → blz 2 wordt gestart terwijl blz 1's **echte** upload/transmissie nog in + de lucht is. Twee echte media-transmissies overlappen in de ene chromium-tab → de 2e + verliest (intermitterend) → staged-ghost. +- Gevolgen: **`MEDIA_SEND_DELAY_MS` kan dit niet oplossen** (wacht op optimistisch + signaal); **poppler/re-render maskeert het alleen** (nieuwe poging = nieuwe race-rol, + soms gewonnen — wassen neus); **`sendMessageViaUi` lost het óók niet op** (wacht op + dezelfde promises, alleen omzeilt het de media-editor); **ack-tracking kan het niet + vangen** (stopt bij gefabriceerde ack ≥ 2). + +Enige betrouwbare signalen: apparaat-waarheid; `getInfo` `read`/`readRemaining` (blijft +leeg bij staged). Aanbevolen richting (nog niet geïmplementeerd): de fork moet ná +`addAndSendMsgToChat` écht wachten op transmissiebevestiging — pas dan serialiseren +multi-page sends vanzelf en is de race weg. ## Media bezorgtest (afbeeldingen die WhatsApp weigert) @@ -200,6 +239,15 @@ ack-tracking (zie hierboven), re-render via poppler en een verse bezorgtest. **Ack-telling:** ack 1 = één grijs vinkje (niet bezorgd), ack 2 = bezorgd op apparaat, ack 3 = geopend. (Eerdere notities "één grijs vinkje (ack 2)" zaten er dus naast.) +> **⚠️ 2026-09-26: onderscheid twee foutklassen.** De hierboven beschreven +> bestand × ontvanger/offline-klasse (ack 1-hang, ontvanger offline of app weigert +> render) is niet hetzelfde als de **staged-ghost-race** (zie RCA hierboven): daar is het +> bestand nooit het probleem en **re-render (poppler) is een wassen neus** — het maskert +> de race alleen. Check bij een "niet aangekomen bladzijde" eerst: was het de 2e pagina +> van een paar binnen ~1s? Dan is het de race, geen bestand. Echte remedie is nog in +> ontwerp (fork: wachten op transmissiebevestiging); voor nu: opnieuw versturen (eventuele +> nieuwe poging) en apparaat als waarheid. + **Na elke omruiling van de dienstenmap: de volledige media-test doorlopen vóór ingebruikname.** **Afbeeldingsformaat:** sinds `07b511e` leidt `sendImageMessage` de mimetype af uit de diff --git a/README.md b/README.md index bc20706..ec5ee8c 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,15 @@ Bij elke interactie met de groep controleert de bot of het pinned bericht nog be ## Ack-tracking (ghost-send detectie, sinds 2026-09-24) +> **⚠️ 2026-09-26 — ack-tracking is voor media ongeldig gebleken.** Zie de RCA in +> AGENTS.md ("staged-ghost = race-conditie"): de bridge resolvet media-sends optimistisch +> op lokale staging en fabriceert daarna óók de live `message_ack`-events (bewijs: dienst +> 5219 blz 2 kreeg `ack=2` voor een bericht dat nooit getransmitteerd was). Daardoor: +> (1) de tracker stopt bij gefabriceerde ack ≥ 2 en mist de ghost; (2) "live ack" is géén +> bezorgbewijs meer — enige waarheid is het apparaat en `getInfo` (`read` blijft leeg). +> De klasse is een **race bij 2+ media achter elkaar** (altijd de 2e pagina), niet een +> bestandprobleem; `MEDIA_SEND_DELAY_MS` en re-render (poppler) lossen het niet op. + Sommige mediabestand-verzendingen keren `success` + message-ID terug maar worden nooit **bezorgd**: ze blijven op één grijs vinkje hangen (ack 1) en de ontvanger ziet niets aankomen. Dit is een bestand×ontvanger-interactie in WhatsApp's mediaverwerking (niet de @@ -181,15 +190,17 @@ sessie — zie AGENTS.md). De bot detecteert dit via `message_ack`-events en waa | `ACK_STUCK_TIMEOUT_MS` | Leeftijd zonder ack 2 = stuck | `300000` (5 min) | | `ACK_PRUNED_AFTER_MS` | Stale tracking opruimen | `1800000` (30 min) | | `ACK_CHECK_INTERVAL_MS` | Checkinterval (alleen actief bij pending) | `30000` | -| `MEDIA_SEND_DELAY_MS` | Pauze tussen pagina's van één dienst (niet vóór de eerste) — geeft de wwebjs media-pipeline lucht | `500` | +| `MEDIA_SEND_DELAY_MS` | Wacht ná élke media-send (media-pipeline is één tab; voorkomt overlappende sends naar elk doel) | `1500` | Opvallende logregels: `[ack] track …`, `[ack] delivered ack=…`, `[ack] pruned …`. > **Let op — bridgestore is geen bezorgbewijs (2026-09-24):** de lokale store van wwebjs-api > (`/chat/fetchMessages`-acks) kan ack 1/2/3 tonen voor berichten die de ontvanger nooit zag -> (ghost-sends). Trek hier géén leveringsconclusie uit. Ack-tracking gebruikt de **live -> `message_ack`-progressie**, niet de store; bevestiging komt van het apparaat van de -> ontvanger. +> (ghost-sends). Trek hier géén leveringsconclusie uit. **En sinds 2026-09-26 geldt hetzelfde +> voor de live `message_ack`-progressie** (zie waarschuwing bovenin deze sectie): ook die +> kan gefabriceerd zijn bij een staged-ghost. Enig betrouwbaar bridgesignaal is `getInfo` +> (`message.getInfo()` → `read`/`readRemaining`); eindoordeel is altijd het apparaat van +> de ontvanger. ## Spam protectie diff --git a/src/messageHandler.js b/src/messageHandler.js index a94f579..74affee 100644 --- a/src/messageHandler.js +++ b/src/messageHandler.js @@ -68,20 +68,16 @@ async function handleWebhook(payload) { return } + // Pacing gebeurt nu centraal in sendImageMessage (na élke media-send), zodat óók + // afbeeldingen naar verschillende ontvangers niet overlappen in de ene chromium-tab. const pngPaths = getPngFilePaths(dienst) if (!pngPaths || pngPaths.length === 0) { await sendTextMessage(author, `Geen pagina's gevonden voor dienst ${dienstnummer}.`) return } - // Defensieve spreiding tussen pagina's (niet vóór de eerste): verlaagt de kans op - // gelijktijdige storage/upload-conflicten in de wwebjs-media-pipeline. - const mediaSendDelayMs = parseInt(process.env.MEDIA_SEND_DELAY_MS, 10) || 500 for (let i = 0; i < pngPaths.length; i++) { const pngPath = pngPaths[i] - if (i > 0 && mediaSendDelayMs > 0) { - await new Promise(resolve => setTimeout(resolve, mediaSendDelayMs)) - } const filename = pngPath.split('/').pop() try { await sendImageMessage(author, pngPath, filename) diff --git a/src/whatsappApi.js b/src/whatsappApi.js index 1d8e71a..e5b0534 100644 --- a/src/whatsappApi.js +++ b/src/whatsappApi.js @@ -71,6 +71,14 @@ async function sendImageMessage(chatId, filePath, filename) { }) const messageId = response.data?.message?._data?.id?.id registerSend(messageId, chatId, filename) + // Pacing na élke media-send: de wwebjs-media-pipeline is EEN chromium-tab. Twee + // opeenvolgende media-sends (pagina's van één dienst óf afbeeldingen naar + // verschillende ontvangers) mogen niet overlappen — dat veroorzaakt de staged- + // ghost-race (2e send verliest). Wacht dus altijd MEDIA_SEND_DELAY_MS ná een send. + const mediaSendDelayMs = parseInt(process.env.MEDIA_SEND_DELAY_MS, 10) || 500 + if (mediaSendDelayMs > 0) { + await sleep(mediaSendDelayMs) + } return response.data }, `sendImageMessage ${filename} to ${chatId}`) }