asterisk-book/chapter-10.md

1044 lines
88 KiB
Markdown
Raw Normal View History

# Глава 10. Погружение в диалплан
> *Для получения списка всех способов, которыми технология не смогла улучшить качество жизни, нажмите три.*
>
> Элис Кан
Хорошо. Основы диалплана позади, но вы знаете что это еще не все. Если вы еще не разобрались с [Главой 6](chapter-06.md), пожалуйста, вернитесь и прочтите ее еще раз. Мы собираемся перейти к более сложным темам.
## Выражения и манипуляции с переменными
Мы начинаем наше погружение в более глубокие аспекты диалпланов: пришло время познакомить вас с несколькими инструментами, которые значительно увеличат мощь, которую вы можете использовать в своем диалплане. Эти конструкции добавляют невероятный интеллект к вашему диалплану, позволяя ему принимать решения на основе различных критериев, которые вы определяете. Наденьте свой мыслительный колпачок и давайте начнем.
<table border="1" width="100%" cellpadding="5">
<tr>
<td>
<p><img src="pics/note.png" height="100" align="left">В этой главе мы используем лучшие практики, которые были разработаны на протяжении многих лет при создании диалплана. Основное, что вы заметите, это то, что все первые приоритеты начинаются с приложения <code>NoOp()</code> (что просто означает No Operation - отсутствие операции; ничего функционального не произойдет). Другое заключается в том, что все следующие строки будут начинаться с <code>same => n</code> что является ярлыком, который говорит: "используется тоже (same) расширение, что и ранее". Кроме того, отступ - это два пробела.</p>
</td>
</tr>
</table>
### Базовые выражения
Выражения - это комбинации переменных, операторов и значений, которые соединяются вместе для получения результата. Выражение может проверять значения, изменять строки или выполнять математические вычисления. Допустим, у нас есть переменная под названием COUNT. На простом английском языке два выражения, использующие эту переменную, могут быть [`COUNT` плюс 1] или [`COUNT` делить на 2]. Каждое из этих выражений имеет определенный результат или значение, зависящее от значения данной переменной.
В Asterisk выражения всегда начинаются со знака доллара и открывающей квадратной скобки и заканчиваются закрывающей квадратной скобкой, как показано здесь:
```
$[expression]
```
Таким образом, мы запишем наши два примера следующим образом:
```
$[${COUNT} + 1]
$[${COUNT} / 2]
```
Когда Asterisk встречает выражение в диалплане, он заменяет все выражение результирующим значением. Важно отметить, что это происходит *после* подстановки переменных. Для демонстрации рассмотрим следующий код:<sup><a href="#sn1">1</a></sup>
```
exten => 321,1,NoOp()
same => n,Answer()
same => n,Set(COUNT=3)
same => n,Set(NEWCOUNT=$[${COUNT} + 1])
same => n,SayNumber(${NEWCOUNT})
```
Во втором приоритете мы присваиваем значение `3` переменной с именем `COUNT`.
В третьем приоритете задействовано только одно приложение - `Set()`, но на самом деле происходят три вещи:
1. Asterisk заменяет `${COUNT}` на число `3` в выражении. Выражение фактически становится таким: `same => n,Set(NEWCOUNT=$[3 + 1])`
2. Asterisk вычисляет выражение, прибавляя `1` к `3`, и заменяет его вычисленным значением `4`: `same => n,Set(NEWCOUNT=4)`
3. Приложение `Set()` присваивает значение `4` новой переменной `COUNT`.
Третий приоритет просто вызывает приложение `SayNumber()`, которое проговаривает текущее значение переменной `${NEWCOUNT}` \(устанавливается в значение `4` во втором приоритете\).
Попробуйте это в своём диалплане.
### Операторы
Когда вы создаете диалплан Asterisk: вы действительно пишете код на специализированном языке сценариев. Это означает, что диалплан Asterisk, как и любой язык программирования, распознает символы, называемые операторами, позволяющие управлять переменными. Давайте рассмотрим типы операторов, доступных в Asterisk:
огические операторы_
Эти операторы оценивают "истинность" утверждения. В вычислительных терминах это по существу относится к тому, является ли утверждение чем-то или ничем (ненулевым или нулевым, истинным или ложным, включенным или выключенным и т.д.). Логическими операторами являются:
_`expr1 | expr2`_
Этот оператор (называемый оператором “или” или “пайп”) возвращает оценку _`expr1`_ если она истинна (ни одна строка не равна нулю). В противном случае он возвращает оценку _`expr2`_.
_`expr1 & expr2`_
Этот оператор (называемый “и”) возвращает вычисление _`expr1`_, если оба выражения истинны (т.е. ни одно из выражений не является в пустой строкой или нулем). В противном случае возвращает ноль.
_`expr1 {=, >, >=, <, <=, !=} expr2`_
Эти операторы возвращают результаты сравнения целых чисел, если оба аргумента являются целыми числами; в противном случае возвращают результаты сравнения строк. Результат каждого сравнения равен 1, если указанное отношение истинно, или 0 если отношение ложно. (Если вы выполняете сравнение строк - они будут выполняться в соответствии с текущими локальными настройками вашей операционной системы.)
_Математические операторы_
Хотите выполнить расчет? Вам понадобится один из них:
_`expr1 {+, -} expr2`_
Эти операторы возвращают результат сложения или вычитания целочисленных аргументов.
_`expr1 {*, /, %} expr2`_
Возвращают, соответственно, результат умножения, целочисленного деления или остатка деления целочисленных аргументов.
_Операторы регулярных выражений_
Вы также можете использовать операторы регулярных выражений в Asterisk:
<table border="1" width="100%" cellpadding="5">
<tr>
<td>
<p><img src="pics/note.png" height="100" align="left">Дополнительную информацию об особенностях работы оператора регулярного выражения в Asterisk можно найти на <a href="https://wjd.nu/notes/2011#asterisk-dialplan-peculiarities-regex">сайте Уолтера Докса</a>.</p>
</td>
</tr>
</table>
_`expr1 : expr2`_
Этот оператор сопоставляет _`expr1`_ с _`expr2`_, где _`expr2`_ должно быть регулярным выражением.<sup><a href="#sn2">2</a></sup> Регулярное выражение привязывается к началу строки с неявным `^`.<sup><a href="#sn3">3</a></sup>
Если шаблон не содержит подвыражения - возвращается количество совпадающих символов. Он вернет `0` если совпадений не найдено. Если шаблон содержит подвыражение -- \\(... \\) -- возвращается строка, соответствующая `\1`. Если совпадение не найдено - возвращается пустая строка.
_`expr1 =~ expr2`_
Этот оператор работает так же, как и оператор `:`, за исключением того, что он не привязан к началу.
## Функции диалплана
Функции диалплана позволяют добавить больше мощи к вашим выражениям; вы можете думать о них как об интеллектуальных переменных. Функции диалплана позволяют вычислять длины строк, даты и время, контрольные суммы MD5 и т.д. в пределах выражений диалплана.
<table border="1" width="100%" cellpadding="5">
<tr>
<td>
<p><img src="pics/note.png" height="100" align="left">Вы увидите использование функции <code>Playback(silence/1)</code> во всех примерах в этой главе. Мы делаем так поскольку она ответит на линию, если еще не ответили и воспроизведёт некоторую тишину на линии. Это позволяет другим приложениям, таким как <code>SayNumber()</code>, воспроизводить звук без пропусков.</p>
</td>
</tr>
</table>
### Синтаксис
Функции диалплана имеют следующий базовый синтаксис:
```
FUNCTION_NAME(argument)
```
Вы ссылаетесь на имя функции так же, как и на имя переменной, но на значение функции ссылаются с добавлением знака доллара, открывающейся и закрывающейся фигурной скобки:
```
${FUNCTION_NAME(argument)}
```
Функции также могут инкапсулировать другие функции, например:
```
${FUNCTION_NAME(${FUNCTION_NAME(argument)})}
^ ^ ^ ^ ^^^^
1 2 3 4 4321
```
Как вы, вероятно, уже поняли необходимо быть очень осторожными, чтобы убедиться в наличии соответствующих круглых и фигурных скобок. В предыдущем примере мы обозначили открывающие круглые и фигурные скобки цифрами, а их соответствующие закрывающие аналоги - теми же цифрами.
### Примеры функций диалплана
Функции часто используются совместно с приложением `Set()` для получения или установки значения переменной. В качестве простого примера рассмотрим функцию `LEN()`. Эта функция вычисляет длину строки своего аргумента:
```
exten => 205,1,Answer()
same => n,SayDigits(123)
same => n,SayNumber(123)
same => n,SayNumber(${LEN(123)})
```
Давайте рассмотрим еще один простой пример. Если бы мы хотели установить один из различных таймаутов канала - мы могли бы использовать функцию `TIMEOUT()`. Функция `TIMEOUT()` принимает три аргумента: `absolute`, `digit` и `response`. Чтобы установить тайм-аут с помощью функции `TIMEOUT()`, мы могли бы использовать приложение `Set()`, например:
```
exten => 206,1,Answer()
same => n,Set(TIMEOUT(response)=1)
same => n,Background(enter-ext-of-person)
same => n,WaitExten() ; TIMEOUT() установлен в значение 1
same => n,Playback(like_to_tell_valid_ext)
same => n,Set(TIMEOUT(response)=5)
same => n,Background(enter-ext-of-person)
same => n,WaitExten() ; Теперь должно быть 5 секунд
same => n,Playback(укажитеействительный_файл)
same => n,Hangup()
```
Обратите внимание на отсутствие `${ }` вокруг назначения с помощью функции. Так же, как если бы мы присваивали значение переменной, мы присваиваем значение функции без использования инкапсуляции `${}`; однако, если мы хотим использовать значение, возвращаемое функцией, то нам нужна инкапсуляция.
```
exten => 207,1,Answer()
same => n,Set(TIMEOUT(response)=1)
same => n,SayNumber(${TIMEOUT(response)})
same => n,Set(TIMEOUT(response)=5)
same => n,SayNumber(${TIMEOUT(response)})
same => n,Hangup()
```
Вы можете получить список всех активных функций с помощью следующей команды CLI:
```
*CLI> core show functions
```
Или по определенной функции, например `CALLERID()`, командой:
```
*CLI> core show function CALLERID
```
Ближе к концу этой главы мы рассмотрим несколько функций, с которыми вы захотите поэкспериментировать. Далее в книге мы покажем вам как создавать функции на основе баз данных с помощью `func_odbc`.
## Условное ветвление
Расширенная логика, предоставляемая через выражения и функции, позволит вашему диалплану принимать более сложные решения, что часто приводит к _условному ветвлению_.
### Приложение GotoIf()
Ключом к условному ветвлению является приложение `GotoIf()`. `GotoIf()` вычисляет выражение и отправляет вызывающий объект в определенное место назначения в зависимости от того, имеет ли выражение значение истинности или лжи.
`GotoIf()` использует специальный синтаксис, часто называемый _условным синтаксисом_:
```
GotoIf(expression?destination1:destination2)
```
Если выражение принимает значение "истина" - вызывающий объект отправляется в _destination1_. Если выражение оказывается ложным - вызывающий объект отправляется во второе назначение. Итак, что же такое истина и что такое ложь? Пустая строка и число 0 оцениваются как ложь. _Все остальное оценивается как истина._
Каждый из пунктов назначения может быть одним из следующих:
- Метка приоритета в пределах одного расширения, например `weasels`
- Расширение и метка приоритета в одном контексте, например `123,weasels`
- Контекст, расширение и метка приоритета, такие как `incoming,123,weasels`
Давайте используем `GotoIf()` в качестве примера. Вот небольшое приложение для подбрасывания монет. Вызовите его несколько раз, чтобы проверить правильность.
```
exten => 209,1,Noop(Test use of conditional branching to labels)
same => n,GotoIf($[ ${RAND(0,1)} = 1 ]?weasels:iguanas)
; same => n,GotoIf(${RAND(0,1)}?weasels:iguanas) ;тоже работает, но не в каждой ситуации
same => n(weasels),Playback(weasels-eaten-phonesys) ; ПРИМЕЧАНИЕ: ТО ЖЕ РАСШИРЕНИЕ
same => n,Hangup()
same => n(iguanas),Playback(office-iguanas) ; ВСЕ ТО ЖЕ РАСШИРЕНИЕ
same => n,Hangup()
```
<table border="1" width="100%" cellpadding="5">
<tr>
<td>
<p><img src="pics/note.png" height="100" align="left">Вы заметите, что мы использовали приложение <code>Hangup()</code> после каждого использования приложения <code>Playback()</code>. Это делается для того, чтобы при переходе к метке <code>weasels</code> вызов останавливался до того, как он попадет на звуковой файл <i>office-iguanas</i>. Становится все более распространенным видеть расширения, разбитые на несколько компонентов (защищенных друг от друга командой <code>Hangup()</code>), каждый из которых представляет собой отдельную последовательность шагов, выполняемых после <code>GotoIf()</code>.</p>
</td>
</tr>
</table>
<table border="1" width="100%" cellpadding="5">
<tr>
<td>
<p align="center"><b>Предоставление только ложного условного пути</b></p>
<p>Любой из пунктов назначения может быть опущен (но не оба). Если выражение оценивается как пустое назначение - Asterisk просто переходит к следующему приоритету в текущем расширении.</p>
<p>Мы могли бы выполнить предыдущий пример следующим образом:</p>
<p><pre><code>
exten => 209,1,Noop(Test use of conditional branching)
same => n,GotoIf($[ ${RAND(0,1)} = 1 ]?:<b>iguanas</b>)
same => n,Playback(weasels-eaten-phonesys) ; больше нет ярлыка weasels
same => n,Hangup()
same => n(iguanas),Playback(office-iguanas) ; ОБРАТИТЕ ВНИМАНИЕ ЧТО ЭТО ТО ЖЕ РАСШИРЕНИЕ
same => n,Hangup()
</code></pre></p>
<p>Между <code>?</code> и <code>:</code> ничего нет, так что если оператор оценивается как истина, выполнение будет продолжено на следующем шаге. Поскольку это то, что мы хотим - ярлык не нужен.</p>
<p>Мы действительно не рекомендуем делать так, потому что это трудночиемо. Тем не менее - вы увидите такие диалпланы, поэтому хорошо знать, что этот синтаксис технически корректен.</p>
</td>
</tr>
</table>
Вместо того, чтобы использовать метки (лейблы), мы могли бы также отправить вызов на различные расширения. Поскольку они недоступны - мы можем использовать буквы, а не цифры для "номера" расширения. В этом примере условная ветвь отправляет вызов на совершенно разные расширения в одном и том же контексте. В остальном результат тот же.
```
exten => 210,1,Noop(Test use of conditional branching to extensions)
same => n,GotoIf($[ ${RAND(0,1)} = 1 ]?weasels,1:iguanas,1)
exten => weasels,1,Playback(weasels-eaten-phonesys) ; РАЗЛИЧНЫЕ РАСШИРЕНИЯ
same => n,Hangup()
exten => iguanas,1,Playback(office-iguanas) ; ТАКЖЕ РАЗЛИЧНЫЕ РАСШИРЕНИЯ
same => n,Hangup()
```
Рассмотрим еще один пример условного ветвления. На этот раз мы будем использовать оба `Goto()` и `GotoIf()` для обратного отсчета от `5`, а затем повесим трубку:
```
exten => 211,1,NoOp()
same => n,Answer()
same => n,Set(COUNT=5)
same => n(start),GotoIf($[ ${COUNT} > 0 ]?:goodbye)
same => n,SayNumber(${COUNT})
same => n,Set(COUNT=$[ ${COUNT} - 1 ])
same => n,Goto(start)
same => n(goodbye),Playback(vm-goodbye)
same => n,Hangup()
```
Давайте проанализируем этот пример. Во втором приоритете мы задаем переменную `COUNT` равную `5`. Далее, проверяем, чтобы увидеть если `COUNT` больше `0`. Если это так - мы переходим к следующему приоритету. (Не забывайте, что если мы опустим назначение в приложении `GotoIf()` - управление перейдет к следующему приоритету.) Там мы произносим число, вычитаем `1` из него и возвращаемся к метке приоритета `start`. Опять же, если `COUNT` меньше или равен `0`, управление переходит к метке приоритета `goodbye`; в противном случае мы запускаем цикл еще раз.
<table border="1" width="100%" cellpadding="5">
<tr>
<td>
<p align="center"><b>Кавычки и префиксы переменных в условных ветвлениях</b></p>
<p>Сейчас самое время воспользоваться моментом и посмотреть на некоторые небрежные вещи с условными ветвлениями. В Asterisk недопустимо иметь нулевое значение по обе стороны от оператора сравнения. Давайте рассмотрим примеры, которые могли бы привести к ошибке:</p>
<p><pre><code>$[ = 0 ]
$[ foo = ]
$[ > 0 ]
$[ 1 + ]</code></pre></p>
<p>Любой из наших примеров вызовет такое предупреждение:</p>
<p><pre><code>WARNING[28400][C-000000eb]: ast_expr2.fl:470 ast_yyerror: ast_yyerror():
syntax error: syntax error, unexpected '=', expecting $end; Input:
= 0
^
</code></pre></p>
<p>Это маловероятно (если у вас нет опечатки), что вы целенаправленно реализуете что-то из наших примеров. Однако, когда вы выполняете математическое действие или сравнение с неназначенной переменной канала, это фактически то, что делаете Вы. Примеры, используемые нами чтобы показать вам как работает условное ветвление, являются недопустимыми. Мы сначала инициализировали переменную и можем ясно видеть, что переменная канала, которую мы используем в нашем сравнении, была установлена, поэтому мы в безопасности. Но что, если вы не всегда так уверены? В Asterisk строки необязательно должны быть заключены в двойные или одинарные кавычки, как во многих языках программирования. Фактически, если вы используете двойные или одинарные кавычки, это будет буквенной конструкцией в строке. Если мы посмотрим на следующие фрагменты расширения...</p>
<p><pre><code> same => n,Set(TEST_1=foo)
same => n,Set(TEST_2='foo')
same => n,NoOp(Are TEST_1 and TEST_2 equiv? $[${TEST_1} = ${TEST_2}])
</code></pre></p>
<p>...мы должны отметить, что значение, возвращаемое нашим сравнением в <code>NoOp()</code>, не будет значением <code>1</code> (значения совпадают или <i>истина</i>), возвращаемое значение будет <code>0</code> (значения не совпадают или <i>ложь</i>). Мы можем использовать это в своих интересах при выполнении сравнений, обертывая наши переменные канала в одинарные или двойные кавычки. Делая это, мы удостоверяемся, что даже когда переменная канала не может быть установлена, наше сравнение будет допустимым синтаксисом. В следующем примере мы получим ошибку:</p>
<p><pre><code><b>exten => 212,1,NoOp()
same => n,GotoIf($[ ${TEST} != valid ]?error_handling)
same => n,Hangup() ; We're getting an error and ending up here
same => n(error_handling),Playback(goodbye)
same => n,Hangup()</b>
</code></pre></p>
<p>Однако, мы можем обойти это - обернув то, что мы сравниваем, в дополнительные символы (в данном случае кавычки). Тот же пример, но сделан допустимым:</p>
<p><pre><code><b>exten => 213,1,NoOp()
same => n,GotoIf($[ "${TEST}" != "valid" ]?error_handling)
same => n,Hangup()
same => n(error_handling),Playback(goodbye)
same => n,Hangup()</b>
</code></pre></p>
<p>Даже если <code>${TEST}</code> не была установлена (другими словами - она не существует и поэтому не имеет значения), мы все равно делаем сравнение чего-то:</p>
<p><pre><code>$["" != "valid"]
</code></pre></p>
<p>Если вы привыкнете распознавать эти ситуации и использовать методы обертки и префикса, описанные нами, вы напишете гораздо более безопасные диалпланы. Обратите внимание еще раз, что символ кавычки не имеет никакого особого значения здесь. Мы использовали его только потому, что это логический символ для этой цели. Следующее тоже работает:</p>
<p><pre><code> same => n,GotoIf($[_${TEST}_ != _valid_]?error_handling)
;ИЛИ
same => n,GotoIf($[AAAAA${TEST}AAAAA != AAAAAvalidAAAAA]?error_handling)
</code></pre></p>
<p>Не все символы будут работать, так как некоторые могут иметь другие значения для Asterisk и вызвать проблемы. Придерживайтесь кавычек и всё должно быть в порядке.</p>
</td>
</tr>
</table>
Классический пример условного ветвления ласково называют логикой "психо-экс". Если CallerID входящего вызова совпадает с номером телефона человека, с которым вы больше никогда не захотите разговаривать, Asterisk выдает другое сообщение, чем для любого другого абонента. Хотя он несколько прост и примитивен в данном случае это хороший пример для изучения условного ветвления в диалплане Asterisk.
В этом примере используется функция `CALLERID()`, позволяющая получить информацию об CallerID при входящем вызове. Предположим, ради этого примера, что номер телефона жертвы 888-555-1212:<sup><a href="#sn4">4</a></sup>
```
exten => 214,1,NoOp(CALLERID(num): ${CALLERID(num)} CALLERID(name): ${CALLERID(name)})
same => n,GotoIf($[ ${CALLERID(num)} = 8885551212 ]?reject:allow)
same => n(allow),Dial(${UserA_DeskPhone})
same => n,Hangup()
same => n(reject),Playback(abandon-all-hope)
same => n,Hangup()
```
В приоритете `1` мы вызываем приложение `GotoIf()`. Оно сообщает Asterisk перейти к приоритету с меткой `reject`, если номер CallerID соответствует `8885551212`, а в противном случае перейти к метке приоритета `allow` (мы могли бы просто опустить имя метки в результате чего `GotoIf()` просто провалился).<sup><a href="#sn5">5</a></sup> Если CallerID абонента совпадает - управление вызовом переходит к метке приоритета `reject`, которая воспроизводит тонкий намёк нежелательному абоненту. В противном случае вызов пытается набрать получателя по каналу, на который ссылается глобальная переменная `UserA_DeskPhone`.
### Условное ветвление по времени с GotoIfTime()
Другой способ использования условного ветвления в диалплане - это использование приложения `GotoIfTime()`. В то время, как `GotoIf()` оценивает выражение для дальнейших действий, `GotoIfTime()` смотрит на текущее системное время и использует его, чтобы решить, следует ли следовать другой ветви в диалплане.
Наиболее очевидное использование этого приложения - это озвучивание вашим абонентам другого приветствия до и после рабочих часов.
Синтаксис приложения `GotoIfTime()` выглядит следующим образом:
```
GotoIfTime(times,days_of_week,days_of_month,months?label)
```
Короче говоря, `GotoIfTime()` отправляет вызов на указанный `label`, если текущая дата и время соответствуют критериям, указанным _`times`_, _`days\_of\_week`_, _`days\_of\_month`_ и _`months`_. Давайте рассмотрим каждый аргумент более подробно:
_`times`_
Это список одного или нескольких временных диапазонов в 24-часовом формате. Например, с 9:00 утра до 5:00 вечера будет указано как 09:00-17:00. День начинается в 0:00 и заканчивается в 23:59.
<table border="1" width="100%" cellpadding="5">
<tr>
<td>
<p><img src="pics/note.png" height="100" align="left">Стоит отметить, что время будет правильно оборачиваться. Таким образом, если вы хотите указать время закрытия вашего офиса, то можете указать 18:00-9:00 в параметре <i><code>times</code></i>, и оно будет работать как и ожидалось. Обратите внимание, что этот метод работает также и для других компонентов <code>GotoIfTime()</code>. Например, вы можете написать sat-sun, чтобы указать выходные дни.</p>
</td>
</tr>
</table>
_`days\_of\_week`_
Это список из одного или нескольких дней недели. Дни должны быть указаны как `mon`, `tue`, `wed`, `thu`, `fri`, `sat` и/или `sun`. С понедельника по пятницу будет выражаться как `mon-fri`. Вторник и четверг будут обозначены как `tue&thu`.
<table border="1" width="100%" cellpadding="5">
<tr>
<td>
<p><img src="pics/note.png" height="100" align="left">Обратите внимание, что можно указать совокупность диапазонов и одного дня, как: <code>sun-mon&wed&fri-sat</code> или более просто: <code>wed&fri-mon</code>.</p>
</td>
</tr>
</table>
_`days\_of\_month`_
Это список чисел дней месяца. Дни указываются цифрами от `1` до `31`. С 7-го по 12-е число будет выражено как `7-12`, а 15-е и 30-е числа месяца будут записаны как `15&30`. Это может быть полезно для праздников, которые обычно приходятся на один и тот же день месяца, но не на один и тот же день недели.<sup><a href="#sn6">6</a></sup>
_`months`_
Это список из одного или нескольких месяцев в году. Месяцы должны быть записаны как `jan-apr` для диапазона и разделены амперсандами когда требуется включить месяцы не последовательно, как например `jan&mar&jun`. Вы также можете комбинировать их так: `jan-apr&jun&oct-dec`.
Если вы хотите сопоставить все возможные значения для любого из этих аргументов - просто поставьте * в этом аргументе.
Аргумент `label` может быть любым из следующих:
- Метка приоритета в пределах одного расширения, например `time_has_passed`
- Расширение и приоритет в одном контексте, например `123,time_has_passed`
- Контекст, расширение и также приоритет, например `incoming,123,time_has_passed`
Теперь, когда мы рассмотрели синтаксис, давайте рассмотрим несколько примеров. Следующий пример будет соответствовать с _9:00 утра до 5:59 вечера_, с _понедельника по пятницу_, в _любой день месяца_, в _любом месяце года_:
```
exten => s,1,NoOp()
same => n,GotoIfTime(09:00-17:59,mon-fri,*,*?open,s,1)
```
Если абонент звонит в течение этих часов - вызов будет направлен на первый приоритет расширения `start` в контексте с именем `open`. Если вызов выполняется вне указанного времени - он просто продолжит работу со следующего приоритета текущего расширения. Мы собираемся добавить новый контекст с именем `[closed]` сразу после примера соответствия шаблону `55512XX` и изменить контекст `[Test Menu]`, который мы создали в [Главе 6](chapter-06.md), чтобы обработать наше новое правило по времени.
```
exten => _55512XX,1,Answer()
same => n,Playback(tt-monkeys)
same => n,Hangup()
exten => *98,1,NoOp(Access voicemail retrieval.)
same => n,VoiceMailMain()
[closed]
exten => start,1,Noop(after hours handler)
same => n,Playback(go-away2)
same => n,Hangup()
[TestMenu]
exten => start,1,Noop(main autoattendant)
same => n,GotoIfTime(16:59-08:00,mon-fri,*,*?closed,start,1)
same => n,GotoIfTime(11:59-09:00,sat,*,*?closed,start,1)
same => n,GotoIfTime(00:00-23:59,sun,*,*?closed,start,1)
same => n,Background(enter-ext-of-person)
same => n,WaitExten(5)
exten => 1,1,Dial(${UserA_DeskPhone},10)
same => n,Playback(vm-nobodyavail)
same => n,Hangup()
```
## GoSub
Приложение диалплана `GoSub()` позволяет отправить вызов в отдельный раздел диалплана, сделать что-то полезное, а затем вернуть вызов в точку в диалплане, откуда он пришел. Вы можете передать аргументы в `GoSub()`, а также получить от него код возврата. Оно значительно увеличивает функциональность вашего диалплана.
<table border="1" width="100%" cellpadding="5">
<tr>
<td>
<p><img src="pics/note.png" height="100" align="left">Подпрограммы являются важнейшей способностью в любом языке программирования, и в не меньшей степени в диалплане Asterisk. Для тех, кто новичок в программировании: подпрограмма позволяет создать блок универсального кода, который может быть повторно использован различными частями диалплана для избежания повторения. Подумайте о них как о шаблоне в текстовом документе или пустой форме, и у вас появится представление. Как только вы увидите их в действии - должно стать ясно, насколько полезными они могут быть.</p>
</td>
</tr>
</table>
### Определение подпрограмм
При использовании `GoSub()` в диалплане нет особых требований к именованию. Фактически, вы можете использовать `GoSub()` в том же контексте и расширении если пожелаете. В большинстве случаев, однако, ваши подпрограммы должны быть написаны в отдельных контекстах: один контекст для каждой подпрограммы. При создании контекста мы рекомендуем добавить к имени `sub`, чтобы знать что контекст вызывается из приложения `GoSub()`.
Давайте рассмотрим очевидный пример того, где подпрограмма была бы полезна.
Как вы могли заметить, при создании нашего примера диалплана для пользователей, которых мы добавили, логика диалплана для каждого пользователя может потребовать несколько строк кода.
```
[sets]
exten => 100,1,Dial(${UserA_DeskPhone},12)
same => n,Voicemail(100@default)
same => n,GotoIf($["${DIALSTATUS}" = "BUSY"]?busy:unavail)
same => n(unavail),VoiceMail(100@default,u)
same => n,Hangup()
same => n(busy),VoiceMail(100@default,b)
same => n,Hangup()
exten => 101,1,Dial(${UserA_SoftPhone})
same => n,GotoIf($["${DIALSTATUS}" = "BUSY"]?busy:unavail)
same => n(unavail),VoiceMail(101@default,u)
same => n,Hangup()
same => n(busy),VoiceMail(101@default,b)
same => n,Hangup()
exten => 102,1,Dial(${UserB_DeskPhone},10)
same => n,Playback(vm-nobodyavail)
same => n,Hangup()
exten => 103,1,Dial(${UserB_SoftPhone})
same => n,Hangup()
```
Мы предоставили только двум пользователям реальную, рабочую голосовую почту, и определили только четыре телефона как внутренние номера, и все же у нас уже есть беспорядок в виде повторяющегося кода, который будет все труднее поддерживать и расширять. Это быстро станет неуправляемым, если мы не найдем способа получше.
Давайте напишем подпрограмму для обработки набора номера наших пользователей. Добавьте следующее в самый конец вашего диалплана:
```
; SUBROUTINES
[subDialUser]
exten => _[0-9].,1,Noop(Dial extension ${EXTEN},channel: ${ARG1}, mailbox: ${ARG2})
same => n,Noop(mboxcontext: ${ARG3}, timeout ${ARG4})
same => n,Dial(${ARG1},${ARG4})
same => n,GotoIf($["${DIALSTATUS}" = "BUSY"]?busy:unavail)
same => n(unavail),VoiceMail(${ARG2}@${ARG3},u)
same => n,Hangup()
same => n(busy),VoiceMail(${ARG2}@${ARG3},b)
same => n,Hangup()
```
Теперь измените верхнюю часть своего диалплана следующим образом:
```
[OLD_sets] ; что было [sets] теперь [OLD_sets] (называйте как угодно, имя изменить недолго)
exten => 100,1,Dial(${UserA_DeskPhone},12)
same => n,Voicemail(100@default)
same => n,GotoIf($["${DIALSTATUS}" = "BUSY"]?busy:unavail)
;(и тд)
```
Мы переименовали наш контекст `[sets]`, который, конечно, сломает наш диалплан, так как наши телефоны входят в диалплан в нем. Итак, мы собираемся снова добавить его немного ниже:
```
exten => 103,1,Dial(${UserB_SoftPhone})
same => n,Hangup()
[sets]
exten => 110,1,Dial(${UserA_DeskPhone}&${UserA_SoftPhone}&${UserB_SoftPhone})
same => n,Hangup()
;(etc)
```
Итак, теперь у нас снова есть наш контекст `[sets]`, а также `[OLD_sets]`, в котором есть наш старый, осиротевший код. Как мы набираем наши телефоны? Как эта подпрограмма, которую мы только что написали, поможет нам?
```
exten => 103,1,Dial(${UserB_SoftPhone})
same => n,Hangup()
[sets]
;subDialUser args:
; - ARG1 канал(ы) для вызова
; - ARG2 почтовый ящик
; - ARG3 контекст почтового ящика
; - ARG4 Тайм-аут
exten => 100,1,Gosub(subDialUser,${EXTEN},1(${UserA_DeskPhone},${EXTEN},default,12))
exten => 101,1,Gosub(subDialUser,${EXTEN},1(${UserA_SoftPhone},${EXTEN},default,3))
exten => 102,1,Gosub(subDialUser,${EXTEN},1(${UserB_DeskPhone},${EXTEN},default,6))
exten => 103,1,Gosub(subDialUser,${EXTEN},1(${UserB_SoftPhone},${EXTEN},default,24))
exten => 110,1,Dial(${UserA_DeskPhone}&${UserA_SoftPhone}&${UserB_SoftPhone})
same => n,Hangup()
```
Сохраните его, перезагрузите диалплан и выполните несколько тестовых вызовов. Поиграйте с параметрами и посмотрите что изменится. Добавьте несколько почтовых ящиков в свою базу данных и посмотрите что произойдет. Если вы вдохновлены - напишите новую подпрограмму `subDialUserNEW` и посмотрите что сможете придумать. На этом этапе вы также можете удалить весь код в контексте `[OLD_sets]`, поскольку он теперь заброшен, но вы также можете оставить его, поскольку он не причиняет вреда.
Теперь вы можете добавить сотни внутренних номеров и каждый из них будет использовать только одну строку диалплана.
Всякий раз, когда вы обнаружите, что где-то пишете дубликат кода диалплана, остановитесь. Вполне вероятно, что пришло время написать подпрограмму.
### Возврат из подпрограммы
Приложение диалплана `GoSub()` не возвращается автоматически после выполнения подпрограммы. Если вы закончили с вызовом, то можете, конечно, использовать `Hangup()`, однако, если вы не хотите отключаться, а скорее вернуть вызов туда, откуда он пришел, вы можете использовать приложение `Return()`.
Поскольку вы можете вложить подпрограмму в подпрограмму, а также выполнять их одну за другой, когда попадаете в более сложные подпрограммы, то вскоре обнаружите, что это весьма полезная возможность.
## Локальные (Local) каналы
Каналы Local - это метод выполнения других областей диалплана из приложения `Dial()` (в отличие от отправки вызова из канала). Думайте о них как о подпрограммах, которые вы можете вызвать из `Dial()`.
Они могут показаться немного странной концепцией когда вы впервые начинаете их использовать, но поверьте нам - когда мы говорим вам, что они могут быть ответом на проблему, которую вы не можете решить никаким другим способом. Вы почти наверняка захотите использовать их, когда начнете писать расширенные диалпланы. Лучший способ проиллюстрировать использование локальных каналов - на примере. Предположим, у нас есть ситуация, когда нам нужно позвонить нескольким людям, но нам нужно обеспечить задержки разной длины перед набором каждого из участников. Использование локальных каналов является решением проблемы.
С помощью приложения `Dial()` вы, конечно, можете звонить на несколько конечных точек (см. расширение 110 в вашем диалплане для иллюстрации этого), но все три канала будут звонить одновременно и в течение одного и того же периода времени.
```
exten => 110,1,Dial(${UserA_DeskPhone}&${UserA_SoftPhone}&${UserB_SoftPhone})
same => n,Hangup()
```
Однако, допустим, мы хотим ввести некоторые задержки до звонка пользователю, а также прекратить звонить в разные места в разное время. Использование локальных каналов дает нам независимое управление над каждым из каналов, которые мы хотим набрать, поэтому мы можем вводить задержки и контролировать период времени, в течение которого каждый канал звонит независимо.
Допустим, у нас есть небольшая компания, где в первую очередь на входящие звонки отвечает администратор, но есть также два участника команды, которым поручено отвечать на вызовы, и, наконец, может помочь владелец, если это необходимо.
Требования таковы:
- Телефон на стойке регистрации должен звонить сразу и продолжать звонить и не останавливаться, пока не ответят.
- Телефоны участников команды не должны звонить в течение первых 9 секунд, после чего они могут звонить, пока не ответят.
- Телефон владельца должен звонить только в том случае, если вызов оставался без ответа в течение 12 секунд. Кроме того, мы притворяемся, что это сотовый телефон, и поэтому должны прекратить звонить через 18 секунд, чтобы на вызов не ответила голосовая почта сотового телефона.
Мы будем использовать наши существующие настроенные каналы чтобы использовать различные функции. Если у вас есть какой-либо способ сделать это, пожалуйста, постарайтесь, чтобы все они были зарегистрированы где-то, чтобы они могли звонить при вызове. Это даст вам гораздо лучшее представление о том, что происходит при тестировании.<sup><a href="#sn7">7</a></sup>
Это прекрасное время для подпрограммы:
```
[subDialDelay]
exten => _[a-zA-Z0-9].,1,Noop(channel ${ARG1}, pre-delay ${ARG2}, timeout ${ARG3})
; same => n,Progress() ; Optional; Signals back that the call is proceeding
same => n,Wait(${ARG2}) ; how long to wait before dialing
same => n,Dial(${ARG1},${ARG3}) ; timeout can be blank (infinite)
same => n,Hangup()
```
<table border="1" width="100%" cellpadding="5">
<tr>
<td>
<p><img src="pics/note.png" height="100" align="left">У вас уже есть подпрограмма в нижней части файла. Добавьте новую туда же, чтобы все ваши подпрограммы были сгруппированы вместе.</p>
</td>
</tr>
</table>
Теперь нам нужен контекст, в котором мы будем создавать расширения, которые будут использоваться локальным каналом:
```
;LOCAL CHANNELS
[localDialDelay]
exten => receptionist,1,Gosub(subDialDelay,${EXTEN},1(${UserA_DeskPhone},0,600))
exten => team_one,1,Gosub(subDialDelay,${EXTEN},1(${UserA_SoftPhone},9,600))
exten => team_two,1,Gosub(subDialDelay,${EXTEN},1(${UserB_DeskPhone},9,600))
exten => owner,1,Gosub(subDialDelay,${EXTEN},1(${UserB_SoftPhone},12,18))
```
<table border="1" width="100%" cellpadding="5">
<tr>
<td>
<p><img src="pics/note.png" height="100" align="left">Несмотря на то, что назначение для локального канала на самом деле является просто диалпланом — так же, как вы могли бы перейти с помощью <code>Goto()</code>, эти конструкции, как правило, очень специализированы и вписываются в диалплан лучше в своей собственной области - внизу с подпрограммами. Вот почему мы назвали контекст с префиксом <code>local</code>. Это необязательно, но делает вещи легче для понимания.</p>
</td>
</tr>
</table>
Теперь мы сшиваем все это вместе в нашем контексте `[sets]`.
Во-первых, давайте предоставим возможность набирать каждый локальный канал индивидуально, чтобы мы могли проверить каждый канал и убедиться что он делает то, что должен.
```
exten => 103,1,Gosub(subDialUser,${EXTEN},1(${UserB_SoftPhone},${EXTEN},default,24))
; Они предназначены для тестирования по отдельности, прежде чем мы соберем их вместе
exten => 104,1,Dial(Local/receptionist@localDialDelay)
exten => 105,1,Dial(Local/team_one@localDialDelay)
exten => 106,1,Dial(Local/team_two@localDialDelay)
exten => 107,1,Dial(Local/owner@localDialDelay)
```
Наконец, давайте доставим готовый продукт.
```
exten => 107,1,Dial(Local/owner@localDialDelay)
; Мы собираемся назначить некоторые переменные,
; чтобы сохранить простоту чтения строки набора
exten => 108,1,Noop(DialDelay)
same => n,Set(Recpn=Local/receptionist@localDialDelay)
same => n,Set(Team1=Local/team_one@localDialDelay)
same => n,Set(Team2=Local/team_two@localDialDelay)
same => n,Set(Boss=Local/owner@localDialDelay)
same => n,Dial(${Recpn}&${Team1}&${Team2}&${Boss},600)
```
Вам действительно нужно зарегистрировать несколько телефонов и попробовать, чтобы увидеть все это в работе.
<table border="1" width="100%" cellpadding="5">
<tr>
<td>
<p>Решение, которое мы создали, идеально подходит для изучения локальных каналов, но у него есть несколько проблем, которые нужно понять, если вы когда-нибудь захотите запустить его в продакшен:</p>
<ul>
<li>Несмотря на то, что мы установили тайм-аут набора номера, вы обнаружите, что конечные точки SIP имеют собственное мнение об этом. Это не редкость для конечной точки SIP, чтобы иметь свои собственные идеи о таймауте. Таким образом, вы можете установить его на звонок в течение 600 секунд и задаться вопросом: почему он сбрасывает вызов через минуту или около того. Вы можете потратить часы на устранение неполадок вашего диалплана, только чтобы обнаружить, что проблема была настройкой на другом конце. <i>Проверьте каждый кусок, прежде чем склеить их все вместе.</i></li>
<li>Сотовые телефоны имеют свою собственную голосовую почту, и если она отвечает на вызов Asterisk подключит вызов к этому “отвеченному” каналу. Один из способов обойти это - повесить трубку до того, как это произойдет, а затем немедленно перезвонить. Это некрасиво и не рекомендуется.</li>
<li>Сотовые телефоны часто сразу переходят на голосовую почту, если находятся вне зоны действия сети или выключены. <i>Это считается ответом для Asterisk.</i> Данное решение не справляется с подобной проблемой.</li>
<li>Настройка вызова на сотовый телефон (т.е. время между моментом набора и началом вызова) обычно занимает около дюжины секунд или около того.</li>
<li>Помните, что софтфон на мобильном телефоне совсем не похож на телефонный звонок на этот мобильный телефон. Первый - это SIP-соединение, а другой - это вызов ТфОП. (Вы можете звонить одновременно если хотите, но это не всегда хорошая идея.)</li>
<li>Некоторые типы смартфонов будут отдавать приоритет входящим GSM-вызовам. Если вы отвечаете на вызов по софтфону, и кто-то звонит на ваш номер мобильного телефона, софтфон может быть поставлен на удержание. Разные телефоны справляются с этим по-разному.</li>
<li>Мы действительно не справились с переполнением здесь. Что будет если <i>никто</i> не ответит? Это не имеет значения в лаборатории, но можете быть уверены, что это будет иметь значение в продакшене.</li>
<li><code>Dial()</code> ожидает ответный звонок из пункта назначения. Если все ваши локальные каналы имеют задержку <code>Wait()</code>, вызывающий абонент будет слышать тишину, пока что-то не укажет на звонок. Вы можете исправить это, используя <code>Dial()</code> и имитируя сигнал вызова опцией <code>'r'</code> или добавив фиктивный локальный канал, который просто возвращает сигнал вызова.</li>
</ul>
</td>
</tr>
</table>
<table border="1" width="100%" cellpadding="5">
<tr>
<td>
<p><img src="pics/note.png" height="100" align="left">Если вы проверите образец диалплана, мы добавили решение проблемы тишины на задержанных локальных каналах.</p>
</td>
</tr>
</table>
Вот и все. Локальные каналы: создавайте их по частям, и вы в кратчайшие сроки получите мощный диалплан.
Они невероятно полезны при создании сложных приложений очередей.
## Использование базы данных Asterisk
Asterisk предоставляет простой механизм для хранения данных, называемый _Asterisk database_ (AstDB). Это не внешняя реляционная база данных, а просто серверная часть на основе SQLite для хранения простых пар ключ/значение.
База данных Asterisk хранит свои данные в группах, называемых _семействами (families)_, со значениями, определяемыми _ключами (keys)_. В семействе ключ может быть использован только один раз. Например, если бы у нас было семейство `test`, мы могли бы хранить только одно значение с ключом `count`. Каждое сохраненное значение должно быть связано с семейством.
### Хранение данных в AstDB
Чтобы сохранить новое значение в базе данных Asterisk - мы используем приложение `Set()` с функцией `DB()`. Например, чтобы присвоить ключу `count` в семействе `test` значение `1`, мы напишем следующее:
<pre><code><b>
exten => 216,1,NoOp()
same => n,Set(DB(testkey/count)=1)
</b></code></pre>
Сделайте тестовый вызов на 216 чтобы установить значение. Обратите внимание, что если ключ с именем `count` уже существует в семействе `test`, его значение будет перезаписано новым (в этом случае значение жестко закодировано, поэтому оно будет перезаписано с тем же значением, но позже мы увидим, как можем изменить значение и сохранить его).
Вы также можете сохранять значения из командной строки Asterisk, запустив команду <pre>database put <i>family</i> <i>key</i> <i>value</i></pre>. Для нашего примера, вы должны ввести `database put test count 1`.
Итак, пока мы это делаем, давайте также добавим значение в базу данных из консоли:
```
*CLI> database put somekey somevalue 42
```
А теперь запросим базу данных из консоли, чтобы увидеть, какие значения там находятся:
```
*CLI> database show
```
Если все хорошо, вы должны увидеть результат, подобный следующему:
```
/pbx/UUID : d562019a-d2c4-4b88-bcd9-602b3b46fe07
/somekey/count : 1
/somekey/somevalue : 42
/testkey/count : 1
4 results found.
localhost*CLI>
```
### Получение данных из AstDB
Чтобы извлечь значение из базы данных Asterisk и присвоить его переменной, мы снова будем использовать приложение `Set()` и функцию `DB()`. Давайте получим значение `somevalue` (из семейства `somekey`), назначим его переменной `THE_ANSWER`, а затем передадим значение вызывающему объекту:
```
exten => 217,1,NoOp()
same => n,Set(THE_ANSWER=${DB(somekey/somevalue)})
same => n,Answer()
same => n,SayNumber(${THE_ANSWER})
```
Вы также можете проверить значение данного ключа из командной строки Asterisk, запустив команду <pre>database get <i>family</i> <i>key</i></pre>. Чтобы просмотреть все содержимое базы данных AstDB, используйте команду `database show`.
### Удаление данных из AstDB
Существует два способа удаления данных из базы данных Asterisk. Для удаления ключа можно использовать приложение `DB_DELETE()`. Оно принимает путь к ключу в качестве аргументов, например:
```
; удаляет ключ и возвращает его значение за один шаг
exten => 218,1,Verbose(0, We just blew away ${DB_DELETE(somekey/somevalue)})
```
Вы также можете удалить все семейство ключей с помощью приложения `DBdeltree()`. Приложение `DBdeltree()` принимает один аргумент: имя семейства ключей для удаления. Чтобы удалить все семейство `test`, выполните следующие действия:
```
exten => 219,1,DBdeltree(somekey)
```
Чтобы удалить ключи и семейства ключей из базы данных AstDB через интерфейс командной строки, используйте команды <pre>database del <i>key</i></pre> и <pre>database deltree <i>family</i></pre> соответственно.
Если вы сейчас позвоните по номеру 217, то увидите, что ничего не сказано, потому что база данных ничего не возвращает. Вы также можете запустить `database show` из CLI и отметить, что это семейство и ключ были удалены.
### Использование AstDB в диалплане
Существует бесконечное количество способов использования базы данных Asterisk в диалплане. Чтобы представить AstDB - мы рассмотрим два простых примера. Первый - простой пример подсчета показывает, что база данных Asterisk является постоянной (она даже переживает перезагрузку системы). Во втором примере мы будем использовать функцию `BLACKLIST()`, чтобы оценить находится ли номер в черном списке и должен ли он быть заблокирован.
Чтобы начать пример с подсчетом, давайте сначала извлечем число (значение ключа count) из базы данных и назначим его переменной с именем `COUNT`. Если ключ не существует - `DB()` вернет значение `NULL` (нет значения). Поэтому мы можем использовать функцию `ISNULL()`, чтобы проверить, было ли возвращено значение. Если нет - мы инициализируем AstDB с помощью приложения `Set()`, где установим значение в базе данных равным `1`. Это произойдет только в том случае, если этой записи базы данных не существует:
```
exten => 220,1,NoOp()
same => n,Set(COUNT=${DB(test/count)}) ; получаем текущее значение базы данных
same => n,GotoIf($[${ISNULL(${COUNT})}]?firstcount:saycount) ; есть ли значение?
same => n(firstcount),Set(DB(test/count)=1) ; устанавливаем значение 1
same => n,Goto(saycount)
same => n(saycount),NoOp()
same => n,Answer
same => n,SayNumber(${COUNT})
same => n,Goto(increment) ; не требуется, но хорошая привычка
same => n(increment),Set(COUNT=$[${COUNT} + 1]) ; увеличение на единицу
same => n,Set(DB(test/count)=${COUNT}) ; и присвоение нового значения в базе
; данных
same => n,Goto(saycount) ; вернемся и повторим снова
```
Проверьте это. Послушайте, как он считает какое-то время, а затем повесьте трубку. Когда вы снова наберете этот номер - отсчет продолжится с того места, где остановился. Значение, сохраненное в базе данных, будет сохраняться даже при перезапуске Asterisk.
В первое время встроенная база данных Asterisk была необходима. Сегодня, однако, она не так часто используется. Она, вероятно, хороша для установки нескольких семафоров здесь и там, но по большей части, если вы хотите хранить данные - используйте один из бэкэндов реляционной базы данных (мы обсудим интеграцию реляционных баз данных в последующих главах).
## Полезные функции Asterisk
Теперь, когда мы рассмотрели некоторые из основ, давайте рассмотрим несколько популярных функций, которые были включены в Asterisk.
### Концеренц-связь с ConfBridge()
Приложение `ConfBridge()` позволяет нескольким абонентам общаться друг с другом как если бы они все физически находились в одном месте. Некоторые из основных функций включают в себя:
- Возможность создания защищенных паролем конференций
- Администрирование конференции (отключение звука, блокировка или выброс участников)
- Возможность отключение всех, кроме одного участника (полезно для объявлений компании, радиопередач и др.)
- Статическое или динамическое создание конференции
- Звук высокой четкости, который может быть микширован при частоте дискретизации от 8 кГц до 96 кГц
- Видео-возможности, включая добавление динамического переключения видео-каналов на самого громкого докладчика
- Динамически управляемая система меню для администраторов конференций и пользователей
- Дополнительные опции доступны в _confbridge.conf_
В этой главе мы сосредоточены на диалплане - поэтому собираемся продемонстрировать только базовый мост аудиоконференции:
```
$ sudo -u asterisk vim /etc/asterisk/confbridge.conf
[general]
[default_user]
type=user
[default_bridge]
type=bridge
```
После создания файла _confbridge.conf_, нам нужно загрузить модуль `app_confbridge.so`. Это можно сделать в консоли Asterisk:
```
*CLI> module load app_confbridge.so
```
С загруженным модулем мы можем создать простой диалплан для доступа к нашему конференц-мосту:
```
exten => 221,1,NoOp()
same => n,ConfBridge(${EXTEN})
```
Это только верхушка айсберга для проведения конференций. Мы сделали базовую конфигурацию, но есть гораздо больше возможностей для настройки. Мы рассмотрим их более подробно в [Главе 11](glava-11.md).
## Полезные функции диалплана
Мы обсуждали функции ранее в этой главе, но у нас есть что сказать ещё. В настоящее время существует около 150 функций, предоставляемых диалпланом Asterisk. Вот небольшой, кураторский список из тех, с которыми стоит поэкспериментировать.
### CALLERID()
`CALLERID()` поддерживает множество различных типов данных, но вы обнаружите, что обычно используете одно из name или num.
```
exten => 222,1,Noop(CALLERID function)
same => n,Noop(CALLERID currently ${CALLERID(all)})
same => n,Set(CALLERID(num)=4169671111)
same => n,Noop(CALLERID now ${CALLERID(all)})
same => n,Set(CALLERID(name)="Somename")
same => n,Noop(CALLERID now ${CALLERID(all)})
same => n,Hangup()
```
Об остальных не беспокойтесь. Если они вам понадобятся - вы будете знать, что они обозначают и почему вы хотите их использовать.
### CHANNEL()
`CHANNEL()` позволяет взаимодействовать с загрузкой абсолютных данных, относящихся к каналу. Некоторые элементы позволяют изменять их, в то время как другие будут полезны только для справки (например, peerip позволит вам прочитать, но не изменить, IP-адрес узла). Существуют также переменные канала, работающие только с определенными типами каналов (например, элементы pjsip, конечно же могут использоваться только на каналах PJSIP).
```
exten => 223,1,Noop(CHANNEL function)
same => n,Answer()
same => n,Noop(CHANNEL(name) is ${CHANNEL(name)})
same => n,Noop(CHANNEL(musicclass) is ${CHANNEL(musicclass)})
same => n,Noop(CHANNEL(rtcp,all_jitter) is ${CHANNEL(rtcp,all_jitter)})
same => n,Noop(CHANNEL(rtcp,all_loss) is ${CHANNEL(rtcp,all_loss)})
same => n,Noop(CHANNEL(rtcp,all_rtt) is ${CHANNEL(rtcp,all_rtt)})
same => n,Noop(CHANNEL(rtcp,txcount) is ${CHANNEL(rtcp,txcount)})
same => n,Noop(CHANNEL(rtcp,rxcount) is ${CHANNEL(rtcp,rxcount)})
same => n,Noop(CHANNEL(pjsip,local_uri) is ${CHANNEL(pjsip,local_uri)})
same => n,Noop(CHANNEL(pjsip,remote_uri) is ${CHANNEL(pjsip,remote_uri)})
same => n,Noop(CHANNEL(pjsip,request_uri) is ${CHANNEL(pjsip,request_uri)})
same => n,Noop(CHANNEL(pjsip,local_tag) is ${CHANNEL(pjsip,local_tag)})
```
### CURL()
`CURL()` - это простая, но мощная функция, предоставляющая однострочный метод разрешения URL-адресов, который во многих случаях является всем необходимым для базового взаимодействия с внешним веб-сервисом.
```
exten => 224,1,Noop(CURL function)
same => n,Set(ExternalIP=${CURL(http://whatismyip.akamai.com)})
same => n,Noop(The external IP address is ${ExternalIP})
```
Если вам нужно более сложное взаимодействие с внешним сервисом - возможно вам понадобится какая-то программа AGI. Тем не менее, вы можете встроить тонну данных в URL и по простоте `CURL()` трудно превзойти.
### CUT()
Если вам нужно нарезать ваши переменные - вы найдете функцию `CUT()` весьма полезной. Форма проста:
```
CUT(varname,char-delim,range-spec)
```
Это может быть визуально сложно, так как символ разделителя может быть трудно увидеть вложенным между двумя запятыми (например, если разделитель был точкой/десятичной дробью). Давайте развернем предыдущий пример, чтобы увидеть, для чего он хорош (и дать вам визуальный пример того, как разделитель может потеряться в синтаксисе).
```
exten => 225,1,Noop(CUT function)
same => n,Set(ExternalIP=${CURL(http://whatismyip.akamai.com)})
same => n,Noop(The external IP address is ${ExternalIP})
same => n,Answer()
same => n,SayDigits(=${CUT(ExternalIP,.,1)})
same => n,Playback(letters/dot)
same => n,SayDigits(=${CUT(ExternalIP,.,2)})
same => n,Playback(letters/dot)
same => n,SayDigits(=${CUT(ExternalIP,.,3)})
same => n,Playback(letters/dot)
same => n,SayDigits(=${CUT(ExternalIP,.,4)})
```
<table border="1" width="100%" cellpadding="5">
<tr>
<td>
<p><img src="pics/note.png" height="100" align="left">Обратите внимание, что вы вызываете функцию <code>CUT()</code> с фигурными скобками <code>${CUT()}</code>, но переменная, на которую ссылаются внутри <code>CUT()</code>, определяется без фигурных скобок. Это связано с тем, что мы вызываем переменную, а не запрашиваем ее содержимое (<code>CUT()</code> будет иметь дело с содержимым, поэтому нам просто нужно назвать переменную, которую она будет резать на ломтики и кубики и погрузится в то, что там хранится).</p>
</td>
</tr>
</table>
### IF() и STRFTIME()
Комбинация `IF()` и `STRFTIME()` является мощной конструкцией и вы найдете ее неотъемлемой частью логики своего диалплана:
```
exten => 226,1,Noop(IF)
same => n,Answer()
same => n,Playback(${IF($[$[${STRFTIME(,,%S)} % 2] = 1]?hear-odd-noise:good-evening)})
```
Подождите...что?<sup><a href="#sn8">8</a></sup>
Давайте разберем это (мы сделаем отступы в коде таким образом, чтобы показать прогрессию вложенных функций и операторов):
```
exten => 227,1,Noop(IF)
same => n,Answer()
same => n,Wait(.5)
same => n,Wait(.5)
same => n,Noop(${STRFTIME(,,%S)}) ; текущее время - только секунды
same => n,Noop($[ ${STRFTIME(,,%S)} % 2 ]) ; разделить на 2 — вернуть остаток
same => n,Noop(${IF($[ $[ ${STRFTIME(,,%S)} % 2 ] = 1 ]?odd:even)})
same => n,Playback(${IF($[ $[ ${STRFTIME(,,%S)} % 2 ] = 1 ]?hear-odd-noise:good-evening)})
```
Функция `IF()` позволяет передавать логику в приложение `Playback()`. Мы фактически говорим "Если это правда, что время в секундах нечетное, проиграть подсказку `hear-odd-noise`, в противном случае - проиграть `good-evening`".
Если мы выстроим код более типичным образом - он будет выглядеть так (обратите внимание, что некоторые необязательные пробелы также были удалены):
```
exten => 228,1,Noop(IF)
same => n,Answer()
same => n,Wait(.5)
same => n,Noop(${STRFTIME(,,%S)})
same => n,Noop($[${STRFTIME(,,%S)} % 2])
same => n,Noop(${IF($[$[${STRFTIME(,,%S)} % 2 ] = 1]?odd:even)})
same => n,Playback(${IF($[$[${STRFTIME(,,%S)} % 2 ] = 1]?hear-odd-noise:good-evening)})
```
Последнюю строку очень трудно понять, если вы не знаете как мы туда попали, но она демонстрирует силу вложенности.
Сначала эти конструкции могут показаться трудными для записи - поэтому разбейте их и выполните построчно, и в конечном итоге они станут проще для пониманпия (и ваш диалплан впоследствии станет более мощным). Играйте с ними.
### LEN()
Возможность возвращать длину чего-либо с помощью функции `LEN()` может быть очень удобной.
```
exten => 229,1,Noop(LEN)
same => n,Set(LengthyString=${RAND(1,2000)})
same => n,Noop(${LEN(${LengthyString})})
same => n,Noop(${IF( $[ ${LEN(${LengthyString})} <= 3 ]?tooshort:youcanride)})
```
### REGEX()
Да, вы можете использовать регулярные выражения в Asterisk. Это несколько продвинутая тема, не потому, что `REGEX()` является сложной функцией сама по себе, а потому что регулярные выражения являются выражениями сами по себе.
Посмотрите <a href="http://www.regular-expressions.info/">http://www.regular-expressions.info/</a> для получения дополнительной информации или возьмите копию книги O'Reilly _Регулярные выражения_ от Джеффри Фридла.
Привыкните к использованию других функций в Asterisk, получите некоторый опыт работы с регулярными выражениями, а затем попробуйте `REGEX()`.
### STRFTIME()
Мы только что видели функцию `STRFTIME()` в нашем примере `IF()`. Она позволяет возвращать время в различных форматах. В общем, ввод должен быть пустым (что по умолчанию соответствует текущему времени). Вы также можете дать этой функции определенную строку времени Unix и она будет работать с ней.
```
exten => 230,1,Noop(STRFTIME)
same => n,Noop(${STRFTIME(,,%S)}) ; мы уже видели это раньше
same => n,Noop(${STRFTIME(,,%B)}) ; месяц
same => n,Noop(${STRFTIME(,,%H)}) ; часы в 24-часовом формате
same => n,Noop(${STRFTIME(,,%m)}) ; месяц в десятичном виде
same => n,Noop(${STRFTIME(,,%M)}) ; минуты
same => n,Noop(${STRFTIME(,,%Y)}) ; год - 4 цифры
same => n,Noop(${STRFTIME(,,%Y-%m-%d %H:%m:%S)}) ; всё в одной строке
```
## Вывод
В этой главе мы рассмотрели еще несколько приложений диалплана Asterisk и, надеюсь, мы дали вам еще несколько инструментов, которые вы можете использовать для дальнейших экспериментов при создании собственных диалпланов. Как и в других главах - мы приглашаем вас вернуться и перечитать любые разделы, которые требуют уточнения.
<ol>
<li id="sn1"> Помните, что когда вы <i>ссылаетесь</i> на переменную - вы можете вызывать ее по ее имени, но когда вы ссылаетесь на <i>значение</i> переменной, вы должны использовать знак доллара и скобки вокруг ее имени.</li>
<li id="sn2"> Для получения дополнительной информации о регулярных выражениях возьмите копию справочника Jeffrey E. F. Friedls <a href="http://shop.oreilly.com/product/9780596528126.do">Mastering Regular Expressions</a> (OReilly, 2006), или посетите <a href="http://www.regular-expressions.info">http://www.regular-expressions.info/</a>.</li>
<li id="sn3"> Если вы не знаете, что ^ имеет отношение к регулярным выражениям, то просто обязаны прочитать <a href="http://shop.oreilly.com/product/9780596528126.do">Mastering Regular Expressions</a> (Освоение регулярный выражений). Это изменит вашу жизнь!</li>
<li id="sn4"> Если вы хотите проверить это (то, что делаете), то можете выбрать одно из ваших рабочих лабораторных устройств, и в базе данных Asterisk под таблицей ps_endpoints установить поле callerid в '8885551212'. Затем вы можете позвонить с него на номер 214, чтобы увидеть блок в действии.
<code>UPDATE asterisk.ps_endpoints SET callerid='8885551212' WHERE id='<endpoint выбранный в качестве жертвы>'</code></li>
<li id="sn5"> Но мы делаем это так, потому что так легче читать.</li>
<li id="sn6"> Мы понятия не имеем, как реализовать Пасху, но открыты для предложений.</li>
<li id="sn7"> Устаревшие телефоны и планшеты на базе Android могут отлично подойти для этого.</li>
<li id="sn8"> Существует функция языка C с именем <code>STRFTIME()</code>, возвращающая текущее время в виде отформатированной строки. Здесь она работает аналогично. Фактически, часть format функции принимает тот же синтаксис, что и функция в C.</li>
</ol>
[Глава 9. Интернационализация](glava-09.md) | [Содержание](summary.md) | [Глава 11. Функции АТС, включая парковку, пейджинг и конференц-связь](glava-11.md)