From e4d817ff801960473346a98c743b779c0a6723f5 Mon Sep 17 00:00:00 2001 From: Eddy de Vink Date: Tue, 26 May 2026 09:20:45 +0200 Subject: [PATCH] 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 --- .env.example | 2 +- README.md | 41 ++++++++++++++---------- src/index.js | 2 +- src/messageHandler.js | 23 ++++---------- src/pinnedMessageService.js | 63 +++++++++++++++++++++++++++++++++++++ 5 files changed, 96 insertions(+), 35 deletions(-) create mode 100644 src/pinnedMessageService.js diff --git a/.env.example b/.env.example index 168e1aa..ce6f5ab 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/README.md b/README.md index 6694f60..28a7229 100644 --- a/README.md +++ b/README.md @@ -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 @@ -114,14 +114,23 @@ npx pm2 logs dienst-bot ``` src/ - index.js # Express webhook server + cron job - messageHandler.js # Berichtverwerking en filtering - rateLimiter.js # Spam protectie (max 5 per dag) - dailyStats.js # Dagelijkse aanvraag-teller - dienstService.js # JSON lookup en PNG bestandspaden - whatsappApi.js # WhatsApp API client (bericht + afbeelding) + index.js # Express webhook server + cron job + messageHandler.js # Berichtverwerking en filtering + rateLimiter.js # Spam protectie (max 5 per dag) + 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 * * *`). diff --git a/src/index.js b/src/index.js index a8d9901..852707e 100644 --- a/src/index.js +++ b/src/index.js @@ -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)) }) diff --git a/src/messageHandler.js b/src/messageHandler.js index e15631f..9158c34 100644 --- a/src/messageHandler.js +++ b/src/messageHandler.js @@ -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) { diff --git a/src/pinnedMessageService.js b/src/pinnedMessageService.js new file mode 100644 index 0000000..5ad73f4 --- /dev/null +++ b/src/pinnedMessageService.js @@ -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 }