artem
21.11.2016
12290

Добавление звонков в amoCRM через API

В предыдущих статьях мы описывали интеграцию Asterisk и amoCRM с помощью соответствующего виджета amoCRM. Основным недостатком полученной интеграции является медленное добавление звонков в amoCRM, причина заключается в том, что amoCRM запрашивает информацию о новых звонках раз в несколько часов и может делать это ещё реже при большой загрузке. В этой статье будет описан способ быстрого добавления звонка в amoCRM сразу после завершения вызова. Для настройки интеграции Asterisk и amoCRM нужно использовать статью, упомянутую ранее, т.к. виджет amoCRM «Asterisk» будет использоваться для реализации всех функций интеграции кроме добавления звонков в amoCRM.

Отключение добавления звонков средствами виджета amoCRM

Прежде чем использовать добавление звонков в amoCRM какими-либо сторонними средствами, нужно отключить добавление звонков средствами виджета amoCRM, иначе в amoCRM появятся задвоенные звонки. Для этого в скрипте amocrm.php нужно закомментировать или удалить код, отдающий CDR-записи, и добавить вместо него код, всегда возвращающий пустой массив в ответ на запрос с параметром _action=cdr (скрипт amocrm.php выкладывается на web-сервер в процессе интеграции amoCRM и Asterisk). Ниже приведён отрывок скрипта amocrm.php начиная с 120-й строки с внесёнными правками:

} 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);
}
*/
answer(array(‘status’=>’ok’,’data’=>array()),true);
} elseif ($action===’pop’){// fill test data. Maybe you will need it. Just comment line below.

Код скрипта для добавления звонков в amoCRM

Ниже приведён код скрипта, который будет использоваться для добавления звонков в amoCRM. Для написания скрипта использовалась документация по API amoCRM https://developers.amocrm.ru/rest_api/.

#!/usr/bin/perl
use strict;
use warnings;
use utf8;
use LWP::UserAgent;
use HTTP::Cookies;
use REST::Client;
use JSON qw(decode_json encode_json);
use Asterisk::AGI;
use POSIX qw(mktime setsid);
use Cwd qw(chdir);
# Модули, указанные ниже, нужны только для отладки
#use Encode qw(decode);
#use Encode::Escape::Unicode;
#use I18N::Langinfo qw(langinfo CODESET);
#use Data::Dumper;
#my $cur_codeset = langinfo(CODESET);
my $use_agi = 1;
my $agi;
if($use_agi) { $agi = new Asterisk::AGI; }
my $max_ext_len = 5;
my $city_prefix = ‘495’;
my $city_num_len = 10 — length($city_prefix);
my $cur_date = time;
my $amo_domain = ‘example.amocrm.ru’;
my $amo_users = {
101 => {
user => ‘[email protected]’,
key => ‘073884b7c98741028ce92986a54f847c’,
},
102 => {
user => ‘[email protected]’,
key => ’08b6706cbdf4add301eb139d03578cdf’,
},
103 => {
user => ‘[email protected]’,
key => ‘68334e05f2aba94202311be7f76e5669’,
},
104 => {
user => ‘[email protected]’,
key => ‘2750aadd9bdaf67ca45ebddef8c0141e’,
},
};
my $rec_prefix = ‘https://pbx.example.com/monitor’;
my $uniqueid = $ARGV[0];
my $billsec = $ARGV[1];
my $disposition = $ARGV[2];
my $rec_file = $ARGV[3];
if(defined $rec_file and $rec_file =~ /^.*-.*-.*-(d{4})(d{2})(d{2})-d{6}-/) {
$rec_file = $rec_prefix.’/’.$1.’/’.$2.’/’.$3.’/’.$rec_file;
} elsif(defined $rec_file) {
$rec_file = $rec_prefix.’/’.$rec_file;
}
my $call_date = $ARGV[4];
if(defined $call_date and $call_date =~ /^0*(d+)-0*(d+)-0*(d+) 0*(d+):0*(d+):0*(d+)$/) {
$call_date = mktime($6,$5,$4,$3,$2-1,$1-1900,0,0);
} else {
$call_date = $cur_date;
}
my $num_a = $ARGV[5];
my $num_b = $ARGV[6];
if(not defined $disposition or $disposition ne ‘ANSWERED’) {
log_msg($agi,$use_agi,’NOTICE Not logging unanswered calls (Arguments: ‘.join(‘ ‘,@ARGV).’)’);
exit;
}
my $call_status = ‘4’;
unless($billsec =~ /^d+$/) {
log_msg($agi,$use_agi,’ERROR Malformed billsec number (Arguments: ‘.join(‘ ‘,@ARGV).’)’);
exit;
}
my ($user,$peer);
my $call_type = »;
my $note_type = 0; # 10 — входящий вызов, 11 — исходящий вызов
if($num_a =~ /^+?d+$/) {
if(length($num_a) <= $max_ext_len) {
$call_type = ‘outbound’;
$note_type = 11;
$user = $num_a;
$peer = $num_b;
}
if($num_b =~ /^+?d+$/) {
if(length($num_b) <= $max_ext_len) {
if($note_type == 11) {
log_msg($agi,$use_agi,’NOTICE Internal call, nothing to do’);
exit;
}
$call_type = ‘inbound’;
$note_type = 10;
$user = $num_b;
$peer = $num_a;
}
} else {
log_msg($agi,$use_agi,’ERROR Malformed called number (Arguments: ‘.join(‘ ‘,@ARGV).’)’);
exit;
}
} else {
log_msg($agi,$use_agi,’ERROR Malformed calling number (Arguments: ‘.join(‘ ‘,@ARGV).’)’);
exit;
}
if($note_type == 0 or $call_type eq ») {
log_msg($agi,$use_agi,’ERROR Couldn’t determine call direction (Arguments: ‘.join(‘ ‘,@ARGV).’)’);
exit;
}
unless(defined $amo_users->{$user}) {
log_msg($agi,$use_agi,’NOTICE Not logging calls for user ‘.$user.’, nothing to do (Arguments: ‘.join(‘ ‘,@ARGV).’)’);
exit;
}
my $amo_user = $amo_users->{$user};
if($use_agi) {
daemonize(*STDERR, *STDOUT, *STDIN);
open STDIN, ‘</dev/null’ or exit;
open STDOUT, ‘>/dev/null’ or exit;
open STDERR, ‘>/dev/null’ or exit;
}
my $resp;
my $cookies = HTTP::Cookies->new();
my $ua = LWP::UserAgent->new(cookie_jar => $cookies);
my $rc = REST::Client->new({
host => ‘https://’.$amo_domain,
useragent => $ua,
timeout => 8,
});
$rc->POST(‘/private/api/auth.php?type=json’,’USER_LOGIN=’.$amo_user->{user}.’&USER_HASH=’.$amo_user->{key},{‘Content-type’ => ‘application/x-www-form-urlencoded’});
$resp = decode_json($rc->responseContent());
if(not defined $resp->{‘response’}->{‘auth’} or $resp->{‘response’}->{‘auth’} ne ‘true’) {
unless($use_agi) {
log_msg($agi,$use_agi,’NOTICE Failed to login into amoCRM with user ‘.$user.’ (Arguments: ‘.join(‘ ‘,@ARGV).’)’);
}
exit;
}
if(length($peer) == $city_num_len) {
$peer = $city_prefix.$peer;
} elsif($peer =~ /^(+|9)?[78]d{10}$/) {
$peer = substr($peer,-10);
} else {
$peer =~ s/^810// or $peer =~ s/^9810//;
$peer =~ s/^+//;
}
$rc->GET(‘/private/api/v2/json/contacts/list?query=’.$peer,{‘Content-type’ => ‘application/x-www-form-urlencoded’});
$resp = $rc->responseContent();
my $call_added = 0;
if($resp ne ») {
### Добавление звонка с помощью метода notes/set
$resp = decode_json($resp);
if(@{$resp->{response}->{contacts}}) {
my $contact = $resp->{response}->{contacts}->[0];
if(defined $contact->{id} and defined $contact->{type}) {
my $element_id = $contact->{id};
my $element_type = 1;
if($contact->{type} eq ‘deal’) {
$element_type = 2;
} elsif($contact->{type} eq ‘company’) {
$element_type = 3;
}
my $post_json = encode_json({
request => {
notes => {
add => [
{
element_id => $element_id,
date_create => $call_date,
element_type => $element_type,
note_type => $note_type,
text => encode_json({
UNIQ => $uniqueid,
LINK => $rec_file,
PHONE => $peer,
DURATION => int($billsec),
SRC => ‘asterisk’,
}),
}
]
}
}
});
$rc->POST(‘/private/api/v2/json/notes/set’,$post_json,{‘Content-type’ => ‘application/json’});
$call_added = 1;
}
}
}
if(not $call_added and $call_type eq ‘inbound’) {
### Добавление звонка в неразобранное с помощью метода unsorted/add
my ($username,$phone_field_id,$phone_field_enum);
$rc->GET(‘/private/api/v2/json/accounts/current’);
$resp = $rc->responseContent();
if($resp ne ») {
$resp = decode_json($resp);
if(@{$resp->{response}->{account}->{users}}) {
foreach my $cur_acc_user (@{$resp->{response}->{account}->{users}}) {
if($cur_acc_user->{login} eq $amo_user->{user}) {
$username = $cur_acc_user->{id};
}
}
} else {
unless($use_agi) {
log_msg($agi,$use_agi,’NOTICE Failed to get current account users (Arguments: ‘.join(‘ ‘,@ARGV).’)’);
}
$username = $user;
}
my $target_phone_enum = ‘WORK’;
if(length($peer) == 10 and substr($peer,0,1) eq ‘9’) {
$target_phone_enum = ‘MOB’;
}
if(@{$resp->{response}->{account}->{custom_fields}->{contacts}}) {
foreach my $field (@{$resp->{response}->{account}->{custom_fields}->{contacts}}) {
if(defined $field->{code} and $field->{code} eq ‘PHONE’) {
$phone_field_id = $field->{id};
if(defined $field->{enums} and %{$field->{enums}}) {
foreach my $enum (keys %{$field->{enums}}) {
if($field->{enums}->{$enum} eq $target_phone_enum) {
$phone_field_enum = $enum;
last;
}
}
last;
}
last;
}
}
}
my $post_struct = {
request => {
unsorted => {
category => ‘sip’,
add => [
{
source => $user,
source_uid => $uniqueid,
date_create => $cur_date,
data => {
leads => [
{
name => $peer,
}
],
contacts => [
{
name => $peer,
}
]
},
source_data => {
from => $peer,
to => $username,
date => $call_date,
duration => int($billsec),
link => $rec_file,
service => ‘asterisk’,
},
}
],
}
}
};
if(defined $phone_field_id and defined $phone_field_enum) {
$post_struct->{request}->{unsorted}->{add}->[0]->{data}->{contacts}->[0]->{custom_fields} = [
{
id => $phone_field_id,
values => [
{
enum => $phone_field_enum,
value => length($peer) == 10 ? ‘8’.$peer : $peer,
}
],
}
];
}
my $post_json = encode_json($post_struct);
$rc->POST(‘/api/unsorted/add/?api_key=’.$amo_user->{key}.’&login=’.$amo_user->{user},$post_json,{‘Content-type’ => ‘application/json’});
}
}
sub log_msg {
my ($agi,$use_agi,$message) = @_;
if($use_agi) {
$agi->verbose($message,3);
} else {
print $message.»n»;
}
}
sub daemonize {
my $pid = fork;
exit 0 if $pid;
exit 1 if not defined $pid;
setsid();
$pid = fork;
exit 0 if $pid;
exit 1 if not defined $pid;
chdir ‘/’ or die $!;
umask 0;
close $_ for @_;
}

Для работы скрипта требуются Perl-модули LWP::UserAgent, HTTP::Cookies, REST::Client, JSON, Asterisk::AGI, POSIX и Cwd. Перед использованием скрипта нужно установить недостающие модули с помощью утилиты cpan и/или из репозиториев вашего дистрибутива.

Описание настроек скрипта

Настройки скрипта указываются в переменных в начале скрипта:
$use_agi — переменная для тестирования, если установить её значение равным 0, скрипт не будет взаимодействовать с AGI и не будет выполнять fork, весь вывод скрипта будет виден в консоли.
$max_ext_len — наибольшая длина внутреннего номера. Номера, длина которых меньше или равна указанной, будут считаться внутренними номерами.
$city_prefix — код города. Он будет подставляться к номерам, длина которых равна (10 — <длина кода города>).
$amo_domain- ваш домен amoCRM.
$amo_users — хэш с данными о пользователях amoCRM, структура хэш:
$amo_users = {
<внутренний номер пользователя> => {
user => ‘<логин пользователя amoCRM>’,
key => ‘<API-ключ пользователя amoCRM>’,
},
#…
};
API-ключ пользователя указан в разделе «Настройки» → «API».
Скрипт добавляет в amoCRM только отвеченные внешние вызовы (входящие и исходящие) пользователей, указанных в хэше $amo_users.
$rec_prefix — web URL, указывающий на директорию с записями (подробности в статье /kb/asterisk-configuration/amocrm-asterisk/, пункт 7). Значение переменной $rec_prefix подставляется к имени файла записи вызова.

Описание параметров скрипта

Скрипт рассчитан на получение семи параметров. Параметры по порядку:
1 — уникальный идентификатор вызова, можно использовать переменную UNIQUEID. Параметр обязателен.
2 — длительность вызова, можно использовать функцию CDR(billsec). Параметр обязателен.
3 — результат вызова — значение функции CDR(disposition). Параметр обязателен.
4 — имя файла записи, например значение функции CDR(recordingfile) в FreePBX. Параметр необязателен, если указать вместо него пустое значение, к звонку в amoCRM не будет добавлена запись вызова.
5 — время начала вызова, можно использовать функцию CDR(start). Параметр необязателен, если указать вместо него пустое значение, время запуска скрипта будет считаться временем начала вызова.
6 — номер звонящего, можно использовать функцию CDR(src). Параметр обязателен.
7 — вызываемый номер, можно использовать функцию CDR(dst). Параметр обязателен.

Использование скрипта для добавления звонков в amoCRM

1. Сохраните скрипт в директории для AGI-скриптов Asterisk с произвольным именем, например /var/lib/asterisk/agi-bin/amo_add_call.pl.
2. Перед использованием скрипта внесите в скрипт все настройки, описанные в прошлом разделе. Установите значение переменной $use_agi равным 0 и попробуйте выполнить скрипт из консоли:
./amo_add_call.pl $(date +%s) 15 ANSWERED » ‘2016-09-01 12:00:00’ 81112223333 101
У пользователя с номером 101 должен добавиться входящий вызов с номера 81112223333 без записи, сделанный 01.09.2016 в 12:00. Если у пользователя с номером 101 нет контакта с номером 81112223333, вызов попадёт в неразобранное.
На этом этапе обнаружатся проблемы с недостающими модулями Perl и ошибки в настройках скрипта. После того, как скрипт заработает нормально, установите значение переменной $use_agi равным 1.
3. Добавьте в файл /etc/asterisk/cdr.conf параметр endbeforehexten=yes и примените настройки Asterisk командой «core reload» в CLI Asterisk. Этот параметр нужен для того, чтобы CDR-записи формировались до начала выполнения экстеншена h и обработчиков завершения вызова, при этом во время выполнения экстеншена h и обработчиков завершения вызова, функции CDR(duration) и CDR(billsec) будут выдавать правильные значения.
4. Добавьте в диалплан Asterisk следующий контекст:
[sub-amo-add-call]
exten => s,1,AGI(amo_add_call.pl,${UNIQUEID},${CDR(billsec)},${CDR(disposition)},${CDR(recordingfile)},${CDR(start)},${CDR(src)},${CDR(dst)})
same => n,Return
ПРИМЕЧАНИЕ: Поле CDR recordingfile используется в диалплане FreePBX, но не является стандартным полем CDR, возможно вам нужно будет указать другую переменную вместо CDR(recordingfile).

Теперь нужно добавить вызов контекста sub-amo-add-call при завершении вызова с помощью экстеншена h или обработчика завершения вызова. Для FreePBX можно переобозначить экстеншены h в различных контекстах в файле /etc/asterisk/extensions_override_freepbx.conf, пример:

[ext-group]
exten => h,1,Gosub(sub-amo-add-call,s,1)
same => n,Macro(hangupcall,)
[ext-queues]
exten => h,1,Gosub(sub-amo-add-call,s,1)
same => n,Macro(hangupcall,)
[ext-local]
exten => h,1,Gosub(sub-amo-add-call,s,1)
same => n,Macro(hangupcall,)
[macro-dial-one]
exten => h,1,Gosub(sub-amo-add-call,s,1)
same => n,Macro(hangupcall,)
[macro-dialout-trunk]
exten => h,1,Gosub(sub-amo-add-call,s,1)
same => n,Macro(hangupcall,)

Вместо этого можно ещё добавить на каналы обработчик завершения вызова (для Asterisk версии 11 и выше):

same => n,Set(CHANNEL(hangup_handler_push)=sub-amo-add-call,s,1)

Готово, можно тестировать.
Напоминаю, что скрипт добавляет в amoCRM только отвеченные внешние вызовы пользователей amoCRM. Можно также добавлять в amoCRM неотвеченные и внутренние вызовы пользователей amoCRM, для этого потребуется незначительное изменение скрипта. Добавлять в amoCRM вызовы, не имеющие отношения к пользователям amoCRM, можно только с помощью виджета.

Подписаться
Уведомить о
guest
1 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Александр
Александр
01.02.2022 15:40

Можно как то скачать отформатированный amo_add_call.pl ?

Остались вопросы?

Я - Компаниец Никита, менеджер компании Voxlink. Хотите уточнить детали или готовы оставить заявку? Укажите номер телефона, я перезвоню в течение 3-х секунд.

VoIP оборудование


ближайшие курсы

10 доводов в пользу Asterisk

Распространяется бесплатно.

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

Безопасен в использовании.

Любое программное обеспечение может стать объектом интереса злоумышленников, в том числе телефонная станция. Однако, сам Asterisk, а также операционная система, на которой он работает, дают множество инструментов защиты от любых атак. При грамотной настройке безопасности у злоумышленников нет никаких шансов попасть на станцию.

Надежен в эксплуатации.

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

Гибкий в настройке.

Зачастую возможности Asterisk ограничивает только фантазия пользователя. Ни один конструктор шаблонов не сравнится с Asterisk по гибкости настройки. Это позволяет решать с помощью Asterisk любые бизнес задачи, даже те, в которых выбор в его пользу не кажется изначально очевидным.

Имеет огромный функционал.

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

Интегрируется с любыми системами.

То, что Asterisk не умеет сам, он позволяет реализовать за счет интеграции. Это могут быть интеграции с коммерческими телефонными станциями, CRM, ERP системами, биллингом, сервисами колл-трекинга, колл-бэка и модулями статистики и аналитики.

Позволяет телефонизировать офис за считанные часы.

В нашей практике были проекты, реализованные за один рабочий день. Это значит, что утром к нам обращался клиент, а уже через несколько часов он пользовался новой IP-АТС. Безусловно, такая скорость редкость, ведь АТС – инструмент зарабатывания денег для многих компаний и спешка во внедрении не уместна. Но в случае острой необходимости Asterisk готов к быстрому старту.

Отличная масштабируемость.

Очень утомительно постоянно возвращаться к одному и тому же вопросу. Такое часто бывает в случае некачественного исполнения работ или выбора заведомо неподходящего бизнес-решения. С Asterisk точно не будет такой проблемы! Телефонная станция, построенная на Asterisk может быть масштабируема до немыслимых размеров. Главное – правильно подобрать оборудование.

Повышает управляемость бизнеса.

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

Снижает расходы на связь.

Связь между внутренними абонентами IP-АТС бесплатна всегда, независимо от их географического расположения. Также к Asterisk можно подключить любых операторов телефонии, в том числе GSM сим-карты и настроить маршрутизацию вызовов по наиболее выгодному тарифу. Всё это позволяет экономить с первых минут пользования станцией.