forked from clairton/unoapi-cloud
-
Notifications
You must be signed in to change notification settings - Fork 3
ARQUITETURA
wiki-bot edited this page Feb 12, 2026
·
10 revisions
Este documento explica como o Unoapi integra o Baileys para expor uma API no formato do WhatsApp Cloud, descrevendo os módulos principais e o fluxo ponta‑a‑ponta de mensagens (incluindo Status/Broadcast).
- API HTTP (Express)
- Rotas e Controllers recebem requisições REST e encaminham para os serviços.
- Services
- Incoming/Outgoing orquestram envio/recebimento.
- Client (Baileys ou Forward) encapsula o transporte WhatsApp.
- Socket (wrapper do WASocket) gerencia ciclo de vida da conexão Baileys e operações low‑level.
- Listener processa eventos de entrada e repassa para webhooks/broadcast.
- DataStore abstrai persistência (Redis ou Arquivo), cacheando IIDs, mensagens, metadados de grupos e URLs de mÃdia.
- Broadcast publica eventos internos via Socket.IO para UI.
- Infraestrutura
- Redis e RabbitMQ (opcionais) para filas e estado.
- MinIO/S3 (opcional) para armazenamento de mÃdias.
- Cliente chama
POST /vXX.Y/{phone}/messagescom payload compatÃvel com Cloud API. -
MessagesController.indexnormaliza body/opções (ex.:statusIidList,broadcast) e delega paraIncoming.send. -
IncomingBaileys.sendobtém/cria umClientpara{phone}viagetClientBaileyse chamaclient.send(payload, options). -
ClientBaileys.send:- Monta o conteúdo Baileys (templates, checagem de mÃdias, conversão opcional de áudio).
- Aplica polÃticas para grupos e checagens brandas de participação.
- Para Status (Stories), garante
broadcaste preparastatusIidList. - Chama
sendMessageprovido porsocket.ts.
-
socket.tsmantém oWASocketconectado e expõesend/exists/read/....- Valida o estado da sessão, prioriza LID internamente quando possÃvel (1:1 e grupos), mapeia LID⇄PN e pré‑assegura sessões (LID primeiro) para reduzir erros de decrypt/ack.
- Para
status@broadcast, resolve cada entrada destatusIidListviaexists()e remove números sem WhatsApp. Só destinatários válidos são relayados.
- O Baileys envia a mensagem, o Unoapi persiste chaves/mensagem no DataStore e retorna resposta no formato Cloud API.
- Entrada:
to = "status@broadcast",type = text|image|video|...,options.statusIidList = [números | IIDs]. -
socket.tsresolve cada entrada comexists(raw):- Mantém apenas quem tem WhatsApp (filtra inválidos).
- Normaliza LID→PN conforme
STATUS_ALLOW_LID. - Remove duplicados.
- Envia uma vez e usa
relayMessagecom a lista filtrada. - Resposta adiciona:
-
status_skipped: entradas ignoradas por não terem WhatsApp. -
status_recipients: quantidade de destinatários válidos.
-
-
STATUS_BROADCAST_ENABLED(env): quando definido comofalse, o envio parastatus@broadcasté bloqueado antes de chegar ao WhatsApp. Útil para evitar risco de bloqueio de conta quando a polÃtica não permite uso de Status.
-
socket.tsassina eventos do Baileys (messages.upsert, update, receipts, groups, calls, etc.). -
ListenerBaileysnormaliza e envia para webhooks ou processamento local. -
Broadcastemite eventos de UI via Socket.IO (/ws) para QR code e notificações.
-
StoreprovêsessionStoreedataStore(Redis ou Arquivo):-
data_store_*: cache de IIDs (onWhatsApp), mensagens, URLs de mÃdia, metadados de grupos. - Cache PN↔LID: mantido por sessão (arquivo/redis) e populado por eventos do Baileys e consultas.
-
session_store: máquina de estados de conexão (connecting/online/offline/standby), timeouts e reconexões.
-
- Checagens antes de enviar:
- Valida estado da sessão (connecting/offline/disconnected/standby) → mapeado em códigos
SendError. - Para grupos, checagem branda de participação; pré‑assert de sessões dos participantes (prioridade LID).
- Auto‑retry em ack 421 alternando modo de endereçamento (PN⇄LID).
- Valida estado da sessão (connecting/offline/disconnected/standby) → mapeado em códigos
- Desconexões:
- Detecta
loggedOut/connectionReplaced/restartRequired, notifica e reconecta conforme configuração.
- Detecta
- Em casos raros, o libsignal pode retornar “No sessions� ao enviar em grupos (falta de sessão de cifra para algum participante).
- O socket realiza um fallback automático:
- Consulta os participantes do grupo (inclui variantes PN/LID e a própria identidade).
- Executa
assertSessionspara todos (massa → chunks → divisão PN/LID quando ajuda), respeitando limites para evitar sobrecarga. - Aplica um atraso adaptativo para propagação do sender-key e tenta enviar novamente uma vez; se ainda falhar, alterna o addressingMode (PN↔LID) para uma última tentativa.
- Esse comportamento reduz falhas intermitentes sem alterar a API de chamada.
HeurÃsticas para grupos grandes
- Quando o grupo é “grande� (ver
GROUP_LARGE_THRESHOLD), o cliente prefere endereçamento PN e evita asserts pesados, usando atraso adaptativo. - Asserts disparados por recibos (
message-receipt.updatecom retry) são limitados por grupo e quantidade de alvos para evitar loops e alta CPU.
- Webhooks preferem PN para
wa_id,fromerecipient_id. Se não for possÃvel obter PN com segurança, o LID/IID é retornado como fallback. - Internamente (envio e asserts), usamos LID sempre que possÃvel: 1:1 tenta aprender PN→LID em tempo de execução; em grupos, asserts são feitos com LID prioritariamente.
- Imagens de perfil: salvas e consultadas por um identificador canônico PN quando possÃvel (inclusive em S3), evitando duplicidade entre PN/LID.
-
GROUP_SEND_ADDRESSING_MODE(''|lid|pn): vazio implica LID por padrão. -
GROUP_SEND_PREASSERT_SESSIONS(true): habilita assert prévio de sessões em grupos (LID primeiro). -
GROUP_LARGE_THRESHOLD(800): acima disso, evita asserts pesados e usa atrasos adaptativos. -
JIDMAP_CACHE_ENABLED(true) eJIDMAP_TTL_SECONDS(604800): cache PNâ?"LID. -
STATUS_BROADCAST_ENABLED(true): habilita/desabilita o envio parastatus@broadcast.
Para ver logs de aprendizado PN→LID e asserts, ajuste LOG_LEVEL/UNO_LOG_LEVEL para debug.
- Caminho de entrega
- Webhooks de saÃda são produzidos em
UNOAPI_QUEUE_OUTGOINGe consumidos porjobs/outgoing.ts, que chamaOutgoingCloudApi.sendHttp(). - Eventos gerados pela API HTTP (
/messages) e pelo listener Baileys também disparam webhooks dentro de consumidores AMQP; portanto, herdam o mesmo modelo de retentativa.
- Webhooks de saÃda são produzidos em
- Modelo de retry (envelope AMQP)
- Se o consumidor lançar erro (HTTP não‑2xx do webhook, timeout ou exceção), a mensagem é republicada com atraso fixo de 60s.
- As retentativas seguem até
UNOAPI_MESSAGE_RETRY_LIMIT(padrão 5). - Ao atingir o limite, a mensagem vai para a dead‑letter da fila.
- Timeouts e delays
- Timeout HTTP por webhook:
webhook.timeoutMs(AbortSignal timeout). - Timeout global do consumidor:
CONSUMER_TIMEOUT_MS(padrão 15000ms). - Atraso de retry: 60s, via exchange delayed.
- Timeout HTTP por webhook:
- Notificação de falhas
- Com
NOTIFY_FAILED_MESSAGES=true, ao estourar as retentativas, um texto de diagnóstico é enviado para o número da sessão com detalhes do erro/stack.
- Com
- Reenvio a partir de dead‑letter (opcional)
- O processo
wakerconsome dead‑letters e reenfileira nas filas principais, dando nova chance às mensagens.
- O processo
- Sessão/Conexão:
CONNECTION_TYPE,QR_TIMEOUT_MS,VALIDATE_SESSION_NUMBER,CLEAN_CONFIG_ON_DISCONNECT. - Logs:
LOG_LEVEL,UNO_LOG_LEVEL. - Status:
STATUS_ALLOW_LID(manter LID ou normalizar para PN). - Grupos:
GROUP_SEND_MEMBERSHIP_CHECK,GROUP_SEND_PREASSERT_SESSIONS,GROUP_SEND_ADDRESSING_MODE. - MÃdia: S3/MinIO
STORAGE_*,FETCH_TIMEOUT_MS, conversão opcional de áudio para PTT.
- Controllers:
src/controllers/* - Transporte:
src/services/client_baileys.tssrc/services/socket.tssrc/services/listener_baileys.ts
- Integração:
src/services/incoming_baileys.tssrc/services/outgoing.tssrc/services/broadcast.ts
- Dados/Estado:
-
src/services/data_store_file.ts/src/services/data_store_redis.ts src/services/session_store.ts
-
- Comum:
src/services/transformer.tssrc/defaults.ts
-
READ_ON_RECEIPT
- Flag global/por sessão que marca como lidas as mensagens ao receber.
- Implementado em
src/services/client_baileys.ts: após tratarmessages.upsert, se habilitado e a mensagem não forfromMe, chamareadMessages([key]). - Atua apenas em mensagens futuras (não é retroativo).
-
READ_ON_REPLY (por sessão)
- Quando habilitado, após um envio bem-sucedido para um chat, o Unoapi marca como lida a última mensagem recebida (não‑fromMe) daquele chat.
- Persistência do ponteiro (última recebida por chat):
- Interface:
getLastIncomingKey/setLastIncomingKeyemsrc/services/data_store.ts. - File store: mapa em memória em
src/services/data_store_file.ts. - Redis store: chaves
unoapi-last-incoming:<session>:<jid>emsrc/services/redis.ts, com ponte emsrc/services/data_store_redis.ts.
- Interface:
- Atualização do ponteiro (ao receber):
src/services/listener_baileys.tsatualiza sempre que processa mensagem de entrada não‑fromMe. - Disparo ao responder (após enviar):
src/services/socket.tsverificaconfig.readOnReply; se true, busca o ponteiro para o IID alvo (normalizando LID→PN quando necessário) e chamareadMessages([key]). -
readMessagespré‑assegura sessões dos IIDs envolvidos para evitar “No sessions� ao aplicar leitura. - Privacidade segue o WhatsApp: o visto depende das configurações de leitura de ambos os usuários.
Flags e configuração
-
READ_ON_RECEIPT,READ_ON_REPLYestão emsrc/defaults.tse são ligadas ao config por sessão emsrc/services/config_by_env.ts(camposreadOnReceiptereadOnReplyemsrc/services/config.ts).
-
Objetivos
- Preferir PN (dÃgitos) em vez de LID para usuários em todos os payloads de webhook.
- Manter IIDs de grupo (
@g.us) intactos. - Preencher o mapeamento PN↔LID de forma oportunista para melhorar conversões futuras.
-
Fontes de decisão
- Participantes do grupo (
groupMetadata.participantsdo Baileys): fornecem PN IIDs; usados para resolver LID → PN imediatamente. -
jidNormalizedUser(lid): derivação PN IID a partir do LID (fallback quando não há cache).
- Participantes do grupo (
-
Onde ocorre
-
src/services/transformer.ts(fromBaileysMessageContent): resolve o telefone do remetente usando campos PN, participantes do grupo e, por último, LID normalizado. -
src/services/outgoing_cloud_api.ts: antes de enviar o webhook, converte-
contacts[*].wa_id•messages[*].from•statuses[*].recipient_idpara PN quando possÃvel; se vier@lide o cache estiver vazio, deriva PN viajidNormalizedUsere atualiza o mapping.
-
-
src/jobs/transcriber.ts: aplica a mesma conversão antes de enviar o webhook de transcrição.
-
-
Armazenamento do mapping
- DataStore expõe
getPnForLid/getLidForPn/setIidMapping(Redis ou Arquivo). - Cache é por sessão e compartilhado entre Listener/Outgoing/Transcriber.
- DataStore expõe
-
Cache de contato (por sessão)
- API do DataStore:
-
setContactInfo(jid, { name?, pnIid?, lidIid?, pn? })egetContactInfo(jid) -
setContactName/getContactNamecontinuam; passam a ler/escrever junto com o info.
-
- Persistência:
- Arquivo: mapas em memória persistidos via
toISON/fromISON. - Redis: chaves
unoapi-contact-info:<sessão>:<jid>(ISON) eunoapi-contact-name:<sessão>:<jid>.
- Arquivo: mapas em memória persistidos via
- API do DataStore:
-
Alimentação do cache
-
messages.upsert: upsert (nome deverifiedBizNameoupushName) parakey.participant/key.remoteIid; preenche pnIid/lidIid/pn; logaCONTACT_CACHE upsert …. -
contacts.set,contacts.upsert,contacts.update: upserts de roster (verifiedName|businessName|name|notify); logaCONTACT_CACHE set|upsert …. -
getMessageMetadata(grupo): injetagroupMetadata.namesconsultando o cache e cria apelidos por dÃgitos (PN) para facilitar substituição de menções.
-
-
Normalização de menções (@...)
- Local:
src/services/transformer.tsnos tiposconversation/extendedTextMessage. - Usa
contextInfo.mentionedIid(Baileys) egroupMetadata.namesquando presente. - Preferência de substituição no body:
@nome(quando houver) →@PN→@LID-dÃgitos. - Logging: emite um debug
MENTION normalized: "antes" -> "depois"por mensagem.
- Local:
Visão geral (quando SEND_PROFILE_PICTURE=true):
[Baileys] --profilePictureUrl(jid)--> [socket.fetchImageUrl]
│ │
│ chama DataStore.loadImageUrl(jid, sock)
│ │
│ ┌──── se já houver URL em cache, retorna ────�
│ │ │
│ └─────────────────────────────────────────────┘
│ │
│ busca URL CDN no WhatsApp (primeira vez)
│ │
│ persiste via mediaStore.saveProfilePicture
│ │
├──────── backend S3 ────────────────┴──────── backend filesystem ────────�
│ PutObject em <phone>/profile-pictures/<canonico>.jpg │
│ retorna URL pré‑assinada (expira em DATA_URL_TTL) │
│ │
│ grava arquivo em <baseStore>/medias
│ retorna BASE_URL/v15.0/download/...
└──────────────────────────────────────────────────────────────────────────┘
O Transformer injeta a URL no payload do webhook:
- Contato: contacts[0].profile.picture
- Grupo: group_picture
Retenção e limpeza:
- Objetos seguem DATA_TTL. Com S3+AMQP, um job com delay remove o objeto; no FS, a remoção é local quando necessário.
- Envio → Controller → Incoming → Client → Socket.send → Baileys → DataStore → Response
- Status → Normaliza
statusIidList(filtro exists) → sendMessage → relayMessage(válidos) - Recebimento → Eventos Socket → Listener → Webhooks/Broadcast
- Novo tipo de envio: estender transformer e mapear em
ClientBaileys.send. - Novo store: implementar DataStore/SessionStore e ligar via config.
- Comportamento de broadcast: ajustar
STATUS_*emdefaults.ts.