В сети можно найти инструкции разной степени давности и полноты представленной информации по теме вынесенной в заголовок статьи, но даже собрав их все воедино, потребуются прямые руки, напильник и некоторое количество терпения для достижения желанного катарсиса.



Здесь я представлю свой опыт подключения Asterisk к amoCRM в виде пошаговой инструкции, осветив все необходимые нюансы, начиная от получения ssl-сертификата, настройки web-сервера и заканчивая демонстрацией работы получившейся связки.

Вводные


На нашем тестовом стенде установлены:

  • ОС Debian

    lsb_release -a
    No LSB modules are available.
    Distributor ID:	Debian
    Description:	Debian GNU/Linux 8.7 (jessie)
    Release:	8.7
    Codename:	jessie

  • IP PBX Asterisk

    *CLI> core show version 
    Asterisk 13.14.0 built by root @ asterisk.vistep.ru on a x86_64 running Linux on 2017-03-29 05:47:19 UTC

  • web-сервер NGINX

    sudo nginx -v
    nginx version: nginx/1.10.3

  • PHP-FPM

    php5-fpm -v
    PHP 5.6.30-0+deb8u1 (fpm-fcgi) (built: Feb  8 2017 08:51:18)
    Copyright (c) 1997-2016 The PHP Group
    Zend Engine v2.6.0, Copyright (c) 1998-2016 Zend Technologies
        with Zend OPcache v7.0.6-dev, Copyright (c) 1999-2016, by Zend Technologies

  • Доменное имя для теста

    tawny-owl:~$ dig +short asterisk.vistep.ru
    138.201.164.52
    

Получаем ssl-сертификат


В данном гайде мы будем использовать бесплатный сертификат от Let’s Encrypt.

Изначально я планировал использовать StartSSL и написал пошаговую инструкцию по получению сертификатов там, но только после заметил, что их корневые сертификаты не принимает ни один браузер.

Процедура его получения достаточно тривиальна, но я все же опишу ее по шагам.

  1. Переходим на сайт letsencrypt.org и жмем «Get Started»

    скрин


  2. Далее нас интересует раздел With Shell Access, в котором мы найдем все необходимые инструкции

    скрин

  3. Переходим на certbot.eff.org и выбираем наше ПО

    скрин

  4. После чего следуем инструкциям и выполняем

    несколько команд в косноли
    
    echo "deb http://ftp.debian.org/debian jessie-backports main" >> /etc/apt/sources.list
    apt-get update
    apt-get install certbot -t jessie-backports
    

  5. Затем необходимо отправить запрос на получение сертификата при помощи утилиты certbot.
    Я пошел по наиболее примитивному пути:

    вбил команду

    certbot certonly

    и следовал этапам мастера, где указал свой email, путь к webroot, имя домена и пр.
    скрины





  6. На выходе видим

    заветное
    IMPORTANT NOTES:
     - Congratulations! Your certificate and chain have been saved at
       /etc/letsencrypt/live/asterisk.vistep.ru/fullchain.pem. Your cert
       will expire on 2017-06-27. To obtain a new or tweaked version of
       this certificate in the future, simply run certbot again. To
       non-interactively renew *all* of your certificates, run "certbot
       renew"
     - If you like Certbot, please consider supporting our work by:
    
       Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
       Donating to EFF:                    https://eff.org/donate-le
    
  7. Копируем сертификаты в места их дислокации

    Скрытый текст
    
    cp /etc/letsencrypt/live/asterisk.vistep.ru/privkey.pem /etc/nginx/certs/vistep.ru.key
    cp /etc/letsencrypt/live/asterisk.vistep.ru/fullchain.pem /etc/nginx/certs/vistep.ru.pem
    

Как справедливо заметили в комментариях, время жизни полученных сертификатов — 3 месяца и их нужно будет обновлять. Примите это во внимание!

Настройка web-сервера


Как и было сказано во вводной, мы будем использовать web-сервер NGINX.

Не стану разводить hollywar'ов и как-то мотивировать свой выбор, просто — у нас стоит NGINX и мы будем настраивать его.

Основой конфига послужила замечательная статья DimaSmirnov «Nginx и https. Получаем класс А+», за что ему, пользуясь случаем, выражаю благодарность.

Итак, конфигурационный файл web-сервера имеет следующий вид (некоторые комментарии даны непосредственно в конфиге):

/etc/nginx/conf.d/asterisk.vistep.ru.conf
server {
    server_name asterisk.vistep.ru;
    listen 138.201.164.52:80;
    rewrite ^  https://asterisk.vistep.ru$request_uri? permanent;
}
server {
    access_log /var/log/nginx/asterisk.vistep.ru.access.log;
    error_log /var/log/nginx/asterisk.vistep.ru.error.log;
    listen 443 ssl;
    server_name asterisk.vistep.ru;
    resolver 8.8.8.8;
    ssl_stapling on;
    ssl on;
    ssl_certificate /etc/nginx/certs/vistep.ru.pem;
    ssl_certificate_key /etc/nginx/certs/vistep.ru.key;
    ssl_dhparam /etc/nginx/certs/dhparam.pem;
    ssl_session_timeout 24h;
    ssl_session_cache shared:SSL:2m;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers kEECDH+AES128:kEECDH:kEDH:-3DES:kRSA+AES128:kEDH+3DES:DES-CBC3-SHA:!RC4:!aNULL:!eNULL:!MD5:!EXPORT:!LOW:!SEED:!CAMELLIA:!IDEA:!PSK:!SRP:!SSLv2;
    ssl_prefer_server_ciphers on;
    add_header Strict-Transport-Security "max-age=31536000;";
    add_header Content-Security-Policy-Report-Only "default-src https:; script-src https: 'unsafe-eval' 'unsafe-inline'; style-src https: 'unsafe-inline'; img-src https: data:; font-src https: data:; report-uri /csp-report";
	root /var/www/asterisk;
	index index.php index.html index.htm index.nginx-debian.html;

    location records/ {
    autoindex off;
    allow 89.108.120.223;
        allow 89.108.122.9;
        allow 95.213.171.78;
        allow 95.213.156.46;
        allow 209.160.27.20;
        allow 89.189.163.20; # адреса выше - адреса amoCRM и они нужны, а этот - мой домашний, не нужно его вставлять в конфиг ;) актуальный список адресов - https://www.amocrm.ru/security/iplist.txt
    deny all;
}
 
	location / {
		try_files $uri $uri/ =404;
        allow 89.108.120.223; 
        allow 89.108.122.9;
        allow 95.213.171.78;
        allow 95.213.156.46;
        allow 209.160.27.20;
        allow 89.189.163.20; # адреса выше - адреса amoCRM и они нужны, а этот - мой домашний, не нужно его вставлять в конфиг ;) актуальный список адресов - https://www.amocrm.ru/security/iplist.txt
	deny all;
	}
	location ~ \.php$ {
    allow 89.108.120.223;
        allow 89.108.122.9;
        allow 95.213.171.78;
        allow 95.213.156.46;
        allow 209.160.27.20;
        allow 89.189.163.20; # адреса выше - адреса amoCRM и они нужны, а этот - мой домашний, не нужно его вставлять в конфиг ;) актуальный список адресов - https://www.amocrm.ru/security/iplist.txt
    deny all;
	        fastcgi_pass unix:/var/run/php5-fpm.sock;
	       	fastcgi_index index.php;
	        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
	        include fastcgi_params;
		fastcgi_buffers 16 16k; 
		fastcgi_buffer_size 32k;
	}
}


В папке /var/www/asterisk/ (в моем случае) нужно создать симлинк на папку, где будут храниться файлы записей разговоров (о настройке записей разговоров расскажу ниже)

Скрытый текст
cd /var/www/asterisk/
ln -s /var/calls/ records

Еще несколько слов о сертификатах. Помимо уже размещенных на своих местах vistep.ru.key и vistep.ru.pem, нам также понадобится dhparam.pem.

создадим его
openssl dhparam -out /etc/nginx/certs/dhparam.pem 4096


За сим с настройкой NGINX закончим и перейдем к настройке Asterisk.

Настройка IP PBX Asterisk


Для того, чтобы amoCRM могла коммуницировать с нашей Asterisk, manager.conf и http.conf нужно привести к виду:

manager.conf

[general]
enabled = yes
port = 5038
bindaddr = 0.0.0.0
webenabled = yes
httptimeout = 60
debug = on

[amocrm]
secret = JD3clEB8f4-_3ry84gJ
deny = 0.0.0.0/0.0.0.0
permit = 127.0.0.1/255.255.255.0
read = cdr,reporting,originate
write = reporting,originate


http.conf

[general]
enabled=yes
enablestatic=yes
bindaddr=0.0.0.0
bindport=8088
prefix=asterisk


Перезапустим Asterisk и проверим все ли поднялось как нам нужно

выхлоп
asterisk*CLI> http show status
HTTP Server Status:
Prefix: /asterisk
Server: Asterisk/13.14.0
Server Enabled and Bound to 0.0.0.0:8088

Enabled URI's:
/asterisk/httpstatus => Asterisk HTTP General Status
/asterisk/phoneprov/... => Asterisk HTTP Phone Provisioning Tool
/asterisk/amanager => HTML Manager Event Interface w/Digest authentication
/asterisk/arawman => Raw HTTP Manager Event Interface w/Digest authentication
/asterisk/manager => HTML Manager Event Interface
/asterisk/rawman => Raw HTTP Manager Event Interface
/asterisk/static/... => Asterisk HTTP Static Delivery
/asterisk/amxml => XML Manager Event Interface w/Digest authentication
/asterisk/mxml => XML Manager Event Interface
/asterisk/ari/... => Asterisk RESTful API
/asterisk/ws => Asterisk HTTP WebSocket

Enabled Redirects:
None.
asterisk*CLI> manager show settings

Global Settings:
----------------
Manager (AMI): Yes
Web Manager (AMI/HTTP): Yes
TCP Bindaddress: 0.0.0.0:5038
HTTP Timeout (minutes): 60
TLS Enable: No
TLS Bindaddress: Disabled
TLS Certfile: asterisk.pem
TLS Privatekey:
TLS Cipher:
Allow multiple login: Yes
Display connects: Yes
Timestamp events: No
Channel vars:
Debug: Yes


Пример диалплана (я использую ael, но уверен, что любой сможет перевести в lua или conf при желании):

extensions.ael
globals {
    WAV=/var/calls; //Временный каталог с WAV
    MP3=/var/calls; //Куда выгружать mp3 файлы
    RECORDING=1; // Запись, 1 - включена.
};

macro recording (calling,called) {
        if ("${RECORDING}" = "1"){
              Set(fname=${UNIQUEID}-${STRFTIME(${EPOCH},,%Y-%m-%d-%H_%M)}-${calling}-${called});
	      Set(datedir=${STRFTIME(${EPOCH},,%Y/%m/%d)});
	      System(mkdir -p ${WAV}/${datedir});
              Set(monopt=nice -n 19 /usr/bin/lame -b 32  --silent "${WAV}/${datedir}/${fname}.wav"  "${MP3}/${datedir}/${fname}.mp3" && chmod o+r "${MP3}/${datedir}/${fname}.*");
              Set(CDR(filename)=${fname}.mp3);
	      Set(CDR(recordingfile)=${fname}.wav);
              Set(CDR(realdst)=${called});
              MixMonitor(${WAV}/${datedir}/${fname}.wav,b,${monopt});

       };
};


context dial_out {
// звоним друг другу
_[71]XX => {
        &recording(${CALLERID(number)},${EXTEN});
        Dial(SIP/${EXTEN},,tTr);
        Hangup();
        }

// кому позвонить решит amoCRM!
100500 => {
        Set(DEFMAN=123); // по умолчанию звоним на 123
        Set(TOEXT=${SHELL(wget -O - --quiet "https://vistepru.amocrm.ru/private/acceptors/asterisk_new/?redirect=Y&number=${CALLERID(num)}&USER_LOGIN=ceo@vistep.ru&USER_HASH=1dc1444b0d3172c1113ffea9078c575c")}); // получаем номер ответственного менеджера
        Dial(SIP/${TOEXT},,tTr); // звоним ответственному менеджеру // если он не отвечает или ошибка, звоним на номер по умолчанию
        if ("${DIALSTSTUS}" != "ANSWERED") {
            Dial(SIP/${DEFMAN},,tTr);
        }
        HangUP();
} // end 100500

_XXXXXX => {
NoOP(=== CALL FROM ${CALLERID(number)} TO ${EXTEN} ===);
&recording(${CALLERID(number)},${EXTEN});
Dial(SIP/83843${EXTEN}@multifon,180,tT);
HangUP();
} // end of _XXXXXX

_[78]XXXXXXXXXX => {
NoOP(=== CALL TO ${EXTEN} ===);
&recording(${CALLERID(number)},${EXTEN});
Dial(SIP/${EXTEN}@multifon,180,tT);
HangUP();
}// end of _[78]XXXXXXXXXX


_+7XXXXXXXXXX => {
NoOP(=== CALL TO ${EXTEN} ===);
&recording(${CALLERID(number)},${EXTEN});
Dial(SIP/${EXTEN}@multifon,180,tT);
HangUP();
}// end of _+7XXXXXXXXXX


//все остальные звонки, не прописанные выше, идут в лес
_X. => {
        Hangup();
        }

}


context default {
// в контексте по умолчанию все отправляется лесом
_X. => {
        Hangup();
        }
};



context incoming {
_[87]XXXXXXXXXX => {
	&recording(${CALLERID(number)},${EXTEN});
	Answer();
	Set(CHANNEL(musicclass)=vistep.ru);
	Set(CUSTOMER_NAME=${SHELL(wget -O - --quiet  "https://vistepru.amocrm.ru/private/acceptors/asterisk_new/?number=${CALLERID(num)}&USER_LOGIN=ceo@vistep.ru&USER_HASH=1dc1444b0d3172c1113ffea9078c575c"|cut -d "|" -f1)});
	Set(CALLERID(name)=${CUSTOMER_NAME});
	Queue(queue_1,tT);
	NoOp(=== ${HANGUPCAUSE} ===);
	HangUP();
}
}


Важно!

В контексте incoming (так я назвал контекст, где обрабатываю входящие вызовы), в единственном экстеншене, есть такая строка:

Set(CUSTOMER_NAME=${SHELL(wget -O - --quiet  "https://vistepru.amocrm.ru/private/acceptors/asterisk_new/?number=${CALLERID(num)}&USER_LOGIN=ceo@vistep.ru&USER_HASH=1dc1444b0d3172c1113ffea9078c575c"|cut -d "|" -f1)});

Эта команда позволяет нам отобразить на телефонах сотрудников ФИО звонящих клиентов, подцепляя их из amoCRM.

Разберем линк из этой команды на составляющие:

  1. vistepru.amocrm.ru/private/acceptors/asterisk_new? где вместо vistepru у вас должен быть прописан ваш поддомен в amocrm
  2. USER_LOGIN=ceo@vistep.ru где вместо моего email должен быть указан ваш (админский)
  3. USER_HASH=1dc1444b0d3172c1119593ffea9078c575c где вместо моего API ключа (в интерфейсе amoCRM «Настройки» > «API) укажите свой API ключ

Пример работы команды

Скрытый текст


Теперь о специальном экстеншене 100500. Напомню, в диалплане он выглядит

так

// кому позвонить решит amoCRM!
100500 => {
        Set(DEFMAN=123); // по умолчанию звоним на 123
        Set(TOEXT=${SHELL(wget -O - --quiet "https://vistepru.amocrm.ru/private/acceptors/asterisk_new/?redirect=Y&number=${CALLERID(num)}&USER_LOGIN=ceo@vistep.ru&USER_HASH=1dc1444b0d3172c1113ffea9078c575c")}); // получаем номер ответственного менеджера
        Dial(SIP/${TOEXT},,tTr); // звоним ответственному менеджеру // если он не отвечает или ошибка, звоним на номер по умолчанию
        if ("${DIALSTSTUS}" != "ANSWERED") {
            Dial(SIP/${DEFMAN},,tTr);
        }
        HangUP();
} // end 100500


Линк для wget практически идентичен и для него действуют правила описанные выше. А нужен он для т.н. „умной переадресации“, когда поступивший вызов переадресуется сотрудником на 100500, а дальше Asterisk и amoCRM уже сами решают кому его направить (читай направить ответственному менеджеру или менеджеру „по умолчанию“).

Почему это полезно, спросите вы? Представим обычную для офиса ситуацию:

- Входящий звонок от ООО "Шубы Саурона"
- Звонок принимает менеджер Боромир, понимает, что это не его клиент и начинает кричать в рупор на весь офис: - Чей клиент "Шубы Сарумана"? (еще и ошибается в добавок!)
- Галадриель из конца кабинета кричит, что ее
- Боромир спрашивает какой у нее внутренний номер и только затем переводит вызов.

В связке с amoCRM это будет выглядеть так:

- Входящий звонок от ООО "Шубы Саурона"
- Звонок принимает менеджер Боромир, понимает, что это не его клиент и переводит вызов на 100500
- Asterisk и amoCRM путем не сложной магии сами решают, что вызов нужно отправить Галадриель
- PROFIT!

за информацию спасибо ребятам из voxlink — voxlink.ru/kb/integraciya-s-crm/amocrm-asterisk

И да, совсем забыл, если ваша Asterisk еще не настроена на ведение БД в MySQL, то в данной статье вы найдете все необходимые инструкции.

Также не забудьте добавить в табличку CDR еще одно поле (нужно для возможности слушать разговоры в карточке клиента в amoCRM)

Скрытый текст
ALTER TABLE `cdr` ADD `recordingfile` VARCHAR (120) NOT NULL

и выполнить еще пару команд
Заголовок спойлера

echo "alias recordingfile => recordingfile" >> cdr_mysql.conf
asterisk -rx 'core restart now'



Настройка amoCRM


В данном пункте нас ждет наибольшее количество грабель, поэтому будьте внимательны.
Прежде всего подключим Asterisk в интерфейсе amoCRM.

Для этого идем в „Настройки“ > „Интеграции“ > находим там Asterisk и жмем „Установить“.
Нам предстанет описание интеграции и некоторое количество ссылок на гайды, все это смело пролистываем в самый низ до полей ввода информации.

Логин — amocrm (из manager.conf)
Пароль — JD3clEB8f4-_3ry84gJ (из manager.conf)
Путь к скрипту — _https://asterisk.vistep.ru/amocrm.php

А также внутренние номера сотрудников вашей компании.

скрин


Следующим шагом будет настройка скрипта amocrm.php.

Его можно скачать по ссылке в описании интеграции, но я хочу обратить внимание, что выложенный здесь исправлен под конкретный диалплан, точнее конкретный контекст оригинации вызовов dial_out (строка 99), дабы соответствовать настройкам Asterisk на стенде. Имейте это в виду и измените на ваш контекст, если он будет отличаться (это нужно для совершения вызовов в пару кликов прямо из amoCRM).

amocrm.php
<?php
/*
	amoCRM to  asterisk integration.
	QSOFT LLC,  All rights reserved.
	mailto:      support@amocrm.com.
	Date:   10.04.2012   rev: 102703
	Cannot be redistributed  without
	     a written permission.
                         _____ _____  __  __
                        / ____|  __ \|  \/  |
   __ _ _ __ ___   ___ | |    | |__) | \  / |
  / _` | '_ ` _ \ / _ \| |    |  _  /| |\/| |
 | (_| | | | | | | (_) | |____| | \ \| |  | |_
  \__,_|_| |_| |_|\___/ \_____|_|  \_\_|  |_(_)



 */
ini_set('log_errors','On');
ini_set('error_log', '/var/log/php_errors.log');
define('AC_HOST','localhost'); // где слушает  AMI/AJAM
define('AC_PORT',8088); // какой порт слушает (у нас 8088) см. http.conf Asterisk'а
define('AC_PREFIX','/asterisk/'); // см. http.conf Asterisk'а
define('AC_TLS',false);
define('AC_DB_CS','mysql:host=localhost;port=3306;dbname=asterisk'); //хост, где крутится MySQL с БД Asterisk'а, порт и имя БД
define('AC_DB_UNAME','asterisk_user'); //каким юзером цепляться к БД
define('AC_DB_UPASS','232wwQd293f_2edxse3e'); //пароль этого юзера
define('AC_TIMEOUT',0.75);
define('AC_RECORD_PATH','https://asterisk.vistep.ru/records/%Y/%m/%d/#'); //путь, по которому забирать файлы записей разговоров
define('AC_TIME_DELTA',7); // hours. Ex. GMT+4 = 4


$db_cs=AC_DB_CS;
$db_u=!strlen(AC_DB_UNAME)?NULL:AC_DB_UNAME;
$db_p=!strlen(AC_DB_UPASS)?NULL:AC_DB_UPASS;
date_default_timezone_set('UTC');


if (AC_PORT<1) die('Please, configure settings first!'); // die if not
if (defined('AC_RECORD_PATH') AND !empty($_GET['GETFILE'])){
	//get file. Do not check auth. (uniqueid is rather unique)
	$p=AC_RECORD_PATH;
	if (empty($p)) die('Error while getting file from asterisk');
	try {
		$dbh = new PDO($db_cs, $db_u, $db_p);
		$sth = $dbh->prepare('SELECT calldate,recordingfile FROM cdr WHERE uniqueid= :uid');
		$sth->bindValue(':uid',strval($_GET['GETFILE']));
		$sth->execute();
		$r = $sth->fetch(PDO::FETCH_ASSOC);
		if ($r===false OR empty($r['recordingfile'])) die('Error while getting file from asterisk');
		$date=strtotime($r['calldate']);
		$replace=array();
		$replace['#']=$r['recordingfile'];
		$dates=array('d','m','Y','y');
		foreach ($dates as $d) $replace['%'.$d]=date($d,$date); // not a good idea!
		$p=str_replace(array_keys($replace),array_values($replace),$p);
		if (empty($_GET['noredirect'])) header('Location: '.$p);
		die($p);
	} catch (PDOException $e) {
		die('Error while getting file from asterisk');
	}
}


// filter parameters from _GET
foreach (array('login','secret','action') as $k){
	if (empty($_GET['_'.$k])) die('NO_PARAMS');
	$$k=strval($_GET['_'.$k]);
}
// trying to check accacess
$loginArr=array(
	'Action'=>'Login',
	'username'=>$login,
	'secret'=>$secret,
//	'Events'=>'off',
);
$resp=asterisk_req($loginArr,true);
// problems? exiting
if ($resp[0]['response']!=='Success') answer(array('status'=>'error','data'=>$resp[0]));

//auth OK. Lets perform actions
if ($action==='status'){ // list channels status
	$params=array( 'action'=>'status');
	$resp=asterisk_req($params);
	// report error of any
	if ($resp[0]['response']!=='Success') answer(array('status'=>'error','data'=>$resp[0]));
	// first an last chunks are useless
	unset($resp[end(array_keys($resp))],$resp[0]);
	// renumber keys for JSON
	$resp=array_values($resp);
	// report OK
	answer(array('status'=>'ok','action'=>$action,'data'=>$resp));

}elseif ($action==='call'){ // originate a call
	$params=array(
		'action'=>'Originate',
		'channel'=>'SIP/'.intval($_GET['from']),
		'Exten'=>strval($_GET['to']),
		'Context'=>'dial_out', //was from-internal
		'priority'=>'2',
		'Callerid'=>'"'.strval($_GET['as']).'" <'.intval($_GET['from']).'>',
		'Async'=>'Yes',
		// Not Implemented:
		//'Callernumber'=>'150',
		//'CallerIDName'=>'155',
	);
	$resp=asterisk_req($params,true);
	if ($resp[0]['response']!=='Success') answer(array('status'=>'error','data'=>$resp[0]));
	answer(array('status'=>'ok','action'=>$action,'data'=>$resp[0]));

} elseif ($action==='test_cdr'){ // test if DB connection params are OK.
	if (!class_exists('PDO')) answer(array('status'=>'error','data'=>'PDO_NOT_INSTALLED')); // we use PDO for accessing mySQL pgSQL sqlite within same algorythm
	try {
		$dbh = new PDO($db_cs, $db_u, $db_p);
	} catch (PDOException $e) {
		answer(array('status'=>'error','data'=>$e->getMessage()));
	}
	answer(array('status'=>'ok','data'=>'connection ok'));
} elseif ($action==='cdr'){ // fetch call history
	try {
		$dbh = new PDO($db_cs, $db_u, $db_p);

		foreach (array('date_from','date_to') as $k){
			$v=doubleval( (!empty($_GET[$k]))?intval($_GET[$k]):0 );
			if ($v<0) $v=time()-$v;
			$$k=$v;
		}
		if ($date_from<time()-10*24*3600) $date_from=time()-7*24*3600; //retr. not more than 10d before
		$date_from=($date_from?$date_from+AC_TIME_DELTA*3600:0); //default 01-01-1970
		$date_to  =($date_to  ?$date_to  +AC_TIME_DELTA*3600:time()+AC_TIME_DELTA*3600);//default now()
		$sth = $dbh->prepare('SELECT calldate, src,dst,duration,billsec,uniqueid,recordingfile FROM cdr WHERE disposition=\'ANSWERED\' AND billsec>=:minsec AND calldate> :from AND calldate< :to');
		// BETWEEN is illegal on some bcknds
		header("X-REAL_DATE:" . gmdate('Y-m-d H:i:s',$date_from).'@'. gmdate('Y-m-d H:i:s',$date_to));
		$sth->bindValue(':from', date('Y-m-d H:i:s',$date_from) );
		$sth->bindValue(':to',	 date('Y-m-d H:i:s',$date_to));
		$sth->bindValue(':minsec',!empty($_GET['minsec'])?$_GET['minsec']:5,PDO::PARAM_INT);
		$sth->execute();
		//$sth->debugDumpParams(); 	var_dump($sth->errorInfo());
		$r = $sth->fetchAll(PDO::FETCH_ASSOC);
		foreach ($r as $k=>$v) $r[$k]['calldate']=date('Y-m-d H:i:s',strtotime($v['calldate'])-AC_TIME_DELTA*3600);
		answer(array('status'=>'ok','data'=>$r),true);
	} catch (PDOException $e) {
		answer(array('status'=>'error','data'=>$e->getMessage()),true);
	}
} elseif ($action==='pop'){// fill test data. Maybe you will need it. Just comment line below.
	die();
	$dbh = new PDO($db_cs, $db_u, $db_p);
	for ($i=0;$i<(int)$_GET['n'];$i++){
		$array=array(
			date('Y-m-d H:i:s',time()-rand(100,7*24*3600)),
			'Auto <150>', 150,'791612345678','n/a','n/a','n/a','n/a','n/a',999, rand(7,999), 'ANSWERED',3,'',uniqid(),'','',''
		);
		$str=array();
		foreach ($array as  $v) $str[]="'{$v}'";
		$str=implode(', ',$str);
		$dbh->query("INSERT INTO cdr VALUES ({$str});");
	}
}

/** MakeRequest to asterisk interfacees
 * @param $params -- array of req. params
 * @return array -- response
 */
function asterisk_req($params,$quick=false){
	// lets decide if use AJAM or AMI
	return !defined('AC_PREFIX')?ami_req($params,$quick):ajam_req($params);
}

/**
 * Shudown function. Gently close the socket
 */
function asterisk_socket_shutdown(){ami_req(NULL);}

/*** Make request with AMI
 * @param $params -- array of req. params
 * @param bool $quick -- if we need more than action result
 * @return array result of req
 */
function ami_req($params,$quick=false){
	static $connection;
	if ($params===NULL and $connection!==NULL) {
		// close connection
		fclose($connection);
		return;
	}
	if ($connection===NULL){
		$en=$es='';
		$connection = fsockopen(AC_HOST, AC_PORT, $en, $es, 3);
		// trying to connect. Return an error on fail
		if ($connection) register_shutdown_function('asterisk_socket_shutdown');
		else {$connection=NULL; return array(0=>array('response'=>'error','message'=>'socket_err:'.$en.'/'.$es));}
	}
	// building req.
	$str=array();
	foreach($params as $k=>$v) $str[]="{$k}: {$v}";
	$str[]='';
	$str=implode("\r\n",$str);
	// writing
	fwrite($connection,$str."\r\n");
	// Setting stream timeout
	$seconds=ceil(AC_TIMEOUT);
	$ms=round((AC_TIMEOUT-$seconds)*1000000);
	stream_set_timeout($connection,$seconds,$ms);
	// reading respomse and parsing it
	$str= ami_read($connection,$quick);
	$r=rawman_parse($str);
	//var_dump($r,$str);
	return $r;
}
/*** Reads data from coinnection
 * @param $connection -- active connection
 * @param bool $quick -- should we wait for timeout or return an answer after getting command status
 * @return string RAW response
 */
function ami_read($connection,$quick=false){
	$str='';
	do {
		$line = fgets($connection, 4096);
		$str .= $line;
		$info = stream_get_meta_data($connection);
		if ($quick and $line== "\r\n") break;
	}while ($info['timed_out'] == false );
	return $str;
}

/*** Echo`s data
 * @param $array answer data
 * @param bool $no_callback shold we output as JSON or use callback function
 */
function answer($array,$no_callback=false){
	header('Content-type: text/javascript;');
	if (!$no_callback)  echo "asterisk_cb(".json_encode($array).');';
	else echo json_encode($array);
	die();
}

/** Parse RAW response
 * @param $lines RAW response
 * @return array parsed response
 */
function rawman_parse($lines){
	$lines=explode("\n",$lines);
	$messages=array();
	$message=array();

	foreach ($lines as $l){
		$l=trim($l);
		if (empty($l) and count($message)>0){ $messages[]= $message;  $message=array(); continue;}
		if (empty($l))  continue;
		if (strpos($l,':')===false)  continue;
		list($k,$v)=explode(':',$l);
		$k=strtolower(trim($k));
		$v=trim($v);
		if (!isset( $message[$k]))  $message[$k]=$v;
		elseif (!is_array( $message[$k]))  $message[$k]=array( $message[$k],$v);
		else  $message[$k][]=$v;
	}
	if (count($message)>0) $messages[]= $message;
	return $messages;
}


/** Make request via AJAM
 * @param $params req. params
 * @return array parsed resp.
 */
function ajam_req($params){
	static $cookie;
	// EveryRequest Ajam sends back a cookir, needed for auth handling
	if ($cookie===NULL) $cookie='';
	// make req. and store cookie
	list($body,$cookie)= rq(AC_PREFIX.'rawman?'.http_build_query($params),$cookie);
	// parse an answer
	return rawman_parse($body);
}

/** make http req. to uri with cookie, parse resp and fetch a new cookie
 * @param $url
 * @param string $cookie
 * @return array  ($body,$newcookie)
 */
function rq($url,$cookie=''){
	// get RAW data
	$r=_rq($url,$cookie);
	// divide in 2 parts
	list($headersRaw,$body)=explode("\r\n\r\n",$r,2);
	// parse headers
	$headersRaw=explode("\r\n",$headersRaw);
	$headers=array();
	foreach ($headersRaw as $h){
		if (strpos($h,':')===false) continue;
		list($hname,$hv)=explode(":",$h,2);
		$headers[strtolower(trim($hname))]=trim($hv);
	}
	// fetch cookie
	if (!empty($headers['set-cookie'])){
		$listcookies=explode(';',$headers['set-cookie']);
		foreach ($listcookies as $c){
			list($k,$v)=explode('=',trim($c),2);
			if ($k=='mansession_id') $cookie=$v;
		}
	}

	return array($body,$cookie);
}

/**  mare a request to URI and return RAW resp or false on fail
 * @param $url
 * @param $cookie
 * @return bool|string
 */
function _rq($url,$cookie){
	$errno=$errstr="";
	$fp = fsockopen(AC_HOST, AC_PORT, $errno, $errstr, 3);
	if (!$fp) return false;
	$out = "GET {$url} HTTP/1.1\r\n";
	$out .= "Host: ".AC_HOST."\r\n";
	if (!empty($cookie)) $out.="Cookie: mansession_id={$cookie}\r\n";
	$out .= "Connection: Close\r\n\r\n";
	fwrite($fp, $out);
	$r='';
	while (!feof($fp)) $r.=fgets($fp);
	fclose($fp);
	return $r;
}


Обратите внимание!

Мои пояснения к параметрам в начале скрипта даны прямо в коде.

Проверить работу скрипта можно по следующим линкам (обратите внимание — я использую свои логин/пароль и путь к скрипту, у вас они должны отличаться):

_https://asterisk.vistep.ru/amocrm.php?_login=amocrm&_secret=JD3clEB8f4-_3ry84gJ&_action=test_cdr
_https://asterisk.vistep.ru/amocrm.php?_login=amocrm&_secret=JD3clEB8f4-_3ry84gJ&_action=status
выхлоп должен быть как на

скринах
test_cdr

status


Тестируем получившуюся связку


По итогу выполненных настроек мы получим следующие фичи:

  • отображение звонка в amoCRM (если контакт уже есть, высвечивается ФИО и можно перейти в карточку контакта, если нет, то создать новый в один клик)
  • отображение ФИО контакта из amoCRM на телефоне при входящем звонке
  • возможность совершить вызов из интерфейса amoCRM в пару кликов
  • переадресовать вызов ответственному менеджеру, переведя его на специальный номер

Для демонстрации лучше всего подойдет видео-формат, поэтому извольте:

Заключение


Надеюсь данной статьей я сумел полностью закрыть вопрос интеграции amoCRM и Asterisk.
Если у вас возникнут вопросы, милости прошу в комментарии.

Нет аккаунта на Хабре? — Мои координаты есть профиле, пишите, постараюсь помочь.

Asterisk — это fun!
Всем удачи!
Поделиться с друзьями
-->

Комментарии (14)


  1. doti
    30.03.2017 13:35
    +1

    У вас пароль в файле.рнр от субд, это ничего?


    1. FessAectan
      30.03.2017 13:38

      не, это ок — на самом деле пароль другой


  1. Lodeon
    30.03.2017 15:58
    +1

    Главный вопрос почему звонок не приходит сразу Галадриель? WTF?


    1. FessAectan
      30.03.2017 15:59

      Такой сценарий не сложно реализовать в диалплане,
      используя предоставленную в статье информацию.


      1. Nixhibrid
        01.04.2017 11:53
        +1

        У себя реализовал такую штуку по соединению с ответственным
        member => Local/responsible@in-context,1

        Когда назначен ответственный — пинает звонок к нему, если не поднимет трубку, пойдёт по очереди дальше, Если не назначен ответственный, ругнется в лог и пойдет по очереди дальше


  1. alecx
    30.03.2017 16:29

    7. Копируем сертификаты в места их дислокации

    Так не будет работать автопродление сертификатов, лучше оставить их на месте и поменять пути в nginx.


    1. FessAectan
      30.03.2017 16:30

      скриптами можно и этот нюанс обвязать, не думаю что это проблема


      1. alecx
        30.03.2017 17:08
        +1

        Естественно. Просто Ваш материал настолько полный, что может использоваться новичками «как есть». И через 3 месяца их ждет неприятный сюрприз.


        1. FessAectan
          31.03.2017 07:22

          вы правы, оставил новичкам предостережение


  1. Nixhibrid
    01.04.2017 12:02
    +1

    У меня на предыдущих интеграциях в не разобранное попадали звонки, теперь не попадают. Теперь вопрос, это я рукожоп, или у всех так после обновления?
    Так же думаю необходимо развивать эту тему, Asterisk это самая гибкая АТС, с которой я сталкивался, для бизнеса лучше решения не найти.


    1. FessAectan
      01.04.2017 19:33

      Да, все точно также — в не разобранное не попадают звонки.


  1. Elehandro
    02.04.2017 08:02

    Amocrm починили автоматический завод нового клиента(Лида) при звонке с неизвестного номера или все так же криво не работает?


    1. FessAectan
      03.04.2017 04:09

      На видео видно, что при звонке с неизвестного номера можно открыть карточку нового контакта и внести все необходимые данные, если вы об этом.


      1. Nixhibrid
        04.04.2017 02:17
        +1

        Да только оно работает в зависимости от погоды на Марсе, сегодня на телефоне с ТП провел почти 2 часа, толку 0. Дайте говорю данные, которые можно отправить в сторону CRM, чтобы принудительно карточку клиента(сделки) открывать при звонке, говорят не знаем. В консоли постоянно их JS ругается и отваливается. Вот к чему приводит разработка с абы-каким тестированием ради манимейкинга. Хотя куда ни плюнь, в СНГ почти везде такой подход. Взять тот же Битрикс24 — интеграция Астериска на таком же уровне, всё хотят облачных партнеров продвинуть, на которых ну ни как не гибко. В итоге по тихому пишем свой модуль интеграции с Астериском, используя API. Энтузиастов, увы, не хватает