refactor: switch to dedicated group, remove @mention requirement, add pinned message service

- Change ALLOWED_GROUP to Dienst opvragen EdGPT-EBS (120363407613652082@g.us)
- Remove isBotMentioned check (no @mention needed)
- Match only exact 4-digit messages (regex /^\s*(\d{4})\s*$/)
- Add pinnedMessageService.js: auto-repins welcome message when expired
- Move incrementDaily() to count all 4-digit messages, not just successful ones
- Update daily summary with emoji
- Update docs and .env.example
This commit is contained in:
Eddy de Vink 2026-05-26 09:20:45 +02:00
commit e4d817ff80
5 changed files with 96 additions and 35 deletions

View file

@ -4,7 +4,7 @@ WHATSAPP_SESSION=EdGpt
# Webhook Configuration
WEBHOOK_PORT=3002
ALLOWED_GROUP=120363407356808413@g.us
ALLOWED_GROUP=120363407613652082@g.us
BOT_NAME=EdGpt
# Paths

View file

@ -1,16 +1,17 @@
# Dienst Bot
> Dit project is gemaakt door middel van vibecoding met [OpenCode 1.14.40](https://opencode.ai) en Z.AI/GLM-5.1.
> Dit project is gemaakt door middel van vibecoding met [OpenCode 1.14.40](https://opencode.ai).
> 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, en dagelijks een samenvatting stuurt.
WhatsApp bot die dienstschema's (PNG) privé verstuurt op basis van een 4-cijferig dienstnummer, gepost in een speciale WhatsApp groep.
## Hoe het werkt
1. Iemand tagt **@EdGpt** in de toegestane groep met een 4-cijferig dienstnummer (bijv. `@EdGpt 4002`)
1. Iemand stuurt een **4-cijferig dienstnummer** in de groep (bijv. `4002`)
2. De bot zoekt het dienstschema op in `dienst-tijden.json`
3. De bijbehorende PNG-bestanden worden privé naar de afzender gestuurd
3. De bijbehorende PNG-bestanden worden **privé** naar de afzender gestuurd
4. Via een **vastgezet welkomstbericht** in de groep staat uitleg over het gebruik
De bot gebruikt een **speciale groep** (Dienst opvragen EdGPT-EBS) waarin **elk bericht dat uit exact 4 cijfers bestaat** wordt verwerkt. Geen @mention nodig.
## Vereisten
@ -38,8 +39,7 @@ WHATSAPP_SESSION=EdGpt
# Webhook
WEBHOOK_PORT=3002
ALLOWED_GROUP=120363407356808413@g.us
BOT_NAME=EdGpt
ALLOWED_GROUP=120363407613652082@g.us
# Bestanden
DIENSTEN_JSON=./diensten/dienst-tijden.json
@ -120,8 +120,17 @@ src/
dailyStats.js # Dagelijkse aanvraag-teller
dienstService.js # JSON lookup en PNG bestandspaden
whatsappApi.js # WhatsApp API client (bericht + afbeelding)
pinnedMessageService.js # Vastgezet welkomstbericht beheer
```
## Welkomstbericht
De groep heeft een **vastgezet welkomstbericht** dat automatisch door EdGpt wordt herplaatst als het is verlopen (30 dagen). De tekst luidt:
> *Welkom in Dienst opvragen EdGpt-EBS. Stuur hier je 4 cijferige dienstnummer en EdGpt zal het prive naar je toe sturen, alleen 4 cijfers meer niet.*
Bij elke interactie met de groep controleert de bot of het pinned bericht nog bestaat en plaatst het opnieuw indien nodig.
## Foutafhandeling
- **Ongeldig dienstnummer**: de bot stuurt een melding terug naar de afzender
@ -134,10 +143,10 @@ Elke gebruiker kan maximaal `MAX_REQUESTS_PER_DAY` (standaard 5) diensten per da
## 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).
Elke dag om **10:00** stuurt de bot een WhatsApp-bericht naar `ADMIN_NUMBER` met het totaal aantal **alle** 4-cijferige berichten van de vorige dag (00:00 t/m 23:59).
```
Aantal dienstaanvragen gisteren: N
📊 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 * * *`).

View file

@ -37,7 +37,7 @@ cron.schedule('0 10 * * *', () => {
resetDay()
const total = getYesterdayTotal()
sendTextMessage(adminNumber, `Aantal dienstaanvragen gisteren: ${total}`)
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))
})

View file

@ -2,28 +2,15 @@ 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')
const { checkAndRepinPinnedMessage } = require('./pinnedMessageService')
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/)
const match = body.match(/^\s*(\d{4})\s*$/)
return match ? match[1] : null
}
function isBotMentioned(message) {
if (message._data?.mentionedJidList?.length > 0) {
return true
}
if (message.body) {
const normalized = message.body.toLowerCase().replace(/\s+/g, '')
if (normalized.includes(`@${BOT_NAME.replace(/\s+/g, '')}`)) {
return true
}
}
return false
}
async function handleWebhook(payload) {
if (payload.dataType !== 'message') return
if (payload.sessionId !== process.env.WHATSAPP_SESSION) return
@ -32,11 +19,14 @@ async function handleWebhook(payload) {
if (!message) return
if (message.fromMe) return
if (message.from !== ALLOWED_GROUP) return
if (!isBotMentioned(message)) return
checkAndRepinPinnedMessage()
const dienstnummer = extractDienstnummer(message.body || '')
if (!dienstnummer) return
incrementDaily()
console.log(`Processing dienst ${dienstnummer} for ${message.author}`)
const author = message.author
@ -50,7 +40,6 @@ async function handleWebhook(payload) {
}
increment(author)
incrementDaily()
const dienst = findDienst(dienstnummer)
if (!dienst) {

View file

@ -0,0 +1,63 @@
const axios = require('axios')
const API_BASE = process.env.WHATSAPP_API_ENDPOINT
const SESSION = process.env.WHATSAPP_SESSION
const ALLOWED_GROUP = process.env.ALLOWED_GROUP
const PIN_DURATION = 30 * 24 * 60 * 60
const PINNED_TEXT = 'Welkom in Dienst opvragen EdGpt-EBS. Stuur hier je 4 cijferige dienstnummer en EdGpt zal het prive naar je toe sturen, alleen 4 cijfers meer niet.'
async function getPinnedMessages() {
try {
const url = `${API_BASE}/groupChat/runMethod/${SESSION}`
const response = await axios.post(url, {
chatId: ALLOWED_GROUP,
method: 'getPinnedMessages'
})
return response.data.data || []
} catch (err) {
console.error('Failed to get pinned messages:', err.message)
return []
}
}
async function sendPinnedMessage() {
const url = `${API_BASE}/client/sendMessage/${SESSION}`
const response = await axios.post(url, {
chatId: ALLOWED_GROUP,
contentType: 'string',
content: PINNED_TEXT
})
return response.data
}
async function pinMessage(messageId) {
const url = `${API_BASE}/message/runMethod/${SESSION}`
const response = await axios.post(url, {
chatId: ALLOWED_GROUP,
messageId,
method: 'pin',
options: PIN_DURATION
})
return response.data
}
async function checkAndRepinPinnedMessage() {
try {
const pinned = await getPinnedMessages()
if (pinned.length > 0) return
console.log('No pinned message found, sending and pinning...')
const sent = await sendPinnedMessage()
const messageId = sent?.message?.id?._serialized
if (!messageId) {
console.error('Failed to get messageId from sent message')
return
}
await pinMessage(messageId)
console.log(`Pinned message sent and pinned: ${messageId}`)
} catch (err) {
console.error('Failed to check/repin pinned message:', err.message)
}
}
module.exports = { checkAndRepinPinnedMessage }