Asterisk. Интеграция с amoCRM, step-by-step guid
Здесь я представлю свой опыт подключения 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
Настройка 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$ {
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)
Настройка amoCRM
В данном пункте нас ждет наибольшее количество грабель, поэтому будьте внимательны.
Прежде всего подключим Asterisk в интерфейсе amoCRM.
Для этого идем в «Настройки» → «Интеграции» → находим там Asterisk и жмем «Установить».
Нам предстанет описание интеграции и некоторое количество ссылок на гайды, все это смело пролистываем в самый низ до полей ввода информации.
Логин — amocrm (из manager.conf)
Пароль — JD3clEB8f4-_3ry84gJ (из manager.conf)
Путь к скрипту — _https://asterisk.vistep.ru/amocrm.php
А также внутренние номера сотрудников вашей компании.
Следующим шагом будет настройка скрипта amocrm.php.
Его можно скачать по ссылке в описании интеграции, но я хочу обратить внимание, что выложенный здесь исправлен под конкретный диалплан, точнее конкретный контекст оригинации вызовов dial_out (строка 99), дабы соответствовать настройкам Asterisk на стенде. Имейте это в виду и измените на ваш контекст, если он будет отличаться (это нужно для совершения вызовов в пару кликов прямо из amoCRM)
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
Обратите внимание!
Мои пояснения к параметрам в начале скрипта даны прямо в коде.
Проверить работу скрипта можно по следующим линкам (обратите внимание — я использую свои логин/пароль и путь к скрипту, у вас они должны отличаться):
_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!
Всем удачи!