Asterisk Integration with amoCRM, step-by-step guide

Published on March 30, 2017

Asterisk Integration with amoCRM, step-by-step guide

  • Tutorial
On the network you can find instructions of varying degrees of limitation and completeness of the information presented on the topic in the heading of the article, but even putting them all together, you will need straight arms, a file and some patience to achieve the desired catharsis.



Here I will present my experience of connecting Asterisk to amoCRM in the form of step-by-step instructions, highlighting all the necessary nuances, from receiving an ssl certificate, setting up a web server and ending with a demonstration of the resulting bundle.

* for the impatient, what will result from the manipulations performed, see the
end of the article

Introductory


On our test bench are installed:

  • Debian OS

    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

  • NGINX web server

    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

  • Domain name for the test

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

We receive the ssl certificate


In this guide, we will use a free certificate from Let's Encrypt.

Initially, I planned to use StartSSL and wrote a step-by-step instruction for obtaining certificates there, but only after I noticed that no browser accepts their root certificates.

The procedure for obtaining it is quite trivial, but I will nevertheless describe it in steps.

  1. Go to letsencrypt.org and click "Get Started"

    screen


  2. Next, we are interested in the section With Shell Access, in which we find all the necessary instructions

    screen

  3. Go to certbot.eff.org and choose our software

    screen

  4. Then we follow the instructions and execute

    several teams in cosnoli
    
    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. Then you need to send a certificate request using the certbot utility.
    I went the most primitive way:

    drove a team

    certbot certonly

    and followed the steps of the wizard, where he indicated his email, the path to webroot, domain name, etc.
    screenshots





  6. At the exit we see

    treasured
    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. Copy certificates to their locations

    Hidden text
    
    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
    

As rightly noted in the comments, the lifetime of the certificates received is 3 months and they will need to be updated. Take this into account!

Web server setup


As mentioned in the introduction, we will use the NGINX web server.

I will not breed hollywar'ov and somehow motivate my choice, just - we have NGINX and we will configure it.

The basis of the config was a wonderful article by DimaSmirnov “Nginx and https. We get class A + ” , for which I take this opportunity to express my gratitude.

So, the web server configuration file has the following form (some comments are given directly in the config):

/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;
	}
}


In the folder / var / www / asterisk / (in my case), you need to create a symlink to the folder where the conversation recording files will be stored (I will tell about setting up conversation records below)

Hidden text
cd /var/www/asterisk/
ln -s /var/calls/ records

A few more words about certificates. In addition to vistep.ru.key and vistep.ru.pem already in place, we will also need dhparam.pem.

create it
openssl dhparam -out /etc/nginx/certs/dhparam.pem 4096


For sim with the NGINX configuration, finish and move on to setting up Asterisk.

Configure IP PBX Asterisk


In order for amoCRM to communicate with our Asterisk, manager.conf and http.conf need to be converted to:

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


Restart Asterisk and check if everything has risen as we need

exhaust
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


Dialplan example (I use ael, but I'm sure that anyone can translate to lua or conf if desired):

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();
}
}


Important!

In the incoming context (as I called the context where I process incoming calls), in a single extension, there is such a line:

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)});

This command allows us to display the names of the calling customers on the phones of employees, picking them up from amoCRM.

Let's analyze the link from this command into components:

  1. vistepru.amocrm.ru/private/acceptors/asterisk_new ? where instead of vistepru you should have your subdomain in amocrm registered
  2. USER_LOGIN=ceo@vistep.ru where instead of my email should be your (admin)
  3. USER_HASH = 1dc1444b0d3172c1119593ffea9078c575c where instead of my API key (in the amoCRM interface "Settings" → "API) specify your API key

Team Work Example

Hidden text


Now about the special extension 100500. Let me remind you that in dialplan it looks

So

// кому позвонить решит 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


The link for wget is almost identical and the rules described above apply to it. And he is needed for the so-called. “Smart call forwarding”, when an incoming call is forwarded by an employee to 100500, and then Asterisk and amoCRM decide for themselves to whom it should be sent (read send to the responsible manager or “default” manager).

Why is this useful, you ask? Imagine a typical office situation: In conjunction with amoCRM, it will look like this: thanks for the information, thanks to the guys from voxlink - voxlink.ru/kb/integraciya-s-crm/amocrm-asterisk And yes, I completely forgot if your Asterisk is not configured yet maintaining a database in MySQL, then in this article you will find all the necessary instructions.

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



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





Also, do not forget to add another field to the CDR label (you need to be able to listen to conversations in the client card in amoCRM)

Hidden text
ALTER TABLE `cdr` ADD `recordingfile` VARCHAR (120) NOT NULL

and execute a couple more commands
Spoiler heading

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



AmoCRM setup


At this point, we are waiting for the largest number of rakes, so be careful.
First of all, connect Asterisk in the amoCRM interface.

To do this, go to “Settings” → “Integrations” → find Asterisk there and click “Install”.
We will be presented with a description of integration and a certain number of links to guides, all of this can be safely scrolled down to the bottom to the information input fields.

Login - amocrm (from manager.conf)
Password - JD3clEB8f4-_3ry84gJ (from manager.conf)
The path to the script is _https: //asterisk.vistep.ru/amocrm.php

As well as internal numbers of your company employees.

screen


The next step is to configure the amocrm.php script.

It can be downloaded from the link in the integration description, but I want to pay attention that the information laid out here has been fixed for a specific dialplan, or rather, the specific context of dial_out calls originalization (line 99), in order to correspond to the Asterisk settings on the stand. Keep this in mind and change to your context if it is different (this is necessary to make calls in a couple of clicks directly from 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;
}


Note!

My explanations for the parameters at the beginning of the script are given directly in the code.

You can check the script operation using the following links (note - I use my login / password and the path to the script, they must be different for you):

_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 the
exhaust should be like on

screenshots
test_cdr

status


Testing the resulting bunch


Based on the results of the settings, we get the following features:

  • call display in amoCRM (if a contact is already there, the name is displayed and you can go to the contact card, if not, then create a new one-click)
  • display the name of the contact from amoCRM on the phone with an incoming call
  • the ability to make a call from the amoCRM interface in a couple of clicks
  • forward the call to the responsible manager by transferring it to a special number

The video format is best for demonstration, so please:

Conclusion


I hope in this article I managed to completely close the issue of integrating amoCRM and Asterisk.
If you have any questions, welcome in the comments.

There is no account on Habré? - My coordinates are a profile, write, I will try to help.

Asterisk is fun!
Good luck to all!