Приветствую всех. Когда-то тема использования языка программирования lua при написании диалплана в Астериске для меня стояла довольно жёстко. Дело в том, что мне сильно не нравится работать с различными GUI (типа FreePBX) при настройке Астериска.
Когда я всё настраивал в первый раз, работал с обычным линейным extensions.conf. Время шло, потребности в функционале телефонии росли. Язык lua постепенно немного изучил. И вот пришёл я работать админом в одну крупную компанию в нашем городе (одно крупное агентство недвижимости) — около 45 филиалов на тот момент было, примерно 650 — 700 пользователей, включая межгород и т.д. Там уже стоял Asterisk, но всё настроено было с использованием FreePBX.
Почти сразу руководство начало меня заваливать различными вопросами по наворотам Астериска. Например, хотели, чтобы при входящем звонке в какой-то филиал, звонки внутри филиала были распределены случайным образом. Хотели иметь запись разговоров в mp3, хотели сделать общую группу, куда можно было бы включить вообще все филиалы и при наборе какого-то номера, чтобы случайно попасть на один из филиалов и т.д. Задачи вроде простые, однако сидеть решать даже такие вопросы средствами графического интерфейса лично мне было не очень интересно.
Был ещё один важный момент — качество работы телефонии в целом на тот момент было просто ужасным. Голос постоянно булькал, звонки разрывались, абонента не слышно, сам астер часто крашился и т.д. Смотрю на файлик диалплана, а он размером в 16 мб. Открыл редактором текста — и что тут делать? Там строк в несколько миллионов.
Решил переделать, перекинув всё на lua. Примерно через пару дней после начала разработки я уже смог представить первый прототип диалплана на lua, вполне рабочий, но без существующих «фишечек» и «рюшечек». Заменил им весь старый конфиг и далее ещё в течение недели накидал основные навороты, которые хотело видеть руководство. Так же обновил самого астера до 11-й версии (на тот момент 11.3.0, кажется). Далее в процессе работы иногда поглядывал в файл диалплана и подпиливал то, что сам хотел или хотело руководство. В итоге астер с диалпланом на lua работал значительно быстрее и более стабильно, чем прошлый.
Условия в которых работала «станция»:
cpu: intel xeon e5520 (если не ошибаюсь)
ram: 24gb
и другие «железные» параметры, включая два гигабитных сетевых интерфейса и рэйд1
количество вн.абонентов: около 700
количество транков: около 10 (из которых 2 это провайдеры, остальные это gsm шлюзы addpack).
количество «городских» номеров: около 200 (150 номеров от одного провайдера и примерно50 или чуть больше от второго).
Городские номера тут были закреплены за каждым филиалом. За некоторыми филиалами даже по два или три номера. Поскольку все звонки из города прилетали в контекст, далее я делал разбор по did и передавал звонок на нужный филиал.
Средствами lua реализовал ring groups, сделал два варианта вызова абонента — случайное и по порядку перечисления в группе (за исключением занятых абонентов). Прикрутил lua-sql для записи собственной базы звонков (дополнение к cdr). Это было сделано вот для чего: сотрудник звонит клиенту на сотовый, клиент сейчас не захотел разговаривать (занят или ещё что); через некоторое время он перезванивает на определённый ранее номер и должен попасть к тому же сотруднику, который ему до этого звонил. Я сделал запись события «звонок на мобильный» в отдельную базу. Когда клиент с сотового перезванивает, я по событию «звонок с сотового» поднимаю прошлый звонок и отдаю клиента на нужного сотрудника. Запоминался только один такой сотрудник. Т.е. если этому клиенту позвонит ещё один сотрудник. то соответственно, звонок вернётся к нему.
Сейчас я уже не работаю в той компании, а там где сейчас — меняю старую АТС на Астериска и, конечно, использую старую свою наработку. Вспомнил, что тема интересна была не только мне. Ну а поскольку по этой теме информации крайне мало, решил накидать вот эту статью, вдруг кому-то пригодится.
Теперь перейду к самой сути темы — кодинг на lua. Описывать стадию включения модуля pbx_lua не буду — информации тут много. Например, сейчас у меня стоит Centos 6.6, там в стоке уже есть lua. Я докинул только пакет lua-devel и включил модуль pbx_lua в menuselect.
Дополнительно, если кто собирается использовать ручное подключение к mysql (или к другой базе), то лучше докинуть пакет lua-sql, предварительно установив luarocks и оттуда закачав это дополнение.
Далее в самом диалплане можно описать пользователей и правила набора, что-то типа того:
extensions = {
};
local_ext = { -- когда вн.абонент поднял трубку и набрал другого вн.абонента
h = function() -- обработчик конца разговора (hangup)
app.stopmixmonitor()
d_status = channel["DIALSTATUS"]:get()
if d_status ~= nil then
app.noop("Dial over with status:"..d_status)
-- например, если абонент не дозвонился, тогда затираем имя файла в базе cdr
if d_status ~= "ANSWER" then channel["CDR(recordingfile)"]:set("") end
app.noop("Good buy!")
app.hangup()
end;
app.hangup()
end;
["_14XXX"] = call_local;
["_21XX"] = call_local;
["_4595"] = call_all; -- это описание не номера, а группы номеров. при наборе звоним на случайны номер из группы
["_*99"] = function() -- это специально добавлял для принудительного включения dnd (занятно).
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"};
};
тут ["_XXномер"] — шаблон. Т.е. всё тоже самое, что и в обычном extensions.conf.
call_local — функция на которую ссылается данное описание. Т.е. при наборе номера, скажем, 14555, будет вызываться функция call_local. Так же эта функция может вызываться при входящем внешнем звонке.
function call_local(ctx,ext)
local callerid,cf,uniq,chn
local n,j,i
n = string.sub(ext,3) -- взяли последние 2 символа номера
if n == "90" or n == "79" or n == "80" then -- если оканчивается на 90 и т.д. тогда это звонок на одну из групп филиалов
j = channel["CALLERID(num)"]:get()
app.noop(string.format("Using ring group %s from %s",ext,j))
dial_rg(shuffle(r_group[ext],nil)) -- смешать номера в группе и вызвать
end
-- если пользователь включил режим "отсутствую", тогда звонок полетит к нему на его сотовый
cf = channel["DB(CF/"..ext.."/"..")"]:get()
app.noop("CF:"..cf)
if cf ~= "" then
app.noop(string.format("Call forward detected from %s to %s",ext,cf))
app.goto("mobile_out",cf,"1")
end
callerid = channel["CALLERID(num)"]:get()
app.noop(string.format("Trying to local call %s from %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("Redirect!!!")
app.dial("SIP/4591,60,tT")
end
app.dial(string.format("SIP/%s,60,tT",ext))
end
Тут есть несколько проверок на некоторые группы и статусы. Например, 4550 — это группа технической поддержки. Для неё есть отдельная функция, в которой есть обработка занятости сотрудников, информирование «вн.клиента», запись журнала и сброс предупреждения о пропущенном звонке в тех.поддержку через jabber.
Если вызываемый абонент является группой, тогда смешать список и вызвать случайного абонента.
Почему я использую случайный метод вызова абонентов из групп? Филиалы — это, по сути своей, менеджеры продаж. Если включать последовательный вызов сотрудников филиала, то всегда у первых в списке будет продаж больше, чем у других (читинг). Аналогично обстоят дела и с методом mem-primari (кажется), при котором пользователь ответивший в прошлый раз будет игнорирован. Метод случайного смешивания более честный, ставит всех «продажников» в равные условия. Можно сделать конечно call-all (звонить всем одновременно), но тогда филиалы начинают жаловаться, что в филиале все телефоны «орут» одновременно это не удобно, шумно и т.д.
Для случайного вызова можно было бы так же использовать очереди, но я их почти не использую. Не знаю почему, так получилось.
Далее, входящие из города, описание:
from_trunk = {
h = function()
app.noop("BBBBBBBLLLLAAAAHHHHHH!!!!!!!")
app.stopmixmonitor()
if d_status ~= nil then
d_status = channel["DIALSTATUS"]:get()
app.noop("Dial over with status:"..d_status)
if d_status ~= "ANSWER" then channel["CDR(recordingfile)"]:set("") end
exten = ""
uniqid = ""
app.noop("Good buy!")
app.hangup()
end
app.noop("Some problem!!!")
app.hangUP()
end;
["f1"] = function(e) -- если честно, не помню что я тут делал...
app.goto("local_ext",e,1)
end;
["_."] = foo; -- да да, это функция называется типа foobar...
include = {"local_ext"}
}
Тут я все внешние входящие я заворачиваю в foo.
function foo(ctx,ext)
local chn
tmptab.did = ext
tmptab.rg = g_tab[ext]
if tmptab.did == "99051000227736" then -- тут я делал эксперимент с входящими со Скайпа. работают.
app.noop("Skype TEST!!!")
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 -- кого-то забанил...
if string.find(tmptab.callerid,"79",1) then -- тут я тоже подзабыл, что-то связанное с определением сотовых номеров
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 -- тут я вылавливаю входящие через gsm шлюзы
app.noop("Found channel "..chn)
-- через ранее созданную простейшую базу на mysql выловил абонента и отправил ему клиента
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 это глобальная группа по всем филиалам
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 -- имейте ввиду, все номера тут вымышленные!!!
app.playback(mhold.comp_hello)
if tmptab.did == "3472345" then app.goto("local_ext","4591",1) end
ivr(tmptab.did) -- да, голосовое меню тут тоже есть, но показывать его не буду...
dial_rg(shuffle(tmptab.rg),nil)
else
app.noop("BLAH DETECTED")
dial_rg(tmptab.rg,nil) end
end
app.noop("hungup?")
end
В данном случае определяю сотовые номера и внешние номера (did). Если звонящий звонит на местный сотовый (симка в gsm шлюзе), тогда беру последнего звонившего абонента и отправляю этого клиента к нему. Так же есть определение из списка ngs_rec. Тут номера заранее определены как «рекламные». Это был старый метод (потом переделал, но в этой версии файла из которой беру код данной доработки нет). Все рекламные номера отправляю на спец.номера в конторе и делаю отметку в базе, что был звонок на номер, указанный в рекламе.
Пока, думаю, хватит кода на данный момент. Если у кого-то появится интерес к переходу от старого диалплана к lua, думаю, смогу продолжить и разъяснить более детально некоторые вещи. Хотя, если кто-то уже знает, как программировать на Lua, проблем совершенно не будет.
В завершении хочу сказать, что, конечно, на сегодняшний день существует целая пачка разных навороченных решений, типа VoxImplant и подобных. Многим вообще не привычно работать в консоли и что-то своё кодить. Но, хочу заметить, что когда размеры компании большие (от 50 абонентов и выше), то выстраивание логики работы «станции» при помощи кнопочек и галочек в графическом интерфейсе в итоге могут приводить к проблемам. Выше в начале статья я приводил примеры про бульканье и обрывы. Диалплан на lua весит всего 24кб, 968 строк.
На нём работали почти 700 абонентов без каких-то проблем.
Всё. Всем до свиданья!
Автор: Sayman_nsk