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:
parent
5b83076b33
commit
e4d817ff80
5 changed files with 96 additions and 35 deletions
|
|
@ -4,7 +4,7 @@ WHATSAPP_SESSION=EdGpt
|
||||||
|
|
||||||
# Webhook Configuration
|
# Webhook Configuration
|
||||||
WEBHOOK_PORT=3002
|
WEBHOOK_PORT=3002
|
||||||
ALLOWED_GROUP=120363407356808413@g.us
|
ALLOWED_GROUP=120363407613652082@g.us
|
||||||
BOT_NAME=EdGpt
|
BOT_NAME=EdGpt
|
||||||
|
|
||||||
# Paths
|
# Paths
|
||||||
|
|
|
||||||
29
README.md
29
README.md
|
|
@ -1,16 +1,17 @@
|
||||||
# Dienst Bot
|
# 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) privé verstuurt op basis van een 4-cijferig dienstnummer, gepost in een speciale WhatsApp groep.
|
||||||
|
|
||||||
WhatsApp bot die dienstschema's (PNG) verstuurt op basis van een dienstnummer in een WhatsApp groep, en dagelijks een samenvatting stuurt.
|
|
||||||
|
|
||||||
## Hoe het werkt
|
## 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`
|
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
|
## Vereisten
|
||||||
|
|
||||||
|
|
@ -38,8 +39,7 @@ WHATSAPP_SESSION=EdGpt
|
||||||
|
|
||||||
# Webhook
|
# Webhook
|
||||||
WEBHOOK_PORT=3002
|
WEBHOOK_PORT=3002
|
||||||
ALLOWED_GROUP=120363407356808413@g.us
|
ALLOWED_GROUP=120363407613652082@g.us
|
||||||
BOT_NAME=EdGpt
|
|
||||||
|
|
||||||
# Bestanden
|
# Bestanden
|
||||||
DIENSTEN_JSON=./diensten/dienst-tijden.json
|
DIENSTEN_JSON=./diensten/dienst-tijden.json
|
||||||
|
|
@ -120,8 +120,17 @@ src/
|
||||||
dailyStats.js # Dagelijkse aanvraag-teller
|
dailyStats.js # Dagelijkse aanvraag-teller
|
||||||
dienstService.js # JSON lookup en PNG bestandspaden
|
dienstService.js # JSON lookup en PNG bestandspaden
|
||||||
whatsappApi.js # WhatsApp API client (bericht + afbeelding)
|
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
|
## Foutafhandeling
|
||||||
|
|
||||||
- **Ongeldig dienstnummer**: de bot stuurt een melding terug naar de afzender
|
- **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
|
## 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 * * *`).
|
De teller wordt bijgehouden in memory door `dailyStats.js` en gereset na verzending van de samenvatting. De scheduling gebruikt `node-cron` (`0 10 * * *`).
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ cron.schedule('0 10 * * *', () => {
|
||||||
resetDay()
|
resetDay()
|
||||||
const total = getYesterdayTotal()
|
const total = getYesterdayTotal()
|
||||||
|
|
||||||
sendTextMessage(adminNumber, `Aantal dienstaanvragen gisteren: ${total}`)
|
sendTextMessage(adminNumber, `📊 Aantal dienstaanvragen gisteren: ${total}`)
|
||||||
.then(() => console.log(`Daily summary sent: ${total} requests`))
|
.then(() => console.log(`Daily summary sent: ${total} requests`))
|
||||||
.catch(err => console.error('Failed to send daily summary:', err.message))
|
.catch(err => console.error('Failed to send daily summary:', err.message))
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,28 +2,15 @@ const { findDienst, getPngFilePaths } = require('./dienstService')
|
||||||
const { sendTextMessage, sendImageMessage, notifyAdmin } = require('./whatsappApi')
|
const { sendTextMessage, sendImageMessage, notifyAdmin } = require('./whatsappApi')
|
||||||
const { checkLimit, increment, MAX_REQUESTS_PER_DAY } = require('./rateLimiter')
|
const { checkLimit, increment, MAX_REQUESTS_PER_DAY } = require('./rateLimiter')
|
||||||
const { increment: incrementDaily } = require('./dailyStats')
|
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
|
const ALLOWED_GROUP = process.env.ALLOWED_GROUP
|
||||||
|
|
||||||
function extractDienstnummer(body) {
|
function extractDienstnummer(body) {
|
||||||
const match = body.match(/\b(\d{4})\b/)
|
const match = body.match(/^\s*(\d{4})\s*$/)
|
||||||
return match ? match[1] : null
|
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) {
|
async function handleWebhook(payload) {
|
||||||
if (payload.dataType !== 'message') return
|
if (payload.dataType !== 'message') return
|
||||||
if (payload.sessionId !== process.env.WHATSAPP_SESSION) return
|
if (payload.sessionId !== process.env.WHATSAPP_SESSION) return
|
||||||
|
|
@ -32,11 +19,14 @@ async function handleWebhook(payload) {
|
||||||
if (!message) return
|
if (!message) return
|
||||||
if (message.fromMe) return
|
if (message.fromMe) return
|
||||||
if (message.from !== ALLOWED_GROUP) return
|
if (message.from !== ALLOWED_GROUP) return
|
||||||
if (!isBotMentioned(message)) return
|
|
||||||
|
checkAndRepinPinnedMessage()
|
||||||
|
|
||||||
const dienstnummer = extractDienstnummer(message.body || '')
|
const dienstnummer = extractDienstnummer(message.body || '')
|
||||||
if (!dienstnummer) return
|
if (!dienstnummer) return
|
||||||
|
|
||||||
|
incrementDaily()
|
||||||
|
|
||||||
console.log(`Processing dienst ${dienstnummer} for ${message.author}`)
|
console.log(`Processing dienst ${dienstnummer} for ${message.author}`)
|
||||||
|
|
||||||
const author = message.author
|
const author = message.author
|
||||||
|
|
@ -50,7 +40,6 @@ async function handleWebhook(payload) {
|
||||||
}
|
}
|
||||||
|
|
||||||
increment(author)
|
increment(author)
|
||||||
incrementDaily()
|
|
||||||
|
|
||||||
const dienst = findDienst(dienstnummer)
|
const dienst = findDienst(dienstnummer)
|
||||||
if (!dienst) {
|
if (!dienst) {
|
||||||
|
|
|
||||||
63
src/pinnedMessageService.js
Normal file
63
src/pinnedMessageService.js
Normal 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 }
|
||||||
Loading…
Reference in a new issue