Files
B2BCall-dialer/packages/telephony/src/directory-xml.ts
Matheus 3fe571bfb0 feat(telefonia): edição de ramal (nome/caller ID/contexto/codecs/grupo) junto da senha SIP
Pedido do usuário: abrir a edição completa do ramal no mesmo lugar de
ver/redefinir a senha, em vez de só um campo (grupo de captura) editável
isoladamente. UpdateExtensionDto ganha name/context/codecs (só callGroup
e caller ID eram editáveis antes); codecs valida contra uma lista fixa
e agora realmente vira absolute_codec_string no directory XML — antes
a coluna existia no schema mas não tinha efeito nenhum no FreeSWITCH.

Grupo de captura vira um input com datalist alimentado pelos grupos já
usados em outros ramais do tenant, em vez de digitar de novo do zero.

Testado ponta a ponta com Playwright (tenant/ramais descartáveis):
grupo reaproveitado de outro ramal, edição de nome/caller ID/contexto,
remoção de um codec — tudo persistido e refletido na tela depois de
salvar.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
2026-08-31 14:01:03 -03:00

92 lines
4.0 KiB
TypeScript

/** Escapa texto pra uso seguro dentro de atributos/elementos XML. */
function xmlEscape(value: string): string {
return value
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
}
export interface DirectoryUserParams {
domain: string;
extensionNumber: string;
extensionName: string;
sipPassword: string;
context: string;
callerIdName?: string;
callerIdNumber?: string;
tenantId: string;
extensionId: string;
/** Grupo de captura (secao 178) — vira a variable `callgroup` (sem
* hífen: é o nome que o `mod_dptools`/convenção FreeSWITCH usa,
* confirmado contra a documentação oficial — `${user_data(<ext>@
* <domain> var callgroup)}` no dialplan lê exatamente esse nome). O
* pickup em si não é automático por variável: o dialplan que atende a
* ligação pro ramal precisa forkar um leg `pickup/${...essa var...}`
* no `bridge`, e uma extension separada de feature code (`*8`) chama a
* application `pickup` com o grupo de quem discou. Ver
* docs/EXTENSIONS.md — achado real reportado pelo usuário, verificado
* contra a documentação do FreeSWITCH antes de close (a primeira versão
* usava `call-group`, que não é lido por nada). */
callGroup?: string | null;
/** Lista de codecs de áudio permitidos pro ramal, separados por vírgula
* (ex.: "PCMU,PCMA,OPUS") — vira `absolute_codec_string`, a variável
* que o FreeSWITCH usa pra restringir/ordenar os codecs oferecidos na
* negociação SDP pra este usuário especificamente. */
codecs?: string;
}
/**
* Monta o XML de directory que o FreeSWITCH espera do mod_xml_curl pra
* autenticar/rotear um ramal (agente.md secao 26). `b2bcall_tenant_id` e
* `b2bcall_extension_id` viram channel variables em toda chamada desse
* ramal — é assim que o resto do sistema volta a saber de qual tenant uma
* chamada é (secao 81).
*/
export function buildDirectoryUserXml(params: DirectoryUserParams): string {
const callerIdName = xmlEscape(params.callerIdName ?? params.extensionName);
const callerIdNumber = xmlEscape(params.callerIdNumber ?? params.extensionNumber);
return `<?xml version="1.0" encoding="UTF-8"?>
<document type="freeswitch/xml">
<section name="directory">
<domain name="${xmlEscape(params.domain)}">
<params>
<!-- Sem isto, o endpoint "user/" nao sabe montar o dialstring pra
alcançar o contato registrado (mesmo padrão da config vanilla
em directory/default.xml). -->
<param name="dial-string" value="{^^:sip_invite_domain=\${dialed_domain}:presence_id=\${dialed_user}@\${dialed_domain}}\${sofia_contact(*/\${dialed_user}@\${dialed_domain})}"/>
</params>
<groups>
<group name="default">
<users>
<user id="${xmlEscape(params.extensionNumber)}">
<params>
<param name="password" value="${xmlEscape(params.sipPassword)}"/>
</params>
<variables>
<variable name="user_context" value="${xmlEscape(params.context)}"/>
<variable name="effective_caller_id_name" value="${callerIdName}"/>
<variable name="effective_caller_id_number" value="${callerIdNumber}"/>
<variable name="b2bcall_tenant_id" value="${xmlEscape(params.tenantId)}"/>
<variable name="b2bcall_extension_id" value="${xmlEscape(params.extensionId)}"/>
${params.callGroup ? `<variable name="callgroup" value="${xmlEscape(params.callGroup)}"/>` : ""}
${params.codecs ? `<variable name="absolute_codec_string" value="${xmlEscape(params.codecs)}"/>` : ""}
</variables>
</user>
</users>
</group>
</groups>
</domain>
</section>
</document>`;
}
export const NOT_FOUND_XML = `<?xml version="1.0" encoding="UTF-8"?>
<document type="freeswitch/xml">
<section name="result">
<result status="not found"/>
</section>
</document>`;