dienst_echo_whatsappgroep/docs/superpowers/plans/2025-05-11-dienst-bot-plan.md

367 lines
9.2 KiB
Markdown

# Dienst Bot 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:** Build a Node.js webhook service that listens for WhatsApp group messages mentioning @EdGpt with a 4-digit dienstnummer, and sends corresponding PNG images privately back to the sender.
**Architecture:** Express.js webhook receiver on port 3002 receives POST callbacks from wwebjs-api. Parses messages for @EdGpt mentions + 4-digit numbers, looks up PNGs via dienst-tijden.json, and sends them privately via the wwebjs-api REST endpoint.
**Tech Stack:** Node.js, Express, axios, dotenv
---
### Task 1: Project scaffold and package.json
**Files:**
- Create: `package.json`
- [ ] **Step 1: Create package.json**
```json
{
"name": "dienst-bot",
"version": "1.0.0",
"description": "WhatsApp dienstnummer bot - sends PNG dienstschema's via webhook",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js"
},
"dependencies": {
"axios": "^1.6.0",
"dotenv": "^16.4.0",
"express": "^4.18.0"
}
}
```
- [ ] **Step 2: Install dependencies**
Run: `npm install`
Expected: `node_modules` created, `package-lock.json` generated
- [ ] **Step 3: Update .env with new config vars**
Append to existing `.env`:
```
WEBHOOK_PORT=3002
ALLOWED_GROUP=120363407356808413@g.us
BOT_NAME=EdGpt
DIENSTEN_JSON=./diensten/dienst-tijden.json
DIENSTEN_PNG=./diensten/png
ADMIN_NUMBER=31620248011@c.us
```
- [ ] **Step 4: Commit**
```bash
git init
git add package.json package-lock.json .env
git commit -m "feat: scaffold project with dependencies and env config"
```
---
### Task 2: dienstService module
**Files:**
- Create: `src/dienstService.js`
- [ ] **Step 1: Write dienstService.js**
```js
const fs = require('fs')
const path = require('path')
let dienstenCache = null
function loadDiensten() {
if (dienstenCache) return dienstenCache
const jsonPath = process.env.DIENSTEN_JSON
const raw = fs.readFileSync(jsonPath, 'utf-8')
dienstenCache = JSON.parse(raw)
return dienstenCache
}
function findDienst(dienstnummer) {
const diensten = loadDiensten()
return diensten.find(d => d.dienstnummer === dienstnummer) || null
}
function getPngFilePaths(dienst) {
const pngDir = process.env.DIENSTEN_PNG
return dienst.paginas.map(p => path.join(pngDir, p.bestandsnaam))
}
module.exports = { loadDiensten, findDienst, getPngFilePaths }
```
- [ ] **Step 2: Test lookup manually**
Run: `node -e "require('dotenv').config(); const ds = require('./src/dienstService'); console.log(ds.findDienst('4001')); console.log(ds.getPngFilePaths(ds.findDienst('4001')));"`
Expected: prints dienst object for 4001 with paginas array, then array of PNG file paths
- [ ] **Step 3: Commit**
```bash
git add src/dienstService.js
git commit -m "feat: add dienstService for JSON lookup and PNG file paths"
```
---
### Task 3: WhatsApp API helper
**Files:**
- Create: `src/whatsappApi.js`
- [ ] **Step 1: Write whatsappApi.js**
```js
const axios = require('axios')
const fs = require('fs')
const API_BASE = process.env.WHATSAPP_API_ENDPOINT
const SESSION = process.env.WHATSAPP_SESSION
async function sendTextMessage(chatId, text) {
const url = `${API_BASE}/client/sendMessage/${SESSION}`
const response = await axios.post(url, {
chatId,
contentType: 'string',
content: text
})
return response.data
}
async function sendImageMessage(chatId, filePath, filename) {
const fileBuffer = fs.readFileSync(filePath)
const base64Data = fileBuffer.toString('base64')
const url = `${API_BASE}/client/sendMessage/${SESSION}`
const response = await axios.post(url, {
chatId,
contentType: 'MessageMedia',
content: {
mimetype: 'image/png',
data: base64Data,
filename: filename
}
})
return response.data
}
async function notifyAdmin(errorText) {
const adminNumber = process.env.ADMIN_NUMBER
if (!adminNumber) {
console.error('ADMIN_NUMBER not configured, skipping admin notification')
return
}
try {
await sendTextMessage(adminNumber, `[dienst-bot fout] ${errorText}`)
} catch (err) {
console.error('Failed to notify admin:', err.message)
}
}
module.exports = { sendTextMessage, sendImageMessage, notifyAdmin }
```
- [ ] **Step 2: Commit**
```bash
git add src/whatsappApi.js
git commit -m "feat: add whatsappApi helper for sending messages and images"
```
---
### Task 4: messageHandler module
**Files:**
- Create: `src/messageHandler.js`
- [ ] **Step 1: Write messageHandler.js**
```js
const { findDienst, getPngFilePaths } = require('./dienstService')
const { sendTextMessage, sendImageMessage, notifyAdmin } = require('./whatsappApi')
const BOT_NAME = (process.env.BOT_NAME || 'EdGpt').toLowerCase()
const ALLOWED_GROUP = process.env.ALLOWED_GROUP
function extractDienstnummer(body) {
const match = body.match(/\b(\d{4})\b/)
return match ? match[1] : null
}
function isBotMentioned(message) {
if (message._data?.mentionedJidList?.length > 0) {
return true
}
if (message.body && message.body.toLowerCase().includes(`@${BOT_NAME}`)) {
return true
}
return false
}
async function handleWebhook(payload) {
if (payload.dataType !== 'message') return
if (payload.sessionId !== process.env.WHATSAPP_SESSION) return
const message = payload.data?.message
if (!message) return
if (message.fromMe) return
if (message.from !== ALLOWED_GROUP) return
if (!isBotMentioned(message)) return
const dienstnummer = extractDienstnummer(message.body || '')
if (!dienstnummer) return
const author = message.author
if (!author) return
const dienst = findDienst(dienstnummer)
if (!dienst) {
await sendTextMessage(author, `Dienstnummer ${dienstnummer} niet gevonden.`)
return
}
const pngPaths = getPngFilePaths(dienst)
if (!pngPaths || pngPaths.length === 0) {
await sendTextMessage(author, `Geen pagina's gevonden voor dienst ${dienstnummer}.`)
return
}
for (const pngPath of pngPaths) {
const filename = pngPath.split('/').pop()
try {
await sendImageMessage(author, pngPath, filename)
} catch (err) {
const errorMsg = `Fout bij verzenden ${filename} naar ${author}: ${err.message}`
console.error(errorMsg)
await notifyAdmin(errorMsg)
}
}
}
module.exports = { handleWebhook }
```
- [ ] **Step 2: Commit**
```bash
git add src/messageHandler.js
git commit -m "feat: add messageHandler for webhook payload processing"
```
---
### Task 5: Express webhook server (index.js)
**Files:**
- Create: `src/index.js`
- [ ] **Step 1: Write src/index.js**
```js
require('dotenv').config()
const express = require('express')
const { handleWebhook } = require('./messageHandler')
const { loadDiensten } = require('./dienstService')
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()
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}`)
})
```
- [ ] **Step 2: Start the server and verify**
Run: `node src/index.js`
Expected: prints "dienst-bot listening on port 3002"
Run in another terminal: `curl http://localhost:3002/health`
Expected: `{"status":"ok"}`
Stop the server with Ctrl+C.
- [ ] **Step 3: Commit**
```bash
git add src/index.js
git commit -m "feat: add Express webhook server with health endpoint"
```
---
### Task 6: Update wwebjs-api configuration
**Files:**
- Modify: wwebjs-api `.env` (location depends on where wwebjs-api runs - check Docker/container setup)
- [ ] **Step 1: Find the wwebjs-api .env or docker-compose config**
Check where wwebjs-api is configured. It may be in a Docker container with environment variables set via docker-compose.yml or a .env file in the wwebjs-api project directory.
The required addition is:
```
BASE_WEBHOOK_URL=http://localhost:3002/webhook
```
- [ ] **Step 2: Restart wwebjs-api**
After adding `BASE_WEBHOOK_URL`, restart the wwebjs-api service so it picks up the new config and starts sending webhook callbacks.
- [ ] **Step 3: Verify webhook is receiving events**
Run dienst-bot: `node src/index.js`
Send a test message in the WhatsApp group mentioning @EdGpt with a dienstnummer (e.g. "@EdGpt 4001")
Expected: console log in dienst-bot, PNG images sent privately to sender
---
### Task 7: PM2 setup
- [ ] **Step 1: Start with PM2**
Run: `pm2 start src/index.js --name dienst-bot`
Expected: process starts, shows "online" status
- [ ] **Step 2: Verify PM2 process**
Run: `pm2 logs dienst-bot --lines 5`
Expected: shows "dienst-bot listening on port 3002"
- [ ] **Step 3: Save PM2 process list**
Run: `pm2 save`
Expected: saves process list for auto-restart on reboot
- [ ] **Step 4: Final commit**
```bash
git add -A
git commit -m "feat: complete dienst-bot with PM2 setup"
```