Asterisk: Обеспечим VIP-клиентам первое место в очереди звонков, а так же свяжем клиента с конкретным оператором на заданное время

от автора

Asterisk — это fun!
Каждый раз сталкиваясь с какой-то нестандартной задачей я радуюсь, радуюсь возможности снова погрузиться с головой в это чудесное состояние творчества, работы мысли. В последнее время такие задачи появляются часто и это здорово.
Обозначенные в заголовке были реализованы и работают, а значит пришло время поделиться с сообществом своими решениями.

Расскажу немного подробнее о каждой.

1. Организовать список номеров телефонов VIP-клиентов.
Звонки от VIP-клиентов должны попадать на первое место в очереди Asterisk, для скорейшей обработки именно их обращения. Так же нужно иметь возможность удобно добавлять и удалять контрагентов из этого списка.

2. Связать звонок клиента с конкретным оператором очереди на заданное время.
Настроить Asterisk так, чтобы в его «памяти», на какое-то заданное время, оставалась информация о том, какой из операторов очереди принял вызов. Позвонил человек с номера 8913*75*5*0 и попадает к оператору очереди Алёна и нужно сделать так, чтобы в течение, например суток, входящие звонки с этого номера принимала только Алёна и никто другой.
Но это еще не все, если клиент не хочет общаться с Аленой, то он может нажать клавишу * на своем телефоне и в следующий раз попадет уже к другому оператору.

С вступлением на этом заканчиваю, немного Python, MySQL и хитрого dialplan ждут вас под катом.

Список VIP-клиентов.
Приступив к реализации, я первым делом начал писать web-интерфейс для работы со списком контрагентов, но со временем понял, что это будет куда дольше чем найти что-то готовое. Действительно, спустя пол часа, у меня уже был развернут очень удобный вариант телефонной книги, написаной на php в связке с MySQL — как раз то, что нужно.
Большое спасибо разработчикам PHP Address Book за их труд.
Описывать установку не стану — все тривиально и очень подробно расписано в мануале, вложенном в архив проекта.
Интерфейс очень удобный и понятный

После внесения нужных данных я отправился писать Python-скрипт, который будет принимать от Asterisk CALLERID, обрабатывать его, делать запросы в БД и выставлять нужный приоритет позвонившему исходя из результатов.
Код у меня получился вот такой:

#!/usr/bin/env python #-*- coding: utf-8 -*- import MySQLdb,sys,re  def WhatKindOfNumber(WKONnumber):     if re.match(r'^[8]{1,1}[3]{1,1}[8]{1,1}4{1,1}[3]{1,1}(\d{6,6})$', WKONnumber):         #print "Если 11 цифр и начинается с 83843(8 + код города Новокузнецк): "         return WKONnumber[5:11],"our-town"      if re.match(r'^[3]{1,1}[8]{1,1}4{1,1}[3]{1,1}(\d{6,6})$', WKONnumber):         #print "Если 10 цифр и начинается с 3843(код города Новокузнецк): "         return WKONnumber[4:10],"our-town"      if re.match(r'^[3]{1,1}(\d{6,6})$', WKONnumber):         #print "Если 7 цифр и начинается с 3(город Новокузнецк, только 7 цифр): "         return WKONnumber[1:7],"our-town"      if re.match(r'^(\d{6,6})$', WKONnumber):         #print "Если 6 цифр: "         return WKONnumber,"our-town"      if re.match(r'^\+(\d{11,11})$', WKONnumber):         #print "Если 11 цифр и начинается с +7: "         return WKONnumber[2:12],"mobile"      if re.match(r'^[8]{1,1}[9]{1,1}(\d{9,9})$', WKONnumber) or re.match(r'^[7]{1,1}[9]{1,1}(\d{9,9})$', WKONnumber):         #print "Если 11 цифр и начинается с 89 или 79: "         return WKONnumber[1:11],"mobile"      if re.match(r'^[9]{1,1}(\d{9,9})$', WKONnumber):         #print "Если 10 цифр и начинается с 9: "         return WKONnumber,"mobile"      if re.match(r'^[8]{1,1}[^9]{1,1}(\d{9,9})$', WKONnumber):         #print "Если 11 цифр и начинается с 8, но не сотовый: "         return WKONnumber[1:11],"another-town"      if re.match(r'^[^9]{1,1}(\d{9,9})$', WKONnumber):         #print "Если 10 цифр и не сотовый: "         return WKONnumber,"another-town"      #print "Если не нашлось совпадений, вернем как получили"     return WKONnumber,"default"  def agi_command(cmd):     print cmd     sys.stdout.flush()     return sys.stdin.readline().strip()  def mysqlconnect(sql):     db=MySQLdb.connect(host="127.0.0.1",port=3306,user="asterisk_user",passwd="password",db="asterisk")     cursor = db.cursor()     cursor.execute(sql)     sql = """SELECT FOUND_ROWS(); """     cursor.execute(sql)     row = cursor.fetchone()     db.close()     return row[0]  def main():     number, typeofnumber = WhatKindOfNumber(sys.argv[1])      if typeofnumber == "our-town" or typeofnumber == "default":         sql = """select SQL_CALC_FOUND_ROWS * from addressbook where  mobile= '""" + number + """' or home= '""" + number + """' or  work= '""" + number +  """' or fax='""" + number + """' limit 1;"""      if typeofnumber == "mobile" or typeofnumber == "another-town":         sql = """select SQL_CALC_FOUND_ROWS * from addressbook where mobile like '%""" + number + """' or home like '%""" + number + """' or  work like '%""" + number +  """' or fax like '%""" + number + """' or  mobile like '""" + number + """' or home like '""" + number + """' or  work like '""" + number +  """' or fax like '""" + number + """' limit 1;"""      result = mysqlconnect(sql)     if result == 0:         response = agi_command("EXEC Set QUEUE_PRIO=5")          if result > 0:         response = agi_command("EXEC Set QUEUE_PRIO=10")      sys.exit(0)  if __name__ == "__main__":     main()  

В функции WhatKindOfNumber я обрабатываю полученный номер телефона, при необходимости привожу его к нужному мне виду и определяю его тип. Далее, в зависимости от типа, запрашиваю данные в БД и выставляю в Asterisk’е значение приоритета — 5 если номера нет и 10 если он есть.
БОльшее значение QUEUE_PRIO — бОльший приоритет.

Дело за малым, добавить строчку с вызовом AGI перед Queue.
Например так(диалпланы предпочитаю на ael — не обессудьте):

200601 => { 	&recording(${CALLERID(num)},${EXTEN});         Answer();         AGI(vip_or_not.py,${CALLERID(num)});         Queue(first_TD,tT,,,20);         Hangup();         } 

С этой задачей все, идем далее.

Связка клиент <-> оператор(менеджер).
Примем как данность, что взаимосвязь Asterisk и MySQL уже организована(если нет, то можете подсмотреть как это сделать тут).

Идем в mysql и создаем табличку, в которой у нас будут храниться записи о привязках клиентов к операторам.

mysql>use asterisk; mysql> CREATE TABLE `numbers_remember` (   `id` int(9) unsigned NOT NULL auto_increment,   `number` varchar(80) NOT NULL default 'NULL', `date` varchar(80), `agent` varchar(120) NOT NULL default '',  PRIMARY KEY  (`id`), UNIQUE KEY `ix_phone` (`number`)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=UTF8; mysql> grant all on asterisk.* to 'asterisk_user'@'localhost' identified by 'password'; mysql> flush privileges; 

Теперь внесем в func_odbc.conf запросы, которые будут выполняться из диалплана.

[GET_DATA] dsn=asterisk readsql=SELECT agent, date, number FROM asterisk.numbers_remember WHERE number='${ARG1}' [SET_DATA] dsn=asterisk writesql=INSERT INTO asterisk.numbers_remember (number,date,agent) VALUES ('${SQL_ESC(${VAL1})}','${SQL_ESC(${VAL2})}', '${SQL_ESC(${VAL3})}') [UPDATE_TIME] dsn=asterisk writesql=UPDATE asterisk.numbers_remember SET date='${SQL_ESC(${VAL1})}' WHERE number='${SQL_ESC(${VAL2})}' [DELETE_DATA] dsn=asterisk writesql=DELETE FROM asterisk.numbers_remember WHERE number='${SQL_ESC(${VAL1})}' AND date='${SQL_ESC(${VAL2})}' 

непосредственно сам диалплан:

globals {     TIMEOUT_OF_NUMBER=86400; // таймаут удержания номера в базе в секундах }; 1333 => { Set(__DYNAMIC_FEATURES=delete_number_by_client); //&recording(${CALLERID(number)},${EXTEN}); //Set(DB(clients/number)=${CALLERID(num)}); Set(__CALLFROMNUM=${CALLERID(num)});  Set(ARRAY(AGENT,DATE,NUMBER)=${ODBC_GET_DATA(${CALLERID(num)})}); if("${NUMBER}"!="") {     NoOp(== IF THE NUMBER ISN'T EQUAL "NULL"  ==);     Set(DATERESULT=${MATH(${EPOCH}-${DATE},i)});     if(${DATERESULT}<${TIMEOUT_OF_NUMBER}) {         NoOp(== IF ${DATERESULT} < ${TIMEOUT_OF_NUMBER} ==);         Set(_NUM_TO_DEL=${CALLERID(NUM)});         &recording(${CALLFROMNUM},${EXTEN});         Dial(SIP/${AGENT},20,g);         if("${DIALSTATUS}"!="ANSWER") {             &recording(${CALLFROMNUM},${EXTEN});             Queue(Novokuznetsk,cnF);             Set(AGENT=${CUT(MEMBERINTERFACE,/,2)});             Set(ODBC_DELETE_DATA()=${NUMBER},${DATE});             Set(ODBC_SET_DATA()=${CALLFROMNUM},${EPOCH},${AGENT});             };         Set(ODBC_UPDATE_TIME()=${EPOCH},${NUMBER});};     if(${DATERESULT}>${TIMEOUT_OF_NUMBER}) {             NoOp(== IF ${DATERESULT} > ${TIMEOUT_OF_NUMBER}  ==);             Set(ODBC_DELETE_DATA()=${NUMBER},${DATE});             &recording(${CALLFROMNUM},${CALLFROMNUM});             Queue(Novokuznetsk,cnF);             Set(AGENT=${CUT(MEMBERINTERFACE,/,2)});             Set(ODBC_SET_DATA()=${CALLFROMNUM},${EPOCH},${AGENT});             }; } else {         NoOp(== IF THE NUMBER DOESN'T EXIST IN DB ==);         &recording(${CALLFROMNUM},${EXTEN});         Queue(Novokuznetsk,cnF);         Set(AGENT=${CUT(MEMBERINTERFACE,/,2)});         NoOp(${CALLFROMNUM},${EPOCH},${AGENT});         Set(ODBC_SET_DATA()=${CALLFROMNUM},${EPOCH},${AGENT});         };  HangUp();  }; 

Логика диалплана такая:
Определяем есть ли в базе номер телефона, с которого нам пришел вызов.
1. Если нет, то идем в последний else NoOp(== IF THE NUMBER DOESN’T EXIST IN DB ==); и отправляем звонок в очередь, после чего присваиваем переменной AGENT значение ответившего оператора. И инсертим в БД — НОМЕР, ДАТУ в UTC, АГЕНТА.

2. Если номер в БД есть, то проверяем время. Если дата в БД менее TIMEOUT_OF_NUMBER, то отправим звонок конкретному агенту и обновим время, если больше, то в очередь.

*для того, чтобы иметь возможность заполучить значение переменной MEMBERINTERFACE нужно в конфиге КАЖДОЙ очереди указать параметр setinterfacevar=yes
у меня выглядит так:

[general] persistentmembers = yes autofill = yes updatecdr=yes  [StandardQueue](!) setinterfacevar=yes music=default strategy=rrmemory timeout = 12 retry = 1 timeoutpriority = conf joinempty=yes leavewhenempty=no ringinuse=yes  [Novokuznetsk](StandardQueue) ... [Kemerovo](StandardQueue) ... [Mejdurechensk](StandardQueue) ... 

Регулярно удаляем устаревающие записи из БД.
Я написал вот такой скрипт на bash:

#!/bin/bash PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin TIMEOUT_OF_NUMBER=`grep TIMEOUT_OF_NUMBER= /etc/asterisk/extensions.ael| sed s/[^0-9]//g` CURRENT_DATE=`date +%s` THRESHOLD_DATE=$(($CURRENT_DATE-$TIMEOUT_OF_NUMBER)) mysql -e "delete from numbers_remember where date<$THRESHOLD_DATE;" -uroot -p123 asterisk 

Запускается по крону, хоть раз в минуту — зависит от значения TIMEOUT_OF_NUMBER
Для возможности удаления привязки позвонившим клиентом нужно добавить в features.conf вот такую строчку

delete_number_by_client => *,peer,Macro,delnum

и тогда, если в момент разговора клиент нажмет *, привязка удалится из таблицы.

За основу этого решения я взял статью — habrahabr.ru/post/204048,
но там автор скромно умолчал многие нюансы.

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

ссылка на оригинал статьи http://habrahabr.ru/post/270125/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *