feat(dialplan): IVR — play_and_get_digits + branching por dígito, testado com DTMF real
Continuação de "montar o IVR e as rotas de entrada": com a fundação de
roteamento por DID já funcionando, faltava o menu em si.
`play_and_get_digits` adicionado ao allowlist de applications (só coleta
dígitos numa variable, nunca executa nada — mesmo risco zero dos outros
já permitidos). `ALLOWED_CONDITION_FIELDS` ganhou um segundo modo:
`${variavel}` simples além dos 6 campos fixos, com a mesma garantia
anti-RCE de `data` (o regex exige identificador puro, sem parênteses,
então nunca vira `${funcname(...)}`).
Achado real construindo o primeiro menu de teste: FreeSWITCH resolve
TODAS as condições de um contexto antes de executar qualquer ação — uma
variable setada por `play_and_get_digits` nunca afeta o casamento de
OUTRA extension na mesma passada. Descoberto porque o primeiro desenho
(2 extensions separadas, uma coletando o dígito, outra checando
`${ivr_choice}`) nunca bateu num teste real com DTMF — o regex era
avaliado com a variable ainda vazia. A forma que funciona: um `transfer`
explícito, na MESMA extension, usando o dígito coletado como novo
destination_number — isso dispara uma consulta de dialplan nova (as
variables já setadas persistem), e as extensions seguintes casam por
destination_number normal.
Testado ponta a ponta com DTMF de verdade: como linphonec não tem um
comando de enviar DTMF interativo, usei `fs_cli uuid_recv_dtmf` (API do
FreeSWITCH que simula o dígito chegando como input do chamador, mesmo
caminho que RFC2833 usaria). Softphone externo ligou pro DID, o IVR
atendeu, tocou o prompt, esperou o dígito: "1" bridged com um ramal real
(atendeu, áudio PCMU bidirecional confirmado); "2" foi pra uma resposta
alternativa (tom diferente) — confirma que a ramificação distingue de
verdade, não só cai sempre na primeira opção.
Detalhes completos em docs/INBOUND_ROUTES.md. Falta só a UI dedicada de
autoria de menu (hoje construído à mão no editor genérico de dialplan) —
backend já suporta tudo que ela precisaria gerar.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
@@ -80,20 +80,58 @@ 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).)
|
||||
|
||||
## IVR (PHASE 56, construído em cima da fundação acima)
|
||||
|
||||
`play_and_get_digits` adicionado ao `ALLOWED_DIALPLAN_APPLICATIONS` — só
|
||||
coleta dígitos numa variable, nunca executa nada, mesmo risco zero dos
|
||||
outros já permitidos. `ALLOWED_CONDITION_FIELDS` ganhou um segundo modo:
|
||||
além dos 6 campos fixos, aceita um `${variavel}` simples (regex exige
|
||||
identificador puro, sem parênteses — nunca bate com `${funcname(...)}`,
|
||||
então o mesmo risco de RCE já fechado pra `data` não se aplica aqui).
|
||||
|
||||
**Achado real construindo o primeiro menu de teste**: FreeSWITCH resolve
|
||||
TODAS as `<condition>` de um contexto (decide quais `<extension>`
|
||||
batem) ANTES de executar qualquer `<action>` — variables setadas por uma
|
||||
action (como o dígito que `play_and_get_digits` guarda) NUNCA afetam o
|
||||
casamento de outras `<extension>` no MESMO contexto/mesma passada
|
||||
("Uma condição por extension" já era uma simplificação deliberada desta
|
||||
implementação, mas mesmo o FreeSWITCH puro com múltiplas `<condition>`
|
||||
por extension só re-avalia a PRÓXIMA condição da MESMA extension, não
|
||||
outras extensions). Confirmado testando com DTMF de verdade: duas
|
||||
extensions separadas (`IVR entrada` fazendo `play_and_get_digits`, `IVR
|
||||
opcao 1`/`opcao 2` checando `${ivr_choice}`) NUNCA bateu — o regex era
|
||||
avaliado com a variable ainda vazia.
|
||||
|
||||
**A forma que funciona**: dentro da MESMA extension que colheu o dígito,
|
||||
um `transfer` explícito usando o valor coletado como novo
|
||||
`destination_number` — `transfer data="${ivr_choice} XML
|
||||
<mesmo-contexto>"`. `transfer` dispara uma consulta de dialplan
|
||||
COMPLETAMENTE NOVA (nova passada de parse, variables já setadas
|
||||
persistem), então as extensions seguintes podem casar por
|
||||
`destination_number` normal (`^1$`, `^2$`, ...) — nem precisa do
|
||||
widening de `${variavel}` pra ISSO especificamente (mas o widening
|
||||
continua útil/correto de se ter, por exemplo pra decisões dentro de uma
|
||||
única extension com múltiplas conditions no futuro).
|
||||
|
||||
Testado ponta a ponta com DTMF de verdade (`fs_cli uuid_recv_dtmf` —
|
||||
`linphonec` não tem comando de enviar DTMF interativo, então a
|
||||
verificação usa a API do FreeSWITCH que simula o dígito chegando como
|
||||
input do chamador, o mesmo caminho que RFC2833/inband usaria): softphone
|
||||
externo liga pro DID → IVR atende, toca prompt, espera dígito →
|
||||
dígito `1` → bridge com ramal real registrado (atendeu, áudio PCMU
|
||||
bidirecional confirmado); dígito `2` → resposta alternativa (tom
|
||||
diferente + desliga) — confirma que a ramificação distingue de verdade,
|
||||
não só "sempre cai na primeira opção".
|
||||
|
||||
## O que falta
|
||||
|
||||
- IVR (menu com `play_and_get_digits` + branching por dígito) — a rota de
|
||||
entrada já pode apontar `destinationContext` pra um contexto de IVR
|
||||
dedicado, mas o "menu" em si (dialplan `play_and_get_digits` +
|
||||
condição sobre o dígito coletado) ainda não foi construído. Pontos em
|
||||
aberto: `ALLOWED_CONDITION_FIELDS` hoje é um enum fixo (nunca aceita
|
||||
`${variavel}` arbitrária) — vai precisar de um allowlist mais amplo com
|
||||
a MESMA proteção contra RCE já aplicada em `data` (`IsSafeDialplanData`),
|
||||
já que `field` també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).
|
||||
- Tela de frontend pra autoria de IVR — hoje o menu é construído à mão
|
||||
no editor genérico de dialplan (`Telefonia > Dialplan`, escolhendo um
|
||||
`context` novo), não tem uma UI dedicada de "menu com opções" ainda.
|
||||
Backend já suporta tudo que uma UI assim precisaria gerar.
|
||||
- Tela de frontend "Rotas de Entrada" cobre só CRUD simples (DID →
|
||||
ramal); não tem seletor de "fila" ou "IVR" como destino ainda — hoje é
|
||||
texto livre pro `destinationContext`/`destinationNumber`.
|
||||
- Perda das proteções de toll-fraud do `public.xml` vanilla (unroll de
|
||||
loop de chamada, etc.) — não replicadas no contexto `inbound` novo.
|
||||
Aceitável pra esta fase (sem trunks reais ainda), mas revisar antes de
|
||||
|
||||
Reference in New Issue
Block a user