Розбираємось з 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

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

У спрощеному вигляді діалплан займається лише першим пакетом, іноді також у процесі розмови відбувається переклад дзвінків, натискання кнопок (DTMF), різні цікавості типу FollowMe, RingGroup, IVR та інше.

Що всередині Invite пакету

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

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

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

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

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

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

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

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

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

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

Де в цьому алгоритмі дзвінка ми повинні постачати інформацію про початок дзвінка в 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 роки тому.

Приклад виведення 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:

Мова: en

AccountCode:

Context: from-pstn

Exten: s

Пріоритет: 1

Uniqueid: 1599589046.5244

Linkedid: 1599589046.5244

Application: Set AppData:

CallStart=1599571046

Оскільки 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 викликається

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

  2. Функція реєстрації дзвінка в Битрикс24, яка повертає CallID, необхідний потім для повідомлення про параметри дзвінка та посилання на запис. Потрібний або внутрішній номер або UserID

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

Після закінчення дзвінка викликається функція завантаження запису, яка одночасно повідомляє статус завершення дзвінка (Зайнятий, Ні відповіді, Успіх), а також завантажує посилання на 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] після закінчення дзвінка

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

Веб сервер потрібен тільки для двох речей - завантаження файлів записів бітриксом (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, і так! можна, принцип той самий. Але це вже інша історія, яку потім заливатимемо на гітхаб окремо.

Посилання

Дисклеймер: будь-які збіги з реальністю вигадані і це був не я,

Джерело: habr.com

Додати коментар або відгук