Pull to refresh

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

Reading time 4 min
Views 11K
В 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.

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

Вроде все.
С наступающим тоагисчи
Адекватных клиентов, и хороших исполнителей вам.
Tags:
Hubs:
+11
Comments 3
Comments Comments 3

Articles