В Asterisk 2 типа трансферов:
— слепой: после набора нужного сотрудника переводящий сразу отваливается.
— расширенный: возможность поговорить с тем, кому перевод предназначен, принять callback.
Мне понадобилось совместить простоту первого и функционал второго. Без AMI/ARI/AGI. Без костылей.
Велосипед под катом.
Сразу оговорюсь почему не использовал интерфейсы, которые уже есть: это не очень удобная штука, когда речь заходит о стеке Asterisk, например (да и вообще не люблю я их). Но это так. Лирическое отступление.
Поехали далее. Указатели (расшифровки, или как там еще можно сказать. Для простоты короче):
Сценарий:
Инструменты Asterisk, которые мне понадобились: features.conf.
С него и начну. Так как у меня появляется собственное событие, которое должно быть произведено по нажатию на определенную клавишу (DTMF) во время разговора, то тут самое место использовать features.conf с его возможностью создавать свои события и определять на них свои действия
Мое событие называется customTransfer и выглядит так (описание того как создаются кастомные события есть в features.conf и на wiki.asterisk.org. Не буду расписывать):
То есть по нажатию на # вызываем GoSub и уходим в контекст диалплана.
Тут оговорюсь что использую lua, поэтому далее буду писать функции которые вызываю
Для тех кто не знает, то в lua контекст определяется в таблице extenions и может указывать на функцию которую надо выполнять:
Функция выглядит так ( Комменты описывают, что к чему и рекомендованы к прочтению для понимания)
После того как канал был брошен в transfer и была выполнена проверка, его необходимо направить на принимающего пользователя.
В контексте 'redirectSetup' настраивается вход в данную функцию (аналогично предыдущему)
Сама функция выглядит следующим образом
последний шаг — это организация возврата звонка при неответе/занятости абонента или по какой либо еще причине.
Я думаю уже многие поняли что делать это нужно с помощью переменной TRANSFEREDBY
Свой диалплан полностью выкладывать не буду. Приведу пример маленькой функции, чтобы не вводить в заблуждение — назову ее main.
Касательно возможностей данной организации: у меня диалплан в купе с данной функцией организован таким образом, что проверяет не только доступность локального номера, но и наличие мобильного номера, закрепленного за абонентом.
Если абонент занят, предлагает ему оставаться на линии дожидаясь ответа или выйти из режима ожидания и перезвонить обратно тому, кто перевел (некий микс очереди и IVR), предлагает абоненту вернуться к тому кто перевел, если звонок на сотовый отправил на голосовую почту (у многих операторов данная услуга ничем не отличается от поднятой трубки. Они просто шлют 200 ответ и потом несут ересь...). последнее организовано тоже через customFeature.
В общем-то все это достигается путем линейного программирования и включения головы.
Вроде все.
С наступающим тоагисчи
Адекватных клиентов, и хороших исполнителей вам.
— слепой: после набора нужного сотрудника переводящий сразу отваливается.
— расширенный: возможность поговорить с тем, кому перевод предназначен, принять 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.
В общем-то все это достигается путем линейного программирования и включения головы.
Вроде все.
С наступающим тоагисчи
Адекватных клиентов, и хороших исполнителей вам.
Поделиться с друзьями