Asterisk® SCF™ + LUA: Dialplan em LUA

Saudações a todos. Era uma vez, um tópico de usar a linguagem de programação LUA para escrever um Dialplan no Asterisk® SCF™, era muito difícil para mim. O fato é que eu não gosto muito de trabalhar com GUIs (como FreePBX, Issabel, Trixbox, etc.) ao configurar o Asterisk® SCF™.

Quando configurei tudo pela primeira vez, funcionou com as extensões lineares usuais (extensions.conf). Com o passar do tempo, a demanda por funcionalidade de telefonia cresceu. Aos poucos fui aprendendo a linguagem LUA. E então comecei a trabalhar como administrador em uma grande empresa em nossa cidade (uma grande imobiliária) - havia cerca de 45 filiais naquela época, cerca de 650-700 usuários, incluindo intermunicipais, etc. O Asterisk® SCF™, já estava lá, mas tudo foi configurado usando o FreePBX. Uma lastima!

Quase imediatamente, a gerência começou a me bombardear com várias perguntas sobre os sinos, apitos e renas do Asterisk® SCF™. Por exemplo, eles queriam distribuir chamadas aleatoriamente dentro da agência quando uma chamada era recebida em uma agência. Queríamos ter uma gravação das conversas em MP3, queríamos fazer um grupo geral, onde todos os ramos pudessem ser incluídos em geral, e ao discar um determinado número, para chegar acidentalmente a um dos ramos, etc. As tarefas parecem ser simples, mas eu pessoalmente não estava muito interessado em sentar para resolver até mesmo essas questões usando uma interface gráfica.
 
Havia mais um ponto importante - a qualidade do trabalho de telefonia em geral naquela época era simplesmente terrível. A voz gorgolejava constantemente, as chamadas eram interrompidas, o assinante não era ouvido, o próprio Asterisk® SCF™ travava com frequência (problemas com o channel_sip.so), etc. Eu vejo o arquivo Dialplan (extensions.conf), e ele tem 16 MB. Abri com um editor de texto - e o que posso fazer? Existem vários milhões de linhas.
 
Resolvi refazer, jogando tudo para LUA (extensions.lua). Cerca de alguns dias após o início do desenvolvimento, já consegui apresentar o primeiro protótipo de um Dialplan em LUA, bastante funcional, mas sem os "recursos" e "ajustes" existentes. Substituí toda a configuração antiga por ele e, em uma semana, acrescentei os recursos básicos que a gerência queria ver. Eu também atualizei o próprio Asterisk® SCF™ para a versão 11ª (na época 11.3.0, ao que parece). Além disso, no processo de trabalho, às vezes eu olhava para o arquivo Dialplan e analisava o que de fato a gerência queria ou não queria. Como resultado, o Asterisk® SCF™ com o Dialplan LUA funcionou muito mais rápido e mais estável do que o anterior. Acredito que por não ter o LAMP instalado, e nem a GUI pesada do FreePBX.

As condições em que a "estação" funcionou:

  • CPU: intel xeon e5520 (se não me engano);
  • RAM: 24gb; 
  • E outros parâmetros de "hardware", incluindo duas interfaces de rede Gigabit e e uma rede VPN;
  • Assinantes internos: cerca de 700; 
  • Número de troncos: cerca de 10 (dos quais 2 são ITSP/VoIP, o resto são Gateways GSM/GoIP); 
  • O número de "cidades": cerca de 200 (150 números de um ITSP/VoIP e cerca de 50 ou um pouco mais do segundo ITSP/VoIP).

Os números das cidades aqui foram atribuídos a cada filial. Alguns ramos têm até dois ou três números. Como todas as chamadas da cidade chegaram em contexto, analisei por DID e passei a chamada para a agência desejada.

Usando APPs no Dialplan LUA, implementei grupos de toque, fiz duas opções para ligar para um assinante - aleatório e na ordem de listagem no grupo (exceto para assinantes ocupados). Eu configurei o LUA-SQL para gravar minha própria base de chamadas (além do CDR em ARA). Isso foi feito pelo seguinte motivo: o funcionário liga para o cliente no celular, o cliente não quer falar agora (ocupado ou outra coisa); depois de um tempo, ele liga para um número previamente definido e deve falar com o mesmo funcionário que ligou para ele antes. Eu fiz um registro do evento "call to mobile" em um banco de dados separado. Quando um cliente liga de volta de seu celular, eu atendo a última ligação do evento “ligação de celular” e mando o cliente para o funcionário certo. Apenas um desses funcionários foi lembrado. Essa. se outro funcionário ligar para este cliente. então, consequentemente, a chamada retornará para ele.

Agora não trabalho mais naquela empresa, e onde estou agora, mudei o antigo PABX para um Softswitch PBX IP baseado em Asterisk® SCF™ e, claro, usei meu desenvolvimento do meu antigo trabalho. Lembrei-me que o assunto não era interessante só para mim. Bom, como há muito pouca informação sobre esse assunto, resolvi postar esse post aqui, de repente ele vai ser útil para alguém. Ou até mesmo para mim. Quem sabe?

Agora, irei ao cerne do assunto - codificação em LUA. Não vou descrever o estágio de ativação do módulo pbx_lua.so - há muitas informações aqui. Por exemplo, agora que tenho Centos 7.8, já tem LUA no repositório do mesmo. Eu apenas adicionei o pacote LUA-DEVEL e incluí o módulo pbx_lua.so em menuselect.

Além disso, se alguém vai usar uma conexão manual com o mysql (ou com outro banco de dados), então é melhor baixar o pacote LUA-SQL, tendo instalado o LUAROCKS previamente e baixando este add-on de lá.

Mais adiante no próprio plano de discagem, você pode descrever usuários e regras de discagem, algo assim:

extensions = { }; local_ext = { -- Quando um assinante externo pegou o telefone e discou para outro assinante externo -- h = function() -- Manipulador do HangUp() -- app.stopmixmonitor() d_status = channel["DIALSTATUS"]:get() if d_status ~= nil then app.noop("Status do Dial...:"..d_status) -- Por exemplo, se o assinante não conseguir, então sobrescreveremos o nome do arquivo no bando de dados (CDR) -- if d_status ~= "ANSWER" then channel["CDR(recordingfile)"]:set("") end app.noop("Boa Compra!") app.hangup() end; app.hangup() end; ["_14XXX"] = call_local; ["_21XX"] = call_local; ["_4595"] = call_all; -- Esta não é a descrição de um número, mas um grupo de números. Ao discar, ligamos para um número aleatório do grupo -- ["_*99"] = function() -- Isso foi adicionado especialmente para forçar a ativação do DND (muito interessante). -- local cid, dnd app.answer() cid = channel["CALLERID(num)"]:get() dnd = channel["DB(DND/"..cid.."/)"]:get() app.noop("DND:"..dnd) if dnd == "1" then channel["DB_DELETE(DND/"..cid.."/)"]:get() app.playback("beep") app.playback("beep") app.hangup() else channel["DB(DND/"..cid.."/)"]:set("1") app.playback("beep") app.wait(1) app.hangup() end end; include = {"mobile_out"}; };



Aqui ["_XXnumber"] é um modelo. Esse é tudo igual ao arquivo extensions.conf usual. call_local - a função à qual esta descrição se refere. Essa envia ao discar um número, digamos 14555, a função call_local será chamada. Além disso, essa função pode ser chamada em uma chamada externa de entrada.
 
function call_local(ctx,ext) local callerid,cf,uniq,chn local n,j,i n = string.sub(ext,3) -- Pegamos os dois últimos caracteres do número -- if n == "90" or n == "79" or n == "80" then -- Se terminar em 90, etc. Então essa é uma chamada para um dos grupos de filiais -- j = channel["CALLERID(num)"]:get() app.noop(string.format("Usando o grupo de toque %s de %s",ext,j)) dial_rg(shuffle(r_group[ext],nil)) -- Misture os números do grupo e ligue -- end -- Se o usuário ativou o modo "ausente", a chamada será enviada para ele em seu smartphone via operadora GSM do usuario, como rota de menor custo. -- cf = channel["DB(CF/"..ext.."/"..")"]:get() app.noop("CF:"..cf) if cf ~= "" then app.noop(string.format("Encaminhamento de chamada detectado de %s para %s",ext,cf)) app.goto("mobile_out",cf,"1") end callerid = channel["CALLERID(num)"]:get() app.noop(string.format("Tentando fazer uma ligação local %s de %s",ext,callerid)) if ext ~= "4550" and (CheckChannel(ext)) ~= NOT_INUSE then return end uniq = channel.UNIQUEID:get() chn = channel["CHANNEL"]:get() app.noop(string.format("UNIQUEID: %s",uniq)) app.noop(string.format("CHANNEL: %s",chn)) app.noop(string.format("CALLERID_name: %s",callerid)) app.noop(string.format("EXTEN: %s",ext)) app.noop(string.format("CONTEXT: %s",ctx)) record(string.format("%s-%s-%s",callerid,ext,uniq)) if ext == "4550" then local support = CallSupport(callerid) if support == "failed" then return end end if ext == "4514" or ext == "4592" then app.noop("Redirecionar!!!") app.dial("SIP/4591,60,tT") end app.dial(string.format("SIP/%s,60,tT",ext)) end
 
Existem várias verificações para alguns grupos e status. Por exemplo, 4550 é uma equipe de suporte técnico. Para tal, existe uma função própria, na qual se procede ao processamento da contratação dos colaboradores, informando o “cliente externo”, gravando um log e repondo o aviso de chamada perdida ao suporte técnico via jabber.
 
Se a parte chamada for um grupo, misture a lista e ligue para a parte aleatória.

Por que uso o método aleatório de chamar chamadores de grupos? As filiais são, em essência, gerentes de vendas. Se você incluir uma chamada sequencial para funcionários da filial, o primeiro da lista sempre terá mais vendas do que os outros (isso seria uma trapaça). A situação é semelhante com o método mem-primari (ao que parece), em que o usuário que respondeu pela última vez será ignorado. O método de mistura aleatória é mais honesto e coloca todos os vendedores em pé de igualdade. Você pode, claro, ligar para todos (ligar para todos ao mesmo tempo), mas aí as filiais começam a reclamar que todos os telefones da filial “gritam” ao mesmo tempo, isso não é conveniente, barulhento, etc.

Você também pode usar filas para uma chamada aleatória, mas dificilmente as uso. Não sei porque não confio no metodo round robin.
 
Além disso, entrando na cidade, descrição:
 
from_trunk = { h = function() app.noop("BBBBBBBLLLLAAAAHHHHHH!!!!!!!") app.stopmixmonitor() if d_status ~= nil then d_status = channel["DIALSTATUS"]:get() app.noop("Status da Discagem...:"..d_status) if d_status ~= "ANSWER" then channel["CDR(recordingfile)"]:set("") end exten = "" uniqid = "" app.noop("Boa compra!") app.hangup() end app.noop("Algum problema!!!") app.hangUP() end; ["f1"] = function(e) -- Para ser sincero, não me lembro o que estava fazendo aqui -- app.goto("local_ext",e,1) end; ["_."] = foo; -- Sim Sim, essa função é chamada foobar, rsrsrsr-- include = {"local_ext"} }
 
Aqui, envolvo todas as entradas externas em foo.
 
function foo(ctx,ext) local chn tmptab.did = ext tmptab.rg = g_tab[ext] if tmptab.did == "99051000227736" then -- Aqui fiz uma experiência com o recebimento do Skype, Laboratorios. -- app.noop("Teste com Skype!!!") app.dial("SIP/14553,,tT,M(bar)") end tmptab.callerid = channel["CALLERID(num)"]:get() if string.find(tmptab.callerid,"88005550678",1) then app.hungup() end -- Baniu alguém... -- if string.find(tmptab.callerid,"79",1) then -- Aqui eu também esqueci algo relacionado à determinação do número de células -- tmptab.callerid = "8"..string.sub(tmptab.callerid,2) channel["CALLERID(all)"]:set(tmptab.callerid) end chn = channel["CHANNEL"]:get() app.noop("CHANNEL:"..chn) if string.find(chn,"SIP/gsm_",1) then -- Aqui eu pego os portais entrando pelo GSM/GoIP -- app.noop("Canal encontrado "..chn) -- Aqui eu pego os gateways entrando pelo GSM através do bando de dados simples criado anteriormente no MySQL pegou o assinante e enviou o cliente para ele. -- num = sql.mobile_get(tmptab.callerid) if num then app.goto("local_ext",num,1) end end app.noop("CallerID(num):"..tmptab.callerid) app.noop("by context:"..ctx) app.noop("DID:"..tmptab.did) app.set("CDR(did)="..tmptab.did) if tmptab.did == "4595" then call_all(tmptab.did) end -- 4595 é um grupo global para todos os ramos -- app.answer() app.wait(1) j = channel["DB(ENUM/"..tmptab.did.."/)"]:get() app.noop("tag = "..j) if j == "ngs_rec" then dial_rg(shuffle(tmptab.rg),1) else if tmptab.did ~= "3471234" then -- Lembre-se que todos os números aqui são fictícios!!! -- app.playback(mhold.comp_hello) if tmptab.did == "3472345" then app.goto("local_ext","4591",1) end ivr(tmptab.did) -- Sim, tem um menu de voz aqui também, mas não vou mostrar neste post... -- dial_rg(shuffle(tmptab.rg),nil) else app.noop("Algo Detectado") dial_rg(tmptab.rg,nil) end end app.noop("Desligar?") end

Nesse caso, defino os números das células e os números externos (DID). Se o chamador liga para um celular local (cartão SIM no gateway gsm), então eu atendo o último chamador e mando este cliente para ele. Também existe uma definição da lista ngs_rec. Aqui, os números são predefinidos como "publicidade". Era um método antigo (mudei-o mais tarde, mas esta versão do arquivo do qual tirei o código não contém esta revisão). Envio todos os números de publicidade para números especiais do escritório e anoto na base de dados que houve uma chamada para o número indicado no anúncio.

Por enquanto, acho que é código suficiente para dar uma luz sobre o Dialplan em LUA. Se alguém tiver interesse em migrar do antigo Dialplan para a LUA, acho que em um proximo post, consiga esclarecer algumas coisas com mais detalhes. Porém, se alguém já sabe programar em LUA, não haverá problema algum.

Para concluir, quero dizer que, claro, hoje existe um monte de diferentes soluções sofisticadas, como VoxImplant e similares. Muitas pessoas geralmente não estão acostumadas a trabalhar no console e a programar algo próprio. Mas quero observar que, quando o tamanho da empresa é grande (de 50 assinantes ou mais), construir a lógica da "estação" usando botões e caixas de seleção na interface gráfica pode levar a problemas. Acima, no início do artigo, dei exemplos sobre gorgolejos e penhascos. O Dialplan LUA deste projeto ficou com um peso de apenas 24kb, 968 linhas.

Quase 700 assinantes trabalharam nele sem problemas.
 
Tkat's All Folks! (É por hoje é só, pessoal!).

 
Temos uma comunidade, ainda pequena no Telegram, para estudos exclusivos em Dialplan com LUA no Asterisk® SCF™, você é bem vindo! (Telegram: https://t.me/asteriskluabr)

Nenhum comentário

Toda vez que um homem supera os reveses, torna-se mentalmente e espiritualmente mais forte!

Tecnologia do Blogger.