Запись входящих звонков

    Несколько месяцев назад мой знакомый попросил помочь решить вопрос с записью входящих звонков. Все необходимое или было в наличии, или обещал предоставить.

    image

    Если интересно, мой опыт реализации на python вместе с кодом под катом.

    Знакомый предоставляет услуги технической поддержки и обслуживания компьютерной техники. Характер работы сотрудников – разъездной. Отдельного диспетчера нет, все звонки принимают сами сотрудники в целях экономии. Бывают ситуации, когда сотрудник не может ответить на звонок (в дороге, в диалоге с клиентом) или на сотрудника поступают претензии от клиента (сделал не то или не все, что просили). Такие ситуации надо «разруливать». В общем, надо было ему как-то централизовать прием звонков, не принимая на работу диспетчера.

    У знакомого все инциденты и изменения записываются и управляются в соответствии с требованиями ITIL. Автоматизированы процессы с помощью easla.com. Не хватало только звонков.

    Задача


    Ни о каком полноценном call-центре речь не шла, т.к. разбор звонков осуществляется «постфактум». Поэтому требования были простые:

    • Записывать в базу информацию о звонке (номер, дату, продолжительность)
    • Записывать статус звонка (отвечен, без ответа, занято)
    • Записывать разговор.


    В предоставленной в easla.com базе данных уже был создан объект «Звонок» и все необходимые атрибуты. Оговорили только статусы. Добавили статус «Невозможен» на тот случай, если на счете телефона кончились деньги.

    Решение


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

    Было принято решение использовать Twisted в качестве FastAGI сервера, который бы получал информацию о совершенном звонке и передавал информацию в easla.com посредством SOAP. Все процедуры описаны в руководстве администратора системы.

    Разговор записывается с помощью команды MixMonitor, в качестве имени файла используем переменную ${UNIQUEID}.
    По окончании разговора останавливаем запись разговора и передаем управление FastAGI серверу:
    exten => h,1,StopMixMonitor
    exten => h,n,AGI(agi://127.0.0.1:4573)

    Для реализации протокола FastAGI использовал библиотеку starpy. Информацию о продолжительности звонка получаем через CDR-записи. После получения всей необходимой информации в отдельном потоке записываем её в easla.com.

    Получаем информацию о звонке
    def fastAgiMain( agi ):
        sequence = fastagi.InSequence()
        # Указываем какие переменные необходимо получить.
        cdr_vars = {
                'CDR(start)':'',
                'CDR(disposition)}':'',
                'CDR(duration)':'',
                'CDR(end)':'',
                'DIALSTATUS':'',
                }
        # Получаем информацию и там же отправляем её в easla.com в отдельном потоке
        sequence.append(sendCDR, None, agi, cdr_vars, iter(cdr_vars))
        # После возвращаем упроавление в asterisk
        sequence.append(agi.finish)
        def onFailure( reason ):
            log.error( "Failure: %s", reason.getTraceback())
            agi.finish()
        return sequence().addErrback( onFailure )
    
    # Рекурсивная функция, которая получает все переменные указанные в cdr_vars
    def sendCDR(result, agi, cdr_vars, keys):
    
        def setVar(result, key):
            cdr_vars[key] = result
    
        def notAvailable(reason, key):
            print "key " + key + " not found"
    
        try:
            key = keys.next()
        except StopIteration, err:
            duration = str(timedelta(seconds=int(cdr_vars['CDR(duration)'])))
    	# Не используя getVariable можно получить callerid и uniqueid
            caller_id = agi.variables['agi_callerid']
            wav_file = '/data/wav/' + agi.variables['agi_uniqueid'] + '.wav'
            status = cdr_vars['DIALSTATUS']
            # В отдельном потоке отправляем информацию о звонке
            thread = Thread(target=sendCallInfo, args=(caller_id, duration, wav_file, status))
            thread.start()
            return None
        else:
            return agi.getVariable(key) \ # Получаем переменную key
    			.addCallback(setVar, key) \ # Записываем её в cdr_vars
    			.addErrback(notAvailable, key) \ # Если ошибка во время получения переменной key
    			.addCallback(sendCDR, agi, cdr_vars, keys) # Вызываем себя еще раз
    



    После того, как вернули asterisk-у управление звонком, можно заняться конвертированием wav в mp3 и отправкой информации в easla.com. Здесь необходимо пояснить, почему не используем MixMonitor для конвертирования, как предлагается во многих руководствах. MixMonitor запускает сторонние приложения отдельным процессом и никак не информирует FastAGI о том, что приложение выполнилось, и запросто может случится так, что к моменту отправки информации о звонке не будет доступа к mp3 файлу. Для конвертирования используется библиотека pydub, а suds как SOAP клиент.

    Отправляем
    
    def sendCallInfo(callid, callduration, wav_file ,status):
        raw_params = {
            'incoming_call_number': callid,
            'incoming_call_time': callduration,}
        if status:
            if status == 'ANSWER':
                raw_params['status'] = 'incoming_call_answered'
            if status == 'BUSY':
                raw_params['status'] = 'incoming_call_busy'
            if status == 'NOANSWER':
                raw_params['status'] = 'incoming_call_unanswered'
            if status == 'CANCEL':
                raw_params['status'] = 'incoming_call_unanswered'
            if status == 'CONGESTION':
                raw_params['status'] = 'incoming_call_congestion'
    
        url = 'http://easla.com/user/soap'
        client = Client(url)
        client.service.login('login','password')
    
        call_management_proc = client.service.getProcess('call_management')
        incoming_call_def = client.service.getObjectdef(call_management_proc,
                'incoming_call', 0)
    
        keyval_array = client.factory.create('KeyValuesPairSoapArray')
        # Наполняем массив KeyValuesPairSoapArray для отправки в easla.com
        for key, value in raw_params.iteritems():
            keyval = client.factory.create('KeyValuesPairSoap')
            keyval.key = key
            keyval.values.item.append(value)
            keyval_array.item.append(keyval)
    
        # Создаем объект входящий звонок в easla.com
        incoming_call_obj = client.service.createObjectref(incoming_call_def,
                None, keyval_array)
    
        if os.path.exists(wav_file):
            # asterisk может еще не освободить файл
            while is_locked(wav_file):
                time.sleep(1)
    
            mp3_file = wav2mp3(wav_file)
            with open(mp3_file, "rb") as image_file:
                encoded_string = base64.b64encode(image_file.read())
    
            if encoded_string:
                # Создаем атрибут в котором будет лежать mp3 файл
                file_attr = client.factory.create('KeyValuePairSoapArray')
                file_name = client.factory.create('KeyValuePairSoap')
                file_content = client.factory.create('KeyValuePairSoap')
                file_name.key = 'srcname'
                file_name.value = os.path.basename(mp3_file)
                file_content.key = 'content'
                file_content.value = encoded_string
                file_attr.item.append(file_name)
                file_attr.item.append(file_content)
                # Добавляем атрибут к объекту
                client.service.addFile(incoming_call_obj, 'incoming_call_file',
                            file_attr)
                if wav_file:
                    os.remove(wav_file)
                if mp3_file:
                    os.remove(mp3_file)
    



    Работу модуля удалось обкатать в первую же неделю эксплуатации. Сперва на счете не хватало средств, и успешно проверили регистрацию звонков со статусом «Невозможен». Потом счет пополнили и проверили регистрацию звонков с остальными статусами.
    Выглядит реестр входящих звонков как-то так:



    После начала регистрации звонков удалось добавить еще функцию определения абонента в регистрируемом звонке и создание инцидента на основании зарегистрированного звонка.

    Если кому-то пригодится такое решение, буду рад.

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 18

      0
      mixmonitor и curl? решит все ваши проблемы
        0
        Да MixMonitor, а почему бы и нет? Что Вас смущает?
          0
          скрипт хороший. Но зачем отдавать все во внешнюю обработку, если можно сделать штатными средствами asterisk.
          {CURL(https://dsgf.ru/api/RingStat/?status=1&input=${CALLERID(dnid)}&phone=${CI}&client=${HASH(abon,clientid)})&dom=${HASH(abon,domid)}});

          здесь я передаю например кучу внешних параметров на сторонний сайт
            0
            MYSQL(Query resultid ${connid} INSERT INTO `asterisk`.`message` (`num`, `listen`, `time`) VALUES (${CALLERID(num)}, '1', current_timestamp););

            так например сразу можно положить и что угодно в базу.
            +1
            К сожалению, нет. Из MixMonitor мы не получим продолжительность разговора, начала и конец.
              0
              мы его можем получить из CDR и потом положить куда угодно, я например ложу в базу и отправляю на сторонний сервис данные о CDR. CDR
              это всего лишь поля куда и как вы их положите не имеет значение, может и в curl обернуть. Я лишь хочу сказать что есть и другие подходы, не используя внешние скрипты
                0
                Совсем без скриптов не обойтись, мне ведь необходимо было отправить данные через SOAP. А так не спорю, решение не единственное.
                  0
                  насчет soap единственный выход
            0
            Не очень хорошее решение. В определенных условиях(медленный днс или проблемы коннекта к серверу) может положить сервер телефонии.
            Правильное решение — читать информацию из таблички cdr(при необходимости добавочные поля ложить туда же) и делать все что вам надо внешним скриптом.
            Говорю по своему опыту, опыта работы с астериском 15+ лет.
              0
              Как раз для этого все потенциально опасные операции вынесены в отдельный поток и никак не повлияют на работу asterisk:
                      thread = Thread(target=sendCallInfo, args=(caller_id, duration, wav_file, status))
                      thread.start()
              

              Единственное узкое место может быть в доступности самого FastAGI сервера. Вполне возможно, что на очень нагруженных asterisk серверах использовать FastAGI вызовет проблемы, но для небольшой организации, которая совершает 200-300 звонков в день, работает без сбоев.
                0
                Может и так, но вы сделали много избыточного кода. По сути справляется простой bash скрипт в один поток. Просматриваете cdr старше последнего обработаного, обрабатываете, перемещаете указатель.
                Кстати, такие непрофильные сервисы на сервере телефонии надо запускать через nice. Иначе может получится затык в звуке в момент старта конвертации.
                  0
                  Мне не очень нравится идея постоянно опрашивать базу на наличие новой записи, можно кончено триггер повесить на изменение таблицы, но насколько я знаю во встроенной базой asterisk триггер не установить(могу ошибаться), из-за этого пришлось бы складывать cdr в sql базу и пришлось бы обеспечивать бесперебойную работу этой базы. При недоступности sql базы asterisk вообще не сможет работать, а при недоступности fastAGI сервера у нас будут только ошибки сыпаться в лог, а звонки будут работать.
                  Что касаемо конвертирования в mp3, то лучше её делать вообще на другой машине :) благо fastAGI для этого и делали.
                    0
                    а повесить на базу процедуры при добавлении новой записи? при недоступности базы куда складываются cdr астериск тупо сыпить ошибки и успешно работает
                      0
                      Кто вам такую ерунду сказал? Ядро астериска вообще не зависит от модуля cdr. Если у вас упадет mysql(что ОЧЕНЬ маловероятно), астериск даже не почешется.
                      В my.cnf добавляете query cache 10M, и вот у вас уже этот скрипт вообще не трогает диск до тех пор, пока не появится новая запись.

                      Можно и без базы — через tail /var/log/asterisk/cdr-csv/Master.csv |yourscript
                      0
                      солидарен с вами полностью.
                      0
                      у меня есть в обслуживании сервера у которых одновременно столько разговоров идет(200-400), так там такая вещь вероятнее всего его положит. не позволяем такой роскоши…
                    0
                    Не понял, а чем этот «велосипед» лучше, чем, например, штатный модуль CDR Reports у FreePBX и т.д.?
                      0
                      Тем что он на Phyton а не на php.

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое