Розбираємось з FreePBX та інтегруємо його з Бітрікс24 і не тільки
Бітрікс24 — це величезний комбайн, який поєднує і CRM, і документообіг, і облік і багато різних речей, які дуже подобаються менеджерам і не дуже подобаються IT персоналу. Портал використовують дуже багато невеликих та середніх компаній, у тому числі невеликі клініки, виробничники та навіть салони краси. Основною функцією, яку «люблять» менеджери є інтеграція телефонії та CRM, коли будь-який дзвінок відразу фіксується в CRM, створюються картки клієнта, при вхідному відображається інформація про клієнта і відразу видно, хто він такий, що йому можна продати і скільки він повинен. Але телефонія від Бітрікс24 та її інтеграція з CRM коштує грошей, іноді чималих. У статті я розповім досвід інтеграції з відкритими інструментами та популярною IP АТС Безкоштовна АТС, а також розгляну логіку роботи різних частин
Я працюю на аутсорсі в компанії, яка займається продажем та налаштуванням, інтеграцією IP телефонії. Коли мене запитали, чи можемо ми от тієї й ось цієї компанії запропонувати щось для інтеграції Бітрікс24 з АТС, які стоять у клієнтів, а також з віртуальними АТС на різних VDS компанії, я пішов до Google. І він мені звичайно видав посилання на статтю в хабрде є і опис, і 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. Але тільки не для asterіsk.
Всередині Asterisk є:
AGI — синхронний виклик зовнішніх програм/компонентів, що використовується в основному в діалплані, є бібліотеки типу phpagi, PAGI
AMI - текстовий TCP сокет, що працює за принципом підписки на події та введення текстових команд, нагадує SMTP зсередини, вміє відстежувати події та керувати викликами, є бібліотека ПАМІ - Найпопулярніша для створення зв'язку з Asterisk
Приклад виведення AMI
Event: Newchannel
Privilege: call,all
Channel: PJSIP/VMS_pjsip-0000078b
ChannelState: 4
ChannelStateDesc: Ring
CallerIDNum: 111222
CallerIDName: 111222
ConnectedLineNum:
ConnectedLineName:
Мова: en
AccountCode:
Context: from-pstn
Exten: s
Пріоритет: 1
Uniqueid: 1599589046.5244
Linkedid: 1599589046.5244
ARI - суміш того, іншого, все через REST, WebSocket, у форматі JSON - але ось зі свіжими бібліотеками та обгортками не дуже, на знижку знайшлися (phparia, phpari) які ставали у своєму розвитку 3 роки тому.
Зручність чи незручність, можливість чи неможливість роботи з тим чи іншим API визначаються завданнями, які потрібно вирішити. Завдання для інтеграції з CRM:
Відстежити початок виклику, куди його перевели, витягнути CallerID, DID, часи початку та кінця, можливо дані з директорії (для пошуку зв'язку телефону та користувача CRM)
Почати та закінчити запис дзвінка, зберегти у потрібному форматі, повідомити після закінчення запису де лежить файл
Ініціювати дзвінок за зовнішньою подією (з програми), зателефонувати внутрішньому номеру, зовнішньому та з'єднати їх
Опціонально: інтегрувати з CRM, групами додзвону та FollowME для автоматичного перекладу дзвінків за відсутності на місці (за інформацією CRM)
Всі ці завдання можна вирішити через AMI або ARI, але ARI надає набагато менше інформації, немає багатьох подій, не відстежуються багато змінних, які в AMI все-таки є (наприклад, виклики макросів, завдання змінних усередині макросів, у тому числі запис дзвінків). Тому, для правильного і точного відстеження - виберемо поки що AMI (але не остаточно). Крім того (ну а куди ж без цього, ми люди ліниві) - у вихідній роботі (стаття в хабр) використовують PAMI. *Потім треба спробувати переписати на ARI, але не факт, що вийде.
Вигадуємо інтеграцію заново
Для того, щоб наш FreePBX міг повідомляти в AMI простими способами про початок дзвінка, час закінчення, номери, імена записаних файлів, розраховувати тривалість дзвінка найпростіше скористатися тим же трюком, що і вихідні автори - ввести свої змінні і парсить висновок на їх присутність. PAMI пропонує це робити просто через функцію-фільтр.
Ось приклад завдання своєї змінної для початку дзвінка (s - це спеціальний номер в діалплані, який виконується ДО початку пошуку по DID)
Оскільки FreePBX перезаписує файли extention.conf та extention_additional.conf, ми будемо використовувати файл extention_виготовлений на замовлення.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 викликається
Функція запиту UserID, що відповідає внутрішньому номеру, куди надійшов дзвінок. А якщо це група додзвону? Питання політичне, чи треба створити дзвінок усім одразу (коли дзвонять усі одразу) чи створювати в міру обдзвону при почерговому дзвінку? У більшості клієнтів стоїть стратегія Fisrt Available, тому із цим немає проблем, дзвонить лише один. Але вирішувати питання треба
Функція реєстрації дзвінка в Битрикс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
Сервіс сам перезапускатиметься за потребою (при падіннях). Сервіс стеження за вхідними не вимагає установки веб-сервера, потрібен тільки 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 вашого сайту
Якщо nginx не запустився, значить щось не так з конфігурацією в папці nginx/ssl_docker.conf
Інші інтеграції
А чому б заразом не засунути кілька CRM у скрипти, подумали ми. Вивчили кілька API інших CRM, особливо безкоштовної вбудованої в деякі АТС - ShugarCRM та Vtiger, і так! можна, принцип той самий. Але це вже інша історія, яку потім заливатимемо на гітхаб окремо.