Migreer naar WuzAPI (send via chat/send, payload via event.Info) + multiplexer
This commit is contained in:
parent
da621b52d9
commit
49d6fe15d6
3 changed files with 121 additions and 32 deletions
|
|
@ -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
65
src/multiplexer.js
Normal 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}`))
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue