Запис вхідних дзвінків

Кілька місяців тому мій знайомий попросив допомогти вирішити питання із записом вхідних дзвінків. Все необхідне чи було в наявності, або обіцяв надати.
image
Якщо цікаво, мій досвід реалізації на python разом з кодом під катом.

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

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

Завдання

Ні про яке повноцінне call-центрі мова не йшла, оскільки розбір дзвінків здійснюється «постфактум». Тому вимоги були прості:

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


В наданій в easla.com базі даних вже був створений об'єкт «Дзвінок» та всі необхідні атрибути. Обумовили тільки статуси. Додали статус «Неможливий» на той випадок, якщо на рахунку телефону скінчилися гроші.

Рішення

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

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

Розмова записується з допомогою команди MixMonitor, як ім'я файлу використовуємо змінну ${UNIQUEID}.
По закінченні розмови зупиняємо запис розмови і передаємо управління FastAGI сервера:
розширеннями => h,1,StopMixMonitor
 
розширеннями => 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)



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



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

Якщо комусь стане в нагоді таке рішення, буду радий.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.