В статье пойдет речь о бесплатной АТС Askozia версии 6. При разработке телефонной станции одной из первых задач была организация исходящих звонков.

Как это было


В старой версии Askozia использовались стандартные “шаблоны” dialplan.

  • X! — все номера телефонов
  • XXX — трехзначные цифровые номера
  • .! — абсолютно все номера телефонов

Схематично можно описать пример контекста:

[outgoing]
exten => _XXXXXX!,1,NoOp(Start outgoing calling...) 
	same => n,Dial…

Нам показался такой подход недостаточно гибким. Нет возможности описать следующее правило:

  • Номер начинается с 79
  • Далее следует либо 35 либо 26
  • Остальная часть номера состоит из 7 цифр

Под катом описан выбранный нами подход и итог разработки.

Текущая реализация


Мы решили реализовать этот функционал по другому, с использованием REGEX.

Пример шаблона:

79(25|26)[1-9]{7}

  • (25|26) — это 25 ИЛИ 26
  • [0-9] — цифра от 0 до 9ти, вхождение от 1ого и более раз
  • {7} — кол-во вхождений предыдущего символа

Синтаксис функции REGEX:

REGEX("regular expression" string)
Return '1' on regular expression match or '0' otherwise

Пример использования в Askozia 6:

[outgoing]
exten => _X!,1,NoOp(Start outgoing calling...) 
  same => n,Ringing() 
  same => n,ExecIf($["${REGEX("^[0-9]{6}$" ${EXTEN})}" == "1"]?Gosub(SIP-PR-1-out,${EXTEN},1)) 
  same => n,ExecIf($["${REGEX("^(7|8)[0-9]{10}$" ${EXTEN})}" == "1"]?Gosub(SIP-PR-2-out,${EXTEN},1)) 
  same => n,Hangup()

Для исходящих звонков организована одна точка входа — контекст “outgoing”, в нем происходит вызов функции “ExecIf”:

ExecIf($["${REGEX("^[0-9]{6}$" ${EXTEN})}" == "1"]

Если номер телефона, указанный в переменной “${EXTEN}”, соответствует шаблону, то вызов направляется в sub-контекст средствами функции “Gosub”.

Если в sub-контекст вызов не был прерван, то набор пойдет по следующему подходящему правилу.

Мы таким образом решили проблему с одноканальными линиями. Если линия занята, то вызов идет через следующую, пока не будет отвечен.

Примеры контекстов:

[SIP-PR-1]
exten => _X!,1,ExecIf($["${number}x" == "x"]?Hangup())
	same => n,Dial(SIP/PR-1/${EXTEN},600,TeK))
	same => n,ExecIf($["${DIALSTATUS}" = "ANSWER"]?Hangup())
	same => n,return

[SIP-PR-2]
exten => _X!,1,ExecIf($["${number}x" == "x"]?Hangup())
	same => n,Dial(SIP/PR-2/${EXTEN},600,TeK))
	same => n,ExecIf($["${DIALSTATUS}" = "ANSWER"]?Hangup())
	same => n,return

Обязательно в “sub” — контекст производится проверка “DIALSTATUS”. Если вызов отвечен, то после разговора канал будет завершен средствами функции “Hangup()”. Если этого не сделать, то при завершении звонка клиентом, может произойти повторный набор номера клиента.

Одна важная тонкость, при использовании “Gosub” или “Goto” мы намеренно не меняем ${EXTEN}. Даже если необходимо модифицировать номер телефона (добавить / удалить префикс).

Дело в том, что при модификации EXTEN Asterisk будет модифицировать значение переменной CDR(dst), что приведет к слабо прогнозируемому результату в таблице истории звонков CDR. Считаю, в истории важно сохранять тот номер, который был набран сотрудником.

Будьте аккуратны при описании регулярного выражения. Используйте символы “^”, начало строки и “$” — конец строки, иначе можно получить неожиданный результат.

К примеру шаблон “[0-9]{6}” будет соответствовать всем номерам, где есть 6 и более цифр. Шаблон “^[0-9]{6}$” соответствует только 6ти значным номерам.

Итоги


Мы получили гибкую подсистему, для описания исходящей маршрутизации на АТС.
Список правил отображаем следующим образом:

image

Пример карточки конкретного “Правила”:

image

Комментарии (9)


  1. NortH21
    27.03.2019 15:51

    А будет возможность использовать PHP-AGI скрипты в исходящей маршрутизации? Сейчас можно только во входящих правилах использовать.


    1. boffart Автор
      27.03.2019 15:57

      Это и сейчас разрешено, можно описать custom-контекст и выполнить произвольный AGI скрипт, не важно SHELL / PHP / GO, на чем на пишете ))

      Пример описан тут wiki.mikopbx.com/faq:specific_provider
      О кастомизации опишу отдельную статью.

      Из минусов — это скрипт придется залить на станцию вручную по SFTP, положить можно к примеру на /storage/usbdisk1


  1. turbidit
    27.03.2019 17:04

    Далее следует либо 35 либо 26

    Это настолько редкий кейс, что городить ради него костыли в виде REGEXP — ну такое… Чаще всего это означает что у вас что-то не так в планировании Dialplan, либо же требуется DB со всеми префиксами из реестра Россвязи. Все остальные условия решаются стандартным Pattern Matching.


    1. boffart Автор
      27.03.2019 17:07

      В целом — да, кейс редкий. Часто достаточно направить вызовы через одного, единственного провайдера (60% внедрений).
      На примере нашей компании — используем GSM шлюз с 4 сим картами от разных провайдеров.
      распределяем звонки используя эти правила.


      1. turbidit
        27.03.2019 17:11

        И как вы решаете вопрос с миграцией между операторами абонентов?


        1. boffart Автор
          27.03.2019 17:26

          Миграцию сейчас никак не учитываем. Но есть нюансы. К примеру звонки на городские номера с Мегафон оплачиваются отдельно, сверх пакета минут, а вот если звонить через Yota, то в пакет минут выходят все исходящие не важно на какой номер.

          Есть тонкости в «Домашнем регионе», не все операторы связи включают в пакет минут номера из чужих регионов.

          Так и работаем. То есть принадлежность номера к определенному провайдеру на текущий момент не важна. А вот звонить на городские через tele2 или yota выгоднее чем через мегафон.


  1. arheops
    27.03.2019 20:53

    Гдето я такое уже видел.
    Правда, там это сделали для исходящих правил.
    Штука в том, что когда у вас таких правил 20+, у вас просто обычный звонок без всяких изысков нагружает ядро asterisk. Фишка в том, что в таком варианте самый сложный по CPU звонок — неправильный набор. Что очень плохо.
    Ну про мусор в логах и говорить нечего.
    Вот не помню где. Толи в сангомовском облачном решении, толи еще где.

    А еще это потом очень тяжело расширать/менять(поскольку вы не можете просто вставить custom правило используя asterisk-way).
    Точно помню, что это было убрано из всех продакшн новых версий по причине " у нас 100% cpu и почти нет звонков".

    Ребята, проанализируйте существующие решения. Вы наступаете на грабли, которые все прошли 10-15 лет назад.


  1. arheops
    27.03.2019 21:07

    А вообще у вас очень много анти-паттернов даже в предоставленном кусочке кода

    • использовано execif->gosub вместо gosub-if
    • Hangup в средине диалплана(хотя тут очевидно проще было execif->return). Тут же остутсвие возврата из gosub в одном из случаев.
    • множественные gotoif вырождающиеся потенциально с сотни переходов.
    • повторение одинаковых контекстов отличающихся по сути двумя переменными.
    • CDR могут не отражать попытки неуспешного звонка на некоторые транки(хотя в новых версиях астериск это побороли в ядре, есть настройки).
    • Неправильное использование функции REGEX. Она собственно возвращает 1 для того, чтоб можно было сразу написать
      GosubIf($[ ${REGEX("^[0-9]{6}$" ${EXTEN})} ]?SIP-PR-2-out,${EXTEN},1)


    Кусочек с транками можно сильно проще переписать вот так

    [call-trunk]
    exten => _X!,1,ExecIf($["${number}x" == "x"]?Hangup())
    exten => _X!,2,Noop(placeholder)
    exten => _X!,3,ExecIf($["${DIALSTATUS}" != "ANSWER"]?Return())
    same => n,Hangup

    [SIP-PR-1]
    include => call-trunk
    exten => _X!,2,Dial(SIP/PR-1/${EXTEN},600,TeK))

    [SIP-PR-2]
    include => call-trunk
    exten => _X!,2,Dial(SIP/PR-2/${EXTEN},600,TeK))


    Почему именно так? Так если вы хотите добавить интеграцию, вы это просто меняете в статическом файле, а не лезете куда в ваш код и ищите где генеряться конфиги.
    Ну вот к примеру вы решите не перезванивать если не только ANSWER, но и BUSY.

    зы правильное еще и флаги для транка описать вне диалпана в переменных, но это усложняет понимание данного кусочка.


    1. boffart Автор
      28.03.2019 08:51

      Спасибо Вам за комментарии! Проанализирую и учту замечания.

      Гдето я такое уже видел.
      Правда, там это сделали для исходящих правил.

      Идею подсмотрели у Switchvox PBX. У нас это сделано именно для исходящих правил.