Migreer naar WuzAPI (send via chat/send, payload via event.Info) + multiplexer

This commit is contained in:
Eddy de Vink 2026-10-11 22:33:28 +02:00
commit 49d6fe15d6
3 changed files with 121 additions and 32 deletions

View file

@ -10,6 +10,18 @@ const ALLOWED_GROUP_NAMES = (process.env.ALLOWED_GROUP_NAME || '')
.filter(Boolean) .filter(Boolean)
const ALLOWED_CHAT_IDS = new Set([ALLOWED_GROUP, ...ALLOWED_GROUP_NAMES].filter(Boolean)) const ALLOWED_CHAT_IDS = new Set([ALLOWED_GROUP, ...ALLOWED_GROUP_NAMES].filter(Boolean))
// WuzAPI-webhook-payload-mapping. WuzAPI stuurt (WEBHOOK_FORMAT=json) het volledig
// whatsmeow-event als { "event": {...}, "type": "Message", ... }. De relevante
// velden (uit whatsmeow-types): event.Info.Chat/Sender = {User, Server},
// event.Info.IsFromMe (bool), event.Info.ID, event.Message.conversation.
function jidFromObject(jid) {
// whatsmeow-JID serialiseert als {User, RawAgent, Device, Integrator, Server}
if (!jid || typeof jid !== 'object') return String(jid || '')
const server = jid.Server || 's.whatsapp.net'
const device = jid.Device ? `:${jid.Device}` : ''
return `${jid.User}${device}@${server}`
}
function normalizeChatId(chatId) { function normalizeChatId(chatId) {
return (chatId || '') return (chatId || '')
.replace(/^\u200e/, '') .replace(/^\u200e/, '')
@ -23,26 +35,36 @@ function extractDienstnummer(body) {
} }
async function handleWebhook(payload) { async function handleWebhook(payload) {
if (payload.dataType !== 'message') return if (payload.type !== 'Message') return
if (payload.sessionId !== process.env.WHATSAPP_SESSION) return
const message = payload.data?.message const event = payload.event
if (!message) return if (!event || !event.Info) return
if (message.fromMe) return
if (!ALLOWED_CHAT_IDS.has(normalizeChatId(message.from))) return
const dienstnummer = extractDienstnummer(message.body || '') const chatId = jidFromObject(event.Info.Chat)
const author = jidFromObject(event.Info.Sender)
if (event.Info.IsFromMe) return
if (!ALLOWED_CHAT_IDS.has(normalizeChatId(chatId))) return
const messageId = event.Info.ID
// Tekst: conversation of extendedTextMessage.text; protobuf-veldnamen kunnen
// zowel lowerCamel (protojson) als Go-vorm (Conversation) zijn — beide afdekken.
const msg = event.Message || {}
const body = msg.conversation || msg.Conversation ||
msg.extendedTextMessage?.text || msg.ExtendedTextMessage?.Text || ''
const dienstnummer = extractDienstnummer(body)
if (!dienstnummer) return if (!dienstnummer) return
incrementDaily() incrementDaily()
sendChatSeen(ALLOWED_GROUP).catch(err => if (messageId) {
sendChatSeen(chatId, messageId).catch(err =>
console.error('Failed to mark chat as seen:', err.message) console.error('Failed to mark chat as seen:', err.message)
) )
}
console.log(`Processing dienst ${dienstnummer} for ${message.author}`) console.log(`Processing dienst ${dienstnummer} for ${author}`)
const author = message.author || message.from
if (!author) return if (!author) return
const limit = checkLimit(author) const limit = checkLimit(author)

65
src/multiplexer.js Normal file
View file

@ -0,0 +1,65 @@
// Multiplexer: ontvangt de WuzAPI-webhook (1 sessie, nummer 31616173970) en
// routeert per chat-groep naar de juiste bot.
// dienstgroep 120363407613652082@g.us -> dienst-bot :3002/webhook
// merplus bron 120363429978048879@g.us -> EBS-MerPlus :3006/webhook
const http = require('http')
const axios = require('axios')
const DIENST_GROUP = process.env.DIENST_GROUP || '120363407613652082@g.us'
const MERPLUS_GROUP = process.env.MERPLUS_GROUP || '120363429978048879@g.us'
const DIENST_WEBHOOK = process.env.DIENST_WEBHOOK || 'http://127.0.0.1:3002/webhook'
const MERPLUS_WEBHOOK = process.env.MERPLUS_WEBHOOK || 'http://127.0.0.1:3006/webhook'
const PORT = parseInt(process.env.PORT || '3007', 10)
// whatsmeow-JID serialiseert als {User, Device, Server}
function jidFromObject(jid) {
if (!jid) return ''
if (typeof jid === 'string') return jid
return `${jid.User}${jid.Device ? ':' + jid.Device : ''}@${jid.Server}`
}
function chatJID(payload) {
const chat = payload.event?.Info?.Chat
if (!chat) return ''
// kan object of reeds-string zijn
if (typeof chat === 'object') return jidFromObject(chat)
return chat // "120363407613652082@g.us"
}
const server = http.createServer((req, res) => {
if (req.method !== 'POST' || req.url !== '/webhook') {
res.writeHead(404, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ success: false }))
return
}
let body = ''
req.on('data', c => (body += c))
req.on('end', async () => {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ success: true }))
let payload
try {
payload = JSON.parse(body)
} catch {
console.error('Invalid webhook JSON')
return
}
if (payload.type !== 'Message') return
const chat = chatJID(payload)
let target
if (chat === DIENST_GROUP) target = DIENST_WEBHOOK
else if (chat === MERPLUS_GROUP) target = MERPLUS_WEBHOOK
else {
console.log(`[multiplexer] chat ${chat} niet gerouteerd (geen match)`)
return
}
try {
await axios.post(target, payload, { timeout: 15000 })
console.log(`[multiplexer] ${chat} -> ${target}`)
} catch (err) {
console.error(`[multiplexer] doorsturen naar ${target} faalde: ${err.message}`)
}
})
})
server.listen(PORT, '0.0.0.0', () => console.log(`multiplexer luistert op ${PORT}`))

View file

@ -2,12 +2,13 @@ const axios = require('axios')
const fs = require('fs') const fs = require('fs')
const API_BASE = process.env.WHATSAPP_API_ENDPOINT const API_BASE = process.env.WHATSAPP_API_ENDPOINT
const SESSION = process.env.WHATSAPP_SESSION const API_TOKEN = process.env.WHATSAPP_API_TOKEN
const MAX_RETRIES = 3 const MAX_RETRIES = 3
const RETRY_DELAY_MS = 1000 const RETRY_DELAY_MS = 1000
// Laat de bridge de juiste mimetype gebruiken bij elke beeld-extensie (png/jpg/jpeg // WuzAPI accepteert jpeg en png als data-URI voor /chat/send/image. PNG's met
// vooraf ondersteund; onbekende extensies vallen terug op image/png). // transparante pixels zijn een bekende onbetrouwbare klasse in WhatsApp; de
// bron-repo levert sinds 2026-10-05 JPEG (zie AGENTS.md "1b").
function mimeFromFilename(filename) { function mimeFromFilename(filename) {
const ext = (filename || '').split('.').pop().toLowerCase() const ext = (filename || '').split('.').pop().toLowerCase()
if (ext === 'jpg' || ext === 'jpeg') return 'image/jpeg' if (ext === 'jpg' || ext === 'jpeg') return 'image/jpeg'
@ -42,14 +43,19 @@ async function withRetry(fn, operation) {
} }
} }
// WuzAPI-authenticatie: session/chat-endpoints gebruiken de Token-header met het
// user-token (admin-endpoints gebruiken Authorization). Zie API.md.
function wuzapiHeaders() {
return { Token: API_TOKEN }
}
async function sendTextMessage(chatId, text) { async function sendTextMessage(chatId, text) {
return withRetry(async () => { return withRetry(async () => {
const url = `${API_BASE}/client/sendMessage/${SESSION}` const url = `${API_BASE}/chat/send/text`
const response = await axios.post(url, { const response = await axios.post(url, {
chatId, Phone: chatId,
contentType: 'string', Body: text
content: text }, { headers: wuzapiHeaders() })
})
return response.data return response.data
}, `sendTextMessage to ${chatId}`) }, `sendTextMessage to ${chatId}`)
} }
@ -58,16 +64,11 @@ async function sendImageMessage(chatId, filePath, filename) {
return withRetry(async () => { return withRetry(async () => {
const fileBuffer = fs.readFileSync(filePath) const fileBuffer = fs.readFileSync(filePath)
const base64Data = fileBuffer.toString('base64') const base64Data = fileBuffer.toString('base64')
const url = `${API_BASE}/client/sendMessage/${SESSION}` const url = `${API_BASE}/chat/send/image`
const response = await axios.post(url, { const response = await axios.post(url, {
chatId, Phone: chatId,
contentType: 'MessageMedia', Image: `data:${mimeFromFilename(filename)};base64,${base64Data}`
content: { }, { headers: wuzapiHeaders() })
mimetype: mimeFromFilename(filename),
data: base64Data,
filename: filename
}
})
return response.data return response.data
}, `sendImageMessage ${filename} to ${chatId}`) }, `sendImageMessage ${filename} to ${chatId}`)
} }
@ -93,9 +94,10 @@ async function notifyAdmin(errorText) {
} }
} }
async function sendChatSeen(chatId) { async function sendChatSeen(chatId, messageId) {
const url = `${API_BASE}/client/sendSeen/${SESSION}` const url = `${API_BASE}/chat/markread`
const response = await axios.post(url, { chatId }) const body = { Id: [messageId], Chat: chatId }
const response = await axios.post(url, body, { headers: wuzapiHeaders() })
return response.data return response.data
} }