Ненормальный перевод

от автора

В asterisk 2 типа трансферов:
— слепой: после набора нужного сотрудника переводящий сразу отваливается.
— расширенный: возможность поговорить с тем, кому перевод предназначен, принять callback.

Мне понадобилось совместить простоту первого и функционал второго.
Без AMI/ARI/AGI
Без костылей
Без первых двух

Велосипед под катом.

Сразу оговорюсь почему не использовал интерфейсы, которые уже есть:
Это не очень удобная штука когда речь заходит о стеке Asterisk например (да и вообще не люблю я их).
Но это так. Лирическое отступление.

Поехали далее:
Указатели (расшифровки, или как там еще можно сказать. Для простоты короче):

  • Переводящий — тот кто переводит
  • Принимающий — тот кто принимает перевод

Сценарий:

  • проверить, а доступен ли вообще оператор которому должен прийти перевод звонка. Если недоступен — проиграть переводящему сообщение об этом и вернуть назад звонок
  • если оператор доступен, положить трубку переводящему и отправить звонок на принимающего
  • если принимающий занят (разговаривает например), предложить пользователю подождать или вернуться к переводящему
  • если принимающий не отвечает, вернуться к переводящему*

инструменты Asterisk которые мне понадобились:
features.conf

С него и начну.
Так как у меня появляется собственное событие, которое должно быть произведено по нажатию на определенную клавишу (DTMF) во время разговора, то тут самое место использовать features.conf с его возможностью создавать свои события и определять на них свои действия

Мое событие называется customTransfer и выглядит так (описание того как создаются кастомные события есть в features.conf и на wiki.asterisk.org. Не буду расписывать):

customTransfer => #,self,GoSub(customTransfer,#,1),default

То есть по нажатию на # вызываем GoSub и уходим в контекст диалплана.

Тут оговорюсь что использую lua, поэтому далее буду писать функции которые вызываю
Для тех кто не знает, то в lua контекст определяется в таблице extenions и может указывать на функцию которую надо выполнять:

extensons={         ["customTransfer"]={                    ["#"]=customTransfer --название функции         } }

Функция выглядит так ( Комменты описывают, что к чему и рекомендованы к прочтению для понимания)

 function customTransfer(context,extension)                  --[[ считываю номер, на который делается перевод ]]  	app.Read("TRANSF",nil,10,nil,1,5)		 	 	if channel.READSTATUS:get() == "OK" then                  -- [[ Проверяю доступен ли абонент, на которого я делаю перевод. я использую другой сервер регистрации, но при использовании именно Asterisk как сервера регистрации наличие пользователя можно проверить так.]]                 app.chanIsAvail('SIP/'..chan.TRANSF:get())                 if channel.AVAILCHAN:get()~='' then             		                      --[[ запоминаю кто сделал перевод (переменную канала CALLED_NUMBER я создал когда принимал входящий звонок (здесь ее нет). В ней лежит номер, на который поступил звонок (то есть перевод работает с тем кто принял звонок, а не с тем кто его инициировал)). Кладу во внутренюю БД Asterisk (можно redis, можно вообще все что угодно). Делаю это потому, что  ChannelRedirect создаст новый канал, и уже не увидит переменных, созданных в этом канале.  ]]                       channel.DB('transferedBy/'..channel.BRIDGEPEER:get()):set(channel.CALLED_NUMBER:get()) 		                      -- [[перевожу на новый канал, который уже в свою очередь обработает соединение и отправит в нужный контекст, который в свою очередь вызовет необходимую точку. То есть, по сути я имитирую сценарий создаваемый функцией трансфер (кэп тут)]]                      app.ChannelRedirect(channel.BRIDGEPEER:get(),'redirectSetup',channel.TRANSF:get(),1)                      --[[ закрываю канал того, кто переводил, так как он мне уже не нужен (автоматически кладу трубку)]] 		        app.Hangup() 	       else                      --[[ если считать номер не удалось, информирую об этом ПЕРЕВОДЯЩЕГО и соединяю переводящего и ожидающего пользователя. Переводящий может попробовать перевести еще раз]] 		     app.NoOp("user unavailible") 		     app.Playback(channel.UNAVAILIBLEONTRANSFER:get()) 	       end         else                --[[ если пользователь недоступен, проигрываю сообщение ПЕРЕВОДЯЩЕМУ и соединяю переводящего и ожидающего пользователя. Переводящий может попробовать перевести еще раз]]                app.NoOp("user unavailible") 	       app.Playback(channel.UNAVAILIBLEONTRANSFER:get())        end  end	 

После того как канал был брошен в transfer и была выполнена проверка, его необходимо направить на принимающего пользователя.

В контексте ‘redirectSetup’ настраивается вход в данную функцию (аналогично предыдущему)

Сама функция выглядит следующим образом

function redirectSetup(context,extension) 	app.NoOp("trying to redirect to "..extension) 	         --[[ Эта переменная канала мне понадобится чтобы отправить звонок обратно, если принимающий пользователь не возьмет трубку, например или будет занят. Я храню ее в переменной канала так как в моем случае она путешествует по функциям диалплана и в общем то переменные канала это удобный способ хранить глобальные для канала переменные. В общем то ничто не мешает сохранить ее в переменной lua]] 	channel.__TRANSFEREDBY:set(channel.DB('transferedBy/'..channel.CHANNEL:get()):get()) 	         --[[ В базе данных эта переменная больше не понадобится, поэтому мы можно ее удалить ]] 	channel.DB_DELETE('transferedBy/'..channel.CHANNEL:get()):get()          --[[ Далее я отправляю вызов в основную функцию моего диалплана для обработки соединения, вместо нее вполне может быть просто  app.Dial на необходимый extension ]] 	main(context,extension) end

последний шаг — это организация возврата звонка при неответе/занятости абонента или по какой либо еще причине.

Я думаю уже многие поняли что делать это нужно с помощью переменной TRANSFEREDBY

Свой диалплан полностью выкладывать не буду. Приведу пример маленькой функции, чтобы не вводить в заблуждение — назову ее main

function main(context,extension)       app.Dial("SIP/"..extension)       if channel.DIALSTATUS:get()~="ANSWER" then            app.Playback("olala")            app.Dial("SIP/"..channel.TRANSFEREDBY:get())       end  end

Касательно возможностей данной организации: у меня диалплан в купе с данной функцией организован таким образом, что проверяет не только доступность локального номера, но и наличие мобильного номера, закрепленного за абонентом.
Если абонент занят, предлагает ему оставаться на линии дожидаясь ответа или выйти из режима ожидания и перезвонить обратно тому, кто перевел (некий микс очереди и IVR), предлагает абоненту вернуться к тому кто перевел, если звонок на сотовый отправил на голосовую почту (у многих операторов данная услуга ничем не отличается от поднятой трубки. Они просто шлют 200 ответ и потом несут ересь…). последнее организовано тоже через customFeature.

В общем-то все это достигается путем линейного программирования и включения головы.

Вроде все.
С наступающим тоагисчи
Адекватных клиентов, и хороших исполнителей вам.
ссылка на оригинал статьи https://habrahabr.ru/post/318438/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *