Analizando FreePBX e integrándolo con Bitrix24 y más

Bitrix24 es una enorme herramienta que combina CRM, gestión de documentos, contabilidad y muchas otras cosas que agradan a los gerentes y no tanto al personal de IT. El portal lo utilizan muchas pequeñas y medianas empresas, incluidas clínicas pequeñas, fabricantes e incluso salones de belleza. La función principal que 'les gusta' a los gerentes es la integración de telefonía y CRM, donde cada llamada se registra automáticamente en el CRM, se crean fichas de cliente, al recibir una llamada se muestra la información del cliente y se puede ver inmediatamente quién es, qué se le puede vender y cuánto debe. Sin embargo, la telefonía de Bitrix24 y su integración con el CRM tiene un costo, a veces considerable. En este artículo, hablaré sobre la experiencia de integración con herramientas abiertas y una popular centralita IP. FreePBX, y también revisaré la lógica de funcionamiento de varias partes.

Trabajo en outsourcing en una empresa que se dedica a la venta y configuración, integración de telefonía IP. Cuando me preguntaron si podíamos ofrecer algo para integrar Bitrix24 con las centralitas que tienen los clientes, así como con centralitas virtuales en varios VDS de la empresa, fui a Google. Y, por supuesto, me dio un enlace a un artículo en Habr, donde hay tanto descripción como GitHub, y parece que todo funciona. Pero al intentar utilizar esta solución, salió que Bitrix24 ya no es lo que solía ser y hay que rehacer muchas cosas. Además, FreePBX no es solo un Asterisk en bruto, hay que pensar en cómo combinar la facilidad de uso con el plan de marcado hardcore en los archivos de configuración.

Estudiamos la lógica de funcionamiento

Así que, para empezar, así es como todo debería funcionar. Al recibir una llamada externa en la centralita (evento SIP INVITE del proveedor), comienza el procesamiento del plan de marcado (dialplan) — las reglas de qué hacer y en qué orden con la llamada. Del primer paquete se puede obtener mucha información que luego se puede utilizar en las reglas. Una excelente herramienta para estudiar las entrañas de SIP es el analizor sngrep (enlace) que simplemente se instala en distribuciones populares a través de apt install/yum install y similar, pero también se puede compilar a partir del código fuente. Veamos el registro de la llamada en sngrep.

Analizando FreePBX e integrándolo con Bitrix24 y más

En términos simples, el dialplan se ocupa solo del primer paquete, a veces también durante la conversación se realiza la transferencia de llamadas, la pulsación de botones (DTMF), y otras cosas interesantes como FollowMe, RingGroup, IVR y más.

¿Qué hay dentro del paquete Invite?

Analizando FreePBX e integrándolo con Bitrix24 y más

La mayoría de los dialplans simples trabajan con los dos primeros campos y toda la lógica gira en torno a DID y CallerID. DID es a dónde llamamos, y CallerID es quién llama.

Pero tenemos una empresa y no un solo teléfono, lo que significa que en la central telefónica probablemente haya grupos de llamadas (llamadas simultáneas/secundarias a varios dispositivos) en números urbanos (Ring Group), IVR (Hola, ha llamado a... Presione uno para...), contestadores automáticos (Phrases), condiciones temporales (Time Conditions), y desvío a otros números o a móviles (FollowMe, Forward). Esto hace que sea muy difícil determinar con exactitud a quién llegará la llamada y con quién se hablará al recibirla. Aquí hay un ejemplo del inicio del proceso de una llamada típica en la central telefónica de nuestros clientes.

Analizando FreePBX e integrándolo con Bitrix24 y más

Después de que la llamada ingresa exitosamente a la central telefónica, comienza su viaje a través del dialplan en diferentes "contextos". Un contexto, desde el punto de vista de Asterisk, es un conjunto numerado de comandos, cada uno con un filtro para el número marcado (se llama exten, para una llamada externa en la etapa inicial exten=DID). Las instrucciones en la línea del dialplan pueden ser cualquier cosa: funciones internas (por ejemplo, llamar a un abonado interno — Dial(), colgar — Hangup()), operadores condicionales (IF, ELSE, ExecIF y similares), transiciones a otras reglas de este contexto (Goto, GotoIF), transiciones a otros contextos en forma de llamadas a funciones (Gosub, Macro). También está la directiva include nombre_contexto, que agrega comandos de otro contexto al final del contexto actual. Los comandos incluidos a través de include siempre se ejecutan después de commandos del contexto actual.

Toda la lógica de funcionamiento de FreePBX se basa en incluir diferentes contextos entre sí a través de include y llamadas a través de Gosub, Macro y manejadores Handler. Analicemos el contexto de las llamadas entrantes de FreePBX.

Analizando FreePBX e integrándolo con Bitrix24 y más

La llamada pasa por todos los contextos de arriba a abajo en orden; en cada contexto pueden haber llamadas a otros contextos como macros (Macro), funciones (Gosub) o simplemente transiciones (Goto), por lo tanto, el árbol real de lo que se llama solo puede rastrearse a través de los registros.

El esquema típico de configuración de una centralita de oficina se muestra a continuación. Al recibir una llamada, se busca el DID en las rutas entrantes, se verifican las condiciones de tiempo y, si todo está en orden, se inicia el menú de voz. Desde ahí, al presionar el botón 1 o por tiempo de espera, se accede al grupo de operadores. Al finalizar la llamada, se invoca el macro hangupcall, después del cual no se podrá realizar nada más en el plan de marcación, excepto para manejadores especiales (hangup handler).

Analizando FreePBX e integrándolo con Bitrix24 y más

¿Dónde en este algoritmo de llamada debemos proporcionar información sobre el inicio de la llamada en el CRM, dónde iniciar la grabación, dónde finalizarla y enviarla junto con la información de la llamada al CRM?

Integración con sistemas externos

¿Qué es la integración de la centralita y el CRM? Son configuraciones y programas que convierten datos y eventos entre estas dos plataformas y se envían entre sí. La forma más común de interacción entre sistemas independientes es a través de API, y la forma más popular de acceso a API es HTTP REST. Pero no para Asterisk.

Dentro de Asterisk hay:

  • AGI — llamada síncrona a programas/componentes externos, se utiliza principalmente en el plan de marcación, existen bibliotecas como phpagi, PAGI

  • AMI — socket TCP basado en texto, que funciona bajo el principio de suscripción a eventos y entrada de comandos de texto, se asemeja a SMTP desde adentro, puede rastrear eventos y gestionar llamadas, existe una biblioteca PAMI — la más popular para establecer conexión con Asterisk

Ejemplo de salida de AMI

Evento: Newchannel
Privilegio: call,all
Canal: PJSIP/VMS_pjsip-0000078b
Estado del Canal: 4
Descripción del Estado del Canal: Ring
Número ID del Llamante: 111222
Nombre ID del Llamante: 111222
Número de la Línea Conectada:
Nombre de la Línea Conectada:
Idioma: en
Código de Cuenta:
Contexto: from-pstn
Extensión: s
Prioridad: 1
ID Único: 1599589046.5244
ID Vinculado: 1599589046.5244

  • ARI — una mezcla de ambos, todo a través de REST, WebSocket, en formato JSON — pero no están muy bien con las bibliotecas y envoltorios recientes, encontré (phparia, phpari) que se desarrollaron hace unos 3 años.

Ejemplo de salida de ARI al iniciar una llamada

{ "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" }

La conveniencia o inconveniencia, la posibilidad o imposibilidad de trabajar con una API determinada se definen por las tareas que se deben resolver. Las tareas para la integración con el CRM son las siguientes:

  • Rastrear el inicio de la llamada, a dónde se transfirió, extraer CallerID, DID, tiempos de inicio y finalización, y posiblemente datos del directorio (para encontrar la relación entre el teléfono y el usuario del CRM)

  • Iniciar y finalizar la grabación de una llamada, guardarla en el formato necesario, notificar al finalizar la grabación dónde se encuentra el archivo

  • Iniciar una llamada por un evento externo (desde el programa), llamar a un número interno, externo y conectarlos

  • Opcional: integrar con el CRM, grupos de marcación y FollowME para la transferencia automática de llamadas cuando no se está presente (según la información del CRM)

Todas estas tareas se pueden resolver a través de AMI o ARI, pero ARI proporciona mucha menos información, muchos eventos no son rastreados, muchas variables que sí están presentes en AMI no son seguidas (por ejemplo, llamadas a macros, asignación de variables dentro de las macros, incluida la grabación de llamadas). Por lo tanto, para un seguimiento correcto y preciso usaremos AMI por ahora (pero no de forma definitiva). Además (bueno, ¿quién no lo haría? somos seres humanos perezosos) — en el trabajo original (artículo en Habr) utilizan PAMI. *Luego necesitamos intentar volver a escribir en ARI, pero no hay garantía de que funcione.

Rediseñamos la integración desde cero

Para que nuestro FreePBX pueda comunicar de manera sencilla en AMI sobre el inicio de la llamada, la hora de finalización, los números, los nombres de los archivos grabados y calcular la duración de la llamada, la forma más sencilla es utilizar el mismo truco que los autores originales: introducir nuestras propias variables y analizar la salida en busca de su presencia. PAMI ofrece hacerlo simplemente a través de una función-filtro.

Aquí hay un ejemplo de asignar nuestra propia variable para el tiempo de inicio de la llamada (s es un número especial en el plan de marcado que se ejecuta ANTES de comenzar la búsqueda por DID)

[ext-did-custom]

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

Ejemplo de evento AMI para esta línea

Evento: Newchannel

Privilegio: call,all

Canal: PJSIP/VMS_pjsip-0000078b

Estado del Canal: 4

Descripción del Estado del Canal: Ring

Número ID del Llamante: 111222

Nombre ID del Llamante: 111222

Número de la Línea Conectada:

Nombre de la Línea Conectada:

Idioma: en

Código de Cuenta:

Contexto: from-pstn

Extensión: s

Prioridad: 1

ID Único: 1599589046.5244

ID Vinculado: 1599589046.5244

Aplicación: Set AppData:

CallStart=1599571046

Dado que FreePBX sobrescribe los archivos extention.conf y extention_additional.conf, utilizaremos el archivo extention_custom.conf

Código completo de extention_custom.conf

[globals]	
;; Verifique los caminos y permisos de las carpetas - el usuario asterisk debe tener permisos de escritura
;; Aquí se guardarán las conversaciones
WAV=\/var\/www\/html\/callme\/records\/wav 
MP3=\/var\/www\/html\/callme\/records\/mp3

;; Por estos caminos se reproducirán y descargarán las grabaciones
URLRECORDS=https:\/\/www.host.ru\/callmeplus\/records\/mp3

;; Dirección para el callback en la llamada saliente
URLPHP=https:\/\/www.host.ru\/callmeplus

;; Sí, grabamos las conversaciones
RECORDING=1

;; Este es el macro para grabar las conversaciones en nuestra carpeta. 
;; Se puede usar la grabación del sistema, pero por ahora dejemos esta - 
;; funciona
[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()


;; Este es el contexto principal para iniciar la llamada
[ext-did-custom]

;; Esto es algo arriesgado, hacerlo así aquí, pero funciona - añadimos '8' al número
nexten =>  s,1,Set(CALLERID(num)=8${CALLERID(num)})

;; Aquí hay varias variables para el script
nexten =>  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)})

;; ¡Lo más importante! Manejador de finalización de la llamada. 
;; Las rutas habituales para procesar el final a través de (exten=>h,1,... ) en FreePBX no funcionan - Macro(hangupcall,) lo estropea. 
;; Por lo tanto, establecemos el Hangup_Handler al final de la llamada
nexten => s,n,Set(CHANNEL(hangup_handler_push)=sub-call-from-cid-ended,s,1(${CALLERID(num)},${EXTEN}))

;; Manejador de la finalización de la llamada entrante
[sub-call-from-cid-ended]

;; Informamos sobre los valores al final de la llamada
nexten => s,1,Set(CDR_PROP(disable)=true)
exten => s,n,Set(CallStop=${STRFTIME(epoch,,%s)})
exten => s,n,Set(CallMeDURATION=${MATH(${CallStop}-${CallStart},int)})

;; Estado de la llamada - Contestada, no contestada...
exten => s,n,Set(CallMeDISPOSITION=${CDR(disposition)})
exten => s,n,Return


;; Manejador para las llamadas salientes - todo es análogo
[outbound-allroutes-custom]

;; Grabación
nexten => _.,1,Gosub(recording,~~s~~,1(${CALLERID(number)},${EXTEN}))
;; Variables
nexten => _.,n,Set(__CallIntNum=${CALLERID(num)})
exten => _.,n,Set(CallExtNum=${EXTEN})
exten => _.,n,Set(CallStart=${STRFTIME(epoch,,%s)})
exten => _.,n,Set(CallmeCALLID=${SIPCALLID})

;; Establecemos el Hangup_Handler al final de la llamada
nexten => _.,n,Set(CHANNEL(hangup_handler_push)=sub-call-internal-ended,s,1(${CALLERID(num)},${EXTEN}))

;; Manejador de la finalización de la llamada saliente
[sub-call-internal-ended]

;; variables
nexten => 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)})

;; Llamamos al script que notificará sobre la llamada en CRM - esta es una llamada saliente, 
;; por lo que, de hecho, al finalizar
nexten => 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

La característica y la diferencia con el diagrama de marcado original de los autores del artículo original son

  • El diagrama de marcado en formato .conf, como lo quiere FreePBX (sí, también acepta .ael, pero no todas las versiones y no siempre es conveniente)

  • En lugar de procesar la finalización a través de exten=>h, se ha introducido el tratamiento a través de hangup_handler, porque el diagrama de marcado de FreePBX solo funcionó con este

  • Se ha corregido la línea que llama al script, se han añadido comillas y el número externo de llamada ExtNum

  • Los tratamientos se han trasladado a contextos _custom y permiten no modificar ni ajustar las configuraciones de FreePBX: entrantes a través de [ext-did-custom], salientes a través de [outbound-allroutes-custom]

  • No hay vinculación a números: el archivo es universal y solo necesita la configuración de la ruta y el enlace al servidor

Para comenzar a trabajar, también es necesario habilitar los scripts en AMI mediante nombre de usuario y contraseña; para esto, también hay un archivo _custom en FreePBX

Archivo manager_custom.conf

;; este es el nombre de usuario
[callmeplus]
;; esta es la contraseña
secret = trampampamturlala
deny = 0.0.0.0/0.0.0.0

;; trabajo con la máquina local, pero si es necesario, se pueden especificar otras
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

Ambos archivos deben ubicarse en /etc/asterisk, luego se deben volver a cargar las configuraciones (o reiniciar Asterisk)

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

Ahora pasemos a PHP

Inicialización de scripts y creación de servicio

Dado que el esquema de trabajo con Bitrix 24, el servicio para AMI no es del todo simple y transparente, hay que detenerse en eso por separado. Asterisk, al activar AMI, simplemente abre el puerto y ya está. Al conectar un cliente, solicita autorización, luego el cliente se suscribe a los eventos deseados. Los eventos llegan en texto plano, que PAMI convierte en objetos estructurados y permite establecer una función de filtrado solo por eventos de interés, campos, números, etc.

Tan pronto como se recibe una llamada, ocurre el evento NewExten comenzando desde el contexto padre [from-pstn], luego siguen todos los eventos en el orden de las líneas en los contextos. Al recibir información de las variables _custom CallMeCallerIDName y CallStart especificadas en el diagrama de marcado, se invoca

  1. La función de solicitud del UserID, que corresponde al número interno al que llegó la llamada. ¿Y si se trata de un grupo de marcado? Es una cuestión política, hay que decidir si se llama a todos de una vez (cuando todos llaman simultáneamente) o se crea la llamada a medida que se marca uno tras otro. La mayoría de los clientes tienen la estrategia de Primer Disponible, por lo que no hay problemas, solo llama uno. Pero el tema debe resolverse.

  2. Función de registro de llamadas en Bitrix24, que devuelve el CallID, necesario luego para informar sobre los parámetros de la llamada y el enlace a la grabación. Requiere un número interno o UserID.

Analizando FreePBX e integrándolo con Bitrix24 y más

Al finalizar la llamada, se invoca la función de carga de la grabación, que al mismo tiempo informa sobre el estado de finalización de la llamada (Ocupado, Sin respuesta, Éxito), así como carga el enlace al archivo mp3 con la grabación (si existe).

Dado que el módulo CallMeIn.php debe funcionar continuamente, se creó un archivo de inicio de SystemD para él. callme.service, que debe colocarse en /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

# aquí hay que ver cuáles son los permisos en las carpetas
#User=www-data  #Ubuntu - debian
#User=nginx #Centos

[Install]
WantedBy=multi-user.target

La inicialización y el lanzamiento del script se realizan a través de systemctl o service.

# systemctl enable callme
# systemctl start callme

El servicio se reiniciará automáticamente según sea necesario (en caso de fallos). El servicio de seguimiento de llamadas entrantes no requiere la instalación de un servidor web, solo se necesita PHP (que seguramente está en el servidor FreePBX). Pero si no hay acceso a las grabaciones de llamadas a través del servidor web (además con https), no se podrá escuchar las grabaciones de las conversaciones.

Ahora hablemos de las llamadas salientes. El script CallMeOut.php tiene dos funciones:

  • Iniciar una llamada al recibir una solicitud al script PHP (incluido el botón 'Llamar' en Bitrix mismo). Sin un servidor web no funciona, la solicitud llega a través de HTTP POST, en la solicitud se incluye un token.

  • Informar sobre la llamada, sus parámetros y grabaciones en Bitrix. Esto ocurre por iniciativa de Asterisk en el plan de marcado [sub-call-internal-ended] al finalizar la llamada.

Analizando FreePBX e integrándolo con Bitrix24 y más

El servidor web solo se necesita para dos cosas: cargar los archivos de grabaciones en Bitrix (por HTTPS) y llamar al script CallMeOut.php. Se puede usar el servidor integrado de FreePBX, cuyos archivos están en /var/www/html, se puede instalar otro servidor o definir otra ruta.

Servidor web

Dejaremos la configuración del servidor web para que se estudie de manera independiente (clic aquí, clic aquí, clic aquí). Si no tienes un dominio, puedes probar FreeDomain ( https://www.freenom.com/ru/index.html), que te proporcionarán un nombre para tu IP blanca de forma gratuita (no olvides abrir los puertos 80 y 443 a través del enrutador si la dirección externa está solo en él). Si acabas de crear el dominio DNS, tendrás que esperar (de 15 minutos a 48 horas) hasta que todos los servidores se actualicen. Según mi experiencia con proveedores locales, esto suele tardar de 1 hora a un día.

Automatización de la instalación

Se ha iniciado el desarrollo de un instalador en GitHub para que la instalación sea aún más sencilla. Pero lo que parecía fácil en papel — por ahora estamos configurándolo todo manualmente, aunque al hurgar en todo esto quedó claro quién se lleva bien con quién, adónde va cada uno y cómo depurarlo. Por el momento no hay instalador (

Docker

Si quieres probar rápidamente la solución, hay una opción con Docker: crear un contenedor rápidamente, exponerle puertos, proporcionar archivos de configuración y probar (esta es una opción con el contenedor de LetsEncrypt; si ya tienes el certificado, solo necesitas redirigir el proxy inverso al servidor web FreePBX (a este le hemos dado otro puerto — 88), LetsEncrypt en Docker se basa en de este artículo

Debes ejecutar el archivo en la carpeta del proyecto descargado (después de git clone), pero primero entra en los archivos de configuración de Asterisk (carpeta asterisk) y especifica allí las rutas a las grabaciones y la URL de tu sitio.

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 # para configuración
      - 5060:5060\/udp
      - 5160:5160\/udp
      - 127.0.0.1:5038:5038 # para 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:

Este archivo docker-compose.yaml se ejecuta a través de

docker-compose up -d

Si nginx no se inició, significa que hay algo mal con la configuración en la carpeta nginx\/ssl_docker.conf

Otras integraciones

¿Y por qué no incluir varias CRM en los scripts, pensamos? Estudiamos varias API de otras CRM, especialmente la gratuita integrada en algunos PBX — ShugarCRM y Vtiger, ¡y sí! Es posible, el principio es el mismo. Pero esa es otra historia que subiremos a GitHub por separado.

Enlaces

Descargo de responsabilidad: cualquier coincidencia con la realidad es pura ficción y no fui yo.

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster