[Из песочницы] Диалплан на LUA для Asterisk

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

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

© Habrahabr.ru