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

9.2 KiB

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

{
  "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
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

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
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

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
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

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
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

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
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
git add -A
git commit -m "feat: complete dienst-bot with PM2 setup"