Understanding FreePBX and integrating it with Bitrix24 and more

Bitrix24 is a huge combine that combines CRM, workflow, accounting and many other things that managers really like and IT staff don’t really like. The portal is used by a lot of small and medium-sized companies, including small clinics, manufacturers and even beauty salons. The main function that managers “love” is the integration of telephony and CRM, when any call is immediately recorded in CRM, client cards are created, when incoming, information about the client is displayed and you can immediately see who he is, what he can sell and how much he owes. But telephony from Bitrix24 and its integration with CRM costs money, sometimes a lot. In the article I will tell you the experience of integrating with open tools and the popular IP PBX freepbx, and also consider the logic of the work of various parts

I work as an outsourcer in a company that sells and configures, integrates IP telephony. When I was asked if we could offer something to this and this company to integrate Bitrix24 with PBXs that customers have, as well as with virtual PBXs on various VDS companies, I went to Google. And of course he gave me a link to article in habr, where there is a description, and github, and everything seems to work. But when trying to use this solution, it turned out that Bitrix24 is no longer the same as before, and much needs to be redone. In addition, FreePBX is not a bare asterisk for you, here you need to think about how to combine ease of use and a hardcore dialplan in config files.

We study the logic of work

So for starters, how it should all work. When a call is received from outside the PBX (SIP INVITE event from the provider), the processing of the dialplan (dial plan, dialplan) begins - the rules of what and in what order to do with the call. From the first packet, you can get a lot of information, which can then be used in the rules. An excellent tool for studying the internals of SIP is the analyzer sngrep ( link) which is simply installed in popular distributions via apt install/yum install and the like, but it can also be built from source. Let's look at the call log in sngrep

Understanding FreePBX and integrating it with Bitrix24 and more

In a simplified form, the dialplan deals only with the first packet, sometimes also during the conversation, calls are transferred, button presses (DTMF), various interesting things like FollowMe, RingGroup, IVR and others.

What's inside the Invite Pack

Understanding FreePBX and integrating it with Bitrix24 and more

Actually, most simple dialplans work with the first two fields, and the whole logic revolves around DID and CallerID. DID - where we are calling, CallerID - who is calling.

But after all, we have a company and not one phone - which means that the PBX most likely has call groups (simultaneous / consecutive ringing of several devices) on city numbers (Ring Group), IVR (Hello, you called ... Press one for ...), Answering machines ( Phrases), Time Conditions, Forwarding to other numbers or to a cell (FollowMe, Forward). This means that it is very difficult to unambiguously determine who will actually receive a call and who will have a conversation with when a call arrives. Here is an example of the beginning of a typical call in the PBX of our clients

Understanding FreePBX and integrating it with Bitrix24 and more

After the call successfully enters the PBX, it travels through the dialplan in different "contexts". The context from the point of view of Asterisk is a numbered set of commands, each of which contains a filter by the dialed number (it is called exten, for an external call at the initial stage exten=DID). The commands in the dialplan line can be anything - internal functions (for example, call an internal subscriber - Dial(), put the phone down - Hangup()), conditional operators (IF, ELSE, ExecIF and the like), transitions to other rules of this context (Goto, GotoIF), transition to other contexts in the form of a function call (Gosub, Macro). A separate directive include имя_контекста, which adds commands from another context to the end of the current context. Commands included via include are always executed after commands of the current context.

The whole logic of FreePBX is built on the inclusion of different contexts into each other through include and call through Gosub, Macro and Handler handlers. Consider the context of incoming FreePBX calls

Understanding FreePBX and integrating it with Bitrix24 and more

The call goes through all contexts from top to bottom in turn, in each context there can be calls to other contexts like macros (Macro), functions (Gosub) or just transitions (Goto), so the real tree of what is called can only be tracked in the logs.

A typical setup diagram for a typical PBX is shown below. When calling, DID is searched in incoming routes, temporary conditions are checked for it, if everything is in order, the voice menu is launched. From it, by pressing button 1 or timeout, exit to the group of dialing operators. After the call ends, the hangupcall macro is called, after which nothing can be done in the dialplan, except for special handlers (hangup handler).

Understanding FreePBX and integrating it with Bitrix24 and more

Where in this call algorithm should we supply information about the beginning of the call to CRM, where to start recording, where to end the recording and send it along with information about the call to CRM?

Integration with external systems

What is PBX and CRM integration? These are settings and programs that convert data and events between these two platforms and send them to each other. The most common way for independent systems to communicate is through APIs, and the most popular way to access APIs is HTTP REST. But not for asterisk.

Inside Asterisk is:

  • AGI - synchronous call to external programs / components, used mainly in the dialplan, there are libraries like phpagi, MORNING

  • AMI - a text TCP socket that works on the principle of subscribing to events and entering text commands, resembles SMTP from the inside, can track events and manage calls, there is a library PAMI - the most popular for creating a connection with Asterisk

AMI output example

Event: New channel
Privilege: call, all
Channel: PJSIP/VMS_pjsip-0000078b
Channel State: 4
ChannelStateDesc: Ring
CallerIDNum: 111222
CallerIDName: 111222
ConnectedLineNum:
connected linename:
Language: en
account code:
Context: from-pstn
Exten: s
Priority: 1
Uniqueid: 1599589046.5244
Linkedid: 1599589046.5244

  • ARI is a mixture of both, all via REST, WebSocket, in JSON format - but with fresh libraries and wrappers, not very good, offhand found (phparia, phpari) which became in their development about 3 years ago.

Example of ARI output when a call is initiated

{ "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":"en" }, "asteriskid":"48:5b:aa:aa:aa:aa", "application":"hello-world" }

Convenience or inconvenience, the possibility or impossibility of working with a particular API are determined by the tasks that need to be solved. The tasks for integration with CRM are as follows:

  • Track the beginning of the call, where it was transferred, pull out CallerID, DID, start and end times, maybe data from the directory (to search for a connection between the phone and the CRM user)

  • Start and end the recording of the call, save it in the desired format, inform at the end of the recording where the file is located

  • Initiate a call on an external event (from the program), call an internal number, an external number and connect them

  • Optional: integrate with CRM, dialer groups and FollowME for automatic transfer of calls in the absence of a place (according to CRM)

All these tasks can be solved through AMI or ARI, but ARI provides much less information, there are not many events, many variables that AMI still has (for example, macro calls, setting variables inside macros, including call recording) are not tracked. Therefore, for correct and accurate tracking, let's choose AMI for now (but not completely). In addition (well, where would it be without this, we are lazy people) - in the original work (article in habr) use PAMI. *Then you need to try to rewrite to ARI, but not the fact that it will work.

Reinventing integration

In order for our FreePBX to be able to report to AMI in simple ways about the beginning of the call, end time, numbers, names of recorded files, it is easiest to calculate the duration of the call using the same trick as the original authors - enter your variables and parse the output for their presence. PAMI suggests doing this simply through a filter function.

Here is an example of setting your own variable for the start time of the call (s is a special number in the dialplan that is performed BEFORE starting the DID search)

[ext-did-custom]

exten => s,1,Set(CallStart=${STRFTIME(epoch,,%s)})

An example AMI event for this line

Event: New channel

Privilege: call, all

Channel: PJSIP/VMS_pjsip-0000078b

Channel State: 4

ChannelStateDesc: Ring

CallerIDNum: 111222

CallerIDName: 111222

ConnectedLineNum:

connected linename:

Language: en

account code:

Context: from-pstn

Exten: s

Priority: 1

Uniqueid: 1599589046.5244

Linkedid: 1599589046.5244

Application: Set AppData:

CallStart=1599571046

Because FreePBX overwrites the extention.conf and extention_ filesadditional.conf, we will use the file extension_custom.conf

Full code of 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

Feature and difference from the original dialplan of the authors of the original article -

  • Dialplan in .conf format, as FreePBX wants it (yes, it can .ael, but not all versions and it's not always convenient)

  • Instead of processing the end through exten=>h, processing was introduced through hangup_handler, because the FreePBX dialplan worked only with it

  • Fixed script call string, added quotes and external call number ExtNum

  • Processing is moved to _custom contexts and allows you not to touch or edit FreePBX configs - incoming via [ext-did-custom], outgoing through [outbound-allroutes-custom]

  • No binding to numbers - the file is universal and only needs to be configured for the path and link to the server

To get started, you also need to run scripts in AMI by login and password - for this, FreePBX also has a _custom file

manager_custom.conf file

;;  это логин
[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

Both of these files must be placed in /etc/asterisk, then re-read the configs (or restart the asterisk)

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

Now let's move on to PHP

Initializing scripts and creating a service

Since the scheme for working with Bitrix 24, a service for AMI, is not entirely simple and transparent, it must be discussed separately. Asterisk, when AMI is activated, simply opens the port and that's it. When a client joins, it requests authorization, then the client subscribes to the necessary events. Events come in plain text, which PAMI converts into structured objects and provides the ability to set the filtering function only for events of interest, fields, numbers, etc.

As soon as the call comes in, the NewExten event is fired starting from the parent [from-pstn] context, then all events go in the order of the lines in the contexts. When information is received from the CallMeCallerIDName and CallStart variables specified in the _custom dialplan, the

  1. The function of requesting the UserID corresponding to the extension number where the call came. What if it's a dial-up group? The question is political, do you need to create a call to everyone at once (when everyone calls at once) or create as they call when calling in turn? Most clients have the Fisrt Available strategy, so there is no problem with this, only one calls. But the issue needs to be resolved.

  2. The call registration function in Bitrix24, which returns the CallID, which is then required to report the call parameters and a link to the recording. Requires either extension number or UserID

Understanding FreePBX and integrating it with Bitrix24 and more

After the end of the call, the record download function is called, which simultaneously reports the status of the call completion (Busy, No answer, Success), and also downloads a link to the mp3 file with the record (if any).

Because the CallMeIn.php module needs to run continuously, a SystemD startup file has been created for it callme.service, which must be put in /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

initialization and launch of the script occurs through systemctl or service

# systemctl enable callme
# systemctl start callme

The service will restart itself as needed (in case of crashes). The inbox tracking service does not require a web server to be installed, only php is needed (which is definitely on the FeePBX server). But in the absence of access to call records through the Web server (also with https), it will not be possible to listen to call records.

Now let's talk about outgoing calls. The CallMeOut.php script has two functions:

  • Initiation of a call when a request is received for a php script (including using the "Call" button in the Bitrix itself). It does not work without a web server, the request is received via HTTP POST, the request contains a token

  • Message about the call, its parameters and records in Bitrix. Fired by Asterisk in the [sub-call-internal-ended] dialplan when a call ends

Understanding FreePBX and integrating it with Bitrix24 and more

The web server is needed only for two things - downloading Bitrix record files (via HTTPS) and calling the CallMeOut.php script. You can use the built-in FreePBX server, the files for which are /var/www/html, you can install another server or specify a different path.

Web server

Let's leave the web server setup for independent study (tyts, tyts, tyts). If you don't have a domain, you can try FreeDomain( https://www.freenom.com/ru/index.html), which will give you a free name for your white IP (do not forget to forward ports 80, 443 through the router if the external address is only on it). If you just created a DNS domain, then you have to wait (from 15 minutes to 48 hours) until all servers are loaded. According to the experience of working with domestic providers - from 1 hour to a day.

Installation automation

An installer has been developed on github to make installation even easier. But it was smooth on paper - while we are installing it all manually, since after tinkering with all this it became crystal clear what is friends with whom, who goes where and how to debug it. There is no installer yet

Docker

If you want to quickly try the solution - there is an option with Docker - quickly create a container, give it ports to the outside, slip the settings files and try (this is the option with the LetsEncrypt container, if you already have a certificate, you just need to redirect the reverse proxy to the FreePBX web server (we gave it another port is 88), LetsEncrypt in docker based on this article

You need to run the file in the downloaded project folder (after git clone), but first get into the asterisk configs (asterisk folder) and write the paths to the records and the URL of your site there

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:

This docker-compose.yaml file is run via

docker-compose up -d

If nginx does not start, then something is wrong with the configuration in the nginx/ssl_docker.conf folder

Other integrations

And why not put some CRM into scripts at the same time, we thought. We studied several other CRM APIs, especially the free built-in PBX - ShugarCRM and Vtiger, and yes! yes, the principle is the same. But this is another story, which we will later upload to the github separately.

references

Disclaimer: Any resemblance to reality is fictitious and it wasn't me.

Source: habr.com

Add a comment