Servico auxiliar para conectar a UnoAPI ao modulo @w3nder/whatsapp-voip-wasm.
- Manter a UnoAPI como dona da sessao Baileys
- Isolar o motor VoIP em outro processo
- Receber eventos de chamada da UnoAPI
- Devolver comandos de signaling para a UnoAPI enviar no socket Baileys
- A UnoAPI recebe evento
callno Baileys. - A UnoAPI envia o evento para este servico.
- O servico processa o estado da chamada e aciona o motor VoIP.
- Quando o motor VoIP precisar enviar signaling, este servico devolve comandos no JSON de resposta.
- A UnoAPI usa a sessao Baileys existente para enviar o stanza
call. - Quando a UnoAPI receber signaling bruto do socket, ela chama
/v1/calls/signaling. - A resposta de
/signalingtambem pode devolver comandos para a UnoAPI executar.
GET /healthPOST /v1/calls/eventsPOST /v1/calls/signalingGET /v1/sessions/:session/calls/:callId
O servico devolve a comunicacao de volta para a UnoAPI no proprio corpo da resposta HTTP.
Importante:
- o
unoapi-voip-servicenao precisa conhecer a URL da UnoAPI - hoje nao existe
UNOAPI_URLneste projeto - a UnoAPI inicia a comunicacao chamando este servico
- a resposta volta na mesma requisicao HTTP
- por isso a UnoAPI precisa de
VOIP_SERVICE_URL, mas o servico de VoIP nao precisa de uma URL da UnoAPI
Modelo atual:
UnoAPI -> POST /v1/calls/events -> unoapi-voip-serviceunoapi-voip-service -> HTTP response { state, commands[] } -> UnoAPIUnoAPI -> executa commands[] no socket Baileys
Este projeto so precisaria conhecer a URL da UnoAPI em uma arquitetura diferente, por exemplo:
- callback HTTP assincrono
- websocket entre os dois servicos
- fila/pubsub com workers separados
Formato de retorno de POST /v1/calls/events:
{
"state": {
"session": "5566996269251",
"callId": "abc123",
"from": "123456789012345@lid",
"callerPn": "556696923653@s.whatsapp.net",
"isVideo": false,
"lastEvent": "incoming_call",
"updatedAt": 1774650364000
},
"commands": [
{
"action": "send_call_node",
"session": "5566996269251",
"callId": "abc123",
"peerJid": "123456789012345@lid",
"payloadBase64": "PGNhbGwgdG89IjEyMzQ1Njc4OTAxMjM0NUBsaWQiPjxvZmZlciBjYWxsLWlkPSJhYmMxMjMiLz48L2NhbGw+",
"payloadTag": "call"
}
]
}Formato de retorno de POST /v1/calls/signaling:
{
"state": {
"session": "5566996269251",
"callId": "abc123",
"from": "123456789012345@lid",
"lastEvent": "incoming_call",
"updatedAt": 1774650364000
},
"commands": [
{
"action": "send_call_node",
"session": "5566996269251",
"callId": "abc123",
"peerJid": "123456789012345@lid",
"payloadBase64": "PGNhbGwgdG89IjEyMzQ1Njc4OTAxMjM0NUBsaWQiPjxhY2NlcHQgY2FsbC1pZD0iYWJjMTIzIi8+PC9jYWxsPg==",
"payloadTag": "call"
}
]
}Regras praticas:
commandse sempre um array- quando nao houver nada para a Uno executar, o servico pode devolver
[] send_call_nodee o comando principal para a Uno reenviar signaling no socket BaileyspayloadBase64carrega o stanza ou fragmento XML gerado pelo motor VoIP- a Uno prioriza enviar o stanza completo do WASM quando ele vier com root
call
Request:
{
"session": "5566996269251",
"event": "incoming_call",
"callId": "abc123",
"from": "123456789012345@lid",
"callerPn": "556696923653@s.whatsapp.net",
"isVideo": false,
"timestamp": 1774650364
}Response:
{
"state": {
"session": "5566996269251",
"callId": "abc123",
"from": "123456789012345@lid",
"callerPn": "556696923653@s.whatsapp.net",
"isVideo": false,
"lastEvent": "incoming_call",
"updatedAt": 1774650364000
},
"commands": [
{
"action": "send_call_node",
"session": "5566996269251",
"callId": "abc123",
"peerJid": "123456789012345@lid",
"payloadBase64": "PGNhbGwgdG89IjEyMzQ1Njc4OTAxMjM0NUBsaWQiPjxvZmZlciBjYWxsLWlkPSJhYmMxMjMiLz48L2NhbGw+",
"payloadTag": "call"
}
]
}Request:
{
"session": "5566996269251",
"callId": "abc123",
"peerJid": "123456789012345@lid",
"msgType": "offer",
"payload": "<offer call-id=\"abc123\" call-creator=\"123456789012345@lid\"/>",
"timestamp": 1774650364
}Response:
{
"state": {
"session": "5566996269251",
"callId": "abc123",
"from": "123456789012345@lid",
"lastEvent": "incoming_call",
"updatedAt": 1774650364000
},
"commands": [
{
"action": "send_call_node",
"session": "5566996269251",
"callId": "abc123",
"peerJid": "123456789012345@lid",
"payloadBase64": "PGNhbGwgdG89IjEyMzQ1Njc4OTAxMjM0NUBsaWQiPjxhY2NlcHQgY2FsbC1pZD0iYWJjMTIzIi8+PC9jYWxsPg==",
"payloadTag": "call"
}
]
}No servico novo, os envs ficam assim:
PORT: porta HTTP do servico. Padrao3097VOIP_SERVICE_TOKEN: bearer token interno aceito nas rotas/v1/*
Exemplo:
PORT=3097
VOIP_SERVICE_TOKEN=change-meNa UnoAPI, os envs correspondentes ficam assim:
VOIP_SERVICE_URL=http://localhost:3097
VOIP_SERVICE_TOKEN=change-me
VOIP_SERVICE_TIMEOUT_MS=3000Observacao:
GET /healthcontinua aberto sem token- as rotas
/v1/*exigemAuthorization: Bearer <VOIP_SERVICE_TOKEN>quando o token estiver configurado
Este projeto agora possui arquivos basicos para containerizacao seguindo o estilo da UnoAPI:
Dockerfiledocker-compose.portainer.yml.github/workflows/main.yml
Uso local com Docker:
docker build -t unoapi-voip-service .
docker run --rm -p 3097:3097 \
-e PORT=3097 \
-e VOIP_SERVICE_TOKEN=change-me \
unoapi-voip-serviceUso no Portainer:
- Crie uma stack apontando para este projeto.
- Use o arquivo
docker-compose.portainer.yml. - Defina ao menos:
PORTVOIP_SERVICE_TOKEN
Exemplo de variaveis para a stack:
PORT=3097
VOIP_SERVICE_TOKEN=change-meObservacao importante:
- neste momento o container expoe apenas a porta HTTP
3097 - o plano de controle e signaling ja esta integrado
- o plano de midia real (audio/relay/UDP) ainda nao foi implementado neste servico
- entao ainda nao faz sentido abrir portas UDP de audio no compose atual
- o healthcheck da imagem e do compose usa
node+fetch('/health'), sem depender dewgetoucurl
O projeto agora tem um workflow inspirado na UnoAPI em:
.github/workflows/main.yml
Comportamento:
- push em
mainoudevelop: rodanpm ci,npm run typecheckenpm run build - pull request para
mainoudevelop: roda os mesmos checks - push de tag
v*.*.*: alem dos checks, builda e publica a imagem Docker no GHCR
Imagem publicada:
ghcr.io/<owner>/<repo>:<tag>ghcr.io/<owner>/<repo>:latestnao e forcado pelo workflow atual- tambem sao geradas tags auxiliares de branch e
sha
Exemplo:
- repositorio:
seu-user/unoapi-voip-service - tag Git:
v0.1.0 - imagem publicada:
ghcr.io/seu-user/unoapi-voip-service:v0.1.0
Para isso funcionar no GitHub:
- o repositorio precisa existir no GitHub
- o Actions precisa estar habilitado
- o pacote no GHCR sera publicado com o
GITHUB_TOKENpadrao do workflow
Depois disso, o Portainer pode consumir diretamente a imagem publicada no GHCR em vez de buildar localmente.
O projeto ja possui um adaptador em:
src/services/w3nder_adapter.tssrc/vendor/w3nder-whatsapp-voip-wasm
Ele faz:
- uso local vendor do wrapper e dos recursos WASM
- bootstrap por sessao
initialize()initVoipStack()- captura de
onSignalingXmpp - devolucao de comandos
send_call_node
O wrapper e os recursos do pacote foram vendorados localmente para evitar dependencia de GitHub Packages no deploy.
O build copia worker-bootstrap.js, loader.js, worker-modules.js e whatsapp.wasm para dist/.