# 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" ```