Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b98ef0b714 | ||
|
|
2e19540db4 | ||
|
|
2dabcf0a8e | ||
|
|
f7bc98eaed | ||
|
|
64e239b368 | ||
|
|
5a757ab8be | ||
|
|
9569485f78 |
9 changed files with 573 additions and 5 deletions
15
README.md
15
README.md
|
|
@ -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.
|
||||||
|
|
|
||||||
238
docs/superpowers/plans/2025-05-12-daily-summary.md
Normal file
238
docs/superpowers/plans/2025-05-12-daily-summary.md
Normal 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
|
||||||
|
```
|
||||||
174
docs/superpowers/plans/2026-05-11-spam-protection.md
Normal file
174
docs/superpowers/plans/2026-05-11-spam-protection.md
Normal 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
|
||||||
|
```
|
||||||
96
docs/superpowers/specs/2025-05-12-daily-summary-design.md
Normal file
96
docs/superpowers/specs/2025-05-12-daily-summary-design.md
Normal 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
12
package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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
17
src/dailyStats.js
Normal 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 }
|
||||||
21
src/index.js
21
src/index.js
|
|
@ -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')
|
||||||
|
})
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue