Pedido do usuário: "pode iniciar a montar o IVR e as rotas de entrada". Investigando antes de escrever qualquer XML de IVR, achei que NENHUMA chamada de entrada por tronco tinha como funcionar hoje, IVR ou não: nenhuma carregava `b2bcall_tenant_id` (só REGISTER de ramal e discagem de saída setam essa variable), e mesmo corrigindo isso, o profile "external" apontava pro contexto "public" vanilla — um arquivo ESTÁTICO, que sempre ganha de uma consulta ao mod_xml_curl, então nunca seria dinâmico enquanto se chamasse "public". Perguntei ao usuário a granularidade certa (por DID ou por tronco) antes de desenhar o schema — escolheu por DID, mais flexível (um tronco pode carregar vários números com destinos diferentes). `InboundRoute` nova (RLS real, FORCE ROW LEVEL SECURITY): `didNumber` único GLOBAL entre tenants (mesma exceção já aceita em Tenant.telephonyDomain) — é a ÚNICA forma de descobrir de qual tenant é uma chamada de entrada ANTES de identificar o tenant. Resolvido por fan-out sobre tenants ativos, nunca uma query sem contexto de RLS. Dockerfile repontou o profile external pra context="inbound" (sem arquivo estático, cai no mod_xml_curl de verdade). O XML gerado pra esse contexto injeta b2bcall_tenant_id + domain_name (achado real: sem setar domain_name explicitamente, o bridge da "Discagem interna" resolvia pro domínio GLOBAL default, nunca pro do tenant) e transfere pro dialplan real do tenant — reaproveita 100% do que já existe, inclusive pickup de grupo (PHASE 53). CRUD completo (InboundRoutesController, permissions novas no seed) + tela "Telefonia > Rotas de Entrada" no frontend. Testado com uma chamada REAL: um softphone registrado como ramal normal, outro discando direto pro profile external (porta 5080, sem registrar — exatamente como um provedor de tronco manda) um DID cadastrado. `show channels` confirma: tenant certo, contexto certo, domínio certo no bridge, codec PCMU negociado nos dois lados, ramal tocou e atendeu de verdade. Detalhes completos, inclusive uma tentativa de teste que falhou por limitação do canal `loopback` (não um bug), em docs/INBOUND_ROUTES.md. O IVR em si (menu com play_and_get_digits) fica pra próxima fase — esta é a fundação sem a qual nada de chamada de entrada funcionava. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
5.5 KiB
Rotas de entrada (PHASE 56)
Pedido do usuário: "pode iniciar a montar o IVR e as rotas de entrada". A
investigação achou que nenhuma chamada de tronco tinha como funcionar
hoje, IVR ou não — esse documento cobre a fundação (rota por DID →
tenant); o IVR em si (menu com play_and_get_digits) é a próxima fase,
construída em cima disso.
O achado real
Toda chamada originada de um ramal registrado ou de discagem de saída
carrega o channel variable b2bcall_tenant_id (setado por
buildDirectoryUserXml/originate do discador). Uma chamada de ENTRADA
por tronco nunca carregava isso — não existia nenhum mecanismo pra
identificar de qual tenant uma chamada de entrada é, então
resolveDialplanXml (apps/freeswitch-config) sempre devolvia
"not found" pra ela.
Pior: mesmo se essa variable existisse, o profile external do
FreeSWITCH (onde troncos recebem chamada) aponta pro contexto public
por padrão — um arquivo estático (dialplan/public.xml, vanilla).
Config estática sempre ganha de uma consulta ao mod_xml_curl, então
mesmo trocando a aplicação, esse contexto nunca seria dinâmico enquanto
se chamasse "public".
O mecanismo
-
InboundRoute(nova tabela, tenant-scoped, RLS real) — cada linha édidNumber(o número que o provedor manda no INVITE) →tenantId+destinationContext/destinationNumber(pra onde a chamada cai dentro do dialplan do TENANT DONO).didNumberé@uniqueglobal — mesma exceção já aceita emTenant.telephonyDomain(PHASE 50): é a ÚNICA forma de descobrir de qual tenant é uma chamada de entrada ANTES de saber o tenant. -
Dockerfile: o profile
externalfoi repontado decontext="public"pracontext="inbound"— um contexto sintético, sem nenhum arquivo estático, então toda chamada de entrada cai obrigatoriamente nomod_xml_curl(b2bcall-fs-config, binding já genérico pra qualquer contexto do sectiondialplan).dialplan/public.xmlcontinua no disco, só não é mais alcançado por nenhum profile. -
apps/freeswitch-config: quandoresolveDialplanXmlrecebe uma requisição SEMvariable_b2bcall_tenant_ide comCaller-Context == "inbound", lêCaller-Destination-Number(o DID discado) e faz fan-out sobre os tenants ativos (withTenantContextpor tenant, nunca uma query sem contexto —InboundRoutetemFORCE ROW LEVEL SECURITYde verdade) até achar quem é dono do DID. -
buildInboundRouteXml(packages/telephony) gera um XML de dialplan mínimo pro contextoinbound:set b2bcall_tenant_id=<id>,set domain_name=<domínio do tenant>(achado real testando com chamada de verdade — sem isso obridge data="user/${destination_number}@${domain_name}"da regra "Discagem interna" resolvia pro domínio GLOBAL default, nunca pro do tenant, porque a leg de entrada não é um ramal registrado e nada preenche essa variable sozinho), etransfer <destinationNumber> XML <destinationContext>. Otransferdispara uma nova consulta de dialplan — agora já comb2bcall_tenant_idpresente — reaproveitando 100% do dialplan normal do tenant (a mesma regra "Discagem interna" já testada pro pickup de grupo, PHASE 53).
Testado ponta a ponta com uma chamada real
Nenhum tronco PSTN real disponível neste laboratório — simulado com dois
softphones reais (linphone-cli) em containers Docker na mesma rede: um
registrado como ramal normal (8001@acme.b2bcall.net), outro discando
DIRETO pro profile external (porta 5080, sem registrar — exatamente
como um provedor de tronco manda uma chamada) um número (DID) cadastrado
numa InboundRoute apontando pra 8001. Confirmado via show channels:
a chamada de entrada resolveu o tenant certo, transferiu pro contexto
default do tenant, bridged com o user/8001@acme.b2bcall.net (domínio
correto, não o global) e ficou ACTIVE com codec PCMU negociado nos dois
lados — ramal tocou, atendeu, áudio bidirecional confirmado.
(Tentativa inicial usando originate loopback/.../inbound &park() como
simulação deu INCOMPATIBLE_DESTINATION/488 — isolado como limitação do
canal loopback (sem SDP/codec real negociado), não um bug da resolução:
confirmado testando a mesma resolução com destino simples
answer+playback via loopback (sucesso) e depois com uma chamada SIP
de verdade ponta a ponta (sucesso completo, áudio incluído).)
O que falta
- IVR (menu com
play_and_get_digits+ branching por dígito) — a rota de entrada já pode apontardestinationContextpra um contexto de IVR dedicado, mas o "menu" em si (dialplanplay_and_get_digits+ condição sobre o dígito coletado) ainda não foi construído. Pontos em aberto:ALLOWED_CONDITION_FIELDShoje é um enum fixo (nunca aceita${variavel}arbitrária) — vai precisar de um allowlist mais amplo com a MESMA proteção contra RCE já aplicada emdata(IsSafeDialplanData), já quefieldtambém é expandido pelo FreeSWITCH em tempo de chamada. - Tela de frontend cobre só CRUD simples (DID → ramal); não tem seletor
de "fila" ou "IVR" como destino ainda — hoje é só texto livre pro
destinationContext/destinationNumber(o operador escolhe o contexto e número certos manualmente). - Perda das proteções de toll-fraud do
public.xmlvanilla (unroll de loop de chamada, etc.) — não replicadas no contextoinboundnovo. Aceitável pra esta fase (sem trunks reais ainda), mas revisar antes de conectar um provedor PSTN de verdade.