Asterisk. начало

Интеграция с внешними системами

Что такое интеграция АТС и CRM? Это настройки и программы, которые конвертируют данные и события между двумя этими платформами и пересылают друг другу. Самым распространенным способом взаимодействия независимых систем является API, а самым популярным способом доступа к API является HTTP REST. Но только не для asterisk.

Внутри Asterisk есть:

  • AGI — синхронный вызов внешних программ/компонентов, используется в основном в диалплане, есть библиотеки типа phpagi, PAGI

  • AMI — текстовый TCP сокет, работающий по принципу подписки на события и ввода текстовых команд, напоминает SMTP изнутри, умеет отслеживать события и управлять вызовами, ,есть библиотека PAMI — самая популярная для создания связи с Asterisk

Удобство или неудобство, возможность или невозможность работы с тем или иным API определяются задачами, которые необходимо решить. Задачи для интеграции с CRM следующие:

  • Отследить начало вызова, куда его перевели, вытащить CallerID, DID, времена начала и конца, может быть данные из директории (для поиска связи телефона и пользователя CRM)

  • Начать и окончить запись звонка, сохранить в нужном формате, сообщить по окончании записи где лежит файл

  • Инициировать звонок по внешнему событию (из программы), позвонить внутреннему номеру, внешнему и соединить их

  • Опционально: интегрировать с CRM, группами дозвона и FollowME для автоматического перевода звонков при отсутствии на месте(по информации CRM)

Все эти задачи можно решить через AMI или ARI, но ARI предоставляет гораздо меньше информации, нет многих событий, не отслеживаются многие переменные, которые в AMI все таки есть (например вызовы макросов, задание переменных внутри макросов, в том числе запись звонков). Поэтому, для правильного и точного отслеживания — выберем пока AMI (но не окончательно). Кроме того (ну а куда ж без этого, мы люди ленивые) — в исходной работе (статья в хабр) используют PAMI. *Потом надо попробовать переписать на ARI, но не факт что получится.

Configuration files needed

You should have already run «make samples» if you installed from source, otherwise you may have the sample config files if you installed from packages.

If you have no configuration files in /etc/asterisk/ then grab the sample config files from the source directory by navigating to it and running «make samples».

Files needed for this example:

  • asterisk.conf
  • modules.conf
  • extensions.conf
  • sip.conf or pjsip.conf

You can use the defaults for asterisk.conf and modules.conf, we’ll only need to modify extensions.conf and sip.conf or pjsip.conf.

To get started, go ahead and move to the /etc/asterisk/ directory where the files are located.

Данные по звонкам

Для любой аналитики и отчётов нужны правильно собранные данные и этот блок АТС тоже проходил много проб и ошибок с первой по третью версию. Зачастую данные по звонкам – это табличка. Один звонок = одна запись: кто звонил, кто ответил, сколько проговорили. В более интересных вариантах есть ещё дополнительная табличка, кого из сотрудников АТС вызывала во время звонка. Но всё это закрывает лишь часть потребностей.

Первоначальными требованиями стали:

  • сохранять не только кому звонила АТС, но и кто ответил, т.к. существуют перехваты и при анализе звонков это нужно будет учитывать,
  • время до соединения с сотрудником. Во FreePBX и некоторых других АТС, звонок считается отвеченным, как только АТС поднимет трубку. Но для голосового меню уже нужно поднять трубку, таким образом все звонки становятся отвеченными и время ожидания ответа становится 0-1 секунду. Поэтому решено было сохранять не только время до ответа, но время до соединения с ключевыми модулями (модуль сам устанавливает у себя это флаг. Сейчас это «Сотрудник», «Внешняя линия»),
  • для более сложного диалплана, когда звонок гуляет между разными группами, нужна была возможность каждый элемент исследовать по отдельности.

Лучшим вариантом оказался вариант, когда модули АТС сами о себе отправляют информацию по звонкам и в итоге сохранять информацию в виде дерева.

Выглядит это следующим образом:

Для начала общая информация о звонке(как у всех — ничего особенного).

  1. Поступил звонок по внешней линии «Для теста» в 05:55:52 с номера 89295671458 на номер 89999999999, в итоге на него ответил сотрудник «Секретарь2» с номером 104. Клиент прождал 60 секунд и разговаривал 36 секунд.
  2. Сотрудник «Секретарь2» делает звонок на номер 112 и на него отвечает сотрудник «Менеджер1» спустя 8 секунд. Разговаривают 14 секунд.
  3. Клиента переводят на Сотрудника «менеджер1» где они продолжают разговаривать ещё 13 секунд

Но это вершина айсберга, по каждой записи можно получить подробное прохождение звонка по АТС.

Вся информация представляется в виде вложенности вызовов:

  1. Поступил звонок по внешней линии «Для теста» в 05:55:52 с номера 89295671458 на номер 89999999999.
  2. В 05:55:53 внешняя линия отправляет звонок на Входящую схему «test»
  3. Во время обработки звонка по схеме вызывается модуль «вызов менеджера», в котором звонок находится 16 секунд. Это разработанный под клиента модуль.
  4. Модуль «вызов менеджера» отправляет звонок на ответственного за номер (клиента) сотрудника «Менеджер1» и ожидает ответа 5 секунд. Менеджер не ответил.
  5. Модуль «вызов менеджера» отправляет звонок на группу «Менеджеры КОРП». Это другие менеджеры такого же направления (сидят в одной комнате) и ожидает ответа 11 секунд.
  6. Группа «Менеджеры КОРП» вызывает сотрудников «Менеджер1, Менеджер2, Менеджер3» одновременно по 11 секунд. Ответа нет.
  7. Вызов менеджера завершается. И схема звонок отправляет на модуль «Выбор маршрута из 1с». Тоже написанный под клиента модуль. Тут звонок обрабатывался 0 секунд.
  8. Схема отправляет звонок на голосовое меню «Осн с донабором». Клиент в нём прождал 31 секунду, донабора не было.
  9. Схема отправляет звонок на Группу «Секретари», где клиент прождал 12 секунд.
  10. В группе вызывается одновременно 2 сотрудника «Секретарь1» и «Секретарь2» и спустя 12 секунд отвечает сотрудник «Секретарь2». Ответ на вызов дублируется в родительские вызовы. Получается и в группе ответил «Секретарь2», при вызове схемы ответил «Секретарь2» и на звонок по внешней линии ответил «Секретарь2».

Именно сохранение информации о каждой операции и их вложенности позволит просто сделать отчёты. Отчёт по голосовому меню поможет выяснить, насколько оно помогает или мешает. Построить отчёт о пропущенных сотрудниками звонках с учётом, что звонок перехватили и значит не считается пропущенным, и с учётом, что это был групповой звонок, и кто-нибудь другой взял раньше, а значит тоже звонок не пропущенный.

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

В том числе можно проводить достаточно нетипичные исследования, например, как часто номера, которых нет в базе, набирают правильный добавочный или какой процент исходящих звонков является переадресацией на мобильный.

Настройка веб сервера asterisk’a

sudo nano /etc/asterisk/http.conf

содержимое

http.conf

;
; Asterisk Builtin mini-HTTP server
;
;

;
; Whether HTTP interface is enabled or not.  Default is no.
;
enabled=yes
;
; Whether Asterisk should serve static content from http-static
; Default is no.
;
enablestatic=yes
;
; Address to bind to.  Default is 0.0.0.0
;
;bindaddr=127.0.0.1
;
; Port to bind to (default is 8088)
;
bindport=8088
;
; Prefix allows you to specify a prefix for all requests
; to the server.  The default is "asterisk" so that all
; requests must begin with /asterisk
;
;prefix=asterisk

; The post_mappings section maps URLs to real paths on the filesystem.  If a
; POST is done from within an authenticated manager session to one of the
; configured POST mappings, then any files in the POST will be placed in the
; configured directory.
;
;
;
; In this example, if the prefix option is set to "asterisk", then using the
; POST URL: /asterisk/uploads will put files in /var/lib/asterisk/uploads/.
;uploads = /var/lib/asterisk/uploads/
;

В Ubuntu 8.10 корневая директория веб сервера Asterisk’a располагается в /usr/share/asterisk/static-http/, а GUI устанавливается в /var/lib/asterisk/static-http, поэтому необходимо удалить пустую папку

sudo rmdir /usr/share/asterisk/static-http/

и создать ссылку

sudo ln -s /var/lib/asterisk/static-http/ /usr/share/asterisk/

Выполним проверку

sudo make checkconfig

по прежнему находимся в директории ~/asterisk-gui/2.0

Рестарт asterisk

sudo /etc/init.d/asterisk restart

Заходим в панель управления

http://<you ip>:8088/asterisk/static/config/index.html

Адрес входа в панель может отличаться в разных дистрибутивах. !Внимательно смотрите в чем разница!

http://<you ip>:8088/asterisk/static/config/index.html

http.conf

; Prefix allows you to specify a prefix for all requests
; to the server.  The default is "asterisk" so that all
; requests must begin with /asterisk
;
;prefix=asterisk
http://<you ip>:8088/static/config/index.html

http.conf

; Prefix allows you to specify a prefix for all requests
; to the server.  The default is blank.  If uncommented
; all requests must begin with /asterisk
;
;prefix=asterisk

«asterisk -r» : вызов консоли CLI

CLI> reload : перезагрузка конфигов (без обрыва текущих соединений)

Можно перезагрузить и так

asterisk -rx reload

Asterisk/1.4.21.2~dfsg-1ubuntu3

Asterisk GUI-version : SVN-branch-2.0-r4589

:8088/asterisk/static/config/cfgbasic.html

Docker

Если хочется быстро попробовать решение — есть вариант с Docker — быстро создать контейнер, дать ему порты наружу, подсунуть файлы настроек и попробовать (это вариант с LetsEncrypt контейнером, если сертификат уже есть, просто нужно перенаправить обратный прокси на веб сервер FreePBX (ему мы дали другой порт — 88), LetsEncrypt в докере по мотивам этой статьи

Запускать файл надо в скачанной папке проекта (после git clone), но предварительно залезть в конфиги астериска (папка asterisk) и прописать там пути к записям и URL вашего сайта

Этот файл docker-compose.yaml, запускается через

Если nginx не запустился, значит что то не так с конфигурацией в папке nginx/ssl_docker.conf

Третья версия

Идеей для решения проблемы стало не генерить Asterisk диалплан из php, а использовать FastAGI и все правила обработки писать уже на самом php. FastAGI позволяет Asterisk, для обработки звонка, подключиться к сокету. Получать оттуда команды и отправлять результаты. Таким образом логика диалплана находится уже за границами Asterisk и может быть написана на любом языке, в моём случае на php.

Тут было много проб и ошибок. Главной проблемой являлось, что у меня уже было много классов/файлов. На создание объектов, инициализацию и взаимную регистрацию между ними уходило около 1,5 секунд, и эта задержка на каждый звонок не то, что можно игнорировать.

Инициализация должна была быть только 1 раз и поэтому поиски решения начались с написания сервиса на php с использованием Pthreads. Спустя неделю экспериментов этот вариант был отложен из-за тонкостей работы этого расширения. От асинхронного программирования на php после месяца тестов тоже пришлось отказаться, нужно было что-то простое, знакомое любому новичку php, да и многие расширения для php синхронные.

Решением стал свой многопоточный сервис на ‘си’, который компилировался с PHPLIB. Он подгружает все php файлы АТС, ждёт, когда все модули инициализируются, добавят коллбэк друг к другу и когда всё готово – кэширует. При запросе по FastAGI создаётся поток, в нём воспроизводится копия из кэша всех классов и данных и запрос передается в php функцию.

При таком решении время от отправки звонка в наш сервис до первой команды Asterisk сократилось с 1,5с до 0,05с и это время слабо зависит от размера проекта.

В итоге, время на разработку диалплана сократилось существенно, и я могу это оценить поскольку мне пришлось переписать веcь диалплан всех модулей на php. Во-первых, в php уже должны быть написаны методы для получения объекта из базы, они были нужны для отображения в веб-интерфейсе, а во-вторых, и это главное – наконец-то появилась возможность удобной работы со строками с числами с массивами с базой данных плюс множество расширений php.

Для обработки диалплана в классе модуля нужно реализовать функцию dialplanDynamicCall и аргумент pbxCallRequest будет содержать объект для взаимодействия с Asterisk.

В дополнении появилась возможность отлаживать диалплан (в php есть xdebug и для нашего сервиса оно работает), можно двигаться по шагам просматривая значения переменных.

Наследование переменных специфичных для каналов

Если мы в команде Set присоединим спереди к имени переменной одиночный символ _ , то в этом случае эта переменная будет унаследована каналом, который будет создан основным каналом, например, при использовании команды Dial(Local/…); . Однажды будучи наследована, эта переменная не будет далее унаследована. В случае, если мы присоединим спереди к имени переменной два символа _, переменная будет наследоваться неограниченное число раз. (Работает только для CVS HEAD, не поддерживается в Asterisk 1.0.9.)

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

exten => 100,1,Set( __ FOO=5)
exten => 100,2,Dial(Local/test@CheckInherit)
exten => test,1,NoOp(${FOO})

Как результат, переменная FOO будет унаследована. Без символов подчеркивания, в новом канале типа local эта переменная будет не определена.

Пример

exten => 104,1,Set(FEE=${fee})
exten => 104,2,Set(_FIE=${fie})
exten => 104,3,Set(__FUM=${fum})
exten => 104,4,Dial(Local/105)

exten => 105,1,NoOp(${FEE})
exten => 105,2,NoOp(${FIE})
exten => 105,3,NoOp(${FUM})
exten => 105,4,Dial(Local/106)

exten => 106,1,NoOp(${FEE})
exten => 106,2,NoOp(${FIE})
exten => 106,3,NoOp(${FUM})

как результат получим:

— Executing Set("SIP/oberon-365e", "FEE=fee") in new stack
— Executing Set("SIP/oberon-365e", "_FIE=fie") in new stack
— Executing Set("SIP/oberon-365e", "__FUM=fum") in new stack
— Executing Dial("SIP/oberon-365e", "Local/105") in new stack
— Called 105
— Executing NoOp("Local/105@default-7263,2", "") in new stack
— Executing NoOp("Local/105@default-7263,2", "") in new stack
— Executing NoOp("Local/105@default-7263,2", "fum") in new stack
— Executing Dial("Local/105@default-7263,2", "Local/106") in new stack
— Called 106
— Executing NoOp("Local/106@default-49be,2", "") in new stack
— Executing NoOp("Local/106@default-49be,2", "") in new stack
— Executing NoOp("Local/106@default-49be,2", "fum") in new stack

(Этот пример не будет правильно работать в версиях до релиза Asterisk 1.2.)

Make the call

Go back to the main Zoiper interface, and make sure the account is registered. Select the account from the drop down list and click the Register button next to it. If it says registered, you are good to go. If it doesn’t register, then double check your configuration.

Once registered, enter extension 100 and click the Dial button. The call should be made and you should hear the sound file hello-world!

On the Asterisk CLI, you should see something like:

Now that you have made a very simple call, you may want to start reading through the other sections on the wiki to learn more about Operation, Fundamentals and Configuration.

Переменные, используемые различными приложениями

Команды некоторых приложений используют значения «канальных» переменных или возвращают в них результат своей работы.

  • Команда AgentCallbackLogin? возвращает в ${AGENTBYCALLERID_${CALLERID}}: ID успешно авторизированного агента.
  • Команда ChanIsAvail возвращает в ${AVAILCHAN}: первый доступный канал
  • Команда Dial использует значение переменной ${VXML_URL}: для отправки XML Url в телефон Cisco 7960.
  • Команда Dial использует значение переменной ${ALERT_INFO}: для передачи информации о типе звонка в телефоны Cisco.
  • Команда Dial возвращает в ${CAUSECODE}: Код ошибки, если вызов не удался.
  • Команда Dial возвращает в ${DIALSTATUS}: текстовый код результата последней попытки вызова абонента.
  • Команда Dial использует значение переменной ${TRANSFER_CONTEXT}: Если установлена эта переменная, при выполнении процедуры перевода вызова (#transfer), то будет использоваться для выполнения команд экстеншен из указанного контекста.
  • Команда EnumLookup возвращает в переменной ${ENUM}: результат поиска.
  • Команда Hangup использует значение переменной ${PRI_CAUSE} для установки кодов возврата PRI интерфейса.
  • Команда MeetMe использует значение переменной {MEETME_AGI_BACKGROUND}: для определения AGI скрипта, который необходимо выполнить.
  • Команда MeetMe возвращает в переменной ${MEETMESECS}: количество секунд, которое пользователь провел в конференции.
  • Команда Playback возвращает в переменной ${PLAYBACKSTATUS}: Результат выполнения команды (FAILED|SUCCESS).
  • Команда Queue возвращает в переменной ${QUEUESTATUS}: Причину, по которой, помещенный в очередь, вызов, покинул ее.
  • Команда TXTLookup возвращает в переменной ${TXTCIDNAME}: результат поиска в DNS.
  • Команда VoiceMail возвращает в переменной ${VMSTATUS}: результат выполнения приложения VoiceMail?. Возможные значения: SUCCESS | USEREXIT | FAILED .

Инициализация скриптов и создание сервиса

Поскольку схема работы с Битрикс 24, сервисом для AMI не совсем проста и прозрачна, на ней надо остановится отдельно. Астериск при активации AMI просто открывает порт и все. При присоединении клиента она запрашивает авторизацию, потом клиент подписывается на нужные события. События приходят простым текстом, который PAMI преобразует в структурированные объекты и предоставляет возможность задание функции фильтрации только по интересующим событиям, полям, номерам и т.д.

Как только звонок поступает, возникает событие NewExten начиная с родительского контекста , затем идут все события по порядку следования строк в контекстах. При получении информации из заданных в диалплане _custom переменных CallMeCallerIDName и CallStart вызывается

  1. Функция запроса UserID, соответствующий внутреннему номеру, куда пришел звонок. А если это группа дозвона? Вопрос политический, надо создать звонок всем сразу (когда звонят все сразу) или создавать по мере обзвона при поочередном звонке? У большинства клиентов стоит стратегия Fisrt Available, поэтому с этим нет проблем, звонит только один. Но решать вопрос надо

  2. Функция регистрации звонка в Битрикс24, которая возвращает CallID, необходимый потом для сообщения о параметрах звонка и ссылке на запись. Требует или внутренний номер или UserID

После окончания звонка вызывается функция загрузки записи, которая одновременно сообщает статус завершения звонка (Занят, Нет ответа, Успех), а также загружает ссылку на mp3 файл с записью (если есть).

Поскольку модуль CallMeIn.php должен работать непрерывно, для него был создан SystemD файл запуска callme.service, который надо положить в /etc/systemd/system/callme.service

инициализация и запуск скрипта происходит через systemctl или service

Сервис будет сам перезапускаться по необходимости (при падениях). Сервис слежения за входящими не требует установки веб сервера, нужен только php (который точно есть на сервере FeePBX). Но при отсутствии доступа к записям звонков через Веб сервер (еще и с https) не будет возможности прослушивать записи разговоров.

Теперь поговорим про исходящие звонки. У скрипта CallMeOut.php две функции:

  • Инициация звонка при поступлении запроса на php скрипт (в том числе по кнопке «Позвонить» в самом битриксе). Без веб сервера не работает, запрос поступает через HTTP POST, в запросе содержится токен

  • Сообщение о звонке, его параметрах и записях в Битрикс. Происходит по инициативе Asterisk в диалплане при окончании звонка

Веб сервер нужен только для двух вещей — загрузка файлов записей битриксом (по HTTPS) и вызов скрипта CallMeOut.php. Можно использовать встроенный сервер FreePBX, файлы для которого лежат /var/www/html, можно установить другой сервер или прописать другой путь.

Use an abstraction layer

One of the beautiful things about ARI is that it’s so easy to just bang out a request. But what’s good for development isn’t necessarily what’s good for production.

Please don’t spread lots of direct HTTP calls throughout your application. There are cross-cutting concerns with accessing the API that you’ll want to deal with in a central location. Today, the only concern is authentication. But as the API evolves, other concerns (such as versioning) will also be important.

Note that the abstraction layer doesn’t (and shouldn’t) be complicated. Your client side API can even be something as simple wrapper around GET, POST and DELETE that addresses the cross-cutting concerns. The Asterisk TestSuite has a very simple abstraction library that can be used like this:

In other words: use one of the aforementioned libraries or write your own!

Full Examples:

Phones:

Configuration Notes

This example demonstrates the power of both wizards and templates.

Once the template is created, adding a new phone could be as simple as creating a wizard objectwith nothing more than a username and password.  You don’t even have to specify ‘type’ because it’sinherited from the template.

Of course, you can override ANYTHING in the wizard object or specify everything and not use templates at all.

Trunk to an ITSP requiring registration:

Configuration Notes

This is an example of trunks to 2 different ITSPs each of which has a primary andbackup server.

It also shows most of the endpoint and aor parameters being left at their defaults.

In this scenario, each wizard object takes the place of an endpoint, aor, auth,identify and 2 registrations.

Настройка Asterisk Manager API

Во первых необходимо создать пользователя для управления asterisk.
Для этого достаточно внести соответствующую запись в /etc/asterisk/manager.conf, в примере ниже добавлен пользователь 1cami с паролем PASSWORD1cami.
Важно также в секцию general добавить опции указанные в примере.

manager.conf
enabled = yes
port = 5038
bindaddr = 0.0.0.0
allowmultiplelogin = yes
webenabled = yes
httptimeout = 60

 
secret = PASSWORD1cami
deny=0.0.0.0/0.0.0.0
permit=0.0.0.0/0.0.0.0
read = call,cdr,user,config
write = call,originate,reporting

;;; Дополнительные опции для ASTERISK 11+ ;;; Начало ;;;;
eventfilter=!Event: Newexten
eventfilter=!Event: DeviceStateChange
eventfilter=!Event: NewConnectedLine
eventfilter=!Event: Newchannel
eventfilter=!Event: SoftHangupRequest
eventfilter=!Event: HangupRequest
eventfilter=!Event: BridgeDestroy
eventfilter=!Event: BridgeCreate
eventfilter=!Event: BridgeMerge
eventfilter=!Event: MusicOnHoldStop
eventfilter=!Event: MusicOnHoldStart
eventfilter=!Event: NewCallerid
eventfilter=!Event: LocalBridge
eventfilter=!Event: Unhold
eventfilter=!Event: Hold
eventfilter=!Event: AttendedTransfer
;;; Дополнительные опции для ASTERISK 11+ ;;; Конец ;;;;

Обратите внимание на строки фильтра:

eventfilter=!Event: Newexten

Они актуальны для Asterisk 13. Эта настройка крайне необходима!!!

Что в итоге?

Для обслуживания АТС не требуется специалист, с этим справляется самый обычный администратор – проверено на практике.

Для доработок не нужны специалисты с серьёзной квалификацией достаточно знаний php, т.к. уже написаны модули и для sip протокола, и для очереди, и для вызова сотрудника и другие. Есть класс обёртка для Asterisk. Программист для разработки модуля может (и по-хорошему должен) вызывать уже готовые модули. И знания Asterisk совершенно не нужны, если клиент просит добавить страницу с каким-нибудь новым отчётом. Но практика показывает, что сторонние программисты хоть и справляются, но без документации и нормального покрытия комментариями чувствуют себя неуверенно, поэтому ещё есть куда двигаться.

Модули могут:

  • создавать новые возможности по обработке звонка,
  • добавлять новые блоки в веб-интерфейс,
  • пронаследоваться от любого из существующих модулей, переопределить функции и подменить его или просто быть слегка изменённой копией,
  • добавлять свои настройки в шаблон настроек других модулей и многое другое.

Настройки АТС через API. Как описано выше, все настройки хранятся в базе и читаются в момент вызова, поэтому через API можно менять все настройки АТС

При вызове API не пересоздаётся конфигурация и не перезапускаются модули, следовательно, не важно насколько много у вас настроек и сотрудников. API запросы выполняются быстро и не блокируют друг друга

АТС сохраняет все ключевые операции со звонками с длительностями (ожидания/разговора), вложенностями и в терминах АТС (сотрудник, группа, внешняя линия, а не канал, номер). Это позволяет строить различные отчёты под конкретных клиентов и большая часть работы – сделать удобный интерфейс.

Рейтинг
( Пока оценок нет )
Понравилась статья? Поделиться с друзьями:
Техноарена
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: