è un'enorme piattaforma che combina CRM, gestione documentale, contabilità e molte altre funzionalità che sono molto apprezzate dai manager, ma non sempre dal personale IT. Viene utilizzata da molte piccole e medie imprese, comprese piccole cliniche, produttori e persino saloni di bellezza. La funzione principale che i manager adorano è l'integrazione della telefonia con il CRM, dove ogni chiamata viene registrata nel CRM, vengono create schede cliente, e quando arriva una chiamata, appare subito l'informazione sul cliente, così è immediato capire chi è, cosa può interessargli e quanto deve. Tuttavia, la telefonia di Bitrix24 e la sua integrazione con il CRM comportano costi, a volte non trascurabili. In questo articolo condividerò l'esperienza di integrazione con strumenti open source e una popolare centrale telefonica IP. , e analizzerò inoltre la logica di funzionamento delle diverse parti
Lavoro per un'azienda che si occupa della vendita, configurazione e integrazione della telefonia IP. Quando mi hanno chiesto se potessimo proporre qualcosa per integrare Bitrix24 con i centralini già installati dai clienti, così come con centralini virtuali su diversi VDS dell'azienda, sono andato su Google. E ovviamente mi ha restituito un link a , dove ci sono descrizioni, link a GitHub e sembra che funzioni tutto. Ma nel tentativo di utilizzare questa soluzione, è emerso che Bitrix24 non è più quello di una volta e che molte cose devono essere rifatte. Inoltre, FreePBX non è solo un semplice Asterisk, qui bisogna pensare a come combinare la facilità d'uso con un dialplan hardcore nei file di configurazione.
Studiamo la logica di funzionamento
Quindi, per iniziare, come dovrebbe funzionare tutto questo. Quando arriva una chiamata esterna al centralino (evento SIP INVITE dal fornitore), inizia l'elaborazione del dialplan — le regole su cosa e in quale ordine fare con la chiamata. Dal primo pacchetto si possono ottenere molte informazioni da utilizzare poi nelle regole. Un ottimo strumento per esplorare i dettagli del SIP è l'analizzatore sngrep () che si installa facilmente nelle distribuzioni popolari tramite apt install/yum install e simili, ma è possibile anche compilarlo dai sorgenti. Diamo un'occhiata al log delle chiamate in sngrep

In forma semplificata, il dialplan si occupa solo del primo pacchetto; a volte, durante la chiamata, vengono gestiti anche il trasferimento delle chiamate, la pressione dei tasti (DTMF) e varie altre funzionalità come FollowMe, RingGroup, IVR e altro.
Cosa c'è all'interno del pacchetto Invite

In effetti, la maggior parte dei dialplan semplici lavora con i primi due campi e tutta la logica ruota attorno a DID e CallerID. DID è la destinazione della chiamata, mentre CallerID è l'identità di chi chiama.
Ma noi abbiamo un'azienda, non solo un telefono — quindi nella nostra centralina telefonica ci saranno probabilmente gruppi di chiamata (chiamate simultanee/sequenziali di più telefoni) su numeri urbani (Ring Group), IVR (Benvenuto, ha chiamato… Premere uno per…), Risponditori automatici (Phrases), Condizioni temporali (Time Conditions), Inoltro verso altri numeri o telefoni cellulari (FollowMe, Forward). Questo rende molto difficile determinare con certezza a chi arriverà realmente la chiamata e con chi si avrà conversazione al momento della chiamata. Ecco un esempio dell'inizio del percorso di una chiamata tipica nella centralina dei nostri clienti.

Dopo che la chiamata è stata effettuata con successo, inizia il suo viaggio attraverso il piano di chiamata in diversi "contesti". Un contesto, dal punto di vista di Asterisk, è un insieme numerato di comandi, ciascuno dei quali contiene un filtro per il numero composto (chiamato exten, per la chiamata esterna nella fase iniziale exten=DID). I comandi nel piano di chiamata possono essere di qualsiasi tipo: funzioni interne (ad esempio, chiamare un interno — Dial(), riagganciare — Hangup()), operatori condizionali (IF, ELSE, ExecIF e simili), salti ad altre regole di questo contesto (Goto, GotoIF), passaggi ad altri contesti in forma di chiamate a funzioni (Gosub, Macro). È importante la direttiva include nome_contesto, che aggiunge i comandi di un altro contesto alla fine del contesto corrente. I comandi inclusi tramite include vengono sempre eseguiti dopo alla fine dei comandi del contesto corrente.
Tutta la logica di funzionamento di FreePBX si basa sull'inclusione reciproca di diversi contesti tramite include e chiamate tramite Gosub, Macro e gestori Handler. Esaminiamo il contesto delle chiamate in entrata di FreePBX.

La chiamata avviene in tutti i contesti dall'alto verso il basso in sequenza. In ogni contesto possono esserci chiamate ad altri contesti come macro (Macro), funzioni (Gosub) o semplici salti (Goto). Pertanto, l'albero reale di ciò che viene chiamato può essere tracciato solo nei log.
Lo schema tipico di configurazione di un'Audioconferenza per ufficio è mostrato di seguito. Durante le chiamate, si cerca il DID nei percorsi in ingresso; vengono controllate le condizioni temporali e, se tutto è a posto, si avvia il menu vocale. Da esso, premendo il tasto 1 o scaduto il timeout, si accede al gruppo di operatori. Alla fine della chiamata viene attivato il macro hangupcall, dopo il quale non sarà più possibile eseguire nulla nel piano di chiamata, tranne i gestori speciali (hangup handler).

Dove in questo algoritmo di chiamata dobbiamo fornire informazioni sull'inizio della chiamata al CRM, dove iniziare la registrazione, dove terminare la registrazione e inviarla insieme alle informazioni sulla chiamata al CRM?
Integrazione con sistemi esterni
Che cos'è l'integrazione tra un centrale telefonica (PBX) e un CRM? Si tratta di impostazioni e software che convertono dati ed eventi tra queste due piattaforme e li scambiano tra loro. Il modo più comune di interazione tra sistemi indipendenti è tramite API, mentre il metodo di accesso più popolare alle API è HTTP REST. Ma non per Asterisk.
All'interno di Asterisk ci sono:
AGI — chiamata sincrona a programmi/componenti esterni, utilizzata principalmente nel piano di chiamata, esistono librerie come ,
AMI — socket TCP testuale che funziona su base di sottoscrizione a eventi e invio di comandi testuali, ricorda SMTP dall'interno, è in grado di monitorare eventi e gestire chiamate, esiste la libreria — la più popolare per creare connessioni con Asterisk
Esempio di output AMI
Evento: Nuovo canale
Privilegio: call,all
Canale: PJSIP/VMS_pjsip-0000078b
StatoCanale: 4
DescrizioneStatoCanale: Squillo
NumeroIDChiamante: 111222
NomeIDChiamante: 111222
NumeroLineaCollegata:
NomeLineaCollegata:
Lingua: en
CodiceAccount:
Contesto: from-pstn
Exten: s
Priorità: 1
Uniqueid: 1599589046.5244
Linkedid: 1599589046.5244
ARI — una combinazione di entrambi, tutto attraverso REST, WebSocket, in formato JSON — ma con le librerie e le astrazioni più recenti non va molto bene, a colpo d'occhio ne ho trovate (, ) che si sono sviluppate circa 3 anni fa.
Esempio di output ARI durante l'inizio di una chiamata
{ «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»:»it» }, «asteriskid»:»48:5b:aa:aa:aa:aa», «application»:»hello-world» }
La comodità o l'inconveniente, la possibilità o l'impossibilità di lavorare con un determinato API dipendono dai compiti che devono essere risolti. I compiti per l'integrazione con CRM sono i seguenti:
Monitorare l'inizio della chiamata, dove è stata reindirizzata, estrarre CallerID, DID, orari di inizio e fine, e possibilmente dati dalla rubrica (per cercare il legame tra il telefono e l'utente CRM)
Avviare e concludere la registrazione della chiamata, salvarla nel formato richiesto e comunicare al termine della registrazione dove si trova il file
Iniziare una chiamata tramite un evento esterno (da un programma), chiamare un numero interno, esterno e connette entrambi
Opzionale: integrare con CRM, gruppi di chiamata e FollowME per la trasmissione automatica delle chiamate in assenza (in base alle informazioni del CRM)
Tutte queste operazioni possono essere eseguite tramite AMI o ARI, ma ARI fornisce molte meno informazioni, mancano molti eventi e molte variabili non vengono monitorate, mentre in AMI ci sono (ad esempio le chiamate ai macro, l'assegnazione delle variabili all'interno dei macro, inclusa la registrazione delle chiamate). Pertanto, per un monitoraggio corretto e preciso, scegliamo per ora AMI (ma non in modo definitivo). Inoltre (ma dove andremmo senza questo, siamo esseri umani pigri) — nel lavoro iniziale () usano PAMI.*Poi dobbiamo provare a riscrivere su ARI, ma non è certo che ci riusciremo.
Riprogettiamo l'integrazione
Per consentire al nostro FreePBX di comunicare in AMI in modo semplice l'inizio della chiamata, il tempo di termine, i numeri, i nomi dei file registrati, e calcolare la durata della chiamata, è più semplice utilizzare lo stesso trucco degli autori originali: introdurre le proprie variabili e analizzare l'output per verificarne la presenza. PAMI offre di fare questo facilmente tramite una funzione filtro.
Ecco un esempio di impostazione della propria variabile per il tempo di inizio della chiamata (s è un numero speciale nel piano di chiamata, che viene eseguito PRIMA di iniziare la ricerca per DID)
[ext-did-custom]
exten => s,1,Set(CallStart=${STRFTIME(epoch,,%s)})Esempio di evento AMI per questa riga
Evento: Nuovo canale
Privilegio: call,all
Canale: PJSIP/VMS_pjsip-0000078b
StatoCanale: 4
DescrizioneStatoCanale: Squillo
NumeroIDChiamante: 111222
NomeIDChiamante: 111222
NumeroLineaCollegata:
NomeLineaCollegata:
Lingua: en
CodiceAccount:
Contesto: from-pstn
Exten: s
Priorità: 1
Uniqueid: 1599589046.5244
Linkedid: 1599589046.5244
Applicazione: Imposta AppData:
CallStart=1599571046
Poiché FreePBX sovrascrive i file extention.conf e extention_additional.conf, useremo il file extention_custom.conf
Codice completo di extention_custom.conf
[globals]
;; Controlla i percorsi e i diritti delle cartelle - l'utente asterisk deve avere diritti di scrittura
;; Qui verranno registrate le conversazioni
WAV=/var/www/html/callme/records/wav
MP3=/var/www/html/callme/records/mp3
;; Le registrazioni verranno riprodotte e scaricate da questi percorsi
URLRECORDS=https://www.host.ru/callmeplus/records/mp3
;; Indirizzo per il callback durante la chiamata in uscita
URLPHP=https://www.host.ru/callmeplus
;; Sì, registriamo le conversazioni
RECORDING=1
;; Questo è un macro per registrare le conversazioni nella nostra cartella.
;; Può essere utilizzata anche la registrazione di sistema, ma per ora lasciamo questa -
;; funziona
[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()
;; Questo è il contesto principale per iniziare la conversazione
[ext-did-custom]
;; È un po' un trucco fare così qui, ma funziona - aggiungiamo '8' al numero
nexten => s,1,Set(CALLERID(num)=8${CALLERID(num)})
;; Qui ci sono varie variabili per lo script
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)})
;; La cosa principale! Gestore della fine della conversazione.
;; I normali percorsi di gestione della fine tramite (exten=>h,1,qualcosaqui) in FreePBX non funzionano - Macro(hangupcall,) rovina tutto.
;; Quindi attacchiamo Hangup_Handler alla fine della chiamata
exten => s,n,Set(CHANNEL(hangup_handler_push)=sub-call-from-cid-ended,s,1(${CALLERID(num)},${EXTEN}))
;; Gestore della fine della chiamata in entrata
[sub-call-from-cid-ended]
;; Comunichiamo i valori alla fine della chiamata
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)})
;; Stato della chiamata - Risposta, non risposta...
exten => s,n,Set(CallMeDISPOSITION=${CDR(disposition)})
exten => s,n,Return
;; Gestore delle chiamate in uscita - tutto analogamente
[outbound-allroutes-custom]
;; Registrazione
exten => _.,1,Gosub(recording,~~s~~,1(${CALLERID(number)},${EXTEN}))
;; Variabili
exten => _.,n,Set(__CallIntNum=${CALLERID(num)})
exten => _.,n,Set(CallExtNum=${EXTEN})
exten => _.,n,Set(CallStart=${STRFTIME(epoch,,%s)})
exten => _.,n,Set(CallmeCALLID=${SIPCALLID})
;; Attacchiamo Hangup_Handler alla fine della chiamata
exten => _.,n,Set(CHANNEL(hangup_handler_push)=sub-call-internal-ended,s,1(${CALLERID(num)},${EXTEN}))
;; Gestore della fine della chiamata in uscita
[sub-call-internal-ended]
;; variabili
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)})
;; Chiamata allo script che notificherà della chiamata nel CRM - questo è in uscita,
;; quindi al termine effettivo
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,ReturnLa caratteristica e la differenza rispetto al piano dialettale originale degli autori dell'articolo sorgente —
Piano dialettale in formato .conf, come desidera FreePBX (sì, supporta anche .ael, ma non tutte le versioni e non sempre è conveniente)
Invece di gestire la terminazione tramite exten=>h, è stata introdotta la gestione attraverso hangup_handler, perché il piano dialettale di FreePBX ha funzionato solo con questo
Corretta la stringa di chiamata allo script, aggiunte le virgolette e il numero esterno di chiamata ExtNum
La gestione è stata spostata nei contesti _custom ed evita di modificare i file di configurazione di FreePBX — in entrata attraverso [ext-did-custom], in uscita attraverso [outbound-allroutes-custom]
Nessun legame con i numeri — il file è universale e necessita solo di configurazione per il percorso e il collegamento al server
Per iniziare a lavorare, è inoltre necessario accedere agli script in AMI con login e password — anche in FreePBX esiste un file _custom
File manager_custom.conf
;; questo è il login
[callmeplus]
;; questa è la password
secret = trampampamturlala
deny = 0.0.0.0/0.0.0.0
;; lavoro con una macchina locale - ma se necessario, posso scrivere anche altri
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,originateQuesti due file devono essere posizionati in /etc/asterisk, quindi rileggere le configurazioni (o riavviare Asterisk)
# astrisk -rv
Connected to Asterisk 16.6.2 currently running on freepbx (pid = 31629)
#freepbx*CLI> dialplan reload
Dialplan reloaded.
#freepbx*CLI> exitPassiamo a PHP
Inizializzazione degli script e creazione del servizio
Poiché il modello di lavoro con Bitrix 24, un servizio per AMI, non è proprio semplice e trasparente, è necessario approfondirlo separatamente. Asterisk, all'attivazione di AMI, apre semplicemente una porta e basta. Quando un cliente si collega, viene richiesta l'autorizzazione, poi il cliente si iscrive agli eventi desiderati. Gli eventi vengono inviati in un formato di testo semplice, che PAMI trasforma in oggetti strutturati e offre la possibilità di impostare funzioni di filtraggio solo per eventi, campi, numeri, ecc. di interesse.
Non appena arriva una chiamata, si verifica l'evento NewExten, a partire dal contesto parentale [from-pstn], seguito da tutti gli eventi in ordine di apparizione nelle righe dei contesti. All'arrivo delle informazioni dalle variabili _custom CallMeCallerIDName e CallStart definite nel piano di chiamata, viene attivato
Funzione di richiesta UserID, corrispondente al numero interno al quale è arrivata la chiamata. E se si tratta di un gruppo di chiamate? La questione è politica: dobbiamo creare una chiamata a tutti contemporaneamente (quando tutti chiamano insieme) oppure creare una per volta a seconda dell'ordine delle chiamate? La maggior parte dei clienti adotta la strategia First Available, quindi non ci sono problemi, risponde solo uno. Ma è una questione che bisogna risolvere.
Funzione di registrazione della chiamata in Bitrix24, che restituisce il CallID, necessario poi per comunicare i parametri della chiamata e il link alla registrazione. Richiede o il numero interno o l'UserID.

Al termine della chiamata viene invocata la funzione di caricamento della registrazione, che indica contemporaneamente lo stato di conclusione della chiamata (Occupato, Nessuna risposta, Successo), e carica il link al file mp3 con la registrazione (se disponibile).
Poiché il modulo CallMeIn.php deve funzionare senza interruzioni, è stato creato un file di avvio SystemD per esso. callme.service, che deve essere posizionato 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
# qui bisogna controllare i permessi delle cartelle
#User=www-data #Ubuntu - debian
#User=nginx #Centos
[Install]
WantedBy=multi-user.targetl'inizializzazione e l'avvio dello script avvengono tramite systemctl o service
# systemctl enable callme
# systemctl start callmeIl servizio si riavvierà automaticamente se necessario (in caso di crash). Il servizio di monitoraggio delle chiamate in entrata non richiede l'installazione di un server web, è necessario solo PHP (che è certamente presente sul server FeePBX). Tuttavia, senza accesso alle registrazioni delle chiamate tramite il server web (incluso l'HTTPS), non sarà possibile ascoltare le registrazioni delle conversazioni.
Ora parliamo delle chiamate in uscita. Lo script CallMeOut.php ha due funzioni:
Iniziare una chiamata quando si riceve una richiesta allo script PHP (anche tramite il pulsante "Chiama" nel Bitrix). Senza server web non funziona, la richiesta arriva tramite HTTP POST, nella richiesta è presente un token
Messaggio riguardante la chiamata, i suoi parametri e le registrazioni in Bitrix. Si verifica su iniziativa di Asterisk nel piano di chiamata [sub-call-internal-ended] al termine della chiamata.

Un server web è necessario solo per due cose: caricare file di registrazione di Bitrix (tramite HTTPS) e chiamare lo script CallMeOut.php. Puoi utilizzare il server integrato di FreePBX, i cui file si trovano in /var/www/html, oppure installare un altro server o specificare un percorso diverso.
Server web
Lasceremo la configurazione del server web per un approfondimento autonomo (, , ). Se non hai un dominio, puoi provare FreeDomain ( ), che ti daranno gratuitamente un nome per il tuo IP pubblico (non dimenticare di inoltrare le porte 80 e 443 attraverso il router, se l'indirizzo esterno è disponibile solo su di esso). Se hai appena creato un dominio DNS, dovrai aspettare (da 15 minuti a 48 ore) affinché tutti i server si aggiorno. Dall'esperienza con i provider nazionali, ci vuole da 1 ora a 24 ore.
Automazione dell'installazione
È iniziata su GitHub lo sviluppo di un installer, per rendere l'installazione ancora più semplice. Ma andare tutto liscio era solo sulla carta—per ora installiamo tutto manualmente; tuttavia, dopo aver smanettato in tutto questo, è diventato chiaro come siano le interazioni, dove vanno e come debuggare. L'installer non è ancora disponibile (
Docker
Se desideri provare rapidamente la soluzione, c'è un'opzione con Docker: crea velocemente un contenitore, espandi le porte e carica i file di configurazione per effettuare un test. (Questo è l'approccio con il contenitore Let's Encrypt; se hai già un certificato, basterà reindirizzare il reverse proxy al server web FreePBX, cui abbiamo assegnato un'altra porta — 88). Let's Encrypt in Docker è ispirato a questo.
Devi eseguire il file nella cartella del progetto scaricato (dopo il git clone), ma prima accedi alle configurazioni di Asterisk (nella cartella asterisk) e specifica i percorsi per le registrazioni e l'URL del tuo sito.
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 # per configurazione
- 5060:5060/udp
- 5160:5160/udp
- 127.0.0.1:5038:5038 # per 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:
Questo file docker-compose.yaml si avvia tramite
docker-compose up -d
Se nginx non è partito, significa che c'è qualche problema con la configurazione nella cartella nginx/ssl_docker.conf
Altre integrazioni
E perché non includere anche qualche CRM negli script, ci siamo detti. Abbiamo esaminato diverse API di altre CRM, in particolare quella gratuita integrata in alcuni centralini — ShugarCRM e Vtiger, e sì! si può fare, il principio è lo stesso. Ma questa è un'altra storia, che caricheremo su GitHub a parte.
Link
Il codice su GitHub è —
L'articolo originale per spunto
Dichiarazione: ogni somiglianza con la realtà è puramente casuale e non sono stati io.
Fonte: habr.com
