Comprendre FreePBX i integrar-lo amb Bitrix24 i més

Bitrix24 és una gran combinació que combina CRM, flux de treball, comptabilitat i moltes altres coses que els agrada molt als directius i al personal informàtic que no els agrada molt. El portal és utilitzat per moltes empreses petites i mitjanes, incloses petites clíniques, fabricants i fins i tot salons de bellesa. La funció principal que "estima" els gestors és la integració de la telefonia i el CRM, quan qualsevol trucada es registra immediatament al CRM, es creen targetes de client, quan s'entra, es mostra informació sobre el client i es pot veure immediatament qui és, què és. pot vendre i quant deu. Però la telefonia de Bitrix24 i la seva integració amb CRM costa diners, de vegades molt. A l'article us explicaré l'experiència d'integrar-se amb eines obertes i la popular IP PBX PBX gratuïta, i també considerar la lògica del treball de diverses parts

Treballo com a subcontractista en una empresa que ven i configura, integra telefonia IP. Quan em van preguntar si podíem oferir alguna cosa a aquesta i aquesta empresa per integrar Bitrix24 amb les centrals centrals que tenen els clients, així com amb les centrals virtuals de diverses empreses de VDS, vaig anar a Google. I, per descomptat, em va donar un enllaç article a l'habr, on hi ha una descripció i github, i sembla que tot funciona. Però quan es va intentar utilitzar aquesta solució, va resultar que Bitrix24 ja no és el mateix que abans i s'ha de refer molt. A més, FreePBX no és un asterisc nu per a vosaltres, aquí heu de pensar en com combinar la facilitat d'ús i un pla de marcatge dur als fitxers de configuració.

Estudiem la lògica del treball

Així, per començar, com hauria de funcionar tot. Quan es rep una trucada des de l'exterior a la PBX (esdeveniment SIP INVITE del proveïdor), s'inicia el processament del pla de marcació (pla de marcació, pla de marcació) - les regles de què i en quin ordre fer amb la trucada. Des del primer paquet, podeu obtenir molta informació, que després es pot utilitzar a les normes. Una excel·lent eina per estudiar els aspectes interns de SIP és l'analitzador sngrep (enllaç) que simplement s'instal·la en distribucions populars mitjançant apt install/yum install i similars, però també es pot crear des del codi font. Vegem el registre de trucades a sngrep

Comprendre FreePBX i integrar-lo amb Bitrix24 i més

De forma simplificada, el pla de marcació només tracta el primer paquet, de vegades també durant la conversa, es transfereixen trucades, prems de botons (DTMF), diverses coses interessants com FollowMe, RingGroup, IVR i altres.

Què hi ha dins del paquet d'invitacions

Comprendre FreePBX i integrar-lo amb Bitrix24 i més

De fet, la majoria de plans de marcació simples funcionen amb els dos primers camps, i tota la lògica gira al voltant de DID i CallerID. DID - on estem trucant, CallerID - qui truca.

Però al cap i a la fi, tenim una empresa i no un telèfon, la qual cosa significa que la central té molt probablement grups de trucades (trucada simultània/consecutiva de diversos dispositius) als números de ciutat (Grup de trucada), IVR (Hola, has trucat... Premeu). un per...), Contestadors automàtics (Frases), Condicions de temps, Reenviament a altres números o a una cel·la (FollowMe, Forward). Això vol dir que és molt difícil determinar sense ambigüitats qui rebrà realment una trucada i amb qui tindrà una conversa quan arribi una trucada. Aquí teniu un exemple de l'inici d'una trucada típica a la PBX dels nostres clients

Comprendre FreePBX i integrar-lo amb Bitrix24 i més

Després que la trucada entri amb èxit a la central, viatja pel pla de marcació en diferents "contexts". El context des del punt de vista d'Asterisk és un conjunt numerat d'ordres, cadascuna de les quals conté un filtre pel número marcat (s'anomena exten, per a una trucada externa a l'etapa inicial exten=DID). Les ordres de la línia de dialplan poden ser qualsevol cosa: funcions internes (per exemple, trucar a un abonat intern - Dial(), baixa el telèfon - Hangup()), operadors condicionals (IF, ELSE, ExecIF i similars), transicions a altres regles d'aquest context (Goto, GotoIF), transició a altres contextos en forma de trucada de funció (Gosub, Macro). Una directiva separada include имя_контекста, que afegeix ordres d'un altre context al final del context actual. Les ordres incloses mitjançant include sempre s'executen després ordres del context actual.

Tota la lògica de FreePBX es basa en la inclusió de diferents contextos entre si mitjançant incloure i cridar a través dels controladors Gosub, Macro i Handler. Considereu el context de les trucades entrants de FreePBX

Comprendre FreePBX i integrar-lo amb Bitrix24 i més

La trucada recorre tots els contextos de dalt a baix al seu torn, en cada context hi pot haver trucades a altres contextos com macros (Macro), funcions (Gosub) o simplement transicions (Goto), de manera que l'arbre real del que s'anomena només pot fer un seguiment als registres.

A continuació es mostra un diagrama de configuració típic d'un PBX típic. Quan es truca, es cerca DID a les rutes entrants, se'n comprova les condicions temporals, si tot està en ordre, s'obre el menú de veu. Des d'ell, prement el botó 1 o el temps d'espera, surt al grup d'operadors de marcatge. Un cop finalitza la trucada, es crida a la macro de trucada hangupcall, després de la qual no es pot fer res al pla de marcació, excepte per als controladors especials (controlador de hangup).

Comprendre FreePBX i integrar-lo amb Bitrix24 i més

On en aquest algorisme de trucada hem de proporcionar informació sobre l'inici de la trucada a CRM, on començar la gravació, on finalitzar la gravació i enviar-la juntament amb informació sobre la trucada a CRM?

Integració amb sistemes externs

Què és la integració de PBX i CRM? Es tracta de configuracions i programes que converteixen dades i esdeveniments entre aquestes dues plataformes i s'envien entre si. La manera més habitual de comunicar-se amb els sistemes independents és a través de les API, i la forma més popular d'accedir a les API és HTTP REST. Però no per asterisc.

Dins d'Asterisk hi ha:

  • AGI - trucada sincrònica de programes/components externs, utilitzat principalment en el pla de marcació, hi ha biblioteques com phpagi, PAGI

  • AMI: un sòcol TCP de text que funciona segons el principi de subscriure's a esdeveniments i introduir ordres de text, s'assembla a SMTP des de dins, pot fer un seguiment d'esdeveniments i gestionar trucades, hi ha una biblioteca PAMI - el més popular per crear una connexió amb Asterisk

Exemple de sortida AMI

Esdeveniment: canal nou
Privilegi: trucar, tots
Canal: PJSIP/VMS_pjsip-0000078b
Estat del canal: 4
ChannelStateDesc: sona
Identificador de trucada: 111222
Nom de l'ID de trucada: 111222
ConnectedLineNum:
nom de línia connectat:
Idioma: en
codi de compte:
Context: from-pstn
Extensió: s
Prioritat: 1
Únic: 1599589046.5244
Linkedid: 1599589046.5244

  • ARI és una barreja d'ambdós, tot a través de REST, WebSocket, en format JSON, però amb biblioteques i embolcalls nous, no molt bons, trobats a mà (phparia, phpari) que es van convertir en el seu desenvolupament fa uns 3 anys.

Exemple de sortida ARI quan s'inicia una trucada

{ "variable":"CallMeCallerIDName", "value":"111222", "type":"ChannelVarset", "timestamp":"2020-09-09T09:38:36.269+0000", "channel":{ "id »:»1599644315.5334″, «nom»:»PJSIP/VMSpjsip-000007b6″, "state":"Tron", "callador":{ "nom":"111222″, "número":"111222″ }, "connectat":{ "nom":"", "número" :"" }, "codi de compte":"", "dialplan":{ "context":"from-pstn", "exten":"s", "priority":2, "appname":"Estasi", "aplicaciódata":"hello-world" }, "creationtime":"2020-09-09T09:38:35.926+0000", "language":"ca" }, "asterisc"id":"48:5b:aa:aa:aa:aa", "aplicació":"hola món" }

La comoditat o inconvenient, la possibilitat o impossibilitat de treballar amb una API determinada estan determinats per les tasques que cal resoldre. Les tasques d'integració amb CRM són les següents:

  • Feu un seguiment de l'inici de la trucada, on es va transferir, extreu CallerID, DID, hores d'inici i finalització, potser dades del directori (per cercar una connexió entre el telèfon i l'usuari de CRM)

  • Iniciar i finalitzar l'enregistrament de la trucada, desar-la en el format desitjat, informar al final de l'enregistrament on es troba el fitxer

  • Inicieu una trucada en un esdeveniment extern (del programa), truqueu a un número intern, un número extern i connecteu-los

  • Opcional: integrar-se amb CRM, grups de marcadors i FollowME per a la transferència automàtica de trucades en absència de lloc (segons CRM)

Totes aquestes tasques es poden resoldre mitjançant AMI o ARI, però ARI proporciona molta menys informació, no hi ha molts esdeveniments, moltes variables que encara té AMI (per exemple, trucades de macro, establiment de variables dins de macros, inclosa la gravació de trucades) no es fan un seguiment. Per tant, per a un seguiment correcte i precís, triem ara AMI (però no completament). A més (bé, on seria sense això, som gent mandrosa) - a l'obra original (article a l'habr) utilitzar PAMI. *Aleshores heu de provar de reescriure a ARI, però no el fet que funcioni.

Reinventant la integració

Perquè la nostra FreePBX pugui informar a l'AMI de manera senzilla sobre l'inici de la trucada, l'hora de finalització, els números, els noms dels fitxers gravats, és més fàcil calcular la durada de la trucada utilitzant el mateix truc que els autors originals. - introduïu les vostres variables i analitzeu la sortida per determinar-ne la presència. PAMI suggereix fer-ho simplement mitjançant una funció de filtre.

Aquí teniu un exemple de configuració de la vostra pròpia variable per a l'hora d'inici de la trucada (s és un número especial al pla de marcatge que es realitza ABANS d'iniciar la cerca DID)

[ext-did-custom]

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

Un exemple d'esdeveniment AMI per a aquesta línia

Esdeveniment: canal nou

Privilegi: trucar, tots

Canal: PJSIP/VMS_pjsip-0000078b

Estat del canal: 4

ChannelStateDesc: sona

Identificador de trucada: 111222

Nom de l'ID de trucada: 111222

ConnectedLineNum:

nom de línia connectat:

Idioma: en

codi de compte:

Context: from-pstn

Extensió: s

Prioritat: 1

Únic: 1599589046.5244

Linkedid: 1599589046.5244

Aplicació: estableix AppData:

CallStart=1599571046

Perquè FreePBX sobreescriu els fitxers extention.conf i extention_addicional.conf, utilitzarem el fitxer extensió_costum.conf

Codi complet d'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

Característica i diferència amb el pla de marca original dels autors de l'article original -

  • Dialplan en format .conf, tal com vol FreePBX (sí, pot .ael, però no totes les versions i no sempre és convenient)

  • En lloc de processar el final mitjançant exten => h, el processament es va introduir mitjançant hangup_handler, perquè el pla de marcació FreePBX només funcionava amb ell

  • S'ha corregit la cadena de trucades de l'script, les cometes afegides i el número de trucada extern ExtNum

  • El processament es mou a contextos _personalitzats i us permet no tocar ni editar les configuracions de FreePBX - entrant mitjançant [ext-did-personalitzat], sortint per [sortint-allroutes-personalitzat]

  • Sense vinculació amb números: el fitxer és universal i només s'ha de configurar per a la ruta i l'enllaç al servidor

Per començar, també heu d'executar scripts a l'AMI mitjançant l'inici de sessió i la contrasenya; per això, FreePBX també té un fitxer _custom.

fitxer 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

Tots dos fitxers s'han de col·locar a /etc/asterisk i, a continuació, tornar a llegir les configuracions (o reiniciar l'asterisc)

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

Ara passem a PHP

Inicialització de scripts i creació d'un servei

Com que l'esquema per treballar amb Bitrix 24, un servei per a AMI, no és del tot simple i transparent, s'ha de parlar per separat. Asterisk, quan s'activa l'AMI, simplement obre el port i ja està. Quan un client s'uneix, sol·licita autorització, després el client es subscriu als esdeveniments necessaris. Els esdeveniments vénen en text sense format, que PAMI converteix en objectes estructurats i ofereix la possibilitat de configurar la funció de filtrat només per a esdeveniments d'interès, camps, números, etc.

Tan bon punt arriba la trucada, l'esdeveniment NewExten s'activa a partir del context pare [from-pstn] i, a continuació, tots els esdeveniments van en l'ordre de les línies dels contextos. Quan es rep informació de les variables CallMeCallerIDName i CallStart especificades al pla de marcatge _custom, el

  1. La funció de sol·licitar l'ID d'usuari corresponent al número d'extensió on va arribar la trucada. Què passa si es tracta d'un grup telefònic? La pregunta és política, cal crear una trucada a tothom alhora (quan tots truquen alhora) o crear com criden quan truquen al seu torn? La majoria de clients tenen l'estratègia First Available, així que no hi ha cap problema amb això, només un truca. Però el problema s'ha de resoldre.

  2. La funció de registre de trucades a Bitrix24, que retorna el CallID, que després es requereix per informar dels paràmetres de trucada i un enllaç a l'enregistrament. Requereix el número d'extensió o l'ID d'usuari

Comprendre FreePBX i integrar-lo amb Bitrix24 i més

Un cop finalitzada la trucada, es crida a la funció de descàrrega del registre, que informa simultàniament de l'estat de finalització de la trucada (ocupat, sense resposta, èxit) i també baixa un enllaç al fitxer mp3 amb el registre (si n'hi ha).

Com que el mòdul CallMeIn.php s'ha d'executar contínuament, s'hi ha creat un fitxer d'inici de SystemD trucame.servei, que s'ha de posar a /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

La inicialització i el llançament de l'script es produeixen mitjançant systemctl o servei

# systemctl enable callme
# systemctl start callme

El servei es reiniciarà segons sigui necessari (en cas d'error). El servei de seguiment de la safata d'entrada no requereix la instal·lació d'un servidor web, només cal php (que definitivament es troba al servidor FeePBX). Però a falta d'accés als registres de trucades a través del servidor web (també amb https), no serà possible escoltar els registres de trucades.

Ara parlem de les trucades sortints. L'script CallMeOut.php té dues funcions:

  • Iniciació d'una trucada quan es rep una sol·licitud per a un script php (incloent-hi l'ús del botó "Call" al mateix Bitrix). No funciona sense un servidor web, la sol·licitud es rep mitjançant HTTP POST, la sol·licitud conté un testimoni

  • Missatge sobre la trucada, els seus paràmetres i registres a Bitrix. L'Asterisk l'ha disparat al pla de marcatge [sub-trucada-acabada interna] quan acaba una trucada

Comprendre FreePBX i integrar-lo amb Bitrix24 i més

El servidor web només es necessita per a dues coses: descarregar fitxers de registre Bitrix (mitjançant HTTPS) i cridar l'script CallMeOut.php. Podeu utilitzar el servidor FreePBX integrat, els fitxers del qual són /var/www/html, podeu instal·lar un altre servidor o especificar un camí diferent.

Servidor web

Deixem la configuració del servidor web per a un estudi independent (tyts, tyts, tyts). Si no teniu un domini, podeu provar FreeDomain( https://www.freenom.com/ru/index.html), que us donarà un nom gratuït per a la vostra IP blanca (no us oblideu de reenviar els ports 80, 443 a través de l'encaminador si només hi ha l'adreça externa). Si acabeu de crear un domini DNS, haureu d'esperar (de 15 minuts a 48 hores) fins que es carreguin tots els servidors. Segons l'experiència de treballar amb proveïdors nacionals, d'1 hora a un dia.

Automatització de la instal·lació

S'ha desenvolupat un instal·lador a github per facilitar encara més la instal·lació. Però va ser suau sobre el paper, mentre ho estem instal·lant tot manualment, ja que després de retocar amb tot això va quedar clar què és amics amb qui, qui va a on i com depurar-ho. Encara no hi ha instal·lador

estibador

Si voleu provar ràpidament la solució (hi ha una opció amb Docker), creeu ràpidament un contenidor, doneu-li ports a l'exterior, feu lliscar els fitxers de configuració i proveu (aquesta és l'opció amb el contenidor LetsEncrypt, si ja teniu un certificat, només cal redirigir el servidor intermediari invers al servidor web FreePBX (li vam donar un altre port és 88), LetsEncrypt a Docker basat en aquest article

Heu d'executar el fitxer a la carpeta del projecte descarregat (després del clon de git), però primer entreu a les configuracions d'asterisc (carpeta d'asterisc) i escriviu els camins als registres i l'URL del vostre lloc allà.

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:

Aquest fitxer docker-compose.yaml s'executa mitjançant

docker-compose up -d

Si nginx no s'inicia, hi ha alguna cosa malament amb la configuració a la carpeta nginx/ssl_docker.conf

Altres integracions

I per què no posar una mica de CRM als scripts al mateix temps, vam pensar. Hem estudiat diverses altres API de CRM, especialment la PBX integrada gratuïta: ShugarCRM i Vtiger, i sí! sí, el principi és el mateix. Però aquesta és una altra història, que més tard penjarem al github per separat.

Referències

Exempció de responsabilitat: qualsevol semblança amb la realitat és fictícia i no vaig ser jo.

Font: www.habr.com

Afegeix comentari