Здесь я представлю свой опыт подключения 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 и написал пошаговую инструкцию по получению сертификатов там, но только после заметил, что их корневые сертификаты не принимает ни один браузер.
Процедура его получения достаточно тривиальна, но я все же опишу ее по шагам.
- Переходим на сайт letsencrypt.org и жмем «Get Started»
скрин
- Далее нас интересует раздел With Shell Access, в котором мы найдем все необходимые инструкции
скрин
- Переходим на certbot.eff.org и выбираем наше ПО
скрин
- После чего следуем инструкциям и выполняем
несколько команд в коснолиecho "deb http://ftp.debian.org/debian jessie-backports main" >> /etc/apt/sources.list apt-get update apt-get install certbot -t jessie-backports
- Затем необходимо отправить запрос на получение сертификата при помощи утилиты certbot.
Я пошел по наиболее примитивному пути:
вбил команду
certbot certonly
и следовал этапам мастера, где указал свой email, путь к webroot, имя домена и пр.
скрины
- На выходе видим
заветное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
- Копируем сертификаты в места их дислокации
Скрытый текст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-сервера имеет следующий вид (некоторые комментарии даны непосредственно в конфиге):
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/ (в моем случае) нужно создать симлинк на папку, где будут храниться файлы записей разговоров (о настройке записей разговоров расскажу ниже)
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 нужно привести к виду:
[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
[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 при желании):
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.
Разберем линк из этой команды на составляющие:
- vistepru.amocrm.ru/private/acceptors/asterisk_new? где вместо vistepru у вас должен быть прописан ваш поддомен в amocrm
- USER_LOGIN=ceo@vistep.ru где вместо моего email должен быть указан ваш (админский)
- 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)
и выполнить еще пару команд
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).
<?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
выхлоп должен быть как на
status
Тестируем получившуюся связку
По итогу выполненных настроек мы получим следующие фичи:
- отображение звонка в amoCRM (если контакт уже есть, высвечивается ФИО и можно перейти в карточку контакта, если нет, то создать новый в один клик)
- отображение ФИО контакта из amoCRM на телефоне при входящем звонке
- возможность совершить вызов из интерфейса amoCRM в пару кликов
- переадресовать вызов ответственному менеджеру, переведя его на специальный номер
Для демонстрации лучше всего подойдет видео-формат, поэтому извольте:
Заключение
Надеюсь данной статьей я сумел полностью закрыть вопрос интеграции amoCRM и Asterisk.
Если у вас возникнут вопросы, милости прошу в комментарии.
Нет аккаунта на Хабре? — Мои координаты есть профиле, пишите, постараюсь помочь.
Asterisk — это fun!
Всем удачи!
Комментарии (14)
Lodeon
30.03.2017 15:58+1Главный вопрос почему звонок не приходит сразу Галадриель? WTF?
FessAectan
30.03.2017 15:59Такой сценарий не сложно реализовать в диалплане,
используя предоставленную в статье информацию.Nixhibrid
01.04.2017 11:53+1У себя реализовал такую штуку по соединению с ответственным
member => Local/responsible@in-context,1
Когда назначен ответственный — пинает звонок к нему, если не поднимет трубку, пойдёт по очереди дальше, Если не назначен ответственный, ругнется в лог и пойдет по очереди дальше
alecx
30.03.2017 16:297. Копируем сертификаты в места их дислокации
Так не будет работать автопродление сертификатов, лучше оставить их на месте и поменять пути в nginx.FessAectan
30.03.2017 16:30скриптами можно и этот нюанс обвязать, не думаю что это проблема
alecx
30.03.2017 17:08+1Естественно. Просто Ваш материал настолько полный, что может использоваться новичками «как есть». И через 3 месяца их ждет неприятный сюрприз.
Nixhibrid
01.04.2017 12:02+1У меня на предыдущих интеграциях в не разобранное попадали звонки, теперь не попадают. Теперь вопрос, это я рукожоп, или у всех так после обновления?
Так же думаю необходимо развивать эту тему, Asterisk это самая гибкая АТС, с которой я сталкивался, для бизнеса лучше решения не найти.
Elehandro
02.04.2017 08:02Amocrm починили автоматический завод нового клиента(Лида) при звонке с неизвестного номера или все так же криво не работает?
FessAectan
03.04.2017 04:09На видео видно, что при звонке с неизвестного номера можно открыть карточку нового контакта и внести все необходимые данные, если вы об этом.
Nixhibrid
04.04.2017 02:17+1Да только оно работает в зависимости от погоды на Марсе, сегодня на телефоне с ТП провел почти 2 часа, толку 0. Дайте говорю данные, которые можно отправить в сторону CRM, чтобы принудительно карточку клиента(сделки) открывать при звонке, говорят не знаем. В консоли постоянно их JS ругается и отваливается. Вот к чему приводит разработка с абы-каким тестированием ради манимейкинга. Хотя куда ни плюнь, в СНГ почти везде такой подход. Взять тот же Битрикс24 — интеграция Астериска на таком же уровне, всё хотят облачных партнеров продвинуть, на которых ну ни как не гибко. В итоге по тихому пишем свой модуль интеграции с Астериском, используя API. Энтузиастов, увы, не хватает
doti
У вас пароль в файле.рнр от субд, это ничего?
FessAectan
не, это ок — на самом деле пароль другой