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

в 6:00, , рубрики: asterisk, easla.com, ip-телефония, pydub, python, starpy, suds, twisted, запись разговоров

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

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

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

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

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

Автор: IlyasR

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js