diff --git a/.env.example b/.env.example index 21102fb..dda7ccc 100644 --- a/.env.example +++ b/.env.example @@ -21,11 +21,6 @@ DIENSTEN_DIR=../Whatsapp_Dienst/diensten # Rate limiting MAX_REQUESTS_PER_DAY=5 -# Ack tracking (ghost-send detectie; vereist message_ack aangezet in wwebjs-api .env) -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) - # Admin notifications ADMIN_NUMBER=31612345678@c.us diff --git a/AGENTS.md b/AGENTS.md index 9d52142..978197b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -172,15 +172,14 @@ Deze maatregelen zijn toegevoegd na een incident met HTTP 500 van de wwebjs-api - Bij `EADDRINUSE` wordt automatisch na 5 seconden opnieuw geprobeerd te binden - **Graceful shutdown**: bij SIGTERM/SIGINT worden actieve connecties netjes afgesloten (max 10s timeout) -### Ack-tracking voor hangende media (september 2026) +### Ack-tracking voor hangende media — VERWIJDERD (dwaalspoor, 2026-09-26) -Sommige mediabestand-verzendingen keren `success` + message-ID terug maar worden nooit bezorgd (blijft op één grijs vinkje). Bevinding 2026-09-24: dit is een **bestand × ontvanger-interactie**, geen sessie-probleem — hetzelfde bestand hing (ack 1) voor één ontvanger maar bezorgde (ack 3) bij de admin in dezelfde verse sessie, en hing ook ná een sessie-herstart. Daarom wordt er bij een ack-1-hang **geen sessie-herstart** meer gedaan. - -- `src/ackTracker.js` registreert elke media-verzending en volgt de `message_ack`-events (webhook `message_ack` in `messageHandler.js`). -- **ack ≥ 2** = bezorgd → tracking verwijderd. **Blijft > `ACK_STUCK_TIMEOUT_MS` (5 min) op ack < 2** → éénmalige admin-notificatie met bestandsnaam + chat + advies "opnieuw renderen + bezorgtest". Geen resend naar de aanvrager. -- **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). +Er stond een ack-tracker (`src/ackTracker.js`) die `message_ack`-events volgde en bij +ack<2 na `ACK_STUCK_TIMEOUT_MS` een admin-melding gaf. **Verwijderd uit de code**: de +bridge fabriceert acks (dienst 5219 blz 2 kreeg `ack=2` voor een bericht dat nooit +getransmitteerd was) — ze zijn géén bezorgbewijs en gaven verkeerde signalen. Daarom ook +`message_ack`-verwerking in de bot weg. Enige betrouwbare bronnen: het apparaat en +`getInfo` (`read`/`readRemaining`). Zie RCA hieronder. - **Media-pacing zit bridge-breed** (wwebjs-api, niet de bot): bij een staged-ghost-race (2026-09-26, dienst 5219 blz 2) overlappen opeenvolgende media-sends in de ene chromium-tab en verliest de 2e (bestaat in store + ack2 maar wordt nooit verzonden). @@ -192,8 +191,6 @@ Sommige mediabestand-verzendingen keren `success` + message-ID terug maar worden [[Dienst media bezorgtest]] (RCA 2026-09-26). Tijdelijke mitigatie — permanente fix (fork: send óf echt transmetteren óf zichtbaar falen) staat nog open. -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. **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 @@ -227,50 +224,29 @@ leeg bij staged). Aanbevolen richting (nog niet geïmplementeerd): de fork moet `addAndSendMsgToChat` écht wachten op transmissiebevestiging — pas dan serialiseren multi-page sends vanzelf en is de race weg. -## Media bezorgtest (afbeeldingen die WhatsApp weigert) +## Media-levering: twee foutklassen (verwar bloem niet met onkruid) -Sommige dienst-PNG's worden wél verzonden (success + message-ID) maar komen niet als beeld -aan: één grijs vinkje (ack 2), nooit geopend (ack 3). **Niet te detecteren met een -bestandscheck** (formaat/CRC volledig geldig) — alleen de praktijktest vangt het. -Oorzaak-klasse: wwjs 1.34.7-image-pipeline-regressie (wwebjs #201772/#201732), inhoudsgebonden. +**1. Staged-ghost-race (de hoofdklasse, 2026-09-26)** — zie RCA hierboven: een +direct-opvolgende media-send overlapt in de ene tab en de 2e verliest. +Bestand bytes zijn nooit het probleem; **re-render (poppler) is een dwaalspoor en geen +fix** — het maskeerde de race alleen (nieuwe poging = nieuwe race-rol). Mitigatie = +bridge-breed throttle (`MEDIA_SEND_DELAY_MS`, 500) — zie hierboven. Bij vermoeden: +opnieuw versturen, apparaat als waarheid. -**Bevinding 2026-09-24 (bestand × ontvanger):** de weigering is een interactie tussen -bestand en ontvanger, niet de sessie. Hetzelfde (geldige, decodeerbare) bestand hing op -ack 1 voor de ene ontvanger, terwijl de admin in dezelfde verse sessie ack 3 kreeg; ná een -sessie-herstart hing hetzelfde bestand opnieuw. Consequentie: **een sessie-herstart lost -een ack-1-hang niet op** en wordt sindsdien niet meer ingezet — wel detectie via -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.) +**2. Ontvanger offline/weigert (ack-1-hang)** — bericht staat gequeued, apparaat pakt het +niet op. Check eerst óf het apparaat online is, wacht af; persoonsgebonden. -> **⚠️ 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.** +> **Detectie/verificatie:** de bridgestore en live acks zijn **geen** bezorgbewijs (bridge +> fabriceert ze). Enige betrouwbare bronnen: het apparaat en `getInfo` +> (`message.getInfo()` → `read`/`readRemaining`). `dienst-media-test.js` (server, +> `/home/aiaiaiaiagent/whatsapp-scripts/dienst-media-test.js`) bestaat nog maar meet acks; +> zijn `ack≥3`-criterium is daardoor onbetrouwbaar — bevestig altijd op het apparaat/read. **Afbeeldingsformaat:** sinds `07b511e` leidt `sendImageMessage` de mimetype af uit de extensie — `png`→`image/png`, `jpg`/`jpeg`→`image/jpeg` (onbekend→`image/png`). De bot is dus voorbereid als de bron-repo overgaat naar JPEG; de `bestandsnaam` uit `dienst-tijden.json` wordt al extensie-agnostisch ingelezen (`getPngFilePaths`). -### Werkwijze (gedetailleerd: vault-notitie "Dienst media bezorgtest") - -1. Verzend alle png's gedoseerd naar de beheerderchat via het generieke script - `dienst-media-test.js` (server: `/home/aiaiaiaiagent/whatsapp-scripts/dienst-media-test.js`), - batch 20, wachttijd zodat de beheerder de chat opent. -2. Script meet acks (`ack≥3` = geaccepteerd, geïndexeerd in `dienst-media-index.json`; - idempotent — geaccepteerde worden nooit opnieuw verstuurd). -3. Hangende (`ack<3` na wachttijd) regenereren in volgorde: officiële pdf2png-pipeline → - **poppler** `pdftoppm -png -r 72` (bewezen) → gs → magick. Steekproefsgewijs visueel - controleren dat de juiste dienst op de pagina staat. -4. Her-testen tot ack 3; na max rondes → menselijke-inspectielijst. - ### Script-parameters (voor gebruik) ```bash diff --git a/README.md b/README.md index 98b5145..c5eb39b 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,6 @@ src/ dailyStats.js # Dagelijkse aanvraag-teller dienstService.js # JSON lookup en PNG bestandspaden whatsappApi.js # WhatsApp API client (bericht + afbeelding) - ackTracker.js # Ack-tracking: hangende media detecteren telegramBot.js # Telegram watchdog (apart process, health checks) ``` @@ -143,53 +142,14 @@ Bij elke interactie met de groep controleert de bot of het pinned bericht nog be - **Restart-loop preventie**: systemd `Restart=always` met `RestartSec=5` - **Webhook fout**: wordt gelogd in de logs -## Ack-tracking (ghost-send detectie, sinds 2026-09-24) +## Media-levering: acks & pacing (RCA 2026-09-26) -> **⚠️ 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 -sessie — zie AGENTS.md). De bot detecteert dit via `message_ack`-events en waarschuwt. - -### Hoe het werkt - -1. **Vereiste:** wwebjs-api .env — `message_ack` uit `DISABLED_CALLBACKS` halen - (staat er sinds 2026-09-24 niet meer in) + container herstarten. -2. `sendImageMessage` registreert elke verzending (`src/ackTracker.js`) met message-ID, - chat en bestandsnaam; de webhook (`message_ack` in `messageHandler.js`) update de ack. -3. Bij **ack ≥ 2** (bezorgd op apparaat) wordt de tracking verwijderd. -4. Blijft een bericht **> `ACK_STUCK_TIMEOUT_MS` (5 min) op ack < 2** → éénmalige - admin-notificatie (WhatsApp → Telegram-fallback) met bestandsnaam + chat en het advies - "opnieuw renderen + bezorgtest". Er volgt **geen sessie-herstart** (die helpt niet bij - een ack-1-hang) en geen resend naar de aanvrager. -5. **Pruning:** entries worden na `ACK_PRUNED_AFTER_MS` (30 min) opgeruimd; de check-loop - is **activity-driven** (loopt alleen zolang er tracking open staat, geen 30s-polling - in rust). - -### Ack-semantiek (wwebjs 1.34.7) - -| ack | Betekenis | -|-----|-----------| -| 0 | onbekend/nete verstuurd | -| 1 | verstuurd naar server (één grijs vinkje) — **hangt hier** | -| 2 | bezorgd op apparaat (twee vinkjes) | -| 3 | geopend/gelezen (blauwe vinkjes) | - -### Configuratie - -| Variabele | Doel | Default | -|-----------|------|---------| -| `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` | +**Ack-tracking is VERWIJDERD (dwaalspoor).** De ack-tracker (`message_ack` → admin-melding +bij ack<2) is uit de bot gehaald: de bridge fabriceert acks (dienst 5219 blz 2 kreeg +`ack=2` voor een bericht dat nooit getransmitteerd was) — ze zijn geen bezorgbewijs en +gaven verkeerde signalen. Enige betrouwbare bronnen: het apparaat en `getInfo` +(`message.getInfo()` → `read`/`readRemaining`). De bridge-`delivery`/acs van de store zijn +nooit leveringsbewijs. > **🛡️ Media-pacing zit bridge-breed (2026-09-26), niet in de bot.** Staged-ghost-race: > de wwebjs-media-pipeline is één tab; een direct-opvolgende media-send (van elke client) @@ -198,16 +158,6 @@ sessie — zie AGENTS.md). De bot detecteert dit via `message_ack`-events en waa > ertussen. De bot pauzeert niet. Details/config/locatie: zie AGENTS.md en vault "Whatsapp > rest-api.md". Tijdelijke mitigatie; permanente fix (fork) staat open. -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. **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 Elke gebruiker kan maximaal `MAX_REQUESTS_PER_DAY` (standaard 5) diensten per dag opvragen. Bij overschrijding krijgt de gebruiker een melding. De teller wordt dagelijks automatisch gereset (bij de eerste request na middernacht). diff --git a/src/ackTracker.js b/src/ackTracker.js deleted file mode 100644 index d02bf55..0000000 --- a/src/ackTracker.js +++ /dev/null @@ -1,106 +0,0 @@ -// ackTracker.js -// Tracks delivery acks (message_ack) of outgoing media to detect sends that -// never reach the recipient: a message returns success + a message ID but -// stays below ack 2 (one grey tick). -// -// Findings (2026-09-24): an ack-1 hang is a bestand×ontvanger interaction, NOT -// the session. The same file hung (ack 1) for one recipient yet delivered -// (ack 3) to another in the same fresh session. A session restart does NOT fix -// an ack-1 hang and needlessly disrupts production — so we deliberately do NOT -// auto-restart here. We only detect + notify + name the file, so it can be -// re-rendered and re-tested. -// -// Activity-driven: the caller (index.js) is notified via the pending listener -// when the tracked set becomes non-empty, and starts a check loop only then. -// No polling when nothing is tracked. -// -// Requires the bridge (wwebjs-api) to emit message_ack: remove 'message_ack' -// from DISABLED_CALLBACKS in the wwebjs-api .env and restart the container. - -const STUCK_TIMEOUT_MS = parseInt(process.env.ACK_STUCK_TIMEOUT_MS, 10) || 5 * 60 * 1000 -const PRUNE_AFTER_MS = parseInt(process.env.ACK_PRUNED_AFTER_MS, 10) || 30 * 60 * 1000 - -// messageId -> { chatId, filename, sentAt, ack, notifiedStuck } -const sends = new Map() -let pendingListener = null - -function setPendingListener(fn) { - pendingListener = typeof fn === 'function' ? fn : null -} - -function notifyPendingChange() { - if (pendingListener) pendingListener(sends.size) -} - -function registerSend(messageId, chatId, filename) { - if (!messageId) return - sends.set(messageId, { - chatId: chatId || '', - filename: filename || '', - sentAt: Date.now(), - ack: 0, - notifiedStuck: false - }) - console.log(`[ack] track ${messageId} (${filename} → ${chatId})`) - notifyPendingChange() -} - -function handleAck(messageId, ack) { - if (!messageId) return - const entry = sends.get(messageId) - if (!entry) return - if (ack > entry.ack) entry.ack = ack - if (ack >= 2) { - sends.delete(messageId) // delivered; nothing left to track - console.log(`[ack] delivered ack=${ack} ${messageId} (${entry.filename})`) - notifyPendingChange() - } -} - -// Remove stale entries (older than PRUNE_AFTER_MS). Stuck entries were notified -// once around STUCK_TIMEOUT_MS; keeping them longer only leaks memory and keeps -// the check-loop alive, so they are dropped here. -function prune() { - const now = Date.now() - let removed = 0 - for (const [id, entry] of sends) { - if (now - entry.sentAt > PRUNE_AFTER_MS) { - sends.delete(id) - removed++ - } - } - if (removed > 0) { - console.log(`[ack] pruned ${removed} stale entr${removed === 1 ? 'y' : 'ies'} (leeftijd > ${Math.round(PRUNE_AFTER_MS / 60000)} min)`) - notifyPendingChange() - } -} - -function stuckEntries() { - const now = Date.now() - const stuck = [] - for (const [id, entry] of sends) { - if (entry.ack < 2 && now - entry.sentAt > STUCK_TIMEOUT_MS) { - stuck.push({ id, ...entry }) - } - } - return stuck -} - -function markNotified(messageId) { - const entry = sends.get(messageId) - if (entry) entry.notifiedStuck = true -} - -function pendingCount() { - return sends.size -} - -module.exports = { - registerSend, - handleAck, - stuckEntries, - prune, - markNotified, - pendingCount, - setPendingListener -} \ No newline at end of file diff --git a/src/index.js b/src/index.js index 20a99d5..9a398de 100644 --- a/src/index.js +++ b/src/index.js @@ -6,8 +6,7 @@ const axios = require('axios') const { handleWebhook } = require('./messageHandler') const { loadDiensten } = require('./dienstService') const { resetDay, getYesterdayTotal } = require('./dailyStats') -const { sendTextMessage, notifyAdmin } = require('./whatsappApi') -const ackTracker = require('./ackTracker') +const { sendTextMessage } = require('./whatsappApi') async function notifyTelegramOnline() { const url = process.env.TELEGRAM_BOT_URL @@ -103,38 +102,4 @@ async function startServer() { process.on('SIGINT', () => shutdown('SIGINT')) } -async function checkStuckAcks() { - ackTracker.prune() - const stuck = ackTracker.stuckEntries() - if (stuck.length === 0) return - for (const s of stuck) { - if (!s.notifiedStuck) { - ackTracker.markNotified(s.id) - notifyAdmin( - `[dienst-bot] hangend bericht: ${s.filename} → ${s.chatId} ` + - `(ack ${s.ack}, ${Math.round((Date.now() - s.sentAt) / 1000)}s oud). ` + - `Bestand opnieuw renderen + bezorgtest, geen sessie-herstart (die helpt hier niet).` - ) - } - } -} - -// Lazy, activity-driven check loop: only runs while there is a tracked send -// pending, and stops as soon as the tracked set is empty. No 30s polling in -// idle. Re-arms itself after every tick while pendingCount() > 0. -const ackCheckIntervalMs = parseInt(process.env.ACK_CHECK_INTERVAL_MS, 10) || 30000 -let ackTimer = null - -function scheduleAckCheck() { - if (ackTimer) return - if (ackTracker.pendingCount() === 0) return - ackTimer = setTimeout(() => { - ackTimer = null - checkStuckAcks() - if (ackTracker.pendingCount() > 0) scheduleAckCheck() - }, ackCheckIntervalMs) -} - -ackTracker.setPendingListener(() => scheduleAckCheck()) - startServer() \ No newline at end of file diff --git a/src/messageHandler.js b/src/messageHandler.js index e458fea..bd5edf2 100644 --- a/src/messageHandler.js +++ b/src/messageHandler.js @@ -2,7 +2,6 @@ const { findDienst, getPngFilePaths } = require('./dienstService') const { sendTextMessage, sendImageMessage, notifyAdmin, sendChatSeen } = require('./whatsappApi') const { checkLimit, increment, MAX_REQUESTS_PER_DAY } = require('./rateLimiter') const { increment: incrementDaily } = require('./dailyStats') -const { handleAck } = require('./ackTracker') const ALLOWED_GROUP = process.env.ALLOWED_GROUP const ALLOWED_GROUP_NAMES = (process.env.ALLOWED_GROUP_NAME || '') @@ -24,13 +23,6 @@ function extractDienstnummer(body) { } async function handleWebhook(payload) { - if (payload.dataType === 'message_ack') { - if (payload.sessionId && payload.sessionId !== process.env.WHATSAPP_SESSION) return - const { message, ack } = payload.data || {} - const id = message && (message.id?.id || message._data?.id?.id) - if (id) handleAck(id, ack) - return - } if (payload.dataType !== 'message') return if (payload.sessionId !== process.env.WHATSAPP_SESSION) return diff --git a/src/whatsappApi.js b/src/whatsappApi.js index 1d8e71a..3001523 100644 --- a/src/whatsappApi.js +++ b/src/whatsappApi.js @@ -1,6 +1,5 @@ const axios = require('axios') const fs = require('fs') -const { registerSend } = require('./ackTracker') const API_BASE = process.env.WHATSAPP_API_ENDPOINT const SESSION = process.env.WHATSAPP_SESSION @@ -69,8 +68,6 @@ async function sendImageMessage(chatId, filePath, filename) { filename: filename } }) - const messageId = response.data?.message?._data?.id?.id - registerSend(messageId, chatId, filename) return response.data }, `sendImageMessage ${filename} to ${chatId}`) }