В двух предыдущих постах мы писали о том, как интегрировали Битрикс24 и WhatsApp, о том, почему для облака и для коробки были выбраны различные варианты реализации этой интеграции и о том, как данная интеграция была реализована нами для облачного Битрикс24.
В данном посте я расскажу о том, как мы реализовали кастомный коннектор для интеграции WhatsApp в открытые линии Битрикс24, используя API сервиса Chat2desk.
Как мы уже писали в предыдущих постах, мы отказались от использования приложения, написанного под облачные версии Битрикс в коробке. Главная причина – для коробки можно сделать интеграцию на более глубоком уровне, из-за доступности API Bitrix Framework. В отличии от облачной версии – в коробке мы можем создавать свои собственные коннекторы для открытых линий. Что позволит интегрировать мессенджеры через механизм открытых линий максимально близко к тому, как это делают «стандартные» коннекторы.
Существует официальный курс
по разработке кастомных коннекторов для Битрикс24. Однако не все технические моменты освещены в нем подробно.
Начинаем писать код с создания нового модуля под Битрикс Маркетплейс.
Рис 1. Модуль в списке Доступных решений.
Создаем библиотеку модуля, в котором будет храниться логика его работы. Данный класс будет подключаться каждый раз при вызове модуля. Для отображения коннектора в списке доступных конекторов открытых линий – необходимо подписаться на событие OnImConnectorBuildList модуля imconnector, и по нему вернуть информацию о нашем новом коннекторе.
В библиотеке модуля реализовываем обработчик события OnImConnectorBuildList. Привязку обработчика к событию производим при установке модуля. Соответственно при удалении модуля – привязку снимаем.
Рис 2. Коннектор с списке подключенных каналов открытой линии
В массиве информации о коннекторе, мы должны указать компонент, который будет отображаться в качестве формы настроек коннектора. Этот компонент создаем на основе стандартного компонента imconnector.baseconnector и помещаем в состав нашего модуля.
Рис 3. Форма настроек коннектора
В нашем случае нам необходимо хранить токен для доступа к агрегатору. Здесь сталкиваемся с первой проблемой. Стандартный механизм не предлагает нам способа хранения настроек. Вернее, компонент imconnector.baseconnector нам предлагает хранить настройки в именованном кэше. У нас этот кэш очищался раз в сутки, соответственно настройки сбрасывались. Поэтому мы таким способом не воспользовались и сохранили настройки, воспользовавшись COption::SetOptionString().
Таким образом мы получили коннектор для открытых линий, теперь перейдем к реализации логики его работы.
Начнем с приема сообщений в линию. Для этого нам понадобиться иметь точку входа – доступный извне скрипт, который будет принимать вебхуки от агрегатора, подключать наш модуль и передавать ему данные.
В самом общем виде код приема сообщений выглядит так:
CModule::IncludeModule("imconnector"); CModule::IncludeModule("im"); //message data must be array of arrays $message_unique_id = $data['message_id']; $message_data = array ( 'user' => array('id'=>$data['client_id'], 'name'=>$data['client_name']), 'message' => array('id'=>$message_unique_id, 'date'=>'', 'text'=>$data['message_text']), 'chat' => array('id'=>$data['client_id']."_".$transport, 'name'=>'chat_'.$data['client_id']."_".$transport, 'url'=>''), ); \Bitrix\ImConnector\CustomConnectors::sendMessages(self::CONNECTOR_ID, $line,array($message_data));
Как видите, за отправку сообщений в линию отвечает функция \Bitrix\ImConnector\CustomConnectors::sendMessages. При ее выполнении Битрикс сам создаст новый чат (если нужно), пригласит туда ответственного, опубликует в чате сообщение.
Здесь всплывает следующий подводный камень. Как видно – в массиве message_data в поле user можно задать name (имя, с которым пользователь мессенджера будет добавлен в чат). При его задании – Битрикс будет проводить поиск по базе CRM и пытаться привязать диалог к сущностям CRM. Вроде бы все ничего, но делает он это по имени, которое может быть совсем не уникально. Соответственно диалог может прикрепиться не туда, куда вы можете ожидать. В тоже время он не ищет по номерам телефонов, email и значениям полей мессенджеров, что странно. Обычно внешняя система может прислать вам номер телефона, или id мессенджера, который зачастую является email. Намного больше смысла искать по ним.
Собственно сама привязка заключается в том, что Битрикс добавляет открытую линию в контакты сущности, и создает дела у этой сущности.
В нашем случае, было необходимо привязывать диалог к сделкe в CRM по номеру телефона. Для этого мы написали функцию, которая ищет номера телефонов в CRM сущностях (контактах, лидах, компаниях) и возвращает список сделок связанных с найденными сущностями, к которым нужно осуществить привязку. Тут важно учитывать то, что телефоны могут храниться в разных форматах, поэтому поиск нужно вести с помощью Bitrix\Crm\Integrity\DuplicateCommunicationCriterion
Здесь снова возникает проблема. Дело в том, что \Bitrix\ImConnector\CustomConnectors::sendMessages не возвращает данных о том, что за сообщение она создала, и самое главное — в какой чат. Она возвращает только id пользователя который написал сообщение. А не зная идентификатор чата – мы не сможем его привязать к CRM. Получить id чата можно через само сообщение. В \Bitrix\ImConnector\CustomConnectors::sendMessages мы передавали уникальный идентификатор из внешней системы. По нему можно получить информацию о сообщение, в том числе и чат:
$res=\Bitrix\ImConnector\CustomConnectors::sendMessages(self::CONNECTOR_ID, $line,array($message_data)); $sender_id = $res->getData(); $sender_id = $sender_id['RESULT'][0]['user']; //получим ид пользователя(внешнего) отправившего сообщение. $message_id = CIMMessageParam::GetMessageIdByParam("CONNECTOR_MID", $message_unique_id)[0]; //получим идентификатор сообщения $message_res = new CIMMessage($sender_id); $message = $message_res->GetMessage($message_id); //получим информацию о сообщении $chat_id = $message['CHAT_ID'];
Однако дело не привязывается напрямую к чату, а привязывается к его сессии. То есть у одного диалога (чата) может быть несколько сессий.
Получить идентификатор сессии можно таким образом:
$chat = new \Bitrix\ImOpenLines\Chat(ид чата); $session_res = Bitrix\ImOpenLines\Model\SessionTable::getList(array( 'select' => Array('ID', 'CHAT_ID'), 'filter' => array( '=USER_CODE' => $chat->chat['ENTITY_ID'], '=CLOSED' => 'N' ) )); $session = $session_res->fetch(); //идентификатор лежит в $session['ID']
В функцию \CCrmActivity::Add нужно передать параметр ORIGIN_ID со значением вида IMOL_#ид_сессии#
Наконец-то, мы создаем «Дело» на каждый новый чат и привязываем его к найденным сделкам. В результате в карточке отображаются наши диалоги:
Рис 4. Диалог в карточке CRM
Для добавления ссылки на чат в контакт или лид можно воспользоваться таким кодом:
$chat = new \Bitrix\ImOpenLines\Chat(ид чата); $CrmFieldMultiUpdate=array( 'ENTITY_ID'=>'CONTACT', 'ELEMENT_ID'=>intval($params['bindings']['CONTACT'][0]), TYPE_ID"=>"IM", "VALUE_TYPE"=>"IMOL", "COMPLEX_ID"=>"IM_IMOL", "VALUE"=>'imol|'.$chat->chat['ENTITY_ID'], ); $multi = new CCrmFieldMulti(); $multi->add($CrmFieldMultiUpdate);
Рис 5. Ссылка на чат в карточке CRM
Далее, выясняется, что \Bitrix\ImConnector\CustomConnectors::sendMessages не запускает триггеры автоматизации. Запустить их принудительно можно с помощью Bitrix\ImOpenLines\Crm ->executeAutomationTrigger();
Дополнительно к вышеописанным проблемам на нашем сервере – не отрабатывали нотификации по новым сообщениям. Push-and-Pull был настроен верно, его события создавались, но не запускались «в фоне», а только по перезагрузке страницы. То есть нотификации не приходили не в мобильное приложение, не в десктопное, ни в браузер. Проблема решилась принудительным вызовом:
if (\Bitrix\Main\Loader::includeModule('pull')) { \Bitrix\Pull\Event::send(); }
Теперь прием сообщений работает как надо. Можно переходить к передаче.
Для отправки сообщений из открытой линии во внешнюю систему нужно подписаться на событие OnSendMessageCustomHandler. В обработчик попадет информация о сообщении, которую вы передадите уже во внешнюю систему, например через curl.
Здесь также не обошлось без «особенностей». Мы столкнулись с такой проблемой: внешней системе, для передачи файлов (картинок, pdf) мессенджерам, требуются прямые ссылки на файл. Битрикс, при отправке файла во внешнюю линию передает такой массив данных о файле:
Пример данных, приходящих в обработчик:
[0] => Array ( [name] => Снимок.JPG [type] => image [link] => https://название_портала/~Mz0wS [size] => 26374 )
Как видно, Битрикс отдает ссылку. Но это не ссылка на файл, а ссылка на html страницу, с которой файл можно скачать. Естественно, внешняя система не может этого сделать. Поэтому нам нужна прямая ссылка на файл. Чтобы ее получить – нужно знать ид файла. Но этой информации у нас нет. Поэтому будем в очередной раз изобретать велосипед:
Имея короткую ссылку, можно найти информацию о полной ссылке на файл:
$uri_res = CBXShortUri::GetList(array(), array("SHORT_URI"=>#короткая ссылка#));
Полная ссылка на файл содержит в себе хэш, по которому можно найти файл:
$extLink = Bitrix\Disk\ExternalLink::load(array('=HASH' => #хэш_из_полной_ссылки#)); $dwnLink=\Bitrix\Disk\Driver::getInstance()->getUrlManager()->getUrlExternalLink(array('hash' => $extLink->getHash(),'action' => 'showFile','token' => $downloadToken),true);
То есть, помимо ид файла, нам еще нужен и токен загрузки. Формируется он следующим образом:
$downloadToken = Bitrix\Main\Security\Random::getString(12); $_SESSION['DISK_PUBLIC_VERIFICATION'][$extLink->getObject()->getId()] = $downloadToken;
Так мы получим прямую ссылку на файл. Однако как видно – токен хранится в сессии
И доступ к файлу по этой ссылке, скорее всего, будет только у пользователя, отправившего файл. Такое решение нам не подходит , нам нужно иметь возможность отдавать файл всегда, вне зависимости от сессии. Для этого можно создать собственный скрипт, который по короткой ссылке будет вычислять id файла, и отдавать его содержимое с помощью:
$file = $extLink->getFile(); $file_data = $file->getFile(); CFile::viewByUser($file_data, array('force_download' => false, 'attachment_name' => $file_data['ORIGINAL_NAME']));
Теперь мы можем отдавать файл без проверки токена (со всеми вытекающими предосторожностями). Это уже что-то, но все таки, хочется иметь возможность не только отправлять прямую ссылку на файл (всегда доступную), но и отправлять сам файл. И здесь мы сталкиваемся с неожиданной проблемой: Дело в том, что во время отработки события OnSendMessageCustom пересылаемый файл еще не занял своего положения на Битрикс.Диске, а до окончания выполнения обработчика функции OnSendMessageCustom существует как запись в бд вида:
[ID] => 2805421 [TIMESTAMP_X] => 09.09.2018 16:11:33 [MODULE_ID] => disk [HEIGHT] => 0 [WIDTH] => 0 [FILE_SIZE] => 0 [CONTENT_TYPE] => application/octet-stream [SUBDIR] => disk/a8a [FILE_NAME] => a8a71448e3092e5af419e9fb853d5ea5 [ORIGINAL_NAME] => Снимок.JPG [DESCRIPTION] => [HANDLER_ID] => [EXTERNAL_ID] => 398c0e074fc79967709addb43672966f [~src] =>
Как видите размер у него равен нулю, content-type равен application/octet-stream. При попытке в обработчике найти файл /upload/disk/a8a/a8a71448e3092e5af419e9fb853d5ea5 с помощью Bitrix\Main\IO\File — сообщает что файл не существует. На самом деле — сразу после выполнения обработчика события OnSendMessageCustom — файл перемещается на Диск в другое хранилище (что-то вроде хранилища файлов открытых линий). И тогда становится нормальным, доступным. Но, это все происходит уже после окончания работы обработчика OnSendMessageCustom. Соответственно, внешняя система не сможет скачать файл пока мы находимся в обработчике.
Для решения этой проблемы нужно не отправлять информацию о пересылаемом файле внешней системе в обработчике OnSendMessageCustom, а делать это позже, когда файл уже будет перемещен.
Вот такие интересные особенности разработки кастомного коннектора мы открыли для себя, когда писали интеграцию коробочной версии Битрикс24 с WhatsApp.