Asterisk
Lua
November 2014 13

Диалплан на LUA для Asterisk

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

Всё. Всем до свиданья!
+9
21.3k 150
Comments 54
Top of the day