Глава 10. Погружение в диалплан
Для получения списка всех способов, которыми технология не смогла улучшить качество жизни, нажмите три.
– Элис Кан
Хорошо. Основы диалплана позади, но вы знаете что это еще не все. Если вы еще не разобрались с Главой 6, пожалуйста, вернитесь и прочтите ее еще раз. Мы собираемся перейти к более сложным темам.
Выражения и манипуляции с переменными
Мы начинаем наше погружение в более глубокие аспекты диалпланов: пришло время познакомить вас с несколькими инструментами, которые значительно увеличат мощь, которую вы можете использовать в своем диалплане. Эти конструкции добавляют невероятный интеллект к вашему диалплану, позволяя ему принимать решения на основе различных критериев, которые вы определяете. Наденьте свой мыслительный колпачок и давайте начнем.
В этой главе мы используем лучшие практики, которые были разработаны на протяжении многих лет при создании диалплана. Основное, что вы заметите, это то, что все первые приоритеты начинаются с приложения |
Базовые выражения
Выражения - это комбинации переменных, операторов и значений, которые соединяются вместе для получения результата. Выражение может проверять значения, изменять строки или выполнять математические вычисления. Допустим, у нас есть переменная под названием COUNT. На простом английском языке два выражения, использующие эту переменную, могут быть [COUNT
плюс 1] или [COUNT
делить на 2]. Каждое из этих выражений имеет определенный результат или значение, зависящее от значения данной переменной.
В Asterisk выражения всегда начинаются со знака доллара и открывающей квадратной скобки и заканчиваются закрывающей квадратной скобкой, как показано здесь:
$[expression]
Таким образом, мы запишем наши два примера следующим образом:
$[${COUNT} + 1]
$[${COUNT} / 2]
Когда Asterisk встречает выражение в диалплане, он заменяет все выражение результирующим значением. Важно отметить, что это происходит после подстановки переменных. Для демонстрации рассмотрим следующий код:1
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()
, но на самом деле происходят три вещи:
- Asterisk заменяет
${COUNT}
на число3
в выражении. Выражение фактически становится таким:same => n,Set(NEWCOUNT=$[3 + 1])
- Asterisk вычисляет выражение, прибавляя
1
к3
, и заменяет его вычисленным значением4
:same => n,Set(NEWCOUNT=4)
- Приложение
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:
Дополнительную информацию об особенностях работы оператора регулярного выражения в Asterisk можно найти на сайте Уолтера Докса. |
expr1 : expr2
Этот оператор сопоставляет expr1
с expr2
, где expr2
должно быть регулярным выражением.2 Регулярное выражение привязывается к началу строки с неявным ^
.3
Если шаблон не содержит подвыражения - возвращается количество совпадающих символов. Он вернет 0
если совпадений не найдено. Если шаблон содержит подвыражение – \(… \) – возвращается строка, соответствующая \1
. Если совпадение не найдено - возвращается пустая строка.
expr1 =~ expr2
Этот оператор работает так же, как и оператор :
, за исключением того, что он не привязан к началу.
Функции диалплана
Функции диалплана позволяют добавить больше мощи к вашим выражениям; вы можете думать о них как об интеллектуальных переменных. Функции диалплана позволяют вычислять длины строк, даты и время, контрольные суммы MD5 и т.д. в пределах выражений диалплана.
Вы увидите использование функции |
Синтаксис
Функции диалплана имеют следующий базовый синтаксис:
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()
Вы заметите, что мы использовали приложение |
Предоставление только ложного условного пути Любой из пунктов назначения может быть опущен (но не оба). Если выражение оценивается как пустое назначение - Asterisk просто переходит к следующему приоритету в текущем расширении. Мы могли бы выполнить предыдущий пример следующим образом:
Между Мы действительно не рекомендуем делать так, потому что это трудночиемо. Тем не менее - вы увидите такие диалпланы, поэтому хорошо знать, что этот синтаксис технически корректен. |
Вместо того, чтобы использовать метки (лейблы), мы могли бы также отправить вызов на различные расширения. Поскольку они недоступны - мы можем использовать буквы, а не цифры для “номера” расширения. В этом примере условная ветвь отправляет вызов на совершенно разные расширения в одном и том же контексте. В остальном результат тот же.
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
; в противном случае мы запускаем цикл еще раз.
Кавычки и префиксы переменных в условных ветвлениях Сейчас самое время воспользоваться моментом и посмотреть на некоторые небрежные вещи с условными ветвлениями. В Asterisk недопустимо иметь нулевое значение по обе стороны от оператора сравнения. Давайте рассмотрим примеры, которые могли бы привести к ошибке:
Любой из наших примеров вызовет такое предупреждение:
Это маловероятно (если у вас нет опечатки), что вы целенаправленно реализуете что-то из наших примеров. Однако, когда вы выполняете математическое действие или сравнение с неназначенной переменной канала, это фактически то, что делаете Вы. Примеры, используемые нами чтобы показать вам как работает условное ветвление, являются недопустимыми. Мы сначала инициализировали переменную и можем ясно видеть, что переменная канала, которую мы используем в нашем сравнении, была установлена, поэтому мы в безопасности. Но что, если вы не всегда так уверены? В Asterisk строки необязательно должны быть заключены в двойные или одинарные кавычки, как во многих языках программирования. Фактически, если вы используете двойные или одинарные кавычки, это будет буквенной конструкцией в строке. Если мы посмотрим на следующие фрагменты расширения...
...мы должны отметить, что значение, возвращаемое нашим сравнением в
Однако, мы можем обойти это - обернув то, что мы сравниваем, в дополнительные символы (в данном случае кавычки). Тот же пример, но сделан допустимым:
Даже если
Если вы привыкнете распознавать эти ситуации и использовать методы обертки и префикса, описанные нами, вы напишете гораздо более безопасные диалпланы. Обратите внимание еще раз, что символ кавычки не имеет никакого особого значения здесь. Мы использовали его только потому, что это логический символ для этой цели. Следующее тоже работает:
Не все символы будут работать, так как некоторые могут иметь другие значения для Asterisk и вызвать проблемы. Придерживайтесь кавычек и всё должно быть в порядке. |
Классический пример условного ветвления ласково называют логикой “психо-экс”. Если CallerID входящего вызова совпадает с номером телефона человека, с которым вы больше никогда не захотите разговаривать, Asterisk выдает другое сообщение, чем для любого другого абонента. Хотя он несколько прост и примитивен в данном случае это хороший пример для изучения условного ветвления в диалплане Asterisk.
В этом примере используется функция CALLERID()
, позволяющая получить информацию об CallerID при входящем вызове. Предположим, ради этого примера, что номер телефона жертвы 888-555-1212:4
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()
просто провалился).5 Если 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.
Стоит отметить, что время будет правильно оборачиваться. Таким образом, если вы хотите указать время закрытия вашего офиса, то можете указать 18:00-9:00 в параметре |
days\_of\_week
Это список из одного или нескольких дней недели. Дни должны быть указаны как mon
, tue
, wed
, thu
, fri
, sat
и/или sun
. С понедельника по пятницу будет выражаться как mon-fri
. Вторник и четверг будут обозначены как tue&thu
.
Обратите внимание, что можно указать совокупность диапазонов и одного дня, как: |
days\_of\_month
Это список чисел дней месяца. Дни указываются цифрами от 1
до 31
. С 7-го по 12-е число будет выражено как 7-12
, а 15-е и 30-е числа месяца будут записаны как 15&30
. Это может быть полезно для праздников, которые обычно приходятся на один и тот же день месяца, но не на один и тот же день недели.6
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, чтобы обработать наше новое правило по времени.
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()
, а также получить от него код возврата. Оно значительно увеличивает функциональность вашего диалплана.
Подпрограммы являются важнейшей способностью в любом языке программирования, и в не меньшей степени в диалплане Asterisk. Для тех, кто новичок в программировании: подпрограмма позволяет создать блок универсального кода, который может быть повторно использован различными частями диалплана для избежания повторения. Подумайте о них как о шаблоне в текстовом документе или пустой форме, и у вас появится представление. Как только вы увидите их в действии - должно стать ясно, насколько полезными они могут быть. |
Определение подпрограмм
При использовании 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 секунд, чтобы на вызов не ответила голосовая почта сотового телефона.
Мы будем использовать наши существующие настроенные каналы чтобы использовать различные функции. Если у вас есть какой-либо способ сделать это, пожалуйста, постарайтесь, чтобы все они были зарегистрированы где-то, чтобы они могли звонить при вызове. Это даст вам гораздо лучшее представление о том, что происходит при тестировании.7
Это прекрасное время для подпрограммы:
[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()
У вас уже есть подпрограмма в нижней части файла. Добавьте новую туда же, чтобы все ваши подпрограммы были сгруппированы вместе. |
Теперь нам нужен контекст, в котором мы будем создавать расширения, которые будут использоваться локальным каналом:
;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))
Несмотря на то, что назначение для локального канала на самом деле является просто диалпланом — так же, как вы могли бы перейти с помощью |
Теперь мы сшиваем все это вместе в нашем контексте [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)
Вам действительно нужно зарегистрировать несколько телефонов и попробовать, чтобы увидеть все это в работе.
Решение, которое мы создали, идеально подходит для изучения локальных каналов, но у него есть несколько проблем, которые нужно понять, если вы когда-нибудь захотите запустить его в продакшен:
|
Если вы проверите образец диалплана, мы добавили решение проблемы тишины на задержанных локальных каналах. |
Вот и все. Локальные каналы: создавайте их по частям, и вы в кратчайшие сроки получите мощный диалплан.
Они невероятно полезны при создании сложных приложений очередей.
Использование базы данных Asterisk
Asterisk предоставляет простой механизм для хранения данных, называемый Asterisk database (AstDB). Это не внешняя реляционная база данных, а просто серверная часть на основе SQLite для хранения простых пар ключ/значение.
База данных Asterisk хранит свои данные в группах, называемых семействами (families), со значениями, определяемыми ключами (keys). В семействе ключ может быть использован только один раз. Например, если бы у нас было семейство test
, мы могли бы хранить только одно значение с ключом count
. Каждое сохраненное значение должно быть связано с семейством.
Хранение данных в AstDB
Чтобы сохранить новое значение в базе данных Asterisk - мы используем приложение Set()
с функцией DB()
. Например, чтобы присвоить ключу count
в семействе test
значение 1
, мы напишем следующее:
exten => 216,1,NoOp()
same => n,Set(DB(testkey/count)=1)
Сделайте тестовый вызов на 216 чтобы установить значение. Обратите внимание, что если ключ с именем count
уже существует в семействе test
, его значение будет перезаписано новым (в этом случае значение жестко закодировано, поэтому оно будет перезаписано с тем же значением, но позже мы увидим, как можем изменить значение и сохранить его).
Вы также можете сохранять значения из командной строки Asterisk, запустив команду <pre>database put family key value</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 family key</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 key</pre> и <pre>database deltree family</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.
Полезные функции диалплана
Мы обсуждали функции ранее в этой главе, но у нас есть что сказать ещё. В настоящее время существует около 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)})
Обратите внимание, что вы вызываете функцию |
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)})
Подождите…что?8
Давайте разберем это (мы сделаем отступы в коде таким образом, чтобы показать прогрессию вложенных функций и операторов):
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()
является сложной функцией сама по себе, а потому что регулярные выражения являются выражениями сами по себе.
Посмотрите http://www.regular-expressions.info/ для получения дополнительной информации или возьмите копию книги 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 и, надеюсь, мы дали вам еще несколько инструментов, которые вы можете использовать для дальнейших экспериментов при создании собственных диалпланов. Как и в других главах - мы приглашаем вас вернуться и перечитать любые разделы, которые требуют уточнения.
- Помните, что когда вы ссылаетесь на переменную - вы можете вызывать ее по ее имени, но когда вы ссылаетесь на значение переменной, вы должны использовать знак доллара и скобки вокруг ее имени.
- Для получения дополнительной информации о регулярных выражениях возьмите копию справочника Jeffrey E. F. Friedl’s Mastering Regular Expressions (O’Reilly, 2006), или посетите http://www.regular-expressions.info/.
- Если вы не знаете, что ^ имеет отношение к регулярным выражениям, то просто обязаны прочитать Mastering Regular Expressions (Освоение регулярный выражений). Это изменит вашу жизнь!
- Если вы хотите проверить это (то, что делаете), то можете выбрать одно из ваших рабочих лабораторных устройств, и в базе данных Asterisk под таблицей ps_endpoints установить поле callerid в '8885551212'. Затем вы можете позвонить с него на номер 214, чтобы увидеть блок в действии.
UPDATE asterisk.ps_endpoints SET callerid='8885551212' WHERE id='
'</code></li> - Но мы делаем это так, потому что так легче читать.
- Мы понятия не имеем, как реализовать Пасху, но открыты для предложений.
- Устаревшие телефоны и планшеты на базе Android могут отлично подойти для этого.
- Существует функция языка C с именем
</ol> [Глава 9. Интернационализация](/Definitive-Guide-5th-Edition/glava-09.html) | [Содержание](/Definitive-Guide-5th-Edition/summary.html) | [Глава 11. Функции АТС, включая парковку, пейджинг и конференц-связь](/Definitive-Guide-5th-Edition/glava-11.html)STRFTIME()
, возвращающая текущее время в виде отформатированной строки. Здесь она работает аналогично. Фактически, часть format функции принимает тот же синтаксис, что и функция в C.