Pull to refresh

Разбираемся с FreePBX и интегрируем его с Битрикс24 и не только

Reading time13 min
Views24K

Битрикс24 - это огромный комбайн, который совмещает и CRM, и документооборот, и учет и еще много разных вещей, которые очень нравятся менеджерам и не очень нравятся IT персоналу. Портал используют очень много небольших и средних компаний, в том числе небольшие клиники, производственники и даже салоны красоты. Основной функцией, которую "любят" менеджеры является интеграция телефонии и CRM, когда любой звонок сразу фиксируется в CRM, создаются карточки клиента, при входящем отображается информация о клиенте и сразу видно кто он такой, что ему можно продать и сколько он должен. Но телефония от Битрикс24 и ее интеграция с CRM стоит денег, иногда немалых. В статье я расскажу опыт интеграции с открытыми инструментами и популярной IP АТС FreePBX, а также рассмотрю логику работы различных частей

Я работаю на аутсорсе в компании, которая занимается продажей и настройкой, интеграцией IP телефонии. Когда меня спросили, можем ли мы вон той вот и вот этой вот компании предложить что то для интеграции Битрикс24 с АТС, которые стоят у клиентов, а также с виртуальными АТС на различных VDS компании, я пошел в Гугл. И он мне конечно же выдал ссылку на статью в хабр, где есть и описание, и github, и вроде все работает. Но при попытке попользоваться этим решением вылезло, что Битрикс24 уже не тот, что ранее, и надо многое переделывать. Кроме того, FreePBX это вам не голый астериск, тут думать надо как совместить удобство использования и хардкорный диалплан в конфиг-файлах.

Изучаем логику работы

Итак для начала, как все это должно работать. При поступления звонка извне на АТС (событие SIP INVITE от провайдера) начинается обработка диалплана(плана набора, dialplan) - правил, что и в каком порядке делать со звонком. Из первого пакета можно получить много информации, которую потом можно использовать в правилах. Отличным инструментом для изучения внутренностей SIP является анализатор sngrep (ссылка) который просто ставится в популярных дистрибутивах через apt install/yum install и подобное, но можно и из исходников собрать. Посмотрим лог звонка в sngrep

В упрощенном виде диалплан занимается только первым пакетом, иногда также в процессе разговора совершается перевод звонков, нажатия кнопок (DTMF), разные интересности типа FollowMe, RingGroup, IVR и прочего.

Что внутри Invite пакета

Собственно большинство простых диалпланов работают с первыми двумя полями и вся логика крутится вокруг DID и CallerID. DID - куда звоним, CallerID - кто звонит.

Но ведь у нас фирма а не один телефон - и значит в АТС скорее всего есть группы вызова (одновременный/последовательный звонок нескольких аппаратов) на городских номерах (Ring Group), IVR (Здравствуйте, вы позвонили... Нажмите один для...), Автоответчики (Phrases), Временные условия (Time Conditions), Переадресация на другие номера или на сотовый (FollowMe, Forward). Это значит, что однозначно определить кому на самом деле придет вызов и с кем будет разговор при поступлении вызова очень сложно. Вот пример начала прохождения типового вызова в АТС наших клиентов

После успешного входа звонка в АТС происходит путешествие его по диалплану в разных "контекстах". Контекст с точки зрения Asterisk - это нумерованный набор команд, каждая из которых содержит фильтр по набранному номеру (он называется exten, для наружного вызова на начальном этапе exten=DID). Командами в строке диалплана может быть все что угодно - внутренние функции (например позвонить внутреннему абоненту - Dial(), положить трубку - Hangup()), условные операторы (IF, ELSE, ExecIF и подобные), переходы к другим правилам этого контекста (Goto, GotoIF), переход другим контекстам в виде вызова функций (Gosub, Macro). Отдельно стоит директива include имя_контекста, которая добавляет команды другого контекста в конец текущего контекста. Команды, включенные через include всегда выполняются после команд текущего контекста.

Вся логика работы FreePBX построена на включении друг в друга разных контекстов через include и вызов через Gosub, Macro и обработчики Handler. Рассмотрим контекст входящих вызовов FreePBX

Вызов проходит по всем контекстам сверху вниз по очереди, в кажом контексте могут быть вызовы других контекстов как макросов (Macro), функций(Gosub) или просто переходы (Goto), поэтому реальное дерево того, что вызывается можно отследить только в логах.

Типовая схема настройки типичной офисной АТС показана ниже. При вызове во входящих маршрутах ищется DID, по нему проверяются временные условия, если все в порядке - запускается голосовое меню. Из него по кнопке 1 или таймауту выход на группу дозвона операторов. После окончания звонка вызывается макрос hangupcall, после которого ничего уже в диалплане выполнить не удастся, кроме специальных обработчиков (hangup handler).

Где в этом алгоритме звонка мы должны поставлять информацию о начале звонка в CRM, где начинать запись, где оканчивать запись и отсылать ее вместе с информацией о звонке на CRM?

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

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

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

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

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

Пример вывода AMI

Event: Newchannel Privilege: call,all Channel: PJSIP/VMS_pjsip-0000078b ChannelState: 4 ChannelStateDesc: Ring CallerIDNum: 111222 CallerIDName: 111222 ConnectedLineNum: ConnectedLineName: Language: en AccountCode: Context: from-pstn Exten: s Priority: 1 Uniqueid: 1599589046.5244 Linkedid: 1599589046.5244

  • ARI - смесь того, другого, все через REST, WebSocket, в формате JSON - но вот со свежими библиотеками и обертками не очень, на вскидку нашлись (phparia, phpari) которые становились в своем развитии года 3 назад.

Пример вывода ARI при инициации звонка

{ "variable":"CallMeCallerIDName", "value":"111222", "type":"ChannelVarset", "timestamp":"2020-09-09T09:38:36.269+0000", "channel":{ "id":"1599644315.5334", "name":"PJSIP/VMSpjsip-000007b6", "state":"Ring", "caller":{ "name":"111222", "number":"111222" }, "connected":{ "name":"", "number":"" }, "accountcode":"", "dialplan":{ "context":"from-pstn", "exten":"s", "priority":2, "appname":"Stasis", "appdata":"hello-world" }, "creationtime":"2020-09-09T09:38:35.926+0000", "language":"ru" }, "asteriskid":"48:5b:aa:aa:aa:aa", "application":"hello-world" }

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

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

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

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

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

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

Придумываем интеграцию заново

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

Вот пример задания своей переменной для времени начала звонка (s - это специальный номер в диалплане, который выполняется ДО начала поиска по DID)

[ext-did-custom]

exten => s,1,Set(CallStart=${STRFTIME(epoch,,%s)})
Пример AMI события по этой строке

Event: Newchannel

Privilege: call,all

Channel: PJSIP/VMS_pjsip-0000078b

ChannelState: 4

ChannelStateDesc: Ring

CallerIDNum: 111222

CallerIDName: 111222

ConnectedLineNum:

ConnectedLineName:

Language: en

AccountCode:

Context: from-pstn

Exten: s

Priority: 1

Uniqueid: 1599589046.5244

Linkedid: 1599589046.5244

Application: Set AppData:

CallStart=1599571046

Поскольку FreePBX перезаписывает файлы extention.conf и extention_additional.conf, мы будем использовать файл extention_custom.conf

Полный код extention_custom.conf
[globals]	
;; Проверьте пути и права на папки - юзер asterisk должен иметь права на запись
;; Сюда будет писаться разговоры
WAV=/var/www/html/callme/records/wav 
MP3=/var/www/html/callme/records/mp3

;; По этим путям будет воспроизводится и скачиваться запись
URLRECORDS=https://www.host.ru/callmeplus/records/mp3

;; Адрес для калбека при исходящем вызове
URLPHP=https://www.host.ru/callmeplus

;; Да пишем разговоры
RECORDING=1

;; Это макрос для записи разговоров в нашу папку. 
;; Можно использовать и системную запись, но пока пусть будет эта - 
;; она работает
[recording]
exten => ~~s~~,1,Set(LOCAL(calling)=${ARG1})
exten => ~~s~~,2,Set(LOCAL(called)=${ARG2})
exten => ~~s~~,3,GotoIf($["${RECORDING}" = "1"]?4:14)
exten => ~~s~~,4,Set(fname=${UNIQUEID}-${STRFTIME(${EPOCH},,%Y-%m-%d-%H_%M)}-${calling}-${called})
exten => ~~s~~,5,Set(datedir=${STRFTIME(${EPOCH},,%Y/%m/%d)})
exten => ~~s~~,6,System(mkdir -p ${MP3}/${datedir})
exten => ~~s~~,7,System(mkdir -p ${WAV}/${datedir})
exten => ~~s~~,8,Set(monopt=nice -n 19 /usr/bin/lame -b 32  --silent "${WAV}/${datedir}/${fname}.wav"  "${MP3}/${datedir}/${fname}.mp3" && rm -f "${WAV}/${fname}.wav" && chmod o+r "${MP3}/${datedir}/${fname}.mp3")
exten => ~~s~~,9,Set(FullFname=${URLRECORDS}/${datedir}/${fname}.mp3)
exten => ~~s~~,10,Set(CDR(filename)=${fname}.mp3)
exten => ~~s~~,11,Set(CDR(recordingfile)=${fname}.wav)
exten => ~~s~~,12,Set(CDR(realdst)=${called})
exten => ~~s~~,13,MixMonitor(${WAV}/${datedir}/${fname}.wav,b,${monopt})
exten => ~~s~~,14,NoOp(Finish if_recording_1)
exten => ~~s~~,15,Return()


;; Это основной контекст для начала разговора
[ext-did-custom]

;; Это хулиганство, делать это так и здесь, но работает - добавляем к номеру '8'
exten =>  s,1,Set(CALLERID(num)=8${CALLERID(num)})

;; Тут всякие переменные для скрипта
exten =>  s,n,Gosub(recording,~~s~~,1(${CALLERID(number)},${EXTEN}))
exten =>  s,n,ExecIF(${CallMeCallerIDName}?Set(CALLERID(name)=${CallMeCallerIDName}):NoOp())
exten =>  s,n,Set(CallStart=${STRFTIME(epoch,,%s)})
exten =>  s,n,Set(CallMeDISPOSITION=${CDR(disposition)})

;; Самое главное! Обработчик окончания разговора. 
;; Обычные пути обработки конца через (exten=>h,1,чтототут) в FreePBX не работают - Macro(hangupcall,) все портит. 
;; Поэтому вешаем Hangup_Handler на окончание звонка
exten => s,n,Set(CHANNEL(hangup_handler_push)=sub-call-from-cid-ended,s,1(${CALLERID(num)},${EXTEN}))

;; Обработчик окончания входящего вызова
[sub-call-from-cid-ended]

;; Сообщаем о значениях при конце звонка
exten => s,1,Set(CDR_PROP(disable)=true)
exten => s,n,Set(CallStop=${STRFTIME(epoch,,%s)})
exten => s,n,Set(CallMeDURATION=${MATH(${CallStop}-${CallStart},int)})

;; Статус вызова - Ответ, не ответ...
exten => s,n,Set(CallMeDISPOSITION=${CDR(disposition)})
exten => s,n,Return


;; Обработчик исходящих вызовов - все аналогичено
[outbound-allroutes-custom]

;; Запись
exten => _.,1,Gosub(recording,~~s~~,1(${CALLERID(number)},${EXTEN}))
;; Переменные
exten => _.,n,Set(__CallIntNum=${CALLERID(num)})
exten => _.,n,Set(CallExtNum=${EXTEN})
exten => _.,n,Set(CallStart=${STRFTIME(epoch,,%s)})
exten => _.,n,Set(CallmeCALLID=${SIPCALLID})

;; Вешаем Hangup_Handler на окончание звонка
exten => _.,n,Set(CHANNEL(hangup_handler_push)=sub-call-internal-ended,s,1(${CALLERID(num)},${EXTEN}))

;; Обработчик окончания исходящего вызова
[sub-call-internal-ended]

;; переменные
exten => s,1,Set(CDR_PROP(disable)=true)
exten => s,n,Set(CallStop=${STRFTIME(epoch,,%s)})
exten => s,n,Set(CallMeDURATION=${MATH(${CallStop}-${CallStart},int)})
exten => s,n,Set(CallMeDISPOSITION=${CDR(disposition)})

;; Вызов скрипта, который сообщит о звонке в CRM - это исходящий, 
;; так что по факту окончания
exten => s,n,System(curl -s ${URLPHP}/CallMeOut.php --data action=sendcall2b24 --data ExtNum=${CallExtNum} --data call_id=${SIPCALLID} --data-urlencode FullFname='${FullFname}' --data CallIntNum=${CallIntNum} --data CallDuration=${CallMeDURATION} --data-urlencode CallDisposition='${CallMeDISPOSITION}')
exten => s,n,Return

Особенность и отличием от оригинального диалплана авторов исходной статьи -

  • Диалплан в формате .conf, так как этого хочет FreePBX (да он умеет .ael, но не все версии и не всегда это удобно)

  • Вместо обработки окончания через exten=>h введена обработка через hangup_handler, потому что FreePBX диалплан заработал только с ним

  • Поправлена строка вызова скрипта, добавлены кавычки и внешний номер звонка ExtNum

  • Обработки вынесена в _custom контексты и позволяют не трогать и не править конфиги FreePBX - входящие через [ext-did-custom], исходящие через [outbound-allroutes-custom]

  • Нет привязки к номерам - файл универсален и нуждается в настройке только пути и ссылка на сервер

Для начала работы нужно еще пустить скрипты в AMI по логину и паролю - для этого в FreePBX тоже есть _custom файл

Файл manager_custom.conf
;;  это логин
[callmeplus]
;; это пароль
secret = trampampamturlala
deny = 0.0.0.0/0.0.0.0

;; я работаю с локальной машиной - но если надо, можно и другие прописать
permit = 127.0.0.1/255.255.255.255
read = system,call,log,verbose,agent,user,config,dtmf,reporting,cdr,dialplan
write = system,call,agent,log,verbose,user,config,command,reporting,originate

Эти оба файла надо поместить в /etc/asterisk, затем перечитать конфиги (или перезапустить астериск)

# astrisk -rv
  Connected to Asterisk 16.6.2 currently running on freepbx (pid = 31629)
#freepbx*CLI> dialplan reload
     Dialplan reloaded.
#freepbx*CLI> exit

Теперь перейдем к PHP

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

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

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

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

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

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

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

[Unit]
Description=CallMe

[Service]
WorkingDirectory=/var/www/html/callmeplus
ExecStart=/usr/bin/php /var/www/html/callmeplus/CallMeIn.php 2>&1 >>/var/log/callmeplus.log
ExecStop=/bin/kill -WINCH ${MAINPID}
KillSignal=SIGKILL

Restart=on-failure
RestartSec=10s

#тут надо смотреть,какие права на папки
#User=www-data  #Ubuntu - debian
#User=nginx #Centos

[Install]
WantedBy=multi-user.target

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

# systemctl enable callme
# systemctl start callme

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

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

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

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

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

Веб сервер

Оставим настройку веб сервера на самостоятельное изучение (тыц, тыц, тыц). Если у вас нет домена, можно попробовать FreeDomain( https://www.freenom.com/ru/index.html), которые на халяву дадут вам имя для вашего белого IP (не забудьте пробросить порты 80, 443 через роутер, если внешний адрес есть только на нем). Если вы только создали DNS домен, то надо подождать (от 15 минут до 48 часов) пока все сервера прогрузятся. По опыту работы с отечественными порвайдерами - от 1 часа до суток.

Автоматизация установки

На github начата разработка инсталятора, чтоб можно было устанавливать еще проще. Но гладко было на бумаге - пока устанавливаем все это вручную, благо после ковыряния во всем этом стало кристально ясно , что с кем дружит, кто куда ходит и как это дебажить. Инсталлятора пока нет (

Docker

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

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

version: '3.3'
services:
  nginx:
    image: nginx:1.15-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/ssl_docker.conf:/etc/nginx/conf.d/ssl_docker.conf
  certbot:
    image: certbot/certbot
  freepbx:
    image: flaviostutz/freepbx
    ports:
      - 88:80 # для настройки
      - 5060:5060/udp
      - 5160:5160/udp
      - 127.0.0.1:5038:5038 # для CallMeOut.php
#      - 3306:3306
      - 18000-18100:18000-18100/udp
    restart: always
    environment:
      - ADMIN_PASSWORD=admin123
    volumes:
      - backup:/backup
      - recordings:/var/spool/asterisk/monitor
      - ./callme:/var/www/html/callme
      - ./systemd/callme.service:/etc/systemd/system/callme.conf
      - ./asterisk/manager_custom.conf:/etc/asterisk/manager_custom.conf
      - ./asterisk/extensions_custom.conf:/etc/asterisk/extensions_custom.conf
#      - ./conf/startup.sh:/startup.sh

volumes:
  backup:
  recordings:

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

docker-compose up -d

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

Другие интеграции

А почему бы заодно не сунуть несколько CRM в скрипты, подумали мы. Изучили несколько API других CRM, особенно бесплатной встроенной в некоторые АТС - ShugarCRM и Vtiger, и да! можно, принцип тот же. Но это уже другая история, которую потом будем заливать на гитхаб отдельно.

Ссылки

Дисклеймер: любые совпадения с реальность вымышлены и это был не я,

Tags:
Hubs:
Total votes 5: ↑4 and ↓1+3
Comments3

Articles