Compare commits

...
9 changed files with 573 additions and 5 deletions

View file

@ -4,7 +4,7 @@
> De feature ratelimiting is gemaakt door middel van vibecoding met [OpenCode 1.14.40](https://opencode.ai) en Deepseek v4 Pro > De feature ratelimiting is gemaakt door middel van vibecoding met [OpenCode 1.14.40](https://opencode.ai) en Deepseek v4 Pro
WhatsApp bot die dienstschema's (PNG) verstuurt op basis van een dienstnummer in een WhatsApp groep. WhatsApp bot die dienstschema's (PNG) verstuurt op basis van een dienstnummer in een WhatsApp groep, en dagelijks een samenvatting stuurt.
## Hoe het werkt ## Hoe het werkt
@ -114,9 +114,10 @@ npx pm2 logs dienst-bot
``` ```
src/ src/
index.js # Express webhook server index.js # Express webhook server + cron job
messageHandler.js # Berichtverwerking en filtering messageHandler.js # Berichtverwerking en filtering
rateLimiter.js # Spam protectie (max 5 per dag) rateLimiter.js # Spam protectie (max 5 per dag)
dailyStats.js # Dagelijkse aanvraag-teller
dienstService.js # JSON lookup en PNG bestandspaden dienstService.js # JSON lookup en PNG bestandspaden
whatsappApi.js # WhatsApp API client (bericht + afbeelding) whatsappApi.js # WhatsApp API client (bericht + afbeelding)
``` ```
@ -131,6 +132,16 @@ src/
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). 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).
## Dagelijkse samenvatting
Elke dag om **10:00** stuurt de bot een WhatsApp-bericht naar `ADMIN_NUMBER` met het totaal aantal dienstaanvragen van de vorige dag (00:00 t/m 23:59).
```
Aantal dienstaanvragen gisteren: N
```
De teller wordt bijgehouden in memory door `dailyStats.js` en gereset na verzending van de samenvatting. De scheduling gebruikt `node-cron` (`0 10 * * *`).
## Licentie ## Licentie
Privé project — niet voor distributie. Privé project — niet voor distributie.

View file

@ -0,0 +1,238 @@
# Dagelijkse WhatsApp samenvatting — Implementation Plan
> **Voor agentic workers:** REQUIRED SUB-SKILL: Gebruik superpowers:subagent-driven-development (aanbevolen) of superpowers:executing-plans om dit plan taak-voor-taak te implementeren. Stappen gebruiken checkbox (`- [ ]`) syntax voor tracking.
**Goal:** Elke dag om 10:00 een WhatsApp-bericht naar de admin sturen met het totaal aantal dienstaanvragen van de vorige dag.
**Architecture:** Nieuwe module `dailyStats.js` houdt een dagtotaal bij in memory. `messageHandler.js` roept increment aan bij elke aanvraag. `node-cron` triggert om 10:00 de reset en verzending via bestaande `whatsappApi.sendTextMessage()`.
**Tech Stack:** Node.js, node-cron (nieuwe dependency), bestaande axios/express stack
**Files:**
- Create: `src/dailyStats.js`
- Modify: `src/messageHandler.js`
- Modify: `src/index.js`
- Modify: `package.json`
---
### Task 1: Create dailyStats module
**Files:**
- Create: `src/dailyStats.js`
- [ ] **Step 1: Write the dailyStats module**
```javascript
let dailyTotal = 0
let yesterdayTotal = 0
function increment() {
dailyTotal++
}
function getYesterdayTotal() {
return yesterdayTotal
}
function resetDay() {
yesterdayTotal = dailyTotal
dailyTotal = 0
}
module.exports = { increment, getYesterdayTotal, resetDay }
```
- [ ] **Step 2: Verify the module loads without errors**
Run: `node -e "const m = require('./src/dailyStats'); m.increment(); m.increment(); m.resetDay(); console.log(m.getYesterdayTotal() === 2 ? 'OK' : 'FAIL')"`
Expected: `OK`
- [ ] **Step 3: Commit**
```bash
git add src/dailyStats.js
git commit -m "feat: add dailyStats module for tracking daily request totals"
```
---
### Task 2: Integrate dailyStats into messageHandler
**Files:**
- Modify: `src/messageHandler.js`
- [ ] **Step 1: Add dailyStats import and increment call**
`src/messageHandler.js` line 1-4, add import:
```javascript
const { findDienst, getPngFilePaths } = require('./dienstService')
const { sendTextMessage, sendImageMessage, notifyAdmin } = require('./whatsappApi')
const { checkLimit, increment, MAX_REQUESTS_PER_DAY } = require('./rateLimiter')
const { increment: incrementDaily } = require('./dailyStats')
```
`src/messageHandler.js` after `increment(author)` (line 51), add:
```javascript
increment(author)
incrementDaily()
```
- [ ] **Step 2: Verify messageHandler still loads**
Run: `node -e "require('./src/messageHandler'); console.log('OK')"`
Expected: `OK` (may show dotenv warning, that's fine)
- [ ] **Step 3: Commit**
```bash
git add src/messageHandler.js
git commit -m "feat: integrate dailyStats counter into message handler"
```
---
### Task 3: Add node-cron and schedule daily 10:00 summary
**Files:**
- Modify: `package.json`
- Modify: `src/index.js`
- [ ] **Step 1: Install node-cron**
Run: `npm install node-cron`
Expected: node-cron added to `node_modules` and `package.json`
- [ ] **Step 2: Add cron job to index.js**
`src/index.js` — replace the entire file:
```javascript
require('dotenv').config()
const cron = require('node-cron')
const express = require('express')
const { handleWebhook } = require('./messageHandler')
const { loadDiensten } = require('./dienstService')
const { resetDay, getYesterdayTotal } = require('./dailyStats')
const { sendTextMessage } = require('./whatsappApi')
const app = express()
app.use(express.json({ limit: '10mb' }))
app.post('/webhook', async (req, res) => {
try {
await handleWebhook(req.body)
res.json({ success: true })
} catch (err) {
console.error('Webhook processing error:', err.message)
res.status(500).json({ success: false, error: err.message })
}
})
app.get('/health', (req, res) => {
res.json({ status: 'ok' })
})
const PORT = process.env.WEBHOOK_PORT || 3002
loadDiensten()
cron.schedule('0 10 * * *', () => {
const adminNumber = process.env.ADMIN_NUMBER
if (!adminNumber) {
console.error('ADMIN_NUMBER not configured, skipping daily summary')
return
}
resetDay()
const total = getYesterdayTotal()
sendTextMessage(adminNumber, `Aantal dienstaanvragen gisteren: ${total}`)
.then(() => console.log(`Daily summary sent: ${total} requests`))
.catch(err => console.error('Failed to send daily summary:', err.message))
})
app.listen(PORT, () => {
console.log(`dienst-bot listening on port ${PORT}`)
console.log(`Allowed group: ${process.env.ALLOWED_GROUP}`)
console.log(`Bot name: ${process.env.BOT_NAME}`)
console.log('Daily summary cron job scheduled for 10:00')
})
```
- [ ] **Step 3: Verify server starts without errors**
Run: `timeout 3 node src/index.js || true`
Expected: Output shows `dienst-bot listening on port ...` and `Daily summary cron job scheduled for 10:00`
- [ ] **Step 4: Test cron callback manually**
Run: `node -e "
require('dotenv').config()
const { increment, resetDay, getYesterdayTotal } = require('./src/dailyStats')
increment()
increment()
increment()
resetDay()
console.log('Total:', getYesterdayTotal())
console.log(getYesterdayTotal() === 3 ? 'PASS' : 'FAIL')
"`
Expected: `Total: 3` followed by `PASS`
- [ ] **Step 5: Commit**
```bash
git add package.json package-lock.json src/index.js
git commit -m "feat: add node-cron daily summary job at 10:00"
```
---
### Task 4: Verify end-to-end flow
**Files:** (none, verification only)
- [ ] **Step 1: Start the server**
Run: `node src/index.js &`
Wait 2 seconds for startup.
- [ ] **Step 2: Simulate a webhook request to increment the counter**
Run:
```bash
curl -s -X POST http://localhost:3002/webhook \
-H 'Content-Type: application/json' \
-d '{"dataType":"message","sessionId":"EdGpt","data":{"message":{"from":"120363407356808413@g.us","fromMe":false,"author":"test@c.us","body":"@EdGpt 0001","_data":{"mentionedJidList":["bot@c.us"]}}}}}'
```
Expected: `{"success":true}`
- [ ] **Step 3: Trigger the cron callback directly to verify summary message**
Run:
```bash
node -e "
require('dotenv').config()
const { resetDay, getYesterdayTotal } = require('./src/dailyStats')
const { sendTextMessage } = require('./src/whatsappApi')
resetDay()
const total = getYesterdayTotal()
console.log('Would send to admin:', process.env.ADMIN_NUMBER)
console.log('Message:', 'Aantal dienstaanvragen gisteren: ' + total)
console.log(total > 0 ? 'PASS' : 'FAIL - expected >0')
"
```
Expected: `PASS` (shows message with total > 0)
- [ ] **Step 4: Stop the server**
Run: `kill %1 2>/dev/null; true`
- [ ] **Step 5: Commit any remaining changes**
```bash
git status
# Only commit if there are remaining changes
```

View file

@ -0,0 +1,174 @@
# Spam Protection Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Limit each user to max 5 dienst requests per day, with automatic daily reset.
**Architecture:** In-memory rate limiter module that tracks request counts per user per day. Integrated into the message handler pipeline before dienst lookup. Sends a friendly warning when limit is exceeded.
**Tech Stack:** Node.js, no new dependencies needed
---
### Task 1: Create rate limiter module
**Files:**
- Create: `src/rateLimiter.js`
- [ ] **Step 1: Create `src/rateLimiter.js`**
```js
const MAX_REQUESTS_PER_DAY = parseInt(process.env.MAX_REQUESTS_PER_DAY, 10) || 5
const userCounts = new Map()
let currentDate = new Date().toDateString()
function resetIfNeeded() {
const today = new Date().toDateString()
if (today !== currentDate) {
userCounts.clear()
currentDate = today
console.log('Rate limiter: daily counters reset')
}
}
function checkLimit(userId) {
resetIfNeeded()
const count = userCounts.get(userId) || 0
return {
allowed: count < MAX_REQUESTS_PER_DAY,
count,
max: MAX_REQUESTS_PER_DAY
}
}
function increment(userId) {
resetIfNeeded()
const count = (userCounts.get(userId) || 0) + 1
userCounts.set(userId, count)
return count
}
module.exports = { checkLimit, increment, MAX_REQUESTS_PER_DAY }
```
- [ ] **Step 2: Commit**
```bash
git add src/rateLimiter.js
git commit -m "feat: add rate limiter module"
```
---
### Task 2: Integrate rate limiter into message handler
**Files:**
- Modify: `src/messageHandler.js:1,25-36`
- [ ] **Step 1: Add rate limiter import at top of `src/messageHandler.js`**
Add after line 2:
```js
const { checkLimit, increment, MAX_REQUESTS_PER_DAY } = require('./rateLimiter')
```
- [ ] **Step 2: Add rate limit check in `handleWebhook` after dienstnummer extraction**
Insert between the `dienstnummer` null check (line 36) and the `console.log` (line 38), replacing lines 38-40 with:
```js
console.log(`Processing dienst ${dienstnummer} for ${message.author}`)
const author = message.author
if (!author) return
const limit = checkLimit(author)
if (!limit.allowed) {
await sendTextMessage(author, `Je hebt vandaag het maximum van ${limit.max} diensten bereikt. Probeer het morgen opnieuw.`)
console.log(`Rate limit reached for ${author} (${limit.count}/${limit.max})`)
return
}
increment(author)
```
- [ ] **Step 3: Verify the file looks correct**
Run: `node -e "require('./src/messageHandler')"` (should not throw)
- [ ] **Step 4: Commit**
```bash
git add src/messageHandler.js
git commit -m "feat: integrate rate limiter into message handler"
```
---
### Task 3: Add config to .env and restart
**Files:**
- Modify: `.env`
- Modify: `.env.example`
- [ ] **Step 1: Add `MAX_REQUESTS_PER_DAY` to `.env`**
Append to `.env`:
```
# Rate limiting
MAX_REQUESTS_PER_DAY=5
```
- [ ] **Step 2: Add `MAX_REQUESTS_PER_DAY` to `.env.example`**
Append to `.env.example`:
```
# Rate limiting
MAX_REQUESTS_PER_DAY=5
```
- [ ] **Step 3: Restart bot and verify**
```bash
npx pm2 restart dienst-bot && npx pm2 save
npx pm2 logs dienst-bot --lines 5 --nostream
```
Expected: bot starts without errors, logs show listening on port 3002.
- [ ] **Step 4: Commit and push**
```bash
git add .env.example
git commit -m "feat: add rate limiting config option"
git push
```
---
### Task 4: Update README
**Files:**
- Modify: `README.md`
- [ ] **Step 1: Add rate limiting section to README.md**
Add after the "Foutafhandeling" section:
```markdown
## 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).
```
- [ ] **Step 2: Commit and push**
```bash
git add README.md
git commit -m "docs: add spam protection section to README"
git push
```

View file

@ -0,0 +1,96 @@
# Dagelijkse WhatsApp samenvatting — Design
## Doel
Elke dag om 10:00 uur een WhatsApp-bericht sturen naar de admin (31620248011) met het totaal aantal dienstaanvragen van de vorige dag (00:00 t/m 23:59).
## Requirement samenvatting
| Item | Keuze |
|------|-------|
| Inhoud bericht | Alleen totaal aantal |
| Periode | Vorige kalenderdag (00:00-23:59) |
| Persistentie | In-memory (geen disk) |
| Verzendtijd | 10:00 elke ochtend |
| Ontvanger | `ADMIN_NUMBER` uit `.env` (31620248011@c.us) |
## Architectuur
### Nieuwe module: `src/dailyStats.js`
Houdt twee tellers bij in memory:
- `dailyTotal` — aantal aanvragen vandaag (lopende dag)
- `yesterdayTotal` — aantal aanvragen van gisteren (voor verzending om 10:00)
Publieke API:
- `increment()` — verhoogt `dailyTotal` met 1
- `getYesterdayTotal()` — returned `yesterdayTotal`
- `resetDay()` — zet `yesterdayTotal = dailyTotal`, dan `dailyTotal = 0`
### Scheduling: `node-cron`
Cron expressie `0 10 * * *` triggert exact om 10:00 elke dag een callback die:
1. `dailyStats.resetDay()` aanroept (snapshot van gisteren)
2. `sendTextMessage(adminNumber, bericht)` aanroept om het totaal te versturen
3. Bij falen van verzending: error loggen, maar reset blijft van kracht
### Bestaande modules — wijzigingen
**`src/index.js`**
- Importeert `node-cron` en `dailyStats`
- Registreert cron job bij server startup
- Huidige `express` setup blijft ongewijzigd
**`src/messageHandler.js`**
- Importeert `dailyStats`
- Roept `dailyStats.increment()` aan na de bestaande `increment(author)` (rate limiter)
### Data flow
```
webhook POST /webhook
→ messageHandler.handleWebhook()
→ rateLimiter.increment(author) [bestaand]
→ dailyStats.increment() [nieuw, 1 regel]
om 10:00 (node-cron trigger):
→ dailyStats.resetDay()
→ whatsappApi.sendTextMessage(admin, "Aantal dienstaanvragen gisteren: N")
```
### Foutafhandeling
- Verzending faalt → error wordt gelogd, geen retry, reset is al gedaan
- Server herstart voor 10:00 → `dailyTotal` en `yesterdayTotal` zijn 0, cron start opnieuw, om 10:00 wordt bericht met "0" verstuurd (verlies van telling geaccepteerd conform in-memory keuze)
- Server draait niet om 10:00 → geen bericht die dag
### Afhankelijkheden
- `node-cron` toevoegen aan `package.json`
- Geen nieuwe `.env` variabelen — `ADMIN_NUMBER` wordt hergebruikt
### Berichtformaat
```
Aantal dienstaanvragen gisteren: N
```
Waar `N` het totaal aantal is van 00:00 tot 23:59 van de vorige kalenderdag.
## Teststrategie
1. **`dailyStats` unit tests**
- `increment()` verhoogt `dailyTotal` correct
- `resetDay()` kopieert `dailyTotal` naar `yesterdayTotal` en zet `dailyTotal` op 0
- `getYesterdayTotal()` returned gecachte waarde
2. **Integratietest**
- Cron callback handmatig aanroepen → verifieer dat `sendTextMessage` wordt aangeroepen met juiste admin nummer en bericht
## Scope & grenzen
- Binnen scope: teller bijhouden, cron trigger, WhatsApp verzending
- Buiten scope: persistentie op disk, uitsplitsingen per gebruiker/dienst, herstel van gemiste data na crash, historische rapportages

12
package-lock.json generated
View file

@ -10,7 +10,8 @@
"dependencies": { "dependencies": {
"axios": "^1.6.0", "axios": "^1.6.0",
"dotenv": "^16.4.0", "dotenv": "^16.4.0",
"express": "^4.18.0" "express": "^4.18.0",
"node-cron": "^4.2.1"
} }
}, },
"node_modules/accepts": { "node_modules/accepts": {
@ -658,6 +659,15 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/node-cron": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz",
"integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==",
"license": "ISC",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/object-inspect": { "node_modules/object-inspect": {
"version": "1.13.4", "version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",

View file

@ -9,6 +9,7 @@
"dependencies": { "dependencies": {
"axios": "^1.6.0", "axios": "^1.6.0",
"dotenv": "^16.4.0", "dotenv": "^16.4.0",
"express": "^4.18.0" "express": "^4.18.0",
"node-cron": "^4.2.1"
} }
} }

17
src/dailyStats.js Normal file
View file

@ -0,0 +1,17 @@
let dailyTotal = 0
let yesterdayTotal = 0
function increment() {
dailyTotal++
}
function getYesterdayTotal() {
return yesterdayTotal
}
function resetDay() {
yesterdayTotal = dailyTotal
dailyTotal = 0
}
module.exports = { increment, getYesterdayTotal, resetDay }

View file

@ -1,7 +1,10 @@
require('dotenv').config() require('dotenv').config()
const cron = require('node-cron')
const express = require('express') const express = require('express')
const { handleWebhook } = require('./messageHandler') const { handleWebhook } = require('./messageHandler')
const { loadDiensten } = require('./dienstService') const { loadDiensten } = require('./dienstService')
const { resetDay, getYesterdayTotal } = require('./dailyStats')
const { sendTextMessage } = require('./whatsappApi')
const app = express() const app = express()
app.use(express.json({ limit: '10mb' })) app.use(express.json({ limit: '10mb' }))
@ -24,8 +27,24 @@ const PORT = process.env.WEBHOOK_PORT || 3002
loadDiensten() loadDiensten()
cron.schedule('0 10 * * *', () => {
const adminNumber = process.env.ADMIN_NUMBER
if (!adminNumber) {
console.error('ADMIN_NUMBER not configured, skipping daily summary')
return
}
resetDay()
const total = getYesterdayTotal()
sendTextMessage(adminNumber, `Aantal dienstaanvragen gisteren: ${total}`)
.then(() => console.log(`Daily summary sent: ${total} requests`))
.catch(err => console.error('Failed to send daily summary:', err.message))
})
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`dienst-bot listening on port ${PORT}`) console.log(`dienst-bot listening on port ${PORT}`)
console.log(`Allowed group: ${process.env.ALLOWED_GROUP}`) console.log(`Allowed group: ${process.env.ALLOWED_GROUP}`)
console.log(`Bot name: ${process.env.BOT_NAME}`) console.log(`Bot name: ${process.env.BOT_NAME}`)
console.log('Daily summary cron job scheduled for 10:00')
}) })

View file

@ -1,6 +1,7 @@
const { findDienst, getPngFilePaths } = require('./dienstService') const { findDienst, getPngFilePaths } = require('./dienstService')
const { sendTextMessage, sendImageMessage, notifyAdmin } = require('./whatsappApi') const { sendTextMessage, sendImageMessage, notifyAdmin } = require('./whatsappApi')
const { checkLimit, increment, MAX_REQUESTS_PER_DAY } = require('./rateLimiter') const { checkLimit, increment, MAX_REQUESTS_PER_DAY } = require('./rateLimiter')
const { increment: incrementDaily } = require('./dailyStats')
const BOT_NAME = (process.env.BOT_NAME || 'EdGpt').toLowerCase() const BOT_NAME = (process.env.BOT_NAME || 'EdGpt').toLowerCase()
const ALLOWED_GROUP = process.env.ALLOWED_GROUP const ALLOWED_GROUP = process.env.ALLOWED_GROUP
@ -49,6 +50,7 @@ async function handleWebhook(payload) {
} }
increment(author) increment(author)
incrementDaily()
const dienst = findDienst(dienstnummer) const dienst = findDienst(dienstnummer)
if (!dienst) { if (!dienst) {