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'^[78]3843(\d{6})$', WKONnumber):
#        print "Если 11 цифр и начинается с [78]3843([78] + код города Новокузнецк): "
        return WKONnumber[5:11],"our-town"

    if re.match(r'^[3]843{(\d{6})$', WKONnumber):
#        print "Если 10 цифр и начинается с 3843(код города Новокузнецк): "
        return WKONnumber[4:10],"our-town"

    if re.match(r'^[3](\d{6})$', WKONnumber):
#        print "Если 7 цифр и начинается с 3(город Новокузнецк, только 7 цифр): "
        return WKONnumber[1:7],"our-town"

    if re.match(r'^(\d{6})$', WKONnumber):
#        print "Если 6 цифр: "
        return WKONnumber,"our-town"

    if re.match(r'^\+(\d{11})$', WKONnumber):
#        print "Если 11 цифр и начинается с +7: "
        return WKONnumber[2:12],"mobile"

    if re.match(r'^[78]9(\d{9})$', WKONnumber):
#        print "Если 11 цифр и начинается с 89 или 79: "
        return WKONnumber[1:11],"mobile"

    if re.match(r'^[9](\d{9})$', WKONnumber):
#        print "Если 10 цифр и начинается с 9: "
        return WKONnumber,"mobile"

    if re.match(r'^[78][^9](\d{9})$', WKONnumber):
#        print "Если 11 цифр и начинается с 7 или 8, но не сотовый: "
        return WKONnumber[1:11],"another-town"

    if re.match(r'^[^9](\d{9})$', WKONnumber):
#        print "Если 10 цифр и не сотовый: "
        return WKONnumber,"another-town"

    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 — это целый мир и порой от возможностей, в нем открывающихся, голова идет кругом. Это потрясающее ощущение, когда долго над чем-то работаешь, сумеешь победить, а потом еще и поделишься с другими людьми — будешь кому-то полезным.
На этом я заканчиваю, любите свой труд, удачи вам и интересных, сложных задач!

Комментарии (11)


  1. FessAectan
    04.11.2015 07:38

    Если у кого-то нет аккаунта на Хабре, но будут вопросы — в профиле есть контакты.


    1. fleaump
      04.11.2015 15:25

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

      mysql schedule

      и база сама следит за чистоплотностью.


  1. ragequit
    04.11.2015 09:24
    -3

    Обеспечим VIP-клиентам первое место в очереди звонков

    С позиции предоставления сервиса ААА-класса все клиенты, которые вынуждены обращаться по телефону, а не имеют прямых контактов с руководством/уже работают с вами, должны иметь единый статус. Т.е. все клиенты — VIP-клиенты. Иначе это местячковый шараж-монтаж.

    а так же свяжем клиента с конкретным оператором на заданное время

    Персонал call-центра/саппорта должен обладать достаточной квалификацией для того, чтобы «подхватывать» работу своих коллег без ущерба коммуникации, либо структура этих организаций и их работа должна быть отлажена таким образом, чтобы «ведение» клиента было само собой разумеющимся на любых этапах общения. Велосипед тут выдумывать не стоит, приемлемо банальное ведение хистори и «Здравствуйте, спасибо за ваш звонок. Вы уже обращались к нам? Да, конечно, сейчас мы переключим Вас на специалиста, который уже работал с Вами, или же, если хотите, предоставим другого».

    Все.


    1. ls1
      04.11.2015 10:06
      +1

      все клиенты — VIP-клиенты
      Все клиенты равны, но некоторые равнее других


    1. FessAectan
      04.11.2015 12:07
      +1

      Реализация списка важных клиентов была предоставлена для службы такси и если руководство посчитало, что им нужно сделать так, то пусть будет так.
      ИТ — это инструмент, каждый применяет его так, как считает нужным.

      Касаемо второго, о связи клиент-менеджер(оператор).
      Задача решалась для гильдии перевозчиков нашей области, т.е. в очередь на этом астериске добавлены другие АТС со своими механизмами и операторами обрабатывающими заказы, все перевозчики используют один номер — 1333 и звонки распределяются между ними.


  1. Zagrebelion
    04.11.2015 09:24
    +1

    у вас какие-то удивительно запутанные регекспы. Например,

    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):

    можно записать так

    if re.match(r'^89(\d{9})$', WKONnumber):


    1. Zagrebelion
      04.11.2015 09:40
      +3

      то есть, if re.match(r'^[78]9(\d{9})$', WKONnumber):


      1. FessAectan
        04.11.2015 12:11

        с регекспами был первый опыт,
        спасибо за исправление — внесу в скрипт


        1. MaximChistov
          04.11.2015 13:21

          а с + номер точно не будет? он что по первму регекспу, что по поправленному, не пройдет проверку(просто добавьте "+?" сразу после ^)


          1. FessAectan
            04.11.2015 13:54

            проверку номер с плюсом проходит

            ./vip_or_not.py +79130751580
            Если 11 цифр и начинается с +7: 
            9130751580
            mobile
            EXEC Set QUEUE_PRIO=10
            


            1. MaximChistov
              04.11.2015 14:05

              значит + где-то в другом месте отсекается :)