Модуль CRM ReadyScript содержит базовые механизмы для интеграции с сервисами IP телефонии. К таким механизмам можно отнести:
- Фиксацию информации о звонке
- Всплывающее уведомление о звонке в административной панели
- Единый интерфейс инициализации исходящих звонков
Ввиду наличия различий форматов уведомлений и внешнего API у различных провайдеров телефонии, для подключения каждого провайдера необходим модуль-адаптер. Для реализации модуля-адаптера необходимо создать внешний модуль.
- Заметки
- В базовой поставке модуля CRM ReadyScript имеется модуль-адаптер для телефонии Телфин. Его всегда можно использовать как пример реализации.
Для удобства, далее в примерах будем рассматривать пример создания модуля для вымышленной телефонии CustomTelephony. Структура папок модуля рекомендуется следующая:
- customtelephony
- config
- file.inc.php
- module.xml
- handlers.inc.php
- model
- telephony
- provider.inc.php
- providertest.inc.php
Непосредственно функциональность модуля-адаптера реализована в 2х классах Provider и ProviderTest. Для обоих в системе предусмотрены базовые классы-родители Crm::Model::Telephony::Provider::AbstractProvider и Crm::Model::Telephony::Provider::AbstractProviderTest соответственно.
Класс, описывающий провайдера (provider.inc.php)
Рассмотрим абстрактные методы базового класса Crm::Model::Telephony::Provider::AbstractProvider.
<?php
namespace Crm\Model\Telephony\Provider;
abstract class AbstractProvider
{
private $url_secret;
abstract public function getId();
abstract public function onEvent(Request $url);
abstract public function getAccessToken($params = array(), $force =
false);
abstract function getRecordData(CallHistory $call, $find_local =
true);
}
Рассмотрим некоторые методы более детально.
Метод onEvent(Request $url) - должен получать на вход параметры, которые приходят на URL для входящих уведомлений, а на выходе отдавать в ReadyScript унифицированный объект события телефонии, класса Crm::Model::Telephony::CallEvent.
Пример реализации метода onEvent:
public function onEvent(Request $url)
{
$call_event = new CallEvent($this);
$call_event
->setEventType($url->request('EventType', TYPE_STRING))
->setCallerNumber($url->request('CallerIDNum', TYPE_STRING))
->setCalledNumber($url->request('CalledNumber', TYPE_STRING))
->setCallFlow($url->request('CallFlow', TYPE_STRING))
->setCalledDID($url->request('CalledDID', TYPE_STRING))
->setCallID($url->request('CallID', TYPE_STRING))
->setSubCallID($url->request('SubCallID', TYPE_STRING))
->setRecID($url->request('RecID', TYPE_STRING))
->setDuration($url->request('Duration', TYPE_STRING))
->setCallAPIID($url->request('CallAPIID', TYPE_STRING))
->setEventTime(date('Y-m-d H:i:s', $url->request('EventTime', TYPE_STRING)/1000000 ))
->setCallerId($url->request('CallerExtensionID', TYPE_STRING))
->setCalledId($url->request('CalledExtensionID', TYPE_STRING))
->setData([
'CalledExtension' => $url->request('CalledExtension', TYPE_STRING),
'CallerExtension' => $url->request('CallerExtension', TYPE_STRING),
])
->setReturnData('OK');
if ($call_event->getEventType() == self::EVENT_TYPE_HANGOUT) {
$call_event->setCallSubStatus($url->request('CallStatus', TYPE_STRING));
}
return $call_event;
}
Методы getExtensionIdByUserId, getUserIdByExtensionId - предполагают, что телефония поддерживает сопоставление номера добавочного и пользователя. Модуль может как угодно реализовать данное сопоставление(рекомендовано выносить это в настройку модуля), но с помощью данных методов система всегда сможет получить информацию о связи.
Метод getActionsByCall, позволяет установить какие кнопки отображать во всплывающем уведомлении о звонке, при различных состояниях звонка, которые можно получить из объекта CallHistory. Объект CallHistory содержит в себе сведения о текущем звонке. Пример реализации метода getActionsByCall:
public function getActionsByCall(CallHistory $call)
{
$actions = array();
if ($this->canApiRequest()) {
$router = Manager::obj();
$hangup_url = $router->getAdminUrl('doAction', [
'call_id' => $call['call_id'],
'call_action' => 'hangup'
], 'crm-callactions');
switch ($call['call_status']) {
case CallHistory::CALL_STATUS_ANSWER:
case CallHistory::CALL_STATUS_CALLING:
$actions[] = [
'text' => t('Отклонить'),
'attr' => [
'data-url' => $hangup_url,
'class' => 'btn btn-danger tel-action'
]
];
break;
}
}
return $actions;
}
Код выше описывает, что во время, когда звонок находится в статусе Идет вызов, Идет разговор - необходимо отобразить кнопку Отклонить, которая будет вызывать стандартный URL действия. В call_action мы видим hangup - это означает, что в классе provider должен быть реализован метод doHangup, который выполнит необходимое действие (в данном случае выполнит запрос к телефонии на отбой звонка).
Таким образом для всех идентификаторов действий, которые вернет метод getActionsByCall, должны быть реализованы методы doИМЯ-ДЕЙСТВИЯ.
Метод getEventTestObject - должен возвращать объект класса тестирования, наследника от Crm::Model::Telephony::Provider::AbstractProviderTest. Класс тестирования должен содержать сведения о форме, которую следует показать пользователю в административной панели, а также реализацию метода onTest, который будет вызываться, когда пользователь будет отправлять заполненную форму.
Для выполнения запросов к API телефонии внутри методов класса провайдера, необходимо получать объект класса Crm::Model::Telephony::Requester, с помощью метода $this->getRequester(). Работа через данный объект позволит логировать обмен данными в общем логе телефонии. Если возможностей данного класса будет недостаточно, можно его унаследовать и добавить собственную логику.
Метод authorizeRequester будет вызываться внутри объекта, полученного через $this->getRequester(), во время выполнения запроса. Данный метод должен устанавливать в запрос необходимые заголовки или параметры для выполнения авторизованных запросов к телефонии. Сперва метод будет вызван с аргументом $force=false, что позволит в случае наличия действующего AccessToken в кэше установить его в объект Requester. В случае если запрос будет отклонен по причине некорректного AccessToken, будет повторно один раз вызван метод authorizeRequester с аргументом $force=true, что будет означать, что токен нужно будет переполучить и обновить его в кэше.
Метод authorizeRequester, должен в своем теле непосредственно получать токен через метод getAccessToken и станавливать его в Requester. Метод getAccessToken, будет вызываться напрямую, если в административной панели будет использован механизм "проверки авторизации".
Реализуйте в стандартном конфигурационном классе модуля CustomTelephony\Config\File все настройки, которые необходимы для работы модуля телефонии, включая получение авторизационных токенов или сведений, позволяющих это сделать в автоматическом режиме далее.
Класс теста провайдера (providertest.inc.php)
Суть тестов состоит в том, чтобы предоставить возможность администратору эмитировать входящие события, без поступления реальных событий от телефонии. Так, например, можно эмитировать входящий/исходящий звонок, ответ на звонок, прекращение вызова.
Класс теста должен "подсказать" системе, какую форму запросить у пользователя в окне тестирования и как обработать POST запрос с данными этой формы. А точнее какой входящий запрос сэмитировать на основе введенных пользователем данных в форму.
Рассмотрим абстрактный класс теста для провайдера телефонии Crm::Model::Telephony::Provider::AbstractProviderTest.
<?php
namespace Crm\Model\Telephony\Provider;
use RS\Orm\Type;
abstract class AbstractProviderTest
{
private $provider;
{
$this->provider = $provider;
}
{
return $this->provider;
}
{
$form_object = new FormObject(new PropertyIterator(array(
'call_flow' => new Type\Varchar(array(
'description' => t('Направление'),
'listFromArray' => array(array(
self::CALL_FLOW_IN => t('Входящий звонок'),
self::CALL_FLOW_OUT => t('Исходящий звонок'),
))
)),
'call_event_type' => new Type\Varchar(array(
'description' => t('Действие'),
'listFromArray' => array(array(
self::CALL_EVENT_TYPE_DIAL => t('Звонок'),
self::CALL_EVENT_TYPE_ANSWER => t('Ответ на звонок'),
self::CALL_EVENT_TYPE_HANGUP => t('Завершение вызова'),
))
)),
'called_id' => new Type\Varchar(array(
'description' => t('Добавочный номер абонента')
)),
'caller_number' => new Type\Varchar(array(
'description' => t('Номер звонящего'),
'attr' => array(array(
'placeholder' => t('Например, +7(XXX)xxx-xx-xx')
))
)),
'show_request' => new Type\Integer(array(
'description' => t('Показать запрос в уведомлении'),
'checkboxView' => array(1,0)
))
)));
return $form_object;
}
{
$params->getPropertyIterator()->arrayWrap('provider_fields');
$params->setFormTemplate(strtolower(str_replace('\\', '_', get_class($this))));
$tpl_folder = \Setup::$PATH.\Setup::$MODULE_FOLDER.'/'.$module.\Setup::$MODULE_TPL_FOLDER;
return $params->getForm(array('provider_test' => $this), null, false, null, '%system%/coreobject/tr_form.tpl', $tpl_folder);
}
}
abstract public function onTest(array $data);
{
}
{
}
}
Как видно из кода класс содержит реализацию по умолчанию почти всех методов, кроме одного абстрактного метода onTest. В данном методе необходимо реализовать обработку входящих данных от отправки формы, предоставленной методом getFormObject.
Вот пример реализации данного метода у телефонии Телфин.
<?php
namespace Crm\Model\Telephony\Provider\Telphin;
class TelphinTest extends AbstractProviderTest
{
public function onTest(array $data)
{
$this->last_event_error = null;
$this->last_event_result = null;
$event_data = $this->getEventData($data);
$url = $this->
getProvider()->getEventGateUrl( $event_data[
'EventType'] );
$context = stream_context_create(array(
'http' => array(
'method' => 'POST',
'header' => 'Content-Type: application/x-www-form-urlencoded' . PHP_EOL,
'content' => http_build_query($event_data),
),
));
$result = @file_get_contents($url, false, $context);
if (!empty($data['show_request'])) {
$request = t('. Запрос: %request', array(
'request' => $url.'?'.http_build_query($event_data)
));
}
if ($result !== false) {
$this->last_event_result = t('Уведомление о событии отправлено');
if (!empty($data['show_request'])) {
$this->last_event_result .= $request;
}
return true;
} else {
$this->last_event_error = t('Не удалось выполнить POST запрос на URL `%url`. Статус ответа: %status', [
'url' => $url,
'status' => isset($http_response_header[0]) ? $http_response_header[0] : ''
]);
if (!empty($data['show_request'])) {
$this->last_event_error .= $request;
}
return false;
}
}
private function getEventData(array $data)
{
$call_id = md5($data['called_id'].$data['caller_number']);
$params = [
'CallID' => $call_id,
'SubCallID' => '193958-'.$call_id,
'CallAPIID' => '3584709139-3849618a-cd61-11e9-b297-bd083c9cfa95',
'EventTime' => time() * 1000000
];
if ($data['call_flow'] == self::CALL_FLOW_IN) {
$params += [
'CalledExtension' => $data['called_id'].'@sipproxy.telphin.ru',
'CalledExtensionID' => '193958',
'CallerExtension' => '0000*000@sipproxy.telphin.ru',
'CallerExtensionID' => '193978',
'CalledNumber' => $data['called_id'],
'CalledDID' => '70000000000',
'CallerIDNum' => $data['caller_number'],
'CallerIDName' => 'Testing call',
'CallFlow' => 'in'
];
} else {
$params += [
'CallerExtension' => $data['called_id'].'@sipproxy.telphin.ru',
'CallerExtensionID' => '193958',
'CalledNumber' => $data['caller_number'],
'CallerIDNum' => $data['called_id'],
'CallerIDName' => 'Testing call',
'CallFlow' => 'out',
];
}
switch($data['call_event_type']) {
case self::CALL_EVENT_TYPE_DIAL:
$params += [
'EventType' => ($data['call_flow'] == self::CALL_FLOW_IN) ? 'dial-in' : 'dial-out',
'CallStatus' => 'CALLING',
];
break;
case self::CALL_EVENT_TYPE_ANSWER:
$params += [
'EventType' => 'answer',
'CallStatus' => 'ANSWER',
];
break;
case self::CALL_EVENT_TYPE_HANGUP:
$params += [
'EventType' => 'hangup',
'CallStatus' => 'ANSWER',
'RecID' => '193958-3a849fa6cb6111e9b6b1bd083c9cfa95',
'Duration' => 7529999
];
break;
}
return $params;
}
}
Метод onTest выполняет запросы с данными, точно соответствующими формату входящих запросов от провайдера телефонии.
Регистрация нового модуля-адаптера телефонии в системе
Для регистрации в системе нового модуля-адаптера, необходимо обработать событие crm.telephony.getproviders. Согласно формату моделй в ReadyScript, регистрация обработчика событий производится в файле /config/handlers.inc.php.
Пример регистрации нового модуля-адаптера для телефонии CustomTelephony.
<?php
namespace CustomTelephony\Config;
{
function init()
{
$this->bind('crm.telephony.getproviders');
}
public static function crmTelephonyGetProviders($list)
{
$list[] = new \CustomTelephony\Model\Telephony\Provider();
return $list;
}
}
Контроллеры, обрабатывающие запросы телефонии
Для всех внешних и внутренних запросов, в ReadyScript предусмотрены единые точки входа (фронт-контроллеры). Данные контроллеры уже вызывают необходимые методы у классов провайдеров.
Рассмотрим имеющиеся контроллеры:
Фронт-контроллер для запросов от провайдера телефонии: crm-front-telephonyevents. Пример получения URL для данного контроллера:
'secret' => 'секретный ключ из настроек CRM для телефонии',
'provider' => 'строковый id провайдера'
]);
Внутри шаблонов получить данный URL можно так:
{$router->getUrl('crm-front-telephonyevents', [
'secret' => 'секретный ключ из настроек CRM для телефонии',
'provider' => 'строковый id провайдера'
])}
Контроллер административной панели для выполнения действий из всплывающего окна уведомлений о звонке: crm-callactions. Пример получения URL для данного контроллера:
'call_id' => 'ID объекта CallHistory',
'call_action' => 'действие'
], 'crm-callactions');
Внутри шаблонов получить данные URL можно так:
{adminUrl do="doAction" mod_controller="crm-callactions" call_id="ID объекта CallHistory" call_action="действие"}
{adminUrl do="calling" mod_controller="crm-callactions" number='номер телефона'}
Пример тестирования работы телефонии
Разработка модуля-адаптера для телефонии должна начинаться с разработки класса для тестирования, далее необходимо включить логирование в разделе Веб-сайт->Настройка модулей->CRM->Вкладка Телефония->Включить логирование запросов. После чего можно запускать утилиту "Проверка работы телефонии", расположенную в этом же разделе и эмитировать входящие запросы от телефонии.
Необходимо добиться, чтобы при получение запросов о поступившем или отвеченном звонке выплывало окно с уведомлением, а при поступлении запроса на завершение звонка окно успешно скрывалось.
С помощью утилиты "Просмотреть лог запросов телефонии" можно детально просмотреть на обработку входящих и исходящих запросов.