Bitrix24 es un gran recolector que combina CRM, flujo de documentos, contabilidad y muchas otras cosas que a los gerentes realmente les gustan y al personal de TI no les gusta mucho. El portal es utilizado por muchas empresas pequeñas y medianas, incluidas pequeñas clínicas, trabajadores de fabricación e incluso salones de belleza. La función principal que los gerentes "adoran" es la integración de telefonía y CRM, cuando cualquier llamada se registra de inmediato en CRM, se crean tarjetas de cliente, se muestra información sobre el cliente cuando entra, y se puede ver de inmediato quién es, qué puede vender y cuánto debe. Pero la telefonía de Bitrix24 y su integración con CRM cuesta dinero, a veces mucho. En el artículo te contaré la experiencia de integración con herramientas abiertas y la popular IP PBX FreePBX , y también consideraré la lógica del trabajo de varias partes.
Trabajo como empresa de outsourcing en una empresa que vende y configura, integra telefonía IP. Cuando me preguntaron si podíamos ofrecer a esta y esta empresa algo para integrar Bitrix24 con las PBX que tienen los clientes, así como con las PBX virtuales en varias empresas de VDS, fui a Google. Y él, por supuesto, me dio un enlace a un artículo en Habr , donde hay una descripción y github, y todo parece funcionar. Pero al intentar usar esta solución, resultó que Bitrix24 no es lo que solía ser, y hay mucho que rehacer. Además, FreePBX no es un asterisco desnudo para usted, aquí debe pensar en cómo combinar la usabilidad y un plan de marcado duro en los archivos de configuración.
Estudiamos la lógica del trabajo
Entonces, primero, ¿cómo se supone que funcione? Cuando llega una llamada desde el exterior al PBX (evento SIP INVITE del proveedor), comienza el procesamiento del plan de marcado (plan de marcado): las reglas de qué hacer con la llamada y en qué orden. Se puede obtener mucha información del primer paquete, que luego se puede utilizar en las reglas. Una excelente herramienta para estudiar el interior de SIP es el analizador sngrep ( enlace ), que simplemente se instala en distribuciones populares a través de apt install / yum install y similares, pero también puede compilarlo desde la fuente. Veamos el registro de llamadas en sngrep
En una forma simplificada, el plan de marcación se ocupa solo del primer paquete, a veces durante la conversación, se transfieren llamadas, se presionan botones (DTMF), varias cosas interesantes como FollowMe, RingGroup, IVR y otros.
Que hay dentro del paquete 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 :
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
{ "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
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
# 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, ! , . , .
: ,