diff --git a/src/messageHandler.js b/src/messageHandler.js index bd5edf2..9e2b6a8 100644 --- a/src/messageHandler.js +++ b/src/messageHandler.js @@ -10,6 +10,18 @@ const ALLOWED_GROUP_NAMES = (process.env.ALLOWED_GROUP_NAME || '') .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) { return (chatId || '') .replace(/^\u200e/, '') @@ -23,26 +35,36 @@ function extractDienstnummer(body) { } async function handleWebhook(payload) { - if (payload.dataType !== 'message') return - if (payload.sessionId !== process.env.WHATSAPP_SESSION) return + if (payload.type !== 'Message') return - const message = payload.data?.message - if (!message) return - if (message.fromMe) return - if (!ALLOWED_CHAT_IDS.has(normalizeChatId(message.from))) return + const event = payload.event + if (!event || !event.Info) 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 incrementDaily() - sendChatSeen(ALLOWED_GROUP).catch(err => - console.error('Failed to mark chat as seen:', err.message) - ) + if (messageId) { + sendChatSeen(chatId, messageId).catch(err => + 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 const limit = checkLimit(author) diff --git a/src/multiplexer.js b/src/multiplexer.js new file mode 100644 index 0000000..d297943 --- /dev/null +++ b/src/multiplexer.js @@ -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}`)) \ No newline at end of file diff --git a/src/whatsappApi.js b/src/whatsappApi.js index 3001523..f4155c4 100644 --- a/src/whatsappApi.js +++ b/src/whatsappApi.js @@ -2,12 +2,13 @@ const axios = require('axios') const fs = require('fs') 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 RETRY_DELAY_MS = 1000 -// Laat de bridge de juiste mimetype gebruiken bij elke beeld-extensie (png/jpg/jpeg -// vooraf ondersteund; onbekende extensies vallen terug op image/png). +// WuzAPI accepteert jpeg en png als data-URI voor /chat/send/image. PNG's met +// 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) { const ext = (filename || '').split('.').pop().toLowerCase() 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) { return withRetry(async () => { - const url = `${API_BASE}/client/sendMessage/${SESSION}` + const url = `${API_BASE}/chat/send/text` const response = await axios.post(url, { - chatId, - contentType: 'string', - content: text - }) + Phone: chatId, + Body: text + }, { headers: wuzapiHeaders() }) return response.data }, `sendTextMessage to ${chatId}`) } @@ -58,16 +64,11 @@ async function sendImageMessage(chatId, filePath, filename) { return withRetry(async () => { const fileBuffer = fs.readFileSync(filePath) 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, { - chatId, - contentType: 'MessageMedia', - content: { - mimetype: mimeFromFilename(filename), - data: base64Data, - filename: filename - } - }) + Phone: chatId, + Image: `data:${mimeFromFilename(filename)};base64,${base64Data}` + }, { headers: wuzapiHeaders() }) return response.data }, `sendImageMessage ${filename} to ${chatId}`) } @@ -93,9 +94,10 @@ async function notifyAdmin(errorText) { } } -async function sendChatSeen(chatId) { - const url = `${API_BASE}/client/sendSeen/${SESSION}` - const response = await axios.post(url, { chatId }) +async function sendChatSeen(chatId, messageId) { + const url = `${API_BASE}/chat/markread` + const body = { Id: [messageId], Chat: chatId } + const response = await axios.post(url, body, { headers: wuzapiHeaders() }) return response.data }