feat: server-state baseline (Appium-fallback, event-dedupe, retry-semantics, webhook-reg, unlimited-limit)
This commit is contained in:
parent
c97d662856
commit
1d5a233f68
4 changed files with 129 additions and 8 deletions
|
|
@ -1,23 +1,88 @@
|
|||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const API_BASE = process.env.WHATSAPP_API_ENDPOINT
|
||||
const { findDienst, getPngFilePaths } = require('./dienstService')
|
||||
const { sendTextMessage, sendImageMessage, notifyAdmin, sendChatSeen } = require('./whatsappApi')
|
||||
const { checkLimit, increment, MAX_REQUESTS_PER_DAY } = require('./rateLimiter')
|
||||
const { increment: incrementDaily } = require('./dailyStats')
|
||||
|
||||
const ALLOWED_GROUP = process.env.ALLOWED_GROUP
|
||||
const ALLOWED_GROUP_NAMES = (process.env.ALLOWED_GROUP_NAME || '')
|
||||
.split(',')
|
||||
.map(name => name.trim())
|
||||
.filter(Boolean)
|
||||
const ALLOWED_CHAT_IDS = new Set([ALLOWED_GROUP, ...ALLOWED_GROUP_NAMES].filter(Boolean))
|
||||
const appiumEventCooldown = 24 * 60 * 60 * 1000
|
||||
const appiumEventStatePath = process.env.APPIUM_EVENT_STATE_PATH ||
|
||||
path.join(__dirname, '..', 'appium-event-state.json')
|
||||
let appiumEventState = { events: [] }
|
||||
try {
|
||||
appiumEventState = JSON.parse(fs.readFileSync(appiumEventStatePath, 'utf8'))
|
||||
if (!Array.isArray(appiumEventState.events)) appiumEventState = { events: [] }
|
||||
appiumEventState.events = appiumEventState.events.map(event => {
|
||||
const parts = event.key.split('\u0000')
|
||||
return {
|
||||
key: [normalizeChatId(parts[0]), parts[1], parts[2]].join('\u0000'),
|
||||
processedAt: event.processedAt || event.timestamp || 0,
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
appiumEventState = { events: [] }
|
||||
}
|
||||
|
||||
function normalizeChatId(chatId) {
|
||||
return (chatId || '')
|
||||
.replace(/^\u200e/, '')
|
||||
.replace(/\s*\(\d+ berichten?\)$/, '')
|
||||
.trim()
|
||||
}
|
||||
|
||||
function getAppiumEventKey(payload) {
|
||||
return [normalizeChatId(payload.chatId), payload.sender, payload.text].join('\u0000')
|
||||
}
|
||||
|
||||
function isDuplicateAppiumEvent(payload) {
|
||||
const now = Date.now()
|
||||
appiumEventState.events = appiumEventState.events.filter(event =>
|
||||
now - (event.processedAt || event.timestamp || 0) < appiumEventCooldown
|
||||
)
|
||||
return appiumEventState.events.some(event =>
|
||||
event.key === getAppiumEventKey(payload)
|
||||
)
|
||||
}
|
||||
|
||||
function markAppiumEventProcessed(payload) {
|
||||
appiumEventState.events.push({
|
||||
key: getAppiumEventKey(payload),
|
||||
processedAt: Date.now(),
|
||||
})
|
||||
appiumEventState.events = appiumEventState.events.slice(-500)
|
||||
const temporaryPath = `${appiumEventStatePath}.tmp`
|
||||
fs.writeFileSync(temporaryPath, JSON.stringify(appiumEventState))
|
||||
fs.renameSync(temporaryPath, appiumEventStatePath)
|
||||
}
|
||||
function extractDienstnummer(body) {
|
||||
const match = body.match(/^\s*(\d{4,5})\s*$/)
|
||||
return match ? match[1] : null
|
||||
}
|
||||
|
||||
async function handleWebhook(payload) {
|
||||
if (payload.dataType !== 'message') return
|
||||
if (payload.sessionId !== process.env.WHATSAPP_SESSION) return
|
||||
|
||||
const message = payload.data?.message
|
||||
const isWWebEvent = payload.dataType === 'message'
|
||||
const isAppiumEvent = Boolean(payload.sender && payload.chatId)
|
||||
if (!isWWebEvent && !isAppiumEvent) return
|
||||
if (isAppiumEvent && isDuplicateAppiumEvent(payload)) return
|
||||
if (isWWebEvent && payload.sessionId !== process.env.WHATSAPP_SESSION) return
|
||||
const message = isWWebEvent
|
||||
? payload.data?.message
|
||||
: {
|
||||
fromMe: false,
|
||||
from: payload.chatId,
|
||||
author: payload.sender,
|
||||
body: payload.text || '',
|
||||
}
|
||||
if (!message) return
|
||||
if (message.fromMe) return
|
||||
if (message.from !== ALLOWED_GROUP) return
|
||||
if (!ALLOWED_CHAT_IDS.has(normalizeChatId(message.from))) return
|
||||
|
||||
const dienstnummer = extractDienstnummer(message.body || '')
|
||||
if (!dienstnummer) return
|
||||
|
|
@ -30,7 +95,9 @@ async function handleWebhook(payload) {
|
|||
|
||||
console.log(`Processing dienst ${dienstnummer} for ${message.author}`)
|
||||
|
||||
const author = message.author
|
||||
const author = isWWebEvent
|
||||
? message.author || message.from
|
||||
: message.author
|
||||
if (!author) return
|
||||
|
||||
const limit = checkLimit(author)
|
||||
|
|
@ -45,25 +112,32 @@ async function handleWebhook(payload) {
|
|||
const dienst = findDienst(dienstnummer)
|
||||
if (!dienst) {
|
||||
await sendTextMessage(author, `Dienstnummer ${dienstnummer} niet gevonden.`)
|
||||
if (isAppiumEvent) markAppiumEventProcessed(payload)
|
||||
return
|
||||
}
|
||||
|
||||
const pngPaths = getPngFilePaths(dienst)
|
||||
if (!pngPaths || pngPaths.length === 0) {
|
||||
await sendTextMessage(author, `Geen pagina's gevonden voor dienst ${dienstnummer}.`)
|
||||
if (isAppiumEvent) markAppiumEventProcessed(payload)
|
||||
return
|
||||
}
|
||||
|
||||
let sendFailed = false
|
||||
for (const pngPath of pngPaths) {
|
||||
const filename = pngPath.split('/').pop()
|
||||
try {
|
||||
await sendImageMessage(author, pngPath, filename)
|
||||
} catch (err) {
|
||||
sendFailed = true
|
||||
const errorMsg = `Fout bij verzenden ${filename} naar ${author}: ${err.message}`
|
||||
console.error(errorMsg)
|
||||
await notifyAdmin(errorMsg)
|
||||
if (API_BASE.includes(':3001')) {
|
||||
await notifyAdmin(errorMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!sendFailed && isAppiumEvent) markAppiumEventProcessed(payload)
|
||||
}
|
||||
|
||||
module.exports = { handleWebhook, extractDienstnummer }
|
||||
|
|
|
|||
Loading…
Reference in a new issue