Developers Club geek daily blog

2 years, 2 months ago
Приветствую всех. Когда-то тема использования языка программирования 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 абонентов без каких-то проблем.

Всё. Всем до свиданья!

This article is a translation of the original post at habrahabr.ru/post/243125/
If you have any questions regarding the material covered in the article above, please, contact the original author of the post.
If you have any complaints about this article or you want this article to be deleted, please, drop an email here: sysmagazine.com@gmail.com.

We believe that the knowledge, which is available at the most popular Russian IT blog habrahabr.ru, should be accessed by everyone, even though it is poorly translated.
Shared knowledge makes the world better.
Best wishes.

comments powered by Disqus